claude-phone-local 2.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/.env.example +108 -0
- package/Dockerfile +84 -0
- package/LICENSE +22 -0
- package/README.md +231 -0
- package/claude-api-server/package-lock.json +832 -0
- package/claude-api-server/package.json +20 -0
- package/claude-api-server/server.js +540 -0
- package/claude-api-server/structured.js +208 -0
- package/cli/README.md +231 -0
- package/cli/bin/check-publish-safety.js +53 -0
- package/cli/bin/claude-phone.js +23 -0
- package/cli/bin/cli-main.js +284 -0
- package/cli/bin/postinstall.js +20 -0
- package/cli/lib/commands/api-server.js +77 -0
- package/cli/lib/commands/backup.js +69 -0
- package/cli/lib/commands/config/path.js +30 -0
- package/cli/lib/commands/config/reset.js +73 -0
- package/cli/lib/commands/config/show.js +86 -0
- package/cli/lib/commands/device/add.js +143 -0
- package/cli/lib/commands/device/list.js +55 -0
- package/cli/lib/commands/device/remove.js +83 -0
- package/cli/lib/commands/doctor.js +422 -0
- package/cli/lib/commands/logs.js +186 -0
- package/cli/lib/commands/restore.js +172 -0
- package/cli/lib/commands/setup.js +1246 -0
- package/cli/lib/commands/start.js +346 -0
- package/cli/lib/commands/status.js +139 -0
- package/cli/lib/commands/stop.js +101 -0
- package/cli/lib/commands/uninstall.js +183 -0
- package/cli/lib/commands/update.js +205 -0
- package/cli/lib/config.js +113 -0
- package/cli/lib/docker.js +384 -0
- package/cli/lib/mcp-register.js +108 -0
- package/cli/lib/network.js +109 -0
- package/cli/lib/platform.js +85 -0
- package/cli/lib/port-check.js +135 -0
- package/cli/lib/prereqs/checks/compose.js +147 -0
- package/cli/lib/prereqs/checks/disk.js +89 -0
- package/cli/lib/prereqs/checks/docker.js +155 -0
- package/cli/lib/prereqs/checks/network.js +78 -0
- package/cli/lib/prereqs/checks/node.js +95 -0
- package/cli/lib/prereqs/installers/docker-desktop.js +248 -0
- package/cli/lib/prereqs/installers/docker.js +229 -0
- package/cli/lib/prereqs/installers/node.js +254 -0
- package/cli/lib/prereqs/platform.js +175 -0
- package/cli/lib/prereqs/utils/execute.js +208 -0
- package/cli/lib/prereqs/utils/rollback.js +223 -0
- package/cli/lib/prereqs/utils/sudo.js +177 -0
- package/cli/lib/prereqs.js +228 -0
- package/cli/lib/prerequisites.js +115 -0
- package/cli/lib/process-manager.js +176 -0
- package/cli/lib/utils.js +78 -0
- package/cli/lib/validators.js +219 -0
- package/cli/lib/voice-downloader.js +79 -0
- package/docker/entrypoint.sh +110 -0
- package/docker/supervisord.conf +78 -0
- package/docker-compose.yml +40 -0
- package/docs/CLAUDE-CODE-SKILL.md +45 -0
- package/docs/LANGUAGES.md +113 -0
- package/docs/MCP-SERVER.md +111 -0
- package/docs/TROUBLESHOOTING.md +288 -0
- package/mcp-server/index.js +159 -0
- package/mcp-server/package-lock.json +1201 -0
- package/mcp-server/package.json +9 -0
- package/package.json +73 -0
- package/stt-local/Dockerfile +12 -0
- package/stt-local/server.py +45 -0
- package/tts-local/Dockerfile +9 -0
- package/tts-local/server.py +83 -0
- package/voice-app/API-QUERY-CONTRACT.md +238 -0
- package/voice-app/DEPLOYMENT.md +176 -0
- package/voice-app/Dockerfile +20 -0
- package/voice-app/README-OUTBOUND.md +316 -0
- package/voice-app/config/devices.json.example +18 -0
- package/voice-app/index.js +273 -0
- package/voice-app/lib/audio-fork.js +465 -0
- package/voice-app/lib/claude-bridge.js +113 -0
- package/voice-app/lib/connection-retry.js +58 -0
- package/voice-app/lib/conversation-loop.js +490 -0
- package/voice-app/lib/device-registry.js +171 -0
- package/voice-app/lib/http-server.js +242 -0
- package/voice-app/lib/logger.js +35 -0
- package/voice-app/lib/multi-registrar.js +133 -0
- package/voice-app/lib/outbound-handler.js +299 -0
- package/voice-app/lib/outbound-routes.js +417 -0
- package/voice-app/lib/outbound-session.js +324 -0
- package/voice-app/lib/query-routes.js +484 -0
- package/voice-app/lib/registrar.js +144 -0
- package/voice-app/lib/sip-handler.js +490 -0
- package/voice-app/lib/tts-service.js +205 -0
- package/voice-app/lib/whisper-client.js +140 -0
- package/voice-app/package.json +38 -0
- package/voice-app/static/gotit-beep.wav +0 -0
- package/voice-app/static/hold-music.wav +0 -0
- package/voice-app/static/ready-beep.wav +0 -0
- package/voice-app/test/freeswitch-retry.test.js +100 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import net from 'net';
|
|
2
|
+
import dgram from 'dgram';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if a TCP port is in use
|
|
10
|
+
* @param {number} port - Port number to check
|
|
11
|
+
* @param {number} [timeout=1000] - Timeout in milliseconds
|
|
12
|
+
* @returns {Promise<object>} Port check result
|
|
13
|
+
* @property {number} port - Port that was checked
|
|
14
|
+
* @property {boolean} inUse - True if port is in use
|
|
15
|
+
*/
|
|
16
|
+
export async function checkPort(port, timeout = 1000) {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
const socket = new net.Socket();
|
|
19
|
+
|
|
20
|
+
// Set timeout
|
|
21
|
+
const timer = setTimeout(() => {
|
|
22
|
+
socket.destroy();
|
|
23
|
+
resolve({
|
|
24
|
+
port: port,
|
|
25
|
+
inUse: false
|
|
26
|
+
});
|
|
27
|
+
}, timeout);
|
|
28
|
+
|
|
29
|
+
socket.on('connect', () => {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
socket.destroy();
|
|
32
|
+
resolve({
|
|
33
|
+
port: port,
|
|
34
|
+
inUse: true
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
socket.on('error', (err) => {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
socket.destroy();
|
|
41
|
+
|
|
42
|
+
// ECONNREFUSED means nothing is listening on the port
|
|
43
|
+
// EACCES means permission denied (port is in use but we can't connect)
|
|
44
|
+
// EADDRINUSE means the port is in use
|
|
45
|
+
if (err.code === 'ECONNREFUSED') {
|
|
46
|
+
resolve({
|
|
47
|
+
port: port,
|
|
48
|
+
inUse: false
|
|
49
|
+
});
|
|
50
|
+
} else if (err.code === 'EACCES' || err.code === 'EADDRINUSE') {
|
|
51
|
+
resolve({
|
|
52
|
+
port: port,
|
|
53
|
+
inUse: true
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
// Unknown error, assume port is not available
|
|
57
|
+
resolve({
|
|
58
|
+
port: port,
|
|
59
|
+
inUse: false,
|
|
60
|
+
error: err.message
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Try to connect to the port
|
|
66
|
+
socket.connect(port, '127.0.0.1');
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if a UDP port is in use by trying to bind to it
|
|
72
|
+
* @param {number} port - Port number to check
|
|
73
|
+
* @param {number} [timeout=1000] - Timeout in milliseconds
|
|
74
|
+
* @returns {Promise<boolean>} True if port is in use
|
|
75
|
+
*/
|
|
76
|
+
export async function checkUdpPort(port, timeout = 1000) {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const socket = dgram.createSocket('udp4');
|
|
79
|
+
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
socket.close();
|
|
82
|
+
resolve(false);
|
|
83
|
+
}, timeout);
|
|
84
|
+
|
|
85
|
+
socket.on('error', (err) => {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
socket.close();
|
|
88
|
+
// EADDRINUSE means port is in use
|
|
89
|
+
resolve(err.code === 'EADDRINUSE' || err.code === 'EACCES');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
socket.bind(port, '0.0.0.0', () => {
|
|
93
|
+
// Successfully bound = port was free
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
socket.close();
|
|
96
|
+
resolve(false);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if 3CX SBC process is running
|
|
103
|
+
* @returns {Promise<boolean>} True if 3cxsbc process is running
|
|
104
|
+
*/
|
|
105
|
+
export async function check3cxSbcProcess() {
|
|
106
|
+
try {
|
|
107
|
+
const { stdout } = await execAsync('pgrep -x 3cxsbc || systemctl is-active 3cxsbc 2>/dev/null');
|
|
108
|
+
return stdout.trim().length > 0 || stdout.includes('active');
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Detect if 3CX SBC is running
|
|
116
|
+
* Checks: 1) 3cxsbc process, 2) UDP port 5060, 3) TCP port 5060
|
|
117
|
+
* @returns {Promise<boolean>} True if 3CX SBC detected
|
|
118
|
+
*/
|
|
119
|
+
export async function detect3cxSbc() {
|
|
120
|
+
// Check for 3cxsbc process first (most reliable)
|
|
121
|
+
const processRunning = await check3cxSbcProcess();
|
|
122
|
+
if (processRunning) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check UDP port 5060 (SIP typically uses UDP)
|
|
127
|
+
const udpInUse = await checkUdpPort(5060);
|
|
128
|
+
if (udpInUse) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Fall back to TCP check
|
|
133
|
+
const tcpResult = await checkPort(5060);
|
|
134
|
+
return tcpResult.inUse;
|
|
135
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check Docker Compose availability (plugin or standalone)
|
|
5
|
+
* @param {object} _platform - Platform info from detectPlatform()
|
|
6
|
+
* @returns {Promise<object>} Check result
|
|
7
|
+
*/
|
|
8
|
+
export async function checkCompose(_platform) {
|
|
9
|
+
const requiredVersion = '1.29.0'; // Minimum for standalone
|
|
10
|
+
const requiredPluginVersion = '2.0.0'; // Minimum for plugin
|
|
11
|
+
|
|
12
|
+
// Try plugin first (preferred)
|
|
13
|
+
const pluginResult = checkComposePlugin();
|
|
14
|
+
|
|
15
|
+
if (pluginResult.success) {
|
|
16
|
+
const passed = compareVersions(pluginResult.version, requiredPluginVersion) >= 0;
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
name: 'Docker Compose',
|
|
20
|
+
passed,
|
|
21
|
+
version: pluginResult.version,
|
|
22
|
+
variant: 'plugin',
|
|
23
|
+
required: `>=${requiredPluginVersion}`,
|
|
24
|
+
message: passed
|
|
25
|
+
? `Docker Compose v${pluginResult.version} (plugin)`
|
|
26
|
+
: `Docker Compose v${pluginResult.version} (plugin, requires >=${requiredPluginVersion})`,
|
|
27
|
+
canAutoFix: false // Usually comes with Docker installation
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Fall back to standalone
|
|
32
|
+
const standaloneResult = checkComposeStandalone();
|
|
33
|
+
|
|
34
|
+
if (standaloneResult.success) {
|
|
35
|
+
const passed = compareVersions(standaloneResult.version, requiredVersion) >= 0;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
name: 'Docker Compose',
|
|
39
|
+
passed,
|
|
40
|
+
version: standaloneResult.version,
|
|
41
|
+
variant: 'standalone',
|
|
42
|
+
required: `>=${requiredVersion}`,
|
|
43
|
+
message: passed
|
|
44
|
+
? `Docker Compose v${standaloneResult.version} (standalone)`
|
|
45
|
+
: `Docker Compose v${standaloneResult.version} (standalone, requires >=${requiredVersion})`,
|
|
46
|
+
canAutoFix: false
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Neither available
|
|
51
|
+
return {
|
|
52
|
+
name: 'Docker Compose',
|
|
53
|
+
passed: false,
|
|
54
|
+
version: null,
|
|
55
|
+
variant: null,
|
|
56
|
+
required: `>=${requiredPluginVersion}`,
|
|
57
|
+
message: 'Docker Compose not available',
|
|
58
|
+
canAutoFix: false // Usually installed with Docker
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check for docker compose plugin
|
|
64
|
+
* @returns {{success: boolean, version?: string}} Result
|
|
65
|
+
*/
|
|
66
|
+
function checkComposePlugin() {
|
|
67
|
+
try {
|
|
68
|
+
const output = execSync('docker compose version', {
|
|
69
|
+
encoding: 'utf-8',
|
|
70
|
+
stdio: 'pipe'
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const version = parseComposeVersion(output.trim());
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
success: true,
|
|
77
|
+
version: version || 'unknown'
|
|
78
|
+
};
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return {
|
|
81
|
+
success: false
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check for standalone docker-compose
|
|
88
|
+
* @returns {{success: boolean, version?: string}} Result
|
|
89
|
+
*/
|
|
90
|
+
function checkComposeStandalone() {
|
|
91
|
+
try {
|
|
92
|
+
const output = execSync('docker-compose --version', {
|
|
93
|
+
encoding: 'utf-8',
|
|
94
|
+
stdio: 'pipe'
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const version = parseComposeVersion(output.trim());
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
success: true,
|
|
101
|
+
version: version || 'unknown'
|
|
102
|
+
};
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return {
|
|
105
|
+
success: false
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse Docker Compose version from output
|
|
112
|
+
* @param {string} output - Version output
|
|
113
|
+
* @returns {string|null} Parsed version (e.g., "2.21.0")
|
|
114
|
+
*/
|
|
115
|
+
function parseComposeVersion(output) {
|
|
116
|
+
// Handle multiple formats:
|
|
117
|
+
// Docker Compose version v2.21.0
|
|
118
|
+
// docker-compose version 1.29.2, build 5becea4c
|
|
119
|
+
const match = output.match(/version v?([\d.]+)/);
|
|
120
|
+
|
|
121
|
+
if (match) {
|
|
122
|
+
return match[1];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Compare two semantic versions
|
|
130
|
+
* @param {string} version1 - First version
|
|
131
|
+
* @param {string} version2 - Second version
|
|
132
|
+
* @returns {number} -1 if v1 < v2, 0 if equal, 1 if v1 > v2
|
|
133
|
+
*/
|
|
134
|
+
function compareVersions(version1, version2) {
|
|
135
|
+
const v1parts = version1.split('.').map(Number);
|
|
136
|
+
const v2parts = version2.split('.').map(Number);
|
|
137
|
+
|
|
138
|
+
for (let i = 0; i < 3; i++) {
|
|
139
|
+
const v1 = v1parts[i] || 0;
|
|
140
|
+
const v2 = v2parts[i] || 0;
|
|
141
|
+
|
|
142
|
+
if (v1 > v2) return 1;
|
|
143
|
+
if (v1 < v2) return -1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check disk space availability
|
|
5
|
+
* @param {object} platform - Platform info from detectPlatform()
|
|
6
|
+
* @returns {Promise<object>} Check result
|
|
7
|
+
*/
|
|
8
|
+
export async function checkDisk(platform) {
|
|
9
|
+
const requiredGB = 2;
|
|
10
|
+
const requiredBytes = requiredGB * 1024 * 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const availableBytes = getDiskSpace(platform);
|
|
14
|
+
|
|
15
|
+
if (availableBytes === null) {
|
|
16
|
+
return {
|
|
17
|
+
name: 'Disk Space',
|
|
18
|
+
passed: false,
|
|
19
|
+
available: null,
|
|
20
|
+
required: `${requiredGB}GB`,
|
|
21
|
+
message: 'Could not determine disk space',
|
|
22
|
+
canAutoFix: false
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const availableGB = (availableBytes / (1024 * 1024 * 1024)).toFixed(1);
|
|
27
|
+
const passed = availableBytes >= requiredBytes;
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
name: 'Disk Space',
|
|
31
|
+
passed,
|
|
32
|
+
available: `${availableGB}GB`,
|
|
33
|
+
required: `${requiredGB}GB`,
|
|
34
|
+
message: passed
|
|
35
|
+
? `Disk space ${availableGB}GB free (requires >=${requiredGB}GB)`
|
|
36
|
+
: `Disk space ${availableGB}GB free (requires >=${requiredGB}GB)`,
|
|
37
|
+
canAutoFix: false
|
|
38
|
+
};
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return {
|
|
41
|
+
name: 'Disk Space',
|
|
42
|
+
passed: false,
|
|
43
|
+
available: null,
|
|
44
|
+
required: `${requiredGB}GB`,
|
|
45
|
+
message: 'Error checking disk space',
|
|
46
|
+
canAutoFix: false,
|
|
47
|
+
error: error.message
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get available disk space in bytes
|
|
54
|
+
* @param {object} platform - Platform info
|
|
55
|
+
* @returns {number|null} Available bytes or null on error
|
|
56
|
+
*/
|
|
57
|
+
function getDiskSpace(platform) {
|
|
58
|
+
try {
|
|
59
|
+
if (platform.os === 'darwin' || platform.os === 'linux') {
|
|
60
|
+
// Use df command to get disk space
|
|
61
|
+
const output = execSync('df -k /', {
|
|
62
|
+
encoding: 'utf-8',
|
|
63
|
+
stdio: 'pipe'
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Parse output
|
|
67
|
+
// Filesystem 1K-blocks Used Available Use% Mounted on
|
|
68
|
+
// /dev/disk1s1 245107200 89123456 147294848 38% /
|
|
69
|
+
const lines = output.trim().split('\n');
|
|
70
|
+
|
|
71
|
+
if (lines.length < 2) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const dataLine = lines[1];
|
|
76
|
+
const parts = dataLine.split(/\s+/);
|
|
77
|
+
|
|
78
|
+
// Available is typically the 4th column (index 3)
|
|
79
|
+
if (parts.length >= 4) {
|
|
80
|
+
const availableKB = parseInt(parts[3], 10);
|
|
81
|
+
return availableKB * 1024; // Convert KB to bytes
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check Docker installation and daemon status
|
|
5
|
+
* @param {object} platform - Platform info from detectPlatform()
|
|
6
|
+
* @returns {Promise<object>} Check result
|
|
7
|
+
*/
|
|
8
|
+
export async function checkDocker(platform) {
|
|
9
|
+
// First check if docker command exists
|
|
10
|
+
const installed = checkDockerInstalled();
|
|
11
|
+
|
|
12
|
+
if (!installed.success) {
|
|
13
|
+
return {
|
|
14
|
+
name: 'Docker',
|
|
15
|
+
passed: false,
|
|
16
|
+
version: null,
|
|
17
|
+
required: '>=20.0.0',
|
|
18
|
+
message: 'Docker not installed',
|
|
19
|
+
canAutoFix: true,
|
|
20
|
+
error: 'not_installed'
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check if daemon is running
|
|
25
|
+
const running = checkDockerRunning();
|
|
26
|
+
|
|
27
|
+
if (!running.success) {
|
|
28
|
+
// Distinguish between permission denied and daemon not running
|
|
29
|
+
if (running.error === 'permission_denied') {
|
|
30
|
+
return {
|
|
31
|
+
name: 'Docker',
|
|
32
|
+
passed: false,
|
|
33
|
+
version: installed.version,
|
|
34
|
+
required: '>=20.0.0',
|
|
35
|
+
message: 'Docker installed but permission denied (user not in docker group)',
|
|
36
|
+
canAutoFix: true,
|
|
37
|
+
error: 'permission_denied'
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Docker Desktop on macOS might be installed but not running
|
|
42
|
+
if (platform.os === 'darwin') {
|
|
43
|
+
return {
|
|
44
|
+
name: 'Docker',
|
|
45
|
+
passed: false,
|
|
46
|
+
version: installed.version,
|
|
47
|
+
required: '>=20.0.0',
|
|
48
|
+
message: 'Docker installed but daemon not running',
|
|
49
|
+
canAutoFix: true,
|
|
50
|
+
error: 'not_running'
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
name: 'Docker',
|
|
56
|
+
passed: false,
|
|
57
|
+
version: installed.version,
|
|
58
|
+
required: '>=20.0.0',
|
|
59
|
+
message: 'Docker installed but daemon not running',
|
|
60
|
+
canAutoFix: true,
|
|
61
|
+
error: 'not_running'
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Both installed and running - success
|
|
66
|
+
return {
|
|
67
|
+
name: 'Docker',
|
|
68
|
+
passed: true,
|
|
69
|
+
version: installed.version,
|
|
70
|
+
required: '>=20.0.0',
|
|
71
|
+
message: `Docker v${installed.version}`,
|
|
72
|
+
canAutoFix: false
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if docker command is installed
|
|
78
|
+
* @returns {{success: boolean, version?: string}} Result with version if installed
|
|
79
|
+
*/
|
|
80
|
+
function checkDockerInstalled() {
|
|
81
|
+
try {
|
|
82
|
+
const output = execSync('docker --version', {
|
|
83
|
+
encoding: 'utf-8',
|
|
84
|
+
stdio: 'pipe'
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const version = parseDockerVersion(output.trim());
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
success: true,
|
|
91
|
+
version: version || 'unknown'
|
|
92
|
+
};
|
|
93
|
+
} catch (error) {
|
|
94
|
+
return {
|
|
95
|
+
success: false
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if Docker daemon is running
|
|
102
|
+
* @returns {{success: boolean, error?: string}} Result
|
|
103
|
+
*/
|
|
104
|
+
function checkDockerRunning() {
|
|
105
|
+
try {
|
|
106
|
+
execSync('docker ps', {
|
|
107
|
+
encoding: 'utf-8',
|
|
108
|
+
stdio: 'pipe'
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return { success: true };
|
|
112
|
+
} catch (error) {
|
|
113
|
+
// Check if it's a permission error
|
|
114
|
+
if (error.stderr && error.stderr.includes('permission denied')) {
|
|
115
|
+
return {
|
|
116
|
+
success: false,
|
|
117
|
+
error: 'permission_denied'
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check if it's daemon not running error
|
|
122
|
+
if (
|
|
123
|
+
error.stderr &&
|
|
124
|
+
(error.stderr.includes('Cannot connect to the Docker daemon') ||
|
|
125
|
+
error.stderr.includes('Is the docker daemon running?'))
|
|
126
|
+
) {
|
|
127
|
+
return {
|
|
128
|
+
success: false,
|
|
129
|
+
error: 'not_running'
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Unknown error
|
|
134
|
+
return {
|
|
135
|
+
success: false,
|
|
136
|
+
error: 'unknown'
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Parse Docker version from output
|
|
143
|
+
* @param {string} output - Docker version output
|
|
144
|
+
* @returns {string|null} Parsed version (e.g., "24.0.7")
|
|
145
|
+
*/
|
|
146
|
+
function parseDockerVersion(output) {
|
|
147
|
+
// Docker version 24.0.7, build afdd53b
|
|
148
|
+
const match = output.match(/Docker version ([\d.]+)/);
|
|
149
|
+
|
|
150
|
+
if (match) {
|
|
151
|
+
return match[1];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check network connectivity to required hosts
|
|
5
|
+
* @param {object} _platform - Platform info from detectPlatform()
|
|
6
|
+
* @returns {Promise<object>} Check result
|
|
7
|
+
*/
|
|
8
|
+
export async function checkNetwork(_platform) {
|
|
9
|
+
const hosts = [
|
|
10
|
+
{ name: 'npm', url: 'https://registry.npmjs.org' },
|
|
11
|
+
{ name: 'docker', url: 'https://hub.docker.com' },
|
|
12
|
+
{ name: 'nodesource', url: 'https://deb.nodesource.com' }
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const timeout = 5000; // 5 seconds per host
|
|
16
|
+
const results = {};
|
|
17
|
+
|
|
18
|
+
// Check each host
|
|
19
|
+
for (const host of hosts) {
|
|
20
|
+
results[host.name] = await checkHost(host.url, timeout);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Determine if we have enough connectivity
|
|
24
|
+
const reachable = Object.values(results).filter(r => r).length;
|
|
25
|
+
const total = hosts.length;
|
|
26
|
+
|
|
27
|
+
// We need at least npm to be reachable for auto-fix
|
|
28
|
+
const npmReachable = results.npm;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
name: 'Network',
|
|
32
|
+
passed: true, // Network check is informational, not a hard requirement
|
|
33
|
+
reachable,
|
|
34
|
+
total,
|
|
35
|
+
details: results,
|
|
36
|
+
message: formatNetworkMessage(results),
|
|
37
|
+
canAutoFix: false,
|
|
38
|
+
autoFixDisabled: !npmReachable // Disable auto-fix if npm is unreachable
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if a specific host is reachable
|
|
44
|
+
* @param {string} url - URL to check
|
|
45
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
46
|
+
* @returns {Promise<boolean>} True if reachable
|
|
47
|
+
*/
|
|
48
|
+
async function checkHost(url, timeout) {
|
|
49
|
+
try {
|
|
50
|
+
const response = await axios.head(url, {
|
|
51
|
+
timeout,
|
|
52
|
+
validateStatus: (status) => status < 500 // Accept redirects, etc.
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return response.status < 500;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
// Timeout or network error
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Format network check message
|
|
64
|
+
* @param {object} results - Results object with host statuses
|
|
65
|
+
* @returns {string} Formatted message
|
|
66
|
+
*/
|
|
67
|
+
function formatNetworkMessage(results) {
|
|
68
|
+
const parts = [];
|
|
69
|
+
|
|
70
|
+
parts.push('Network:');
|
|
71
|
+
|
|
72
|
+
for (const [name, reachable] of Object.entries(results)) {
|
|
73
|
+
const symbol = reachable ? '✓' : '✗';
|
|
74
|
+
parts.push(`${symbol} ${name}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return parts.join(' ');
|
|
78
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check Node.js version
|
|
5
|
+
* @param {object} _platform - Platform info from detectPlatform()
|
|
6
|
+
* @returns {Promise<object>} Check result
|
|
7
|
+
*/
|
|
8
|
+
export async function checkNode(_platform) {
|
|
9
|
+
const requiredVersion = '18.0.0';
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
// Run node --version
|
|
13
|
+
const output = execSync('node --version', {
|
|
14
|
+
encoding: 'utf-8',
|
|
15
|
+
stdio: 'pipe'
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const version = parseNodeVersion(output.trim());
|
|
19
|
+
|
|
20
|
+
if (!version) {
|
|
21
|
+
return {
|
|
22
|
+
name: 'Node.js',
|
|
23
|
+
passed: false,
|
|
24
|
+
version: null,
|
|
25
|
+
required: `>=${requiredVersion}`,
|
|
26
|
+
message: 'Node.js version could not be determined',
|
|
27
|
+
canAutoFix: true
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const passed = compareVersions(version, requiredVersion) >= 0;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
name: 'Node.js',
|
|
35
|
+
passed,
|
|
36
|
+
version,
|
|
37
|
+
required: `>=${requiredVersion}`,
|
|
38
|
+
message: passed
|
|
39
|
+
? `Node.js v${version} (requires >=${requiredVersion})`
|
|
40
|
+
: `Node.js v${version} (requires >=${requiredVersion})`,
|
|
41
|
+
canAutoFix: !passed
|
|
42
|
+
};
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// Node not found
|
|
45
|
+
return {
|
|
46
|
+
name: 'Node.js',
|
|
47
|
+
passed: false,
|
|
48
|
+
version: null,
|
|
49
|
+
required: `>=${requiredVersion}`,
|
|
50
|
+
message: 'Node.js not installed',
|
|
51
|
+
canAutoFix: true,
|
|
52
|
+
error: 'command_not_found'
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Parse Node.js version string (handles v20.11.0 or 20.11.0 format)
|
|
59
|
+
* @param {string} versionString - Raw version output
|
|
60
|
+
* @returns {string|null} Parsed version (e.g., "20.11.0")
|
|
61
|
+
*/
|
|
62
|
+
function parseNodeVersion(versionString) {
|
|
63
|
+
// Remove leading 'v' if present
|
|
64
|
+
const cleaned = versionString.replace(/^v/, '');
|
|
65
|
+
|
|
66
|
+
// Match semantic version pattern (major.minor.patch)
|
|
67
|
+
const match = cleaned.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
68
|
+
|
|
69
|
+
if (match) {
|
|
70
|
+
return `${match[1]}.${match[2]}.${match[3]}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Compare two semantic versions
|
|
78
|
+
* @param {string} version1 - First version (e.g., "20.11.0")
|
|
79
|
+
* @param {string} version2 - Second version (e.g., "18.0.0")
|
|
80
|
+
* @returns {number} -1 if v1 < v2, 0 if equal, 1 if v1 > v2
|
|
81
|
+
*/
|
|
82
|
+
function compareVersions(version1, version2) {
|
|
83
|
+
const v1parts = version1.split('.').map(Number);
|
|
84
|
+
const v2parts = version2.split('.').map(Number);
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < 3; i++) {
|
|
87
|
+
const v1 = v1parts[i] || 0;
|
|
88
|
+
const v2 = v2parts[i] || 0;
|
|
89
|
+
|
|
90
|
+
if (v1 > v2) return 1;
|
|
91
|
+
if (v1 < v2) return -1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return 0;
|
|
95
|
+
}
|