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,254 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { downloadFile, showPreview, runWithLogging } from '../utils/execute.js';
|
|
6
|
+
import { withSudo } from '../utils/sudo.js';
|
|
7
|
+
import { checkNode } from '../checks/node.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Install Node.js based on platform
|
|
11
|
+
* @param {object} platform - Platform info from detectPlatform()
|
|
12
|
+
* @returns {Promise<{success: boolean, cancelled?: boolean}>}
|
|
13
|
+
*/
|
|
14
|
+
export async function installNode(platform) {
|
|
15
|
+
console.log(chalk.bold.cyan('\n📦 Node.js Installation\n'));
|
|
16
|
+
|
|
17
|
+
// Confirm installation
|
|
18
|
+
const { confirmed } = await inquirer.prompt([
|
|
19
|
+
{
|
|
20
|
+
type: 'confirm',
|
|
21
|
+
name: 'confirmed',
|
|
22
|
+
message: 'Install Node.js 20 LTS automatically?',
|
|
23
|
+
default: false
|
|
24
|
+
}
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
if (!confirmed) {
|
|
28
|
+
showNodeManualInstructions(platform);
|
|
29
|
+
return { success: false, cancelled: true };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Route to platform-specific installer
|
|
33
|
+
let result;
|
|
34
|
+
|
|
35
|
+
switch (platform.packageManager) {
|
|
36
|
+
case 'apt':
|
|
37
|
+
result = await installNodeApt();
|
|
38
|
+
break;
|
|
39
|
+
case 'dnf':
|
|
40
|
+
case 'yum':
|
|
41
|
+
result = await installNodeDnf(platform.packageManager);
|
|
42
|
+
break;
|
|
43
|
+
case 'pacman':
|
|
44
|
+
result = await installNodePacman();
|
|
45
|
+
break;
|
|
46
|
+
case 'brew':
|
|
47
|
+
result = await installNodeBrew();
|
|
48
|
+
break;
|
|
49
|
+
default:
|
|
50
|
+
console.error(chalk.red(`\n❌ Unsupported package manager: ${platform.packageManager}`));
|
|
51
|
+
showNodeManualInstructions(platform);
|
|
52
|
+
return { success: false };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!result.success) {
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Verify installation
|
|
60
|
+
console.log(chalk.cyan('\n✓ Verifying Node.js installation...'));
|
|
61
|
+
const check = await checkNode(platform);
|
|
62
|
+
|
|
63
|
+
if (check.passed) {
|
|
64
|
+
console.log(chalk.green(`✓ Node.js v${check.version} installed successfully\n`));
|
|
65
|
+
return { success: true };
|
|
66
|
+
} else {
|
|
67
|
+
console.error(chalk.red('\n❌ Node.js installation verification failed'));
|
|
68
|
+
console.log(chalk.yellow('Node.js was installed but version check failed.'));
|
|
69
|
+
return { success: false };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Install Node.js on Ubuntu/Debian using NodeSource repository
|
|
75
|
+
* @returns {Promise<{success: boolean}>}
|
|
76
|
+
*/
|
|
77
|
+
async function installNodeApt() {
|
|
78
|
+
const scriptUrl = 'https://deb.nodesource.com/setup_20.x';
|
|
79
|
+
const tempFile = path.join(os.tmpdir(), `nodesource_setup_${Date.now()}.sh`);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
// Download script
|
|
83
|
+
console.log(chalk.cyan('\nDownloading NodeSource setup script...'));
|
|
84
|
+
await downloadFile(scriptUrl, tempFile);
|
|
85
|
+
|
|
86
|
+
// Show preview
|
|
87
|
+
await showPreview(tempFile);
|
|
88
|
+
|
|
89
|
+
// Confirm execution
|
|
90
|
+
const { executeConfirmed } = await inquirer.prompt([
|
|
91
|
+
{
|
|
92
|
+
type: 'confirm',
|
|
93
|
+
name: 'executeConfirmed',
|
|
94
|
+
message: 'Run NodeSource setup script as root?',
|
|
95
|
+
default: false
|
|
96
|
+
}
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
if (!executeConfirmed) {
|
|
100
|
+
return { success: false, cancelled: true };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Run with sudo
|
|
104
|
+
const commands = [
|
|
105
|
+
`bash ${tempFile}`,
|
|
106
|
+
'apt-get update',
|
|
107
|
+
'apt-get install -y nodejs'
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const result = await withSudo(commands);
|
|
111
|
+
|
|
112
|
+
// Clean up temp file
|
|
113
|
+
try {
|
|
114
|
+
const fs = await import('fs');
|
|
115
|
+
fs.unlinkSync(tempFile);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
// Ignore cleanup errors
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return result;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error(chalk.red(`\n❌ Installation failed: ${error.message}`));
|
|
123
|
+
return { success: false, error: error.message };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Install Node.js on RHEL/Fedora using NodeSource repository
|
|
129
|
+
* @param {string} packageManager - 'dnf' or 'yum'
|
|
130
|
+
* @returns {Promise<{success: boolean}>}
|
|
131
|
+
*/
|
|
132
|
+
async function installNodeDnf(packageManager) {
|
|
133
|
+
const scriptUrl = 'https://rpm.nodesource.com/setup_20.x';
|
|
134
|
+
const tempFile = path.join(os.tmpdir(), `nodesource_setup_${Date.now()}.sh`);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
// Download script
|
|
138
|
+
console.log(chalk.cyan('\nDownloading NodeSource setup script...'));
|
|
139
|
+
await downloadFile(scriptUrl, tempFile);
|
|
140
|
+
|
|
141
|
+
// Show preview
|
|
142
|
+
await showPreview(tempFile);
|
|
143
|
+
|
|
144
|
+
// Confirm execution
|
|
145
|
+
const { executeConfirmed } = await inquirer.prompt([
|
|
146
|
+
{
|
|
147
|
+
type: 'confirm',
|
|
148
|
+
name: 'executeConfirmed',
|
|
149
|
+
message: 'Run NodeSource setup script as root?',
|
|
150
|
+
default: false
|
|
151
|
+
}
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
if (!executeConfirmed) {
|
|
155
|
+
return { success: false, cancelled: true };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Run with sudo
|
|
159
|
+
const commands = [
|
|
160
|
+
`bash ${tempFile}`,
|
|
161
|
+
`${packageManager} install -y nodejs`
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
const result = await withSudo(commands);
|
|
165
|
+
|
|
166
|
+
// Clean up temp file
|
|
167
|
+
try {
|
|
168
|
+
const fs = await import('fs');
|
|
169
|
+
fs.unlinkSync(tempFile);
|
|
170
|
+
} catch (e) {
|
|
171
|
+
// Ignore cleanup errors
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return result;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error(chalk.red(`\n❌ Installation failed: ${error.message}`));
|
|
177
|
+
return { success: false, error: error.message };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Install Node.js on Arch Linux using pacman
|
|
183
|
+
* @returns {Promise<{success: boolean}>}
|
|
184
|
+
*/
|
|
185
|
+
async function installNodePacman() {
|
|
186
|
+
const commands = ['pacman -Sy --noconfirm nodejs npm'];
|
|
187
|
+
|
|
188
|
+
console.log(chalk.cyan('\nInstalling Node.js via pacman...'));
|
|
189
|
+
|
|
190
|
+
return await withSudo(commands);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Install Node.js on macOS using Homebrew
|
|
195
|
+
* @returns {Promise<{success: boolean}>}
|
|
196
|
+
*/
|
|
197
|
+
async function installNodeBrew() {
|
|
198
|
+
try {
|
|
199
|
+
console.log(chalk.cyan('\nInstalling Node.js 20 via Homebrew...'));
|
|
200
|
+
console.log(chalk.gray('This may take several minutes...\n'));
|
|
201
|
+
|
|
202
|
+
const result = await runWithLogging('brew install node@20');
|
|
203
|
+
|
|
204
|
+
if (result.success) {
|
|
205
|
+
// Link node@20
|
|
206
|
+
console.log(chalk.cyan('\nLinking Node.js 20...'));
|
|
207
|
+
await runWithLogging('brew link node@20');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return result;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.error(chalk.red(`\n❌ Installation failed: ${error.message}`));
|
|
213
|
+
return { success: false, error: error.message };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Show manual installation instructions for Node.js
|
|
219
|
+
* @param {object} platform - Platform info
|
|
220
|
+
* @returns {void}
|
|
221
|
+
*/
|
|
222
|
+
function showNodeManualInstructions(platform) {
|
|
223
|
+
console.log(chalk.yellow('\n📋 Manual Node.js Installation\n'));
|
|
224
|
+
|
|
225
|
+
switch (platform.packageManager) {
|
|
226
|
+
case 'apt':
|
|
227
|
+
console.log(chalk.gray('For Ubuntu/Debian:\n'));
|
|
228
|
+
console.log(chalk.cyan(' curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -'));
|
|
229
|
+
console.log(chalk.cyan(' sudo apt-get install -y nodejs\n'));
|
|
230
|
+
break;
|
|
231
|
+
|
|
232
|
+
case 'dnf':
|
|
233
|
+
case 'yum':
|
|
234
|
+
console.log(chalk.gray('For RHEL/Fedora:\n'));
|
|
235
|
+
console.log(chalk.cyan(' curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -'));
|
|
236
|
+
console.log(chalk.cyan(` sudo ${platform.packageManager} install -y nodejs\n`));
|
|
237
|
+
break;
|
|
238
|
+
|
|
239
|
+
case 'pacman':
|
|
240
|
+
console.log(chalk.gray('For Arch Linux:\n'));
|
|
241
|
+
console.log(chalk.cyan(' sudo pacman -Sy nodejs npm\n'));
|
|
242
|
+
break;
|
|
243
|
+
|
|
244
|
+
case 'brew':
|
|
245
|
+
console.log(chalk.gray('For macOS:\n'));
|
|
246
|
+
console.log(chalk.cyan(' brew install node@20\n'));
|
|
247
|
+
break;
|
|
248
|
+
|
|
249
|
+
default:
|
|
250
|
+
console.log(chalk.gray('Download from: https://nodejs.org/\n'));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
console.log(chalk.gray('After installation, run "claude-phone setup" again.\n'));
|
|
254
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detect platform information (OS, distro, arch, package manager)
|
|
7
|
+
* @returns {Promise<object>} Platform information object
|
|
8
|
+
*/
|
|
9
|
+
export async function detectPlatform() {
|
|
10
|
+
const platform = os.platform(); // 'darwin', 'linux', 'win32'
|
|
11
|
+
const arch = os.arch(); // 'x64', 'arm64', 'arm'
|
|
12
|
+
|
|
13
|
+
// Normalize architecture names
|
|
14
|
+
const normalizedArch = normalizeArch(arch);
|
|
15
|
+
const isArm = ['arm64', 'armv7l'].includes(normalizedArch);
|
|
16
|
+
|
|
17
|
+
let distro = null;
|
|
18
|
+
let distroVersion = null;
|
|
19
|
+
let packageManager = null;
|
|
20
|
+
let isPi = false;
|
|
21
|
+
|
|
22
|
+
if (platform === 'darwin') {
|
|
23
|
+
// macOS
|
|
24
|
+
distro = 'macos';
|
|
25
|
+
packageManager = 'brew';
|
|
26
|
+
distroVersion = await getMacOSVersion();
|
|
27
|
+
} else if (platform === 'linux') {
|
|
28
|
+
// Linux - detect distro from /etc/os-release
|
|
29
|
+
const osRelease = parseOsRelease();
|
|
30
|
+
distro = osRelease.id || 'unknown';
|
|
31
|
+
distroVersion = osRelease.version_id || 'unknown';
|
|
32
|
+
packageManager = detectPackageManager(distro);
|
|
33
|
+
isPi = await isRaspberryPi();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
os: platform,
|
|
38
|
+
distro,
|
|
39
|
+
distroVersion,
|
|
40
|
+
arch: normalizedArch,
|
|
41
|
+
packageManager,
|
|
42
|
+
isArm,
|
|
43
|
+
isPi
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Normalize architecture names to consistent format
|
|
49
|
+
* @param {string} arch - Raw arch from os.arch()
|
|
50
|
+
* @returns {string} Normalized arch name
|
|
51
|
+
*/
|
|
52
|
+
function normalizeArch(arch) {
|
|
53
|
+
const mapping = {
|
|
54
|
+
'x64': 'x86_64',
|
|
55
|
+
'x86_64': 'x86_64',
|
|
56
|
+
'arm64': 'arm64',
|
|
57
|
+
'aarch64': 'arm64',
|
|
58
|
+
'arm': 'armv7l',
|
|
59
|
+
'armv7l': 'armv7l'
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return mapping[arch] || arch;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parse /etc/os-release file on Linux
|
|
67
|
+
* @returns {object} Parsed key-value pairs from os-release
|
|
68
|
+
*/
|
|
69
|
+
function parseOsRelease() {
|
|
70
|
+
try {
|
|
71
|
+
const content = fs.readFileSync('/etc/os-release', 'utf-8');
|
|
72
|
+
const lines = content.split('\n');
|
|
73
|
+
const result = {};
|
|
74
|
+
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
const match = line.match(/^([A-Z_]+)=(.*)$/);
|
|
77
|
+
if (match) {
|
|
78
|
+
const key = match[1].toLowerCase();
|
|
79
|
+
let value = match[2];
|
|
80
|
+
|
|
81
|
+
// Remove quotes
|
|
82
|
+
value = value.replace(/^"(.*)"$/, '$1');
|
|
83
|
+
value = value.replace(/^'(.*)'$/, '$1');
|
|
84
|
+
|
|
85
|
+
result[key] = value;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return result;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Detect package manager based on distro
|
|
97
|
+
* @param {string} distro - Distro ID from os-release
|
|
98
|
+
* @returns {string} Package manager command (apt, dnf, yum, pacman, brew)
|
|
99
|
+
*/
|
|
100
|
+
function detectPackageManager(distro) {
|
|
101
|
+
// Debian/Ubuntu family
|
|
102
|
+
if (['ubuntu', 'debian', 'raspbian'].includes(distro)) {
|
|
103
|
+
return 'apt';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// RHEL/Fedora family
|
|
107
|
+
if (['fedora', 'rhel', 'centos', 'rocky', 'almalinux'].includes(distro)) {
|
|
108
|
+
// Newer versions use dnf
|
|
109
|
+
try {
|
|
110
|
+
execSync('which dnf', { stdio: 'pipe' });
|
|
111
|
+
return 'dnf';
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return 'yum';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Arch family
|
|
118
|
+
if (['arch', 'manjaro'].includes(distro)) {
|
|
119
|
+
return 'pacman';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Fallback - try to detect by checking which command exists
|
|
123
|
+
const packageManagers = ['apt', 'dnf', 'yum', 'pacman'];
|
|
124
|
+
for (const pm of packageManagers) {
|
|
125
|
+
try {
|
|
126
|
+
execSync(`which ${pm}`, { stdio: 'pipe' });
|
|
127
|
+
return pm;
|
|
128
|
+
} catch (e) {
|
|
129
|
+
// Continue to next
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return 'unknown';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check if running on Raspberry Pi
|
|
138
|
+
* @returns {Promise<boolean>} True if Raspberry Pi
|
|
139
|
+
*/
|
|
140
|
+
async function isRaspberryPi() {
|
|
141
|
+
try {
|
|
142
|
+
// Check device tree model file
|
|
143
|
+
const modelPath = '/sys/firmware/devicetree/base/model';
|
|
144
|
+
if (fs.existsSync(modelPath)) {
|
|
145
|
+
const model = fs.readFileSync(modelPath, 'utf-8').toLowerCase();
|
|
146
|
+
return model.includes('raspberry pi');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Fallback: Check /proc/cpuinfo
|
|
150
|
+
if (fs.existsSync('/proc/cpuinfo')) {
|
|
151
|
+
const cpuinfo = fs.readFileSync('/proc/cpuinfo', 'utf-8').toLowerCase();
|
|
152
|
+
return cpuinfo.includes('raspberry pi');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return false;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get macOS version
|
|
163
|
+
* @returns {Promise<string>} macOS version string
|
|
164
|
+
*/
|
|
165
|
+
async function getMacOSVersion() {
|
|
166
|
+
try {
|
|
167
|
+
const output = execSync('sw_vers -productVersion', {
|
|
168
|
+
encoding: 'utf-8',
|
|
169
|
+
stdio: 'pipe'
|
|
170
|
+
});
|
|
171
|
+
return output.trim();
|
|
172
|
+
} catch (error) {
|
|
173
|
+
return 'unknown';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { execSync, spawn } from 'child_process';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Download a file from URL to local path
|
|
10
|
+
* @param {string} url - URL to download from
|
|
11
|
+
* @param {string} destPath - Destination file path
|
|
12
|
+
* @returns {Promise<void>}
|
|
13
|
+
*/
|
|
14
|
+
export async function downloadFile(url, destPath) {
|
|
15
|
+
const response = await axios.get(url, {
|
|
16
|
+
responseType: 'stream',
|
|
17
|
+
timeout: 30000 // 30 second timeout
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const writer = fs.createWriteStream(destPath);
|
|
21
|
+
|
|
22
|
+
response.data.pipe(writer);
|
|
23
|
+
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
writer.on('finish', resolve);
|
|
26
|
+
writer.on('error', reject);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read first N lines from a file
|
|
32
|
+
* @param {string} filePath - Path to file
|
|
33
|
+
* @param {number} count - Number of lines to read
|
|
34
|
+
* @returns {Promise<string>} File content (first N lines)
|
|
35
|
+
*/
|
|
36
|
+
export async function readLines(filePath, count) {
|
|
37
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
38
|
+
const lines = content.split('\n');
|
|
39
|
+
const preview = lines.slice(0, count);
|
|
40
|
+
|
|
41
|
+
return preview.join('\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Show preview of a script file before execution
|
|
46
|
+
* @param {string} filePath - Path to script file
|
|
47
|
+
* @param {number} lineCount - Number of lines to preview (default: 50)
|
|
48
|
+
* @returns {Promise<void>}
|
|
49
|
+
*/
|
|
50
|
+
export async function showPreview(filePath, lineCount = 50) {
|
|
51
|
+
console.log(chalk.yellow('\n📄 Script preview (first 50 lines):'));
|
|
52
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
53
|
+
|
|
54
|
+
const preview = await readLines(filePath, lineCount);
|
|
55
|
+
const lines = preview.split('\n');
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < lines.length; i++) {
|
|
58
|
+
console.log(chalk.gray(`${(i + 1).toString().padStart(4)} │ ${lines[i]}`));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (lines.length === lineCount) {
|
|
62
|
+
console.log(chalk.gray(' │ ... (truncated)'));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Run a command with logging to file
|
|
70
|
+
* @param {string} command - Command to run
|
|
71
|
+
* @param {object} options - Execution options
|
|
72
|
+
* @returns {Promise<{success: boolean, output?: string, error?: string}>}
|
|
73
|
+
*/
|
|
74
|
+
export async function runWithLogging(command, options = {}) {
|
|
75
|
+
const logFile = getLogFile();
|
|
76
|
+
|
|
77
|
+
// Log command
|
|
78
|
+
const timestamp = new Date().toISOString();
|
|
79
|
+
fs.appendFileSync(logFile, `\n[${timestamp}] Running: ${command}\n`);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const output = execSync(command, {
|
|
83
|
+
encoding: 'utf-8',
|
|
84
|
+
stdio: options.silent ? 'pipe' : 'inherit',
|
|
85
|
+
...options
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Log success
|
|
89
|
+
fs.appendFileSync(logFile, `[${timestamp}] Success\n`);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
success: true,
|
|
93
|
+
output
|
|
94
|
+
};
|
|
95
|
+
} catch (error) {
|
|
96
|
+
// Log error
|
|
97
|
+
fs.appendFileSync(logFile, `[${timestamp}] Error: ${error.message}\n`);
|
|
98
|
+
|
|
99
|
+
if (error.stderr) {
|
|
100
|
+
fs.appendFileSync(logFile, `[${timestamp}] stderr: ${error.stderr}\n`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
success: false,
|
|
105
|
+
error: error.message,
|
|
106
|
+
stderr: error.stderr
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Run a command and stream output in real-time
|
|
113
|
+
* @param {string} command - Command to run
|
|
114
|
+
* @param {Array<string>} args - Command arguments
|
|
115
|
+
* @param {object} options - Spawn options
|
|
116
|
+
* @returns {Promise<{success: boolean, code: number}>}
|
|
117
|
+
*/
|
|
118
|
+
export async function runWithStreaming(command, args = [], options = {}) {
|
|
119
|
+
const logFile = getLogFile();
|
|
120
|
+
const timestamp = new Date().toISOString();
|
|
121
|
+
const fullCommand = `${command} ${args.join(' ')}`;
|
|
122
|
+
|
|
123
|
+
fs.appendFileSync(logFile, `\n[${timestamp}] Running: ${fullCommand}\n`);
|
|
124
|
+
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const child = spawn(command, args, {
|
|
127
|
+
stdio: 'inherit',
|
|
128
|
+
...options
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
child.on('close', (code) => {
|
|
132
|
+
fs.appendFileSync(
|
|
133
|
+
logFile,
|
|
134
|
+
`[${timestamp}] Exited with code ${code}\n`
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
resolve({
|
|
138
|
+
success: code === 0,
|
|
139
|
+
code
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
child.on('error', (error) => {
|
|
144
|
+
fs.appendFileSync(logFile, `[${timestamp}] Error: ${error.message}\n`);
|
|
145
|
+
reject(error);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Download script to temp file, preview, confirm, and execute
|
|
152
|
+
* @param {string} url - Script URL
|
|
153
|
+
* @param {object} options - Execution options
|
|
154
|
+
* @returns {Promise<{success: boolean, cancelled?: boolean}>}
|
|
155
|
+
*/
|
|
156
|
+
export async function executeScript(url, options = {}) {
|
|
157
|
+
// 1. Download to temp file
|
|
158
|
+
const tempFile = path.join(os.tmpdir(), `prereq_${Date.now()}.sh`);
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
console.log(chalk.cyan(`\nDownloading script from ${url}...`));
|
|
162
|
+
await downloadFile(url, tempFile);
|
|
163
|
+
|
|
164
|
+
// 2. Show preview
|
|
165
|
+
await showPreview(tempFile);
|
|
166
|
+
|
|
167
|
+
// 3. Get confirmation (handled by caller)
|
|
168
|
+
// The caller should confirm before calling executeScript
|
|
169
|
+
|
|
170
|
+
// 4. Execute with logging
|
|
171
|
+
const result = await runWithLogging(`bash ${tempFile}`, options);
|
|
172
|
+
|
|
173
|
+
// 5. Clean up temp file
|
|
174
|
+
try {
|
|
175
|
+
fs.unlinkSync(tempFile);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
// Ignore cleanup errors
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return result;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
// Clean up on error
|
|
183
|
+
try {
|
|
184
|
+
if (fs.existsSync(tempFile)) {
|
|
185
|
+
fs.unlinkSync(tempFile);
|
|
186
|
+
}
|
|
187
|
+
} catch (e) {
|
|
188
|
+
// Ignore cleanup errors
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get log file path
|
|
197
|
+
* @returns {string} Path to log file
|
|
198
|
+
*/
|
|
199
|
+
function getLogFile() {
|
|
200
|
+
const configDir = path.join(os.homedir(), '.claude-phone');
|
|
201
|
+
|
|
202
|
+
// Ensure config directory exists
|
|
203
|
+
if (!fs.existsSync(configDir)) {
|
|
204
|
+
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return path.join(configDir, 'prereq-install.log');
|
|
208
|
+
}
|