apptuner 0.1.1 → 0.1.3

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/dist/cli.js CHANGED
@@ -8,13 +8,24 @@ import { spawn } from "child_process";
8
8
  import path from "path";
9
9
  import { fileURLToPath } from "url";
10
10
  import fs from "fs/promises";
11
+ import net from "net";
11
12
  import chalk from "chalk";
12
13
  import ora from "ora";
13
14
  import { WebSocket } from "ws";
14
15
  import { exec } from "child_process";
15
16
  var __filename = fileURLToPath(import.meta.url);
16
17
  var __dirname = path.dirname(__filename);
17
- function generateSessionId() {
18
+ function findFreePort(start) {
19
+ return new Promise((resolve) => {
20
+ const server = net.createServer();
21
+ server.listen(start, "127.0.0.1", () => {
22
+ const port = server.address().port;
23
+ server.close(() => resolve(port));
24
+ });
25
+ server.on("error", () => resolve(findFreePort(start + 1)));
26
+ });
27
+ }
28
+ function generateId() {
18
29
  const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
19
30
  let result = "";
20
31
  for (let i = 0; i < 6; i++) {
@@ -22,6 +33,18 @@ function generateSessionId() {
22
33
  }
23
34
  return result;
24
35
  }
36
+ async function getOrCreateProjectId(projectPath) {
37
+ const configPath = path.join(projectPath, ".apptuner.json");
38
+ try {
39
+ const raw = await fs.readFile(configPath, "utf-8");
40
+ const config = JSON.parse(raw);
41
+ if (config.projectId) return config.projectId;
42
+ } catch {
43
+ }
44
+ const projectId = generateId();
45
+ await fs.writeFile(configPath, JSON.stringify({ projectId }, null, 2));
46
+ return projectId;
47
+ }
25
48
  var RelayAdapter = class {
26
49
  ws = null;
27
50
  updateWebSocket(ws) {
@@ -62,38 +85,69 @@ async function startCommand(options) {
62
85
  process.exit(1);
63
86
  }
64
87
  spinner.succeed(`Project validated: ${chalk.cyan(packageJson.name || "Unnamed Project")}`);
65
- const sessionId = generateSessionId();
66
- spinner.succeed(`Session ID: ${chalk.cyan(sessionId)}`);
88
+ const sessionId = await getOrCreateProjectId(projectPath);
89
+ spinner.succeed(`Project ID: ${chalk.cyan(sessionId)}`);
67
90
  console.log(chalk.white("\nStarting local services...\n"));
68
91
  const rootDir = path.join(__dirname, "..");
92
+ const metroPort = await findFreePort(3031);
93
+ const watcherPort = await findFreePort(3030);
69
94
  spinner.start("Starting Metro bundler...");
70
95
  const metroProcess = spawn("node", [path.join(rootDir, "metro-server.cjs")], {
71
96
  cwd: projectPath,
72
97
  stdio: "inherit",
73
- detached: false
98
+ detached: false,
99
+ env: { ...process.env, METRO_PORT: String(metroPort) }
74
100
  });
75
101
  await new Promise((resolve) => setTimeout(resolve, 2e3));
76
- spinner.succeed("Metro bundler started (port 3031)");
102
+ spinner.succeed(`Metro bundler started (port ${metroPort})`);
77
103
  spinner.start("Starting file watcher...");
78
104
  const watcherProcess = spawn("node", [path.join(rootDir, "watcher-server.cjs")], {
79
105
  cwd: projectPath,
80
106
  stdio: "inherit",
81
- detached: false
107
+ detached: false,
108
+ env: { ...process.env, WATCHER_PORT: String(watcherPort) }
82
109
  });
83
110
  await new Promise((resolve) => setTimeout(resolve, 1e3));
84
- spinner.succeed("File watcher started (port 3030)");
85
- const relayUrl = process.env.APPTUNER_RELAY_URL || "wss://apptuner-relay.falling-bird-3f63.workers.dev";
111
+ spinner.succeed(`File watcher started (port ${watcherPort})`);
112
+ const relayUrl = process.env.APPTUNER_RELAY_URL || "wss://relay.apptuner.io";
86
113
  const isLocalTesting = relayUrl.includes("localhost") || relayUrl.includes("127.0.0.1");
87
- const dashboardUrl = isLocalTesting ? `http://localhost:1420/?session=${sessionId}` : `https://apptuner-dashboard.pages.dev/?session=${sessionId}`;
114
+ const projectName = encodeURIComponent(packageJson.name || "Unnamed Project");
115
+ const baseDashboardUrl = process.env.APPTUNER_DASHBOARD_URL || (isLocalTesting ? "http://localhost:1420" : "https://apptuner.io");
116
+ const dashboardUrl = `${baseDashboardUrl}/?session=${sessionId}&name=${projectName}`;
88
117
  let isShuttingDown = false;
89
118
  let isFirstConnect = true;
90
119
  let reconnectTimeout = null;
91
120
  let currentWs = null;
92
121
  const relayAdapter = new RelayAdapter();
93
122
  let autoReloadStarted = false;
123
+ let bundleRequester = null;
94
124
  async function startAutoReload() {
95
125
  if (autoReloadStarted) return;
96
126
  autoReloadStarted = true;
127
+ console.log(chalk.gray("\u{1F5D1}\uFE0F Clearing Metro cache for fresh session..."));
128
+ try {
129
+ await new Promise((resolve, reject) => {
130
+ const clearWs = new WebSocket(`ws://localhost:${metroPort}`);
131
+ clearWs.on("open", () => {
132
+ clearWs.send(JSON.stringify({
133
+ type: "clear_cache",
134
+ projectPath
135
+ }));
136
+ });
137
+ clearWs.on("message", (data) => {
138
+ const msg = JSON.parse(data.toString());
139
+ if (msg.type === "cache_cleared") {
140
+ console.log(chalk.gray(`\u2713 Cleared ${msg.count} cached bundles`));
141
+ clearWs.close();
142
+ resolve();
143
+ }
144
+ });
145
+ clearWs.on("error", () => reject());
146
+ setTimeout(() => reject(new Error("Cache clear timeout")), 5e3);
147
+ });
148
+ } catch (error) {
149
+ console.log(chalk.yellow("\u26A0\uFE0F Could not clear Metro cache, continuing..."));
150
+ }
97
151
  let isBundling = false;
98
152
  let pendingBundle = false;
99
153
  async function requestBundle() {
@@ -107,7 +161,7 @@ async function startCommand(options) {
107
161
  console.log(chalk.gray("\u{1F4E6} Bundling..."));
108
162
  try {
109
163
  const code = await new Promise((resolve, reject) => {
110
- const metroWs = new WebSocket("ws://localhost:3031");
164
+ const metroWs = new WebSocket(`ws://localhost:${metroPort}`);
111
165
  metroWs.on("open", () => {
112
166
  metroWs.send(JSON.stringify({
113
167
  type: "bundle",
@@ -145,13 +199,20 @@ async function startCommand(options) {
145
199
  requestBundle();
146
200
  }
147
201
  }
148
- const watcherWs = new WebSocket("ws://localhost:3030");
202
+ bundleRequester = requestBundle;
203
+ const watcherWs = new WebSocket(`ws://localhost:${watcherPort}`);
204
+ let watcherPingInterval = null;
149
205
  watcherWs.on("open", () => {
150
206
  watcherWs.send(JSON.stringify({
151
207
  type: "watch",
152
208
  path: projectPath,
153
209
  extensions: [".js", ".jsx", ".ts", ".tsx", ".json"]
154
210
  }));
211
+ watcherPingInterval = setInterval(() => {
212
+ if (watcherWs.readyState === watcherWs.OPEN) {
213
+ watcherWs.send(JSON.stringify({ type: "ping", timestamp: Date.now() }));
214
+ }
215
+ }, 2e4);
155
216
  });
156
217
  watcherWs.on("message", async (data) => {
157
218
  const msg = JSON.parse(data.toString());
@@ -167,6 +228,10 @@ async function startCommand(options) {
167
228
  console.error(chalk.red("Watcher error:"), err.message);
168
229
  });
169
230
  watcherWs.on("close", () => {
231
+ if (watcherPingInterval) {
232
+ clearInterval(watcherPingInterval);
233
+ watcherPingInterval = null;
234
+ }
170
235
  if (!isShuttingDown) {
171
236
  console.log(chalk.yellow("Watcher disconnected"));
172
237
  }
@@ -205,6 +270,10 @@ async function startCommand(options) {
205
270
  if (message.type === "mobile_connected") {
206
271
  console.log(chalk.green(`
207
272
  \u{1F4F1} Mobile device connected: ${message.deviceName || "Unknown"}`));
273
+ if (bundleRequester) {
274
+ console.log(chalk.gray("\u{1F4E6} Sending bundle to new device..."));
275
+ bundleRequester();
276
+ }
208
277
  }
209
278
  if (message.type === "mobile_disconnected") {
210
279
  console.log(chalk.yellow(`
@@ -255,11 +324,18 @@ async function startCommand(options) {
255
324
  currentWs.close();
256
325
  }
257
326
  if (metroProcess.pid) {
258
- process.kill(metroProcess.pid, "SIGTERM");
327
+ try {
328
+ process.kill(metroProcess.pid, "SIGKILL");
329
+ } catch (e) {
330
+ }
259
331
  }
260
332
  if (watcherProcess.pid) {
261
- process.kill(watcherProcess.pid, "SIGTERM");
333
+ try {
334
+ process.kill(watcherProcess.pid, "SIGKILL");
335
+ } catch (e) {
336
+ }
262
337
  }
338
+ await new Promise((resolve) => setTimeout(resolve, 500));
263
339
  await fs.unlink(pidFile).catch(() => {
264
340
  });
265
341
  console.log(chalk.gray("All services stopped.\n"));
@@ -353,7 +429,10 @@ Session: ${chalk3.cyan(pids.sessionId)}`));
353
429
  // src-cli/cli.ts
354
430
  var program = new Command();
355
431
  program.name("apptuner").description("Hot reload React Native apps instantly").version("0.1.0");
356
- program.command("start").description("Start AppTuner development server").option("-p, --project <path>", "Path to your React Native project", process.cwd()).option("--no-qr", "Disable QR code display").action(startCommand);
432
+ program.command("start").description("Start AppTuner development server").argument("[path]", "Path to your React Native project (or use -p)").option("-p, --project <path>", "Path to your React Native project", process.cwd()).option("--no-qr", "Disable QR code display").action((argPath, options) => {
433
+ if (argPath) options.project = argPath;
434
+ startCommand(options);
435
+ });
357
436
  program.command("stop").description("Stop all AppTuner services").action(stopCommand);
358
437
  program.command("status").description("Show AppTuner connection status").action(statusCommand);
359
438
  program.parse();
package/dist/cli.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src-cli/cli.ts", "../src-cli/commands/start.ts", "../src-cli/commands/stop.ts", "../src-cli/commands/status.ts"],
4
- "sourcesContent": ["#!/usr/bin/env node\n\nimport { Command } from 'commander';\nimport chalk from 'chalk';\nimport { startCommand } from './commands/start.js';\nimport { stopCommand } from './commands/stop.js';\nimport { statusCommand } from './commands/status.js';\n\nconst program = new Command();\n\nprogram\n .name('apptuner')\n .description('Hot reload React Native apps instantly')\n .version('0.1.0');\n\nprogram\n .command('start')\n .description('Start AppTuner development server')\n .option('-p, --project <path>', 'Path to your React Native project', process.cwd())\n .option('--no-qr', 'Disable QR code display')\n .action(startCommand);\n\nprogram\n .command('stop')\n .description('Stop all AppTuner services')\n .action(stopCommand);\n\nprogram\n .command('status')\n .description('Show AppTuner connection status')\n .action(statusCommand);\n\nprogram.parse();\n", "import { spawn } from 'child_process';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport fs from 'fs/promises';\nimport chalk from 'chalk';\nimport ora from 'ora';\nimport { WebSocket } from 'ws';\nimport { exec } from 'child_process';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\ninterface StartOptions {\n project: string;\n qr: boolean;\n}\n\n// Generate random 6-character session ID\nfunction generateSessionId(): string {\n const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';\n let result = '';\n for (let i = 0; i < 6; i++) {\n result += chars.charAt(Math.floor(Math.random() * chars.length));\n }\n return result;\n}\n\n// Adapter that wraps the relay WebSocket for sending bundles - supports hot-swapping on reconnect\nclass RelayAdapter {\n private ws: WebSocket | null = null;\n\n updateWebSocket(ws: WebSocket): void {\n this.ws = ws;\n }\n\n sendBundleUpdate(bundleCode: string): void {\n if (!this.ws || this.ws.readyState !== this.ws.OPEN) {\n console.warn(chalk.yellow('\u26A0\uFE0F Cannot send bundle - relay not connected'));\n return;\n }\n this.ws.send(JSON.stringify({\n type: 'bundle_update',\n payload: { code: bundleCode },\n timestamp: Date.now(),\n }));\n }\n}\n\nexport async function startCommand(options: StartOptions) {\n const spinner = ora();\n\n console.log(chalk.blue.bold('\\n\uD83D\uDE80 AppTuner\\n'));\n\n // Step 1: Validate project\n spinner.start('Validating React Native project...');\n const projectPath = path.resolve(options.project);\n const packageJsonPath = path.join(projectPath, 'package.json');\n\n try {\n await fs.access(packageJsonPath);\n } catch {\n spinner.fail('No package.json found');\n console.error(chalk.red(`\\n\u274C Could not find package.json at ${projectPath}`));\n console.log(chalk.gray('\\nMake sure you\\'re in a React Native project directory.\\n'));\n process.exit(1);\n }\n\n const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));\n\n if (!packageJson.dependencies?.['react-native']) {\n spinner.fail('Not a React Native project');\n console.error(chalk.red('\\n\u274C This is not a React Native project'));\n console.log(chalk.gray('No react-native dependency found in package.json\\n'));\n process.exit(1);\n }\n\n spinner.succeed(`Project validated: ${chalk.cyan(packageJson.name || 'Unnamed Project')}`);\n\n // Step 2: Generate session ID (stays the same across reconnects)\n const sessionId = generateSessionId();\n spinner.succeed(`Session ID: ${chalk.cyan(sessionId)}`);\n\n // Step 3: Start local services\n console.log(chalk.white('\\nStarting local services...\\n'));\n\n // __dirname in compiled dist/cli.js is the dist/ folder, so go up one level to project root\n const rootDir = path.join(__dirname, '..');\n\n // Start Metro bundler\n spinner.start('Starting Metro bundler...');\n const metroProcess = spawn('node', [path.join(rootDir, 'metro-server.cjs')], {\n cwd: projectPath,\n stdio: 'inherit',\n detached: false,\n });\n await new Promise(resolve => setTimeout(resolve, 2000));\n spinner.succeed('Metro bundler started (port 3031)');\n\n // Start file watcher\n spinner.start('Starting file watcher...');\n const watcherProcess = spawn('node', [path.join(rootDir, 'watcher-server.cjs')], {\n cwd: projectPath,\n stdio: 'inherit',\n detached: false,\n });\n await new Promise(resolve => setTimeout(resolve, 1000));\n spinner.succeed('File watcher started (port 3030)');\n\n // Step 4: Connect to relay with auto-reconnect\n const relayUrl = process.env.APPTUNER_RELAY_URL || 'wss://apptuner-relay.falling-bird-3f63.workers.dev';\n const isLocalTesting = relayUrl.includes('localhost') || relayUrl.includes('127.0.0.1');\n const dashboardUrl = isLocalTesting\n ? `http://localhost:1420/?session=${sessionId}`\n : `https://apptuner-dashboard.pages.dev/?session=${sessionId}`;\n\n let isShuttingDown = false;\n let isFirstConnect = true;\n let reconnectTimeout: ReturnType<typeof setTimeout> | null = null;\n let currentWs: WebSocket | null = null;\n const relayAdapter = new RelayAdapter();\n\n // Step 5: Setup direct connections to local watcher + metro servers\n let autoReloadStarted = false;\n\n async function startAutoReload() {\n if (autoReloadStarted) return;\n autoReloadStarted = true;\n\n let isBundling = false;\n let pendingBundle = false;\n\n async function requestBundle(): Promise<void> {\n if (isBundling) {\n pendingBundle = true;\n return;\n }\n isBundling = true;\n pendingBundle = false;\n\n const startTime = Date.now();\n console.log(chalk.gray('\uD83D\uDCE6 Bundling...'));\n\n try {\n const code = await new Promise<string>((resolve, reject) => {\n const metroWs = new WebSocket('ws://localhost:3031');\n\n metroWs.on('open', () => {\n metroWs.send(JSON.stringify({\n type: 'bundle',\n projectPath,\n entryPoint: 'App.tsx',\n }));\n });\n\n metroWs.on('message', (data) => {\n const msg = JSON.parse(data.toString());\n if (msg.type === 'bundle_ready') {\n metroWs.close();\n if (msg.fromCache) {\n console.log(chalk.gray('\u26A1 No changes \u2014 using cached bundle'));\n }\n resolve(msg.code);\n } else if (msg.type === 'bundle_error') {\n metroWs.close();\n reject(new Error(\n typeof msg.error === 'object' ? msg.error.message : (msg.error || 'Bundle failed')\n ));\n }\n });\n\n metroWs.on('error', (err) => reject(err));\n\n // 60s timeout for large projects\n setTimeout(() => reject(new Error('Bundle timeout (60s)')), 60000);\n });\n\n const sizeKB = Math.round(code.length / 1024);\n const timeMs = Date.now() - startTime;\n relayAdapter.sendBundleUpdate(code);\n console.log(chalk.cyan(`\uD83D\uDCE6 Bundle sent: ${chalk.white(sizeKB + ' KB')} in ${chalk.white(timeMs + 'ms')}`));\n } catch (error: any) {\n console.error(chalk.red('\u274C Bundle error:'), error.message);\n }\n\n isBundling = false;\n\n // If another change came in while bundling, run again\n if (pendingBundle) {\n requestBundle();\n }\n }\n\n // Connect to watcher server\n const watcherWs = new WebSocket('ws://localhost:3030');\n\n watcherWs.on('open', () => {\n // Tell watcher to watch the project directory\n watcherWs.send(JSON.stringify({\n type: 'watch',\n path: projectPath,\n extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],\n }));\n });\n\n watcherWs.on('message', async (data) => {\n const msg = JSON.parse(data.toString());\n\n if (msg.type === 'watcher_ready') {\n console.log(chalk.green('\u2705 Auto-reload active - edit your files to see changes!\\n'));\n // Send initial bundle\n requestBundle();\n } else if (msg.type === 'file_changed') {\n console.log(chalk.yellow(`\uD83D\uDCDD ${msg.relativePath} changed`));\n requestBundle();\n }\n });\n\n watcherWs.on('error', (err) => {\n console.error(chalk.red('Watcher error:'), err.message);\n });\n\n watcherWs.on('close', () => {\n if (!isShuttingDown) {\n console.log(chalk.yellow('Watcher disconnected'));\n }\n });\n }\n\n function connectToRelay() {\n if (isShuttingDown) return;\n\n spinner.start('Connecting to relay server...');\n const ws = new WebSocket(`${relayUrl}/cli/${sessionId}`);\n currentWs = ws;\n\n let pingInterval: ReturnType<typeof setInterval> | null = null;\n\n ws.on('open', async () => {\n relayAdapter.updateWebSocket(ws);\n\n if (isFirstConnect) {\n spinner.succeed('Connected to relay');\n isFirstConnect = false;\n\n console.log(chalk.green.bold('\\n\u2705 AppTuner is running!\\n'));\n console.log(chalk.white(`Dashboard: ${chalk.cyan(dashboardUrl)}`));\n console.log(chalk.gray('\\nOpening browser...\\n'));\n\n const openCommand = process.platform === 'darwin' ? 'open' :\n process.platform === 'win32' ? 'start' : 'xdg-open';\n exec(`${openCommand} \"${dashboardUrl}\"`);\n\n await startAutoReload();\n } else {\n spinner.succeed('Reconnected to relay');\n console.log(chalk.green('\u2705 Relay reconnected\\n'));\n }\n\n // Keepalive pings every 20s (Cloudflare drops idle WS after ~30s)\n pingInterval = setInterval(() => {\n if (ws.readyState === ws.OPEN) {\n ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }));\n }\n }, 20000);\n });\n\n ws.on('message', (data) => {\n try {\n const message = JSON.parse(data.toString());\n if (message.type === 'mobile_connected') {\n console.log(chalk.green(`\\n\uD83D\uDCF1 Mobile device connected: ${message.deviceName || 'Unknown'}`));\n }\n if (message.type === 'mobile_disconnected') {\n console.log(chalk.yellow(`\\n\uD83D\uDCF1 Mobile device disconnected`));\n }\n } catch (error) {\n console.error('Error parsing relay message:', error);\n }\n });\n\n ws.on('error', (error) => {\n if (isFirstConnect) {\n spinner.fail('Relay connection failed');\n console.error(chalk.red('\\n\u274C Could not connect to relay server'));\n console.error(chalk.gray(error.message));\n }\n // close event fires after error, triggering reconnect\n });\n\n ws.on('close', (code) => {\n if (pingInterval) {\n clearInterval(pingInterval);\n pingInterval = null;\n }\n\n if (isShuttingDown) return;\n\n if (isFirstConnect) {\n console.error(chalk.red(`\\n\u274C Failed to connect to relay (code ${code})`));\n process.exit(1);\n }\n\n console.log(chalk.yellow(`\\n\u26A0\uFE0F Relay disconnected (code ${code}), reconnecting in 5s...`));\n reconnectTimeout = setTimeout(connectToRelay, 5000);\n });\n }\n\n // Start relay connection\n connectToRelay();\n\n // Save PIDs for cleanup\n const pidFile = path.join(process.cwd(), '.apptuner-pids.json');\n await fs.writeFile(pidFile, JSON.stringify({\n metro: metroProcess.pid,\n watcher: watcherProcess.pid,\n sessionId,\n }, null, 2));\n\n // Graceful shutdown\n process.on('SIGINT', async () => {\n console.log(chalk.yellow('\\n\\n\u23F9\uFE0F Stopping AppTuner...'));\n\n isShuttingDown = true;\n\n if (reconnectTimeout) {\n clearTimeout(reconnectTimeout);\n reconnectTimeout = null;\n }\n\n if (currentWs) {\n currentWs.close();\n }\n\n if (metroProcess.pid) {\n process.kill(metroProcess.pid, 'SIGTERM');\n }\n\n if (watcherProcess.pid) {\n process.kill(watcherProcess.pid, 'SIGTERM');\n }\n\n await fs.unlink(pidFile).catch(() => {});\n\n console.log(chalk.gray('All services stopped.\\n'));\n process.exit(0);\n });\n}\n", "import fs from 'fs/promises';\nimport path from 'path';\nimport chalk from 'chalk';\n\nexport async function stopCommand() {\n console.log(chalk.yellow('\\n\u23F9\uFE0F Stopping AppTuner...\\n'));\n\n const pidFile = path.join(process.cwd(), '.apptuner-pids.json');\n\n try {\n const pidsData = await fs.readFile(pidFile, 'utf-8');\n const pids = JSON.parse(pidsData);\n\n if (pids.metro) {\n try {\n process.kill(-pids.metro, 'SIGTERM');\n console.log(chalk.gray('\u2713 Metro server stopped'));\n } catch (error) {\n console.log(chalk.gray('\u2713 Metro server already stopped'));\n }\n }\n\n if (pids.watcher) {\n try {\n process.kill(-pids.watcher, 'SIGTERM');\n console.log(chalk.gray('\u2713 File watcher stopped'));\n } catch (error) {\n console.log(chalk.gray('\u2713 File watcher already stopped'));\n }\n }\n\n await fs.unlink(pidFile);\n\n console.log(chalk.green('\\n\u2705 All services stopped\\n'));\n } catch (error) {\n console.log(chalk.gray('No running AppTuner services found.\\n'));\n }\n}\n", "import fs from 'fs/promises';\nimport path from 'path';\nimport chalk from 'chalk';\n\nexport async function statusCommand() {\n console.log(chalk.blue.bold('\\n\uD83D\uDCCA AppTuner Status\\n'));\n\n const pidFile = path.join(process.cwd(), '.apptuner-pids.json');\n\n try {\n const pidsData = await fs.readFile(pidFile, 'utf-8');\n const pids = JSON.parse(pidsData);\n\n console.log(chalk.white('Services:'));\n\n // Check if processes are actually running\n let metroRunning = false;\n let watcherRunning = false;\n\n if (pids.metro) {\n try {\n process.kill(pids.metro, 0); // Check if process exists\n metroRunning = true;\n } catch {\n metroRunning = false;\n }\n }\n\n if (pids.watcher) {\n try {\n process.kill(pids.watcher, 0);\n watcherRunning = true;\n } catch {\n watcherRunning = false;\n }\n }\n\n console.log(\n ` ${metroRunning ? chalk.green('\u25CF') : chalk.red('\u25CF')} Metro bundler ${\n metroRunning ? chalk.gray('(port 3031)') : chalk.gray('(stopped)')\n }`\n );\n\n console.log(\n ` ${watcherRunning ? chalk.green('\u25CF') : chalk.red('\u25CF')} File watcher ${\n watcherRunning ? chalk.gray('(port 3030)') : chalk.gray('(stopped)')\n }`\n );\n\n if (pids.sessionId) {\n console.log(chalk.white(`\\nSession: ${chalk.cyan(pids.sessionId)}`));\n }\n\n if (metroRunning && watcherRunning) {\n console.log(chalk.green('\\n\u2705 AppTuner is running\\n'));\n } else {\n console.log(chalk.yellow('\\n\u26A0\uFE0F Some services are not running\\n'));\n }\n } catch (error) {\n console.log(chalk.gray(' No running services\\n'));\n console.log(chalk.gray('Run `apptuner start` to begin.\\n'));\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["#!/usr/bin/env node\n\nimport { Command } from 'commander';\nimport chalk from 'chalk';\nimport { startCommand } from './commands/start.js';\nimport { stopCommand } from './commands/stop.js';\nimport { statusCommand } from './commands/status.js';\n\nconst program = new Command();\n\nprogram\n .name('apptuner')\n .description('Hot reload React Native apps instantly')\n .version('0.1.0');\n\nprogram\n .command('start')\n .description('Start AppTuner development server')\n .argument('[path]', 'Path to your React Native project (or use -p)')\n .option('-p, --project <path>', 'Path to your React Native project', process.cwd())\n .option('--no-qr', 'Disable QR code display')\n .action((argPath, options) => {\n // Positional arg takes priority over -p flag\n if (argPath) options.project = argPath;\n startCommand(options);\n });\n\nprogram\n .command('stop')\n .description('Stop all AppTuner services')\n .action(stopCommand);\n\nprogram\n .command('status')\n .description('Show AppTuner connection status')\n .action(statusCommand);\n\nprogram.parse();\n", "import { spawn } from 'child_process';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport fs from 'fs/promises';\nimport net from 'net';\nimport chalk from 'chalk';\nimport ora from 'ora';\nimport { WebSocket } from 'ws';\nimport { exec } from 'child_process';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\ninterface StartOptions {\n project: string;\n qr: boolean;\n}\n\n// Find a free port starting from the given port number\nfunction findFreePort(start: number): Promise<number> {\n return new Promise((resolve) => {\n const server = net.createServer();\n server.listen(start, '127.0.0.1', () => {\n const port = (server.address() as net.AddressInfo).port;\n server.close(() => resolve(port));\n });\n server.on('error', () => resolve(findFreePort(start + 1)));\n });\n}\n\n// Generate random 6-character ID\nfunction generateId(): string {\n const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';\n let result = '';\n for (let i = 0; i < 6; i++) {\n result += chars.charAt(Math.floor(Math.random() * chars.length));\n }\n return result;\n}\n\n// Get or create a stable project ID stored in .apptuner.json\nasync function getOrCreateProjectId(projectPath: string): Promise<string> {\n const configPath = path.join(projectPath, '.apptuner.json');\n try {\n const raw = await fs.readFile(configPath, 'utf-8');\n const config = JSON.parse(raw);\n if (config.projectId) return config.projectId;\n } catch {\n // File doesn't exist or is invalid \u2014 create it\n }\n const projectId = generateId();\n await fs.writeFile(configPath, JSON.stringify({ projectId }, null, 2));\n return projectId;\n}\n\n// Adapter that wraps the relay WebSocket for sending bundles - supports hot-swapping on reconnect\nclass RelayAdapter {\n private ws: WebSocket | null = null;\n\n updateWebSocket(ws: WebSocket): void {\n this.ws = ws;\n }\n\n sendBundleUpdate(bundleCode: string): void {\n if (!this.ws || this.ws.readyState !== this.ws.OPEN) {\n console.warn(chalk.yellow('\u26A0\uFE0F Cannot send bundle - relay not connected'));\n return;\n }\n this.ws.send(JSON.stringify({\n type: 'bundle_update',\n payload: { code: bundleCode },\n timestamp: Date.now(),\n }));\n }\n}\n\nexport async function startCommand(options: StartOptions) {\n const spinner = ora();\n\n console.log(chalk.blue.bold('\\n\uD83D\uDE80 AppTuner\\n'));\n\n // Step 1: Validate project\n spinner.start('Validating React Native project...');\n const projectPath = path.resolve(options.project);\n const packageJsonPath = path.join(projectPath, 'package.json');\n\n try {\n await fs.access(packageJsonPath);\n } catch {\n spinner.fail('No package.json found');\n console.error(chalk.red(`\\n\u274C Could not find package.json at ${projectPath}`));\n console.log(chalk.gray('\\nMake sure you\\'re in a React Native project directory.\\n'));\n process.exit(1);\n }\n\n const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));\n\n if (!packageJson.dependencies?.['react-native']) {\n spinner.fail('Not a React Native project');\n console.error(chalk.red('\\n\u274C This is not a React Native project'));\n console.log(chalk.gray('No react-native dependency found in package.json\\n'));\n process.exit(1);\n }\n\n spinner.succeed(`Project validated: ${chalk.cyan(packageJson.name || 'Unnamed Project')}`);\n\n // Step 2: Get or create stable project ID (persisted in .apptuner.json)\n const sessionId = await getOrCreateProjectId(projectPath);\n spinner.succeed(`Project ID: ${chalk.cyan(sessionId)}`);\n\n // Step 3: Start local services\n console.log(chalk.white('\\nStarting local services...\\n'));\n\n // __dirname in compiled dist/cli.js is the dist/ folder, so go up one level to project root\n const rootDir = path.join(__dirname, '..');\n\n // Find free ports for Metro and Watcher\n const metroPort = await findFreePort(3031);\n const watcherPort = await findFreePort(3030);\n\n // Start Metro bundler\n spinner.start('Starting Metro bundler...');\n const metroProcess = spawn('node', [path.join(rootDir, 'metro-server.cjs')], {\n cwd: projectPath,\n stdio: 'inherit',\n detached: false,\n env: { ...process.env, METRO_PORT: String(metroPort) },\n });\n await new Promise(resolve => setTimeout(resolve, 2000));\n spinner.succeed(`Metro bundler started (port ${metroPort})`);\n\n // Start file watcher\n spinner.start('Starting file watcher...');\n const watcherProcess = spawn('node', [path.join(rootDir, 'watcher-server.cjs')], {\n cwd: projectPath,\n stdio: 'inherit',\n detached: false,\n env: { ...process.env, WATCHER_PORT: String(watcherPort) },\n });\n await new Promise(resolve => setTimeout(resolve, 1000));\n spinner.succeed(`File watcher started (port ${watcherPort})`);\n\n // Step 4: Connect to relay with auto-reconnect\n const relayUrl = process.env.APPTUNER_RELAY_URL || 'wss://relay.apptuner.io';\n const isLocalTesting = relayUrl.includes('localhost') || relayUrl.includes('127.0.0.1');\n const projectName = encodeURIComponent(packageJson.name || 'Unnamed Project');\n // APPTUNER_DASHBOARD_URL lets you point at a local build (e.g. http://localhost:4173)\n // so fixes to BrowserApp take effect before the next apptuner.io deployment.\n const baseDashboardUrl = process.env.APPTUNER_DASHBOARD_URL ||\n (isLocalTesting ? 'http://localhost:1420' : 'https://apptuner.io');\n const dashboardUrl = `${baseDashboardUrl}/?session=${sessionId}&name=${projectName}`;\n\n let isShuttingDown = false;\n let isFirstConnect = true;\n let reconnectTimeout: ReturnType<typeof setTimeout> | null = null;\n let currentWs: WebSocket | null = null;\n const relayAdapter = new RelayAdapter();\n\n // Step 5: Setup direct connections to local watcher + metro servers\n let autoReloadStarted = false;\n let bundleRequester: (() => void) | null = null; // Set by startAutoReload, used by relay handler\n\n async function startAutoReload() {\n if (autoReloadStarted) return;\n autoReloadStarted = true;\n\n // Clear Metro's cache for this project when starting a new session\n console.log(chalk.gray('\uD83D\uDDD1\uFE0F Clearing Metro cache for fresh session...'));\n try {\n await new Promise<void>((resolve, reject) => {\n const clearWs = new WebSocket(`ws://localhost:${metroPort}`);\n clearWs.on('open', () => {\n clearWs.send(JSON.stringify({\n type: 'clear_cache',\n projectPath,\n }));\n });\n clearWs.on('message', (data) => {\n const msg = JSON.parse(data.toString());\n if (msg.type === 'cache_cleared') {\n console.log(chalk.gray(`\u2713 Cleared ${msg.count} cached bundles`));\n clearWs.close();\n resolve();\n }\n });\n clearWs.on('error', () => reject());\n setTimeout(() => reject(new Error('Cache clear timeout')), 5000);\n });\n } catch (error) {\n console.log(chalk.yellow('\u26A0\uFE0F Could not clear Metro cache, continuing...'));\n }\n\n let isBundling = false;\n let pendingBundle = false;\n\n async function requestBundle(): Promise<void> {\n // also exposed via bundleRequester for external callers (e.g. mobile_connected)\n if (isBundling) {\n pendingBundle = true;\n return;\n }\n isBundling = true;\n pendingBundle = false;\n\n const startTime = Date.now();\n console.log(chalk.gray('\uD83D\uDCE6 Bundling...'));\n\n try {\n const code = await new Promise<string>((resolve, reject) => {\n const metroWs = new WebSocket(`ws://localhost:${metroPort}`);\n\n metroWs.on('open', () => {\n metroWs.send(JSON.stringify({\n type: 'bundle',\n projectPath,\n entryPoint: 'App.tsx',\n }));\n });\n\n metroWs.on('message', (data) => {\n const msg = JSON.parse(data.toString());\n if (msg.type === 'bundle_ready') {\n metroWs.close();\n if (msg.fromCache) {\n console.log(chalk.gray('\u26A1 No changes \u2014 using cached bundle'));\n }\n resolve(msg.code);\n } else if (msg.type === 'bundle_error') {\n metroWs.close();\n reject(new Error(\n typeof msg.error === 'object' ? msg.error.message : (msg.error || 'Bundle failed')\n ));\n }\n });\n\n metroWs.on('error', (err) => reject(err));\n\n // 60s timeout for large projects\n setTimeout(() => reject(new Error('Bundle timeout (60s)')), 60000);\n });\n\n const sizeKB = Math.round(code.length / 1024);\n const timeMs = Date.now() - startTime;\n relayAdapter.sendBundleUpdate(code);\n console.log(chalk.cyan(`\uD83D\uDCE6 Bundle sent: ${chalk.white(sizeKB + ' KB')} in ${chalk.white(timeMs + 'ms')}`));\n } catch (error: any) {\n console.error(chalk.red('\u274C Bundle error:'), error.message);\n }\n\n isBundling = false;\n\n // If another change came in while bundling, run again\n if (pendingBundle) {\n requestBundle();\n }\n }\n\n // Expose requestBundle to relay handler so mobile_connected can trigger a fresh bundle\n bundleRequester = requestBundle;\n\n // Connect to watcher server\n const watcherWs = new WebSocket(`ws://localhost:${watcherPort}`);\n let watcherPingInterval: ReturnType<typeof setInterval> | null = null;\n\n watcherWs.on('open', () => {\n // Tell watcher to watch the project directory\n watcherWs.send(JSON.stringify({\n type: 'watch',\n path: projectPath,\n extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],\n }));\n\n // Keepalive pings every 20s to prevent idle timeout\n watcherPingInterval = setInterval(() => {\n if (watcherWs.readyState === watcherWs.OPEN) {\n watcherWs.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }));\n }\n }, 20000);\n });\n\n watcherWs.on('message', async (data) => {\n const msg = JSON.parse(data.toString());\n\n if (msg.type === 'watcher_ready') {\n console.log(chalk.green('\u2705 Auto-reload active - edit your files to see changes!\\n'));\n // Send initial bundle\n requestBundle();\n } else if (msg.type === 'file_changed') {\n console.log(chalk.yellow(`\uD83D\uDCDD ${msg.relativePath} changed`));\n requestBundle();\n }\n });\n\n watcherWs.on('error', (err) => {\n console.error(chalk.red('Watcher error:'), err.message);\n });\n\n watcherWs.on('close', () => {\n if (watcherPingInterval) {\n clearInterval(watcherPingInterval);\n watcherPingInterval = null;\n }\n if (!isShuttingDown) {\n console.log(chalk.yellow('Watcher disconnected'));\n }\n });\n }\n\n function connectToRelay() {\n if (isShuttingDown) return;\n\n spinner.start('Connecting to relay server...');\n const ws = new WebSocket(`${relayUrl}/cli/${sessionId}`);\n currentWs = ws;\n\n let pingInterval: ReturnType<typeof setInterval> | null = null;\n\n ws.on('open', async () => {\n relayAdapter.updateWebSocket(ws);\n\n if (isFirstConnect) {\n spinner.succeed('Connected to relay');\n isFirstConnect = false;\n\n console.log(chalk.green.bold('\\n\u2705 AppTuner is running!\\n'));\n console.log(chalk.white(`Dashboard: ${chalk.cyan(dashboardUrl)}`));\n console.log(chalk.gray('\\nOpening browser...\\n'));\n\n const openCommand = process.platform === 'darwin' ? 'open' :\n process.platform === 'win32' ? 'start' : 'xdg-open';\n exec(`${openCommand} \"${dashboardUrl}\"`);\n\n await startAutoReload();\n } else {\n spinner.succeed('Reconnected to relay');\n console.log(chalk.green('\u2705 Relay reconnected\\n'));\n }\n\n // Keepalive pings every 20s to maintain idle WebSocket connection\n pingInterval = setInterval(() => {\n if (ws.readyState === ws.OPEN) {\n ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }));\n }\n }, 20000);\n });\n\n ws.on('message', (data) => {\n try {\n const message = JSON.parse(data.toString());\n if (message.type === 'mobile_connected') {\n console.log(chalk.green(`\\n\uD83D\uDCF1 Mobile device connected: ${message.deviceName || 'Unknown'}`));\n // Send bundle to the newly connected device\n if (bundleRequester) {\n console.log(chalk.gray('\uD83D\uDCE6 Sending bundle to new device...'));\n bundleRequester();\n }\n }\n if (message.type === 'mobile_disconnected') {\n console.log(chalk.yellow(`\\n\uD83D\uDCF1 Mobile device disconnected`));\n }\n } catch (error) {\n console.error('Error parsing relay message:', error);\n }\n });\n\n ws.on('error', (error) => {\n if (isFirstConnect) {\n spinner.fail('Relay connection failed');\n console.error(chalk.red('\\n\u274C Could not connect to relay server'));\n console.error(chalk.gray(error.message));\n }\n // close event fires after error, triggering reconnect\n });\n\n ws.on('close', (code) => {\n if (pingInterval) {\n clearInterval(pingInterval);\n pingInterval = null;\n }\n\n if (isShuttingDown) return;\n\n if (isFirstConnect) {\n console.error(chalk.red(`\\n\u274C Failed to connect to relay (code ${code})`));\n process.exit(1);\n }\n\n console.log(chalk.yellow(`\\n\u26A0\uFE0F Relay disconnected (code ${code}), reconnecting in 5s...`));\n reconnectTimeout = setTimeout(connectToRelay, 5000);\n });\n }\n\n // Start relay connection\n connectToRelay();\n\n // Save PIDs for cleanup\n const pidFile = path.join(process.cwd(), '.apptuner-pids.json');\n await fs.writeFile(pidFile, JSON.stringify({\n metro: metroProcess.pid,\n watcher: watcherProcess.pid,\n sessionId,\n }, null, 2));\n\n // Graceful shutdown\n process.on('SIGINT', async () => {\n console.log(chalk.yellow('\\n\\n\u23F9\uFE0F Stopping AppTuner...'));\n\n isShuttingDown = true;\n\n if (reconnectTimeout) {\n clearTimeout(reconnectTimeout);\n reconnectTimeout = null;\n }\n\n if (currentWs) {\n currentWs.close();\n }\n\n // Kill Metro and Watcher processes forcefully\n if (metroProcess.pid) {\n try {\n process.kill(metroProcess.pid, 'SIGKILL');\n } catch (e) {\n // Process might already be dead\n }\n }\n\n if (watcherProcess.pid) {\n try {\n process.kill(watcherProcess.pid, 'SIGKILL');\n } catch (e) {\n // Process might already be dead\n }\n }\n\n // Give processes time to die before exiting\n await new Promise(resolve => setTimeout(resolve, 500));\n\n await fs.unlink(pidFile).catch(() => {});\n\n console.log(chalk.gray('All services stopped.\\n'));\n process.exit(0);\n });\n}\n", "import fs from 'fs/promises';\nimport path from 'path';\nimport chalk from 'chalk';\n\nexport async function stopCommand() {\n console.log(chalk.yellow('\\n\u23F9\uFE0F Stopping AppTuner...\\n'));\n\n const pidFile = path.join(process.cwd(), '.apptuner-pids.json');\n\n try {\n const pidsData = await fs.readFile(pidFile, 'utf-8');\n const pids = JSON.parse(pidsData);\n\n if (pids.metro) {\n try {\n process.kill(-pids.metro, 'SIGTERM');\n console.log(chalk.gray('\u2713 Metro server stopped'));\n } catch (error) {\n console.log(chalk.gray('\u2713 Metro server already stopped'));\n }\n }\n\n if (pids.watcher) {\n try {\n process.kill(-pids.watcher, 'SIGTERM');\n console.log(chalk.gray('\u2713 File watcher stopped'));\n } catch (error) {\n console.log(chalk.gray('\u2713 File watcher already stopped'));\n }\n }\n\n await fs.unlink(pidFile);\n\n console.log(chalk.green('\\n\u2705 All services stopped\\n'));\n } catch (error) {\n console.log(chalk.gray('No running AppTuner services found.\\n'));\n }\n}\n", "import fs from 'fs/promises';\nimport path from 'path';\nimport chalk from 'chalk';\n\nexport async function statusCommand() {\n console.log(chalk.blue.bold('\\n\uD83D\uDCCA AppTuner Status\\n'));\n\n const pidFile = path.join(process.cwd(), '.apptuner-pids.json');\n\n try {\n const pidsData = await fs.readFile(pidFile, 'utf-8');\n const pids = JSON.parse(pidsData);\n\n console.log(chalk.white('Services:'));\n\n // Check if processes are actually running\n let metroRunning = false;\n let watcherRunning = false;\n\n if (pids.metro) {\n try {\n process.kill(pids.metro, 0); // Check if process exists\n metroRunning = true;\n } catch {\n metroRunning = false;\n }\n }\n\n if (pids.watcher) {\n try {\n process.kill(pids.watcher, 0);\n watcherRunning = true;\n } catch {\n watcherRunning = false;\n }\n }\n\n console.log(\n ` ${metroRunning ? chalk.green('\u25CF') : chalk.red('\u25CF')} Metro bundler ${\n metroRunning ? chalk.gray('(port 3031)') : chalk.gray('(stopped)')\n }`\n );\n\n console.log(\n ` ${watcherRunning ? chalk.green('\u25CF') : chalk.red('\u25CF')} File watcher ${\n watcherRunning ? chalk.gray('(port 3030)') : chalk.gray('(stopped)')\n }`\n );\n\n if (pids.sessionId) {\n console.log(chalk.white(`\\nSession: ${chalk.cyan(pids.sessionId)}`));\n }\n\n if (metroRunning && watcherRunning) {\n console.log(chalk.green('\\n\u2705 AppTuner is running\\n'));\n } else {\n console.log(chalk.yellow('\\n\u26A0\uFE0F Some services are not running\\n'));\n }\n } catch (error) {\n console.log(chalk.gray(' No running services\\n'));\n console.log(chalk.gray('Run `apptuner start` to begin.\\n'));\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["fs", "path", "chalk", "fs", "path", "chalk"]
7
7
  }
package/metro-server.cjs CHANGED
@@ -4,7 +4,7 @@ const fs = require('fs');
4
4
  const crypto = require('crypto');
5
5
  const WebSocket = require('ws');
6
6
 
7
- const PORT = 3031;
7
+ const PORT = parseInt(process.env.METRO_PORT || '3031', 10);
8
8
  const wss = new WebSocket.Server({ port: PORT });
9
9
 
10
10
  console.log(`📦 Metro bundler server running on ws://localhost:${PORT}`);
@@ -61,6 +61,21 @@ wss.on('connection', (ws) => {
61
61
  try {
62
62
  const data = JSON.parse(message);
63
63
 
64
+ // Handle cache clearing for new sessions/projects
65
+ if (data.type === 'clear_cache') {
66
+ const { projectPath } = data;
67
+ const cleared = [];
68
+ for (const [key] of bundleCache.entries()) {
69
+ if (key.startsWith(projectPath + '::')) {
70
+ bundleCache.delete(key);
71
+ cleared.push(key);
72
+ }
73
+ }
74
+ console.log(`🗑️ Cleared ${cleared.length} cached bundles for ${projectPath}`);
75
+ ws.send(JSON.stringify({ type: 'cache_cleared', count: cleared.length }));
76
+ return;
77
+ }
78
+
64
79
  if (data.type === 'bundle') {
65
80
  const { projectPath, entryPoint = 'App.tsx' } = data;
66
81
  console.log(`📦 Bundle request for: ${projectPath}/${entryPoint}`);
@@ -215,6 +230,7 @@ function createErrorBundle(error) {
215
230
  // Create a bundle that renders an error screen
216
231
  // This will be executed on the phone just like a normal bundle
217
232
  const errorBundle = `
233
+ (function() {
218
234
  // Error overlay bundle - match normal bundle structure
219
235
  console.log('[ErrorBundle] Starting...');
220
236
 
@@ -396,7 +412,7 @@ if (!React || !ReactNative) {
396
412
  console.log('[ErrorBundle] Set this.App');
397
413
 
398
414
  console.log('[ErrorBundle] Error overlay ready to display');
399
- }.call(this, (typeof global !== 'undefined' ? global : (typeof window !== 'undefined' ? window : this))));
415
+ }).call(this, (typeof global !== 'undefined' ? global : (typeof window !== 'undefined' ? window : this)));
400
416
  `;
401
417
 
402
418
  return errorBundle;
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "apptuner",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Hot reload React Native apps instantly - no Expo needed",
5
5
  "type": "module",
6
6
  "bin": {
7
- "apptuner": "./dist/cli.js"
7
+ "apptuner": "dist/cli.js"
8
8
  },
9
9
  "files": [
10
10
  "dist/cli.js",
@@ -27,7 +27,7 @@
27
27
  "license": "MIT",
28
28
  "repository": {
29
29
  "type": "git",
30
- "url": "https://github.com/pepijnvanderknaap/apptuner.git"
30
+ "url": "git+https://github.com/pepijnvanderknaap/apptuner.git"
31
31
  },
32
32
  "homepage": "https://apptuner.io",
33
33
  "bugs": {
@@ -43,7 +43,7 @@
43
43
  "preview": "vite preview",
44
44
  "watcher": "node watcher-server.cjs",
45
45
  "metro": "node metro-server.cjs",
46
- "relay": "cd relay && npm run dev",
46
+ "relay": "node relay-server.js",
47
47
  "mobile": "cd mobile && npm start",
48
48
  "start:all": "concurrently -n desktop,relay,watcher,metro,mobile -c blue,green,yellow,cyan,magenta \"npm run dev\" \"npm run relay\" \"npm run watcher\" \"npm run metro\" \"npm run mobile\"",
49
49
  "apptuner": "node dist/cli.js",
@@ -7,7 +7,7 @@ const chokidar = require('chokidar');
7
7
  const WebSocket = require('ws');
8
8
  const path = require('path');
9
9
 
10
- const PORT = 3030;
10
+ const PORT = parseInt(process.env.WATCHER_PORT || '3030', 10);
11
11
  const wss = new WebSocket.Server({ port: PORT });
12
12
 
13
13
  console.log(`🔍 File watcher server running on ws://localhost:${PORT}`);