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,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
+ }