spudmobile-bridge 1.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/appicon.png ADDED
Binary file
@@ -0,0 +1,3 @@
1
+ export declare function installAutostart(projectPath?: string): void;
2
+ export declare function uninstallAutostart(): void;
3
+ export declare function isAutostartInstalled(): boolean;
@@ -0,0 +1,234 @@
1
+ import { execSync } from 'child_process';
2
+ import { writeFileSync, unlinkSync, existsSync, mkdirSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { platform, homedir } from 'os';
5
+ const SERVICE_NAME = 'com.spudmobile-bridge';
6
+ const LABEL = 'SpudMobile Bridge';
7
+ /**
8
+ * Get the path to the globally installed bridge binary
9
+ */
10
+ function getBridgePath() {
11
+ try {
12
+ // Try to find the global install
13
+ const globalBin = execSync('which spudmobile-bridge', { encoding: 'utf-8' }).trim();
14
+ if (globalBin)
15
+ return globalBin;
16
+ }
17
+ catch {
18
+ // not found
19
+ }
20
+ // Fallback: use npx
21
+ try {
22
+ const npxPath = execSync('which npx', { encoding: 'utf-8' }).trim();
23
+ return `${npxPath} spudmobile-bridge`;
24
+ }
25
+ catch {
26
+ throw new Error('Neither spudmobile-bridge nor npx found in PATH');
27
+ }
28
+ }
29
+ /**
30
+ * Get the full PATH from current shell (needed for LaunchAgent)
31
+ */
32
+ function getShellPath() {
33
+ try {
34
+ return execSync('echo $PATH', { encoding: 'utf-8', shell: '/bin/zsh' }).trim();
35
+ }
36
+ catch {
37
+ return '/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin';
38
+ }
39
+ }
40
+ // ─── macOS LaunchAgent ────────────────────────────────────
41
+ function getMacPlistPath() {
42
+ return join(homedir(), 'Library', 'LaunchAgents', `${SERVICE_NAME}.plist`);
43
+ }
44
+ function installMac(projectPath) {
45
+ const shellPath = getShellPath();
46
+ const logDir = join(homedir(), '.spudmobile-bridge');
47
+ try {
48
+ if (!existsSync(logDir)) {
49
+ mkdirSync(logDir, { recursive: true });
50
+ }
51
+ }
52
+ catch {
53
+ // Directory might already exist or be created by another process
54
+ }
55
+ // Always use npx with @latest to ensure the latest version runs
56
+ let npxPath;
57
+ try {
58
+ npxPath = execSync('which npx', { encoding: 'utf-8', shell: '/bin/zsh' }).trim();
59
+ }
60
+ catch {
61
+ npxPath = '/usr/local/bin/npx';
62
+ }
63
+ let programArgs = ` <string>${npxPath}</string>\n <string>-y</string>\n <string>spudmobile-bridge@latest</string>`;
64
+ if (projectPath) {
65
+ programArgs += `\n <string>--path</string>\n <string>${projectPath}</string>`;
66
+ }
67
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
68
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
69
+ <plist version="1.0">
70
+ <dict>
71
+ <key>Label</key>
72
+ <string>${SERVICE_NAME}</string>
73
+ <key>ProgramArguments</key>
74
+ <array>
75
+ ${programArgs}
76
+ </array>
77
+ <key>RunAtLoad</key>
78
+ <true/>
79
+ <key>KeepAlive</key>
80
+ <true/>
81
+ <key>EnvironmentVariables</key>
82
+ <dict>
83
+ <key>PATH</key>
84
+ <string>${shellPath}</string>
85
+ <key>HOME</key>
86
+ <string>${homedir()}</string>
87
+ </dict>
88
+ <key>StandardOutPath</key>
89
+ <string>${logDir}/bridge.log</string>
90
+ <key>StandardErrorPath</key>
91
+ <string>${logDir}/bridge-error.log</string>
92
+ <key>WorkingDirectory</key>
93
+ <string>${projectPath || homedir()}</string>
94
+ </dict>
95
+ </plist>`;
96
+ const plistPath = getMacPlistPath();
97
+ writeFileSync(plistPath, plist);
98
+ // Load the agent
99
+ try {
100
+ execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: 'ignore' });
101
+ }
102
+ catch { /* ignore */ }
103
+ execSync(`launchctl load "${plistPath}"`);
104
+ console.log('✅ Auto-launch installed (macOS LaunchAgent)');
105
+ console.log(` Plist: ${plistPath}`);
106
+ console.log(` Logs: ${logDir}/bridge.log`);
107
+ console.log(' Bridge will start automatically on login.');
108
+ }
109
+ function uninstallMac() {
110
+ const plistPath = getMacPlistPath();
111
+ if (!existsSync(plistPath)) {
112
+ console.log('ℹ️ Auto-launch is not installed.');
113
+ return;
114
+ }
115
+ try {
116
+ execSync(`launchctl unload "${plistPath}"`, { stdio: 'ignore' });
117
+ }
118
+ catch { /* ignore */ }
119
+ unlinkSync(plistPath);
120
+ console.log('✅ Auto-launch removed (macOS LaunchAgent)');
121
+ }
122
+ // ─── Linux systemd ───────────────────────────────────────
123
+ function getLinuxServicePath() {
124
+ const dir = join(homedir(), '.config', 'systemd', 'user');
125
+ if (!existsSync(dir)) {
126
+ mkdirSync(dir, { recursive: true });
127
+ }
128
+ return join(dir, 'spudmobile-bridge.service');
129
+ }
130
+ function installLinux(projectPath) {
131
+ const bridgePath = getBridgePath();
132
+ const shellPath = getShellPath();
133
+ const logDir = join(homedir(), '.spudmobile-bridge');
134
+ try {
135
+ if (!existsSync(logDir)) {
136
+ mkdirSync(logDir, { recursive: true });
137
+ }
138
+ }
139
+ catch {
140
+ // ignore
141
+ }
142
+ let execStart = bridgePath;
143
+ if (projectPath) {
144
+ execStart += ` --path "${projectPath}"`;
145
+ }
146
+ const service = `[Unit]
147
+ Description=${LABEL}
148
+ After=network-online.target
149
+ Wants=network-online.target
150
+
151
+ [Service]
152
+ Type=simple
153
+ ExecStart=${execStart}
154
+ Restart=on-failure
155
+ RestartSec=10
156
+ Environment=PATH=${shellPath}
157
+ Environment=HOME=${homedir()}
158
+ WorkingDirectory=${projectPath || homedir()}
159
+
160
+ [Install]
161
+ WantedBy=default.target
162
+ `;
163
+ const servicePath = getLinuxServicePath();
164
+ writeFileSync(servicePath, service);
165
+ // Enable and start
166
+ try {
167
+ execSync('systemctl --user daemon-reload');
168
+ execSync('systemctl --user enable spudmobile-bridge');
169
+ execSync('systemctl --user start spudmobile-bridge');
170
+ }
171
+ catch (err) {
172
+ const msg = err instanceof Error ? err.message : String(err);
173
+ console.log(`⚠️ Could not start service: ${msg}`);
174
+ console.log(' Try manually: systemctl --user start spudmobile-bridge');
175
+ }
176
+ console.log('✅ Auto-launch installed (systemd user service)');
177
+ console.log(` Service: ${servicePath}`);
178
+ console.log(' Bridge will start automatically on login.');
179
+ console.log(' Status: systemctl --user status spudmobile-bridge');
180
+ }
181
+ function uninstallLinux() {
182
+ const servicePath = getLinuxServicePath();
183
+ if (!existsSync(servicePath)) {
184
+ console.log('ℹ️ Auto-launch is not installed.');
185
+ return;
186
+ }
187
+ try {
188
+ execSync('systemctl --user stop spudmobile-bridge', { stdio: 'ignore' });
189
+ execSync('systemctl --user disable spudmobile-bridge', { stdio: 'ignore' });
190
+ }
191
+ catch { /* ignore */ }
192
+ unlinkSync(servicePath);
193
+ try {
194
+ execSync('systemctl --user daemon-reload');
195
+ }
196
+ catch { /* ignore */ }
197
+ console.log('✅ Auto-launch removed (systemd user service)');
198
+ }
199
+ // ─── Public API ──────────────────────────────────────────
200
+ export function installAutostart(projectPath) {
201
+ const os = platform();
202
+ if (os === 'darwin') {
203
+ installMac(projectPath);
204
+ }
205
+ else if (os === 'linux') {
206
+ installLinux(projectPath);
207
+ }
208
+ else {
209
+ console.log(`❌ Auto-launch is not supported on ${os}`);
210
+ console.log(' Supported: macOS (LaunchAgent), Linux (systemd)');
211
+ }
212
+ }
213
+ export function uninstallAutostart() {
214
+ const os = platform();
215
+ if (os === 'darwin') {
216
+ uninstallMac();
217
+ }
218
+ else if (os === 'linux') {
219
+ uninstallLinux();
220
+ }
221
+ else {
222
+ console.log(`❌ Auto-launch is not supported on ${os}`);
223
+ }
224
+ }
225
+ export function isAutostartInstalled() {
226
+ const os = platform();
227
+ if (os === 'darwin') {
228
+ return existsSync(getMacPlistPath());
229
+ }
230
+ else if (os === 'linux') {
231
+ return existsSync(getLinuxServicePath());
232
+ }
233
+ return false;
234
+ }
@@ -0,0 +1,32 @@
1
+ import { ChildProcess } from 'child_process';
2
+ /**
3
+ * Find the Codex CLI binary path
4
+ */
5
+ export declare function findCodexCLI(): string | null;
6
+ export interface CodexOptions {
7
+ model?: string;
8
+ cwd?: string;
9
+ continueSession?: boolean;
10
+ timeout?: number;
11
+ }
12
+ export interface CodexResult {
13
+ output: string;
14
+ exitCode: number;
15
+ killed: boolean;
16
+ }
17
+ /**
18
+ * Run Codex CLI in non-interactive (exec) mode with streaming output
19
+ *
20
+ * Uses `codex exec "prompt"` for one-shot execution
21
+ * Uses `codex exec resume --last "prompt"` to continue the last session
22
+ *
23
+ * @param prompt The prompt to send
24
+ * @param cliPath Path to codex binary
25
+ * @param options Additional options
26
+ * @param onChunk Called with each chunk of stdout for streaming
27
+ * @returns Full result when completed
28
+ */
29
+ export declare function runCodex(prompt: string, cliPath: string, options?: CodexOptions, onChunk?: (chunk: string, fullOutput: string) => void): {
30
+ process: ChildProcess;
31
+ result: Promise<CodexResult>;
32
+ };
package/dist/codex.js ADDED
@@ -0,0 +1,106 @@
1
+ import { spawn, execSync } from 'child_process';
2
+ /**
3
+ * Find the Codex CLI binary path
4
+ */
5
+ export function findCodexCLI() {
6
+ // Try `which` first
7
+ try {
8
+ const result = execSync('which codex', { encoding: 'utf-8' }).trim();
9
+ if (result)
10
+ return result;
11
+ }
12
+ catch {
13
+ // not in PATH
14
+ }
15
+ const commonPaths = [
16
+ '/usr/local/bin/codex',
17
+ '/opt/homebrew/bin/codex',
18
+ `${process.env.HOME}/.local/bin/codex`,
19
+ `${process.env.HOME}/.npm-global/bin/codex`,
20
+ ];
21
+ for (const p of commonPaths) {
22
+ try {
23
+ execSync(`test -x "${p}"`, { stdio: 'ignore' });
24
+ return p;
25
+ }
26
+ catch {
27
+ // not found
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+ /**
33
+ * Run Codex CLI in non-interactive (exec) mode with streaming output
34
+ *
35
+ * Uses `codex exec "prompt"` for one-shot execution
36
+ * Uses `codex exec resume --last "prompt"` to continue the last session
37
+ *
38
+ * @param prompt The prompt to send
39
+ * @param cliPath Path to codex binary
40
+ * @param options Additional options
41
+ * @param onChunk Called with each chunk of stdout for streaming
42
+ * @returns Full result when completed
43
+ */
44
+ export function runCodex(prompt, cliPath, options = {}, onChunk) {
45
+ const args = [];
46
+ if (options.continueSession) {
47
+ // Resume the last session: codex exec resume --last "prompt"
48
+ args.push('exec', 'resume', '--last', prompt);
49
+ }
50
+ else {
51
+ // One-shot: codex exec "prompt"
52
+ args.push('exec', prompt);
53
+ }
54
+ if (options.model) {
55
+ args.push('--model', options.model);
56
+ }
57
+ // Full auto approval mode — no interactive prompts
58
+ args.push('--full-auto');
59
+ const timeout = options.timeout || 10 * 60 * 1000; // 10 min default
60
+ const proc = spawn(cliPath, args, {
61
+ cwd: options.cwd || process.cwd(),
62
+ env: { ...process.env },
63
+ stdio: ['pipe', 'pipe', 'pipe'],
64
+ });
65
+ // Close stdin immediately — exec mode doesn't need it
66
+ if (proc.stdin) {
67
+ proc.stdin.end();
68
+ }
69
+ const result = new Promise((resolve) => {
70
+ let output = '';
71
+ let stderr = '';
72
+ let killed = false;
73
+ const timer = setTimeout(() => {
74
+ killed = true;
75
+ proc.kill('SIGTERM');
76
+ }, timeout);
77
+ proc.stdout?.on('data', (data) => {
78
+ const chunk = data.toString('utf-8');
79
+ output += chunk;
80
+ onChunk?.(chunk, output);
81
+ });
82
+ proc.stderr?.on('data', (data) => {
83
+ stderr += data.toString('utf-8');
84
+ });
85
+ proc.on('close', (code) => {
86
+ clearTimeout(timer);
87
+ if (stderr && !output) {
88
+ output = `⚠️ Codex CLI Error:\n${stderr}`;
89
+ }
90
+ resolve({
91
+ output: output || '(empty response)',
92
+ exitCode: code ?? 1,
93
+ killed,
94
+ });
95
+ });
96
+ proc.on('error', (err) => {
97
+ clearTimeout(timer);
98
+ resolve({
99
+ output: `⚠️ Failed to start Codex CLI: ${err.message}`,
100
+ exitCode: 1,
101
+ killed: false,
102
+ });
103
+ });
104
+ });
105
+ return { process: proc, result };
106
+ }
@@ -0,0 +1,25 @@
1
+ export interface BridgeConfig {
2
+ supabaseUrl: string;
3
+ supabaseAnonKey: string;
4
+ pairId: string | null;
5
+ pairCode: string | null;
6
+ projectPath: string | null;
7
+ port: number;
8
+ }
9
+ interface SavedConfig {
10
+ pairId?: string;
11
+ pairCode?: string;
12
+ projectPath?: string;
13
+ }
14
+ /**
15
+ * Save config to ~/.claude-mobile/config.json
16
+ */
17
+ export declare function saveConfig(updates: Partial<SavedConfig>): void;
18
+ /**
19
+ * Build the full BridgeConfig from env + saved config + CLI args
20
+ */
21
+ export declare function getConfig(options: {
22
+ path?: string;
23
+ port?: number;
24
+ }): BridgeConfig;
25
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,48 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+ // Config file location: ~/.claude-mobile/config.json
5
+ const CONFIG_DIR = join(homedir(), '.spudmobile');
6
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
7
+ /**
8
+ * Load saved config from ~/.claude-mobile/config.json
9
+ */
10
+ function loadSavedConfig() {
11
+ try {
12
+ if (existsSync(CONFIG_FILE)) {
13
+ const data = readFileSync(CONFIG_FILE, 'utf-8');
14
+ return JSON.parse(data);
15
+ }
16
+ }
17
+ catch {
18
+ // ignore
19
+ }
20
+ return {};
21
+ }
22
+ /**
23
+ * Save config to ~/.claude-mobile/config.json
24
+ */
25
+ export function saveConfig(updates) {
26
+ const current = loadSavedConfig();
27
+ const merged = { ...current, ...updates };
28
+ if (!existsSync(CONFIG_DIR)) {
29
+ mkdirSync(CONFIG_DIR, { recursive: true });
30
+ }
31
+ writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), 'utf-8');
32
+ }
33
+ /**
34
+ * Build the full BridgeConfig from env + saved config + CLI args
35
+ */
36
+ export function getConfig(options) {
37
+ const saved = loadSavedConfig();
38
+ const supabaseUrl = process.env.SUPABASE_URL || 'https://dtaegtkfdwgdbyolcxht.supabase.co';
39
+ const supabaseAnonKey = process.env.SUPABASE_ANON_KEY || 'sb_publishable__giFEuM62kLTFecXfufGIw_a-Dv9y6O';
40
+ return {
41
+ supabaseUrl,
42
+ supabaseAnonKey,
43
+ pairId: saved.pairId || null,
44
+ pairCode: saved.pairCode || null,
45
+ projectPath: options.path || process.cwd(),
46
+ port: options.port || 38473,
47
+ };
48
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';