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.
Files changed (96) hide show
  1. package/.env.example +108 -0
  2. package/Dockerfile +84 -0
  3. package/LICENSE +22 -0
  4. package/README.md +231 -0
  5. package/claude-api-server/package-lock.json +832 -0
  6. package/claude-api-server/package.json +20 -0
  7. package/claude-api-server/server.js +540 -0
  8. package/claude-api-server/structured.js +208 -0
  9. package/cli/README.md +231 -0
  10. package/cli/bin/check-publish-safety.js +53 -0
  11. package/cli/bin/claude-phone.js +23 -0
  12. package/cli/bin/cli-main.js +284 -0
  13. package/cli/bin/postinstall.js +20 -0
  14. package/cli/lib/commands/api-server.js +77 -0
  15. package/cli/lib/commands/backup.js +69 -0
  16. package/cli/lib/commands/config/path.js +30 -0
  17. package/cli/lib/commands/config/reset.js +73 -0
  18. package/cli/lib/commands/config/show.js +86 -0
  19. package/cli/lib/commands/device/add.js +143 -0
  20. package/cli/lib/commands/device/list.js +55 -0
  21. package/cli/lib/commands/device/remove.js +83 -0
  22. package/cli/lib/commands/doctor.js +422 -0
  23. package/cli/lib/commands/logs.js +186 -0
  24. package/cli/lib/commands/restore.js +172 -0
  25. package/cli/lib/commands/setup.js +1246 -0
  26. package/cli/lib/commands/start.js +346 -0
  27. package/cli/lib/commands/status.js +139 -0
  28. package/cli/lib/commands/stop.js +101 -0
  29. package/cli/lib/commands/uninstall.js +183 -0
  30. package/cli/lib/commands/update.js +205 -0
  31. package/cli/lib/config.js +113 -0
  32. package/cli/lib/docker.js +384 -0
  33. package/cli/lib/mcp-register.js +108 -0
  34. package/cli/lib/network.js +109 -0
  35. package/cli/lib/platform.js +85 -0
  36. package/cli/lib/port-check.js +135 -0
  37. package/cli/lib/prereqs/checks/compose.js +147 -0
  38. package/cli/lib/prereqs/checks/disk.js +89 -0
  39. package/cli/lib/prereqs/checks/docker.js +155 -0
  40. package/cli/lib/prereqs/checks/network.js +78 -0
  41. package/cli/lib/prereqs/checks/node.js +95 -0
  42. package/cli/lib/prereqs/installers/docker-desktop.js +248 -0
  43. package/cli/lib/prereqs/installers/docker.js +229 -0
  44. package/cli/lib/prereqs/installers/node.js +254 -0
  45. package/cli/lib/prereqs/platform.js +175 -0
  46. package/cli/lib/prereqs/utils/execute.js +208 -0
  47. package/cli/lib/prereqs/utils/rollback.js +223 -0
  48. package/cli/lib/prereqs/utils/sudo.js +177 -0
  49. package/cli/lib/prereqs.js +228 -0
  50. package/cli/lib/prerequisites.js +115 -0
  51. package/cli/lib/process-manager.js +176 -0
  52. package/cli/lib/utils.js +78 -0
  53. package/cli/lib/validators.js +219 -0
  54. package/cli/lib/voice-downloader.js +79 -0
  55. package/docker/entrypoint.sh +110 -0
  56. package/docker/supervisord.conf +78 -0
  57. package/docker-compose.yml +40 -0
  58. package/docs/CLAUDE-CODE-SKILL.md +45 -0
  59. package/docs/LANGUAGES.md +113 -0
  60. package/docs/MCP-SERVER.md +111 -0
  61. package/docs/TROUBLESHOOTING.md +288 -0
  62. package/mcp-server/index.js +159 -0
  63. package/mcp-server/package-lock.json +1201 -0
  64. package/mcp-server/package.json +9 -0
  65. package/package.json +73 -0
  66. package/stt-local/Dockerfile +12 -0
  67. package/stt-local/server.py +45 -0
  68. package/tts-local/Dockerfile +9 -0
  69. package/tts-local/server.py +83 -0
  70. package/voice-app/API-QUERY-CONTRACT.md +238 -0
  71. package/voice-app/DEPLOYMENT.md +176 -0
  72. package/voice-app/Dockerfile +20 -0
  73. package/voice-app/README-OUTBOUND.md +316 -0
  74. package/voice-app/config/devices.json.example +18 -0
  75. package/voice-app/index.js +273 -0
  76. package/voice-app/lib/audio-fork.js +465 -0
  77. package/voice-app/lib/claude-bridge.js +113 -0
  78. package/voice-app/lib/connection-retry.js +58 -0
  79. package/voice-app/lib/conversation-loop.js +490 -0
  80. package/voice-app/lib/device-registry.js +171 -0
  81. package/voice-app/lib/http-server.js +242 -0
  82. package/voice-app/lib/logger.js +35 -0
  83. package/voice-app/lib/multi-registrar.js +133 -0
  84. package/voice-app/lib/outbound-handler.js +299 -0
  85. package/voice-app/lib/outbound-routes.js +417 -0
  86. package/voice-app/lib/outbound-session.js +324 -0
  87. package/voice-app/lib/query-routes.js +484 -0
  88. package/voice-app/lib/registrar.js +144 -0
  89. package/voice-app/lib/sip-handler.js +490 -0
  90. package/voice-app/lib/tts-service.js +205 -0
  91. package/voice-app/lib/whisper-client.js +140 -0
  92. package/voice-app/package.json +38 -0
  93. package/voice-app/static/gotit-beep.wav +0 -0
  94. package/voice-app/static/hold-music.wav +0 -0
  95. package/voice-app/static/ready-beep.wav +0 -0
  96. 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
+ }