@phenx-inc/ctlsurf 0.3.9 → 0.3.10

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.
@@ -238,50 +238,36 @@ if (mode === 'desktop') {
238
238
  try {
239
239
  electronPath = require('electron')
240
240
  } catch {
241
- // Electron not installed prompt to install or fall back
242
- if (process.stdin.isTTY && !args.includes('--terminal')) {
243
- console.log(`\n${B} ctlsurf${R}\n`)
244
- console.log(` Electron is not installed. Desktop mode requires Electron.\n`)
245
- console.log(` ${D}1)${R} Install Electron and launch desktop mode`)
246
- console.log(` ${D}2)${R} Launch in terminal mode instead`)
247
- console.log(` ${D}3)${R} Exit\n`)
248
-
249
- const readline = require('readline')
250
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
251
- rl.question(` Choice ${D}(1/2/3)${R}: `, (answer) => {
252
- rl.close()
253
- answer = answer.trim()
254
- if (answer === '1') {
255
- console.log(`\n${D}Installing Electron (this may take a minute)...${R}\n`)
256
- try {
257
- execSync('npm install electron --no-save', { cwd: ROOT, stdio: 'inherit' })
258
- console.log(`\n${G}✓${R} Electron installed. Launching desktop mode...\n`)
259
- try {
260
- const ep = require('electron')
261
- execFileSync(String(ep), [ROOT, ...args], {
262
- stdio: 'inherit',
263
- env: { ...process.env, CTLSURF_WORKER_CWD: process.env.CTLSURF_WORKER_CWD || process.cwd() }
264
- })
265
- } catch (err) {
266
- process.exit(err.status || 0)
267
- }
268
- } catch (err) {
269
- console.error(`\n${Y}!${R} Electron install failed: ${err.message}`)
270
- console.error(` Try manually: cd ${ROOT} && npm install electron\n`)
271
- process.exit(1)
272
- }
273
- } else if (answer === '2') {
274
- console.log('')
275
- runTerminal()
276
- } else {
277
- process.exit(0)
278
- }
279
- })
280
- return
241
+ // Electron not installed (devDependency, or wiped by `npm update`).
242
+ // Auto-install silently so npm-installed users don't have to think about it.
243
+ if (!installElectron()) {
244
+ // Install failed fall back to terminal mode
245
+ console.error(`\n${Y}!${R} Falling back to terminal mode.\n`)
246
+ runTerminal()
247
+ process.exit(0)
281
248
  }
282
- // Non-interactive: fall back silently
283
- runTerminal()
284
- process.exit(0)
249
+ try {
250
+ // Bust require cache (electron's index.js reads path.txt at require time)
251
+ delete require.cache[require.resolve('electron')]
252
+ electronPath = require('electron')
253
+ } catch (err) {
254
+ console.error(`\n${Y}!${R} Electron resolution failed after install: ${err.message}\n`)
255
+ process.exit(1)
256
+ }
257
+ }
258
+
259
+ // Rebrand Electron.app → ctlsurf.app (idempotent; sentinel file inside
260
+ // node_modules/electron). Re-runs automatically after npm update wipes it.
261
+ try {
262
+ const { rebrand } = require(path.join(ROOT, 'scripts', 'rebrand-electron.js'))
263
+ if (rebrand()) {
264
+ // Path may have changed — re-resolve
265
+ delete require.cache[require.resolve('electron')]
266
+ electronPath = require('electron')
267
+ }
268
+ } catch (err) {
269
+ // Non-fatal — app still launches, just shows as "Electron"
270
+ console.error(`${D}[ctlsurf] rebrand skipped: ${err.message}${R}`)
285
271
  }
286
272
 
287
273
  try {
@@ -296,6 +282,23 @@ if (mode === 'desktop') {
296
282
  runTerminal()
297
283
  }
298
284
 
285
+ function installElectron() {
286
+ console.log(`\n${B} ctlsurf${R} ${D}— first-run setup${R}\n`)
287
+ console.log(` ${D}Installing Electron (one-time, ~100 MB)...${R}`)
288
+ try {
289
+ execSync('npm install electron --no-save --silent', {
290
+ cwd: ROOT,
291
+ stdio: ['ignore', 'ignore', 'inherit']
292
+ })
293
+ console.log(` ${G}✓${R} Electron installed.\n`)
294
+ return true
295
+ } catch (err) {
296
+ console.error(`\n${Y}!${R} Electron install failed: ${err.message}`)
297
+ console.error(` Try manually: cd ${ROOT} && npm install electron\n`)
298
+ return false
299
+ }
300
+ }
301
+
299
302
  function runTerminal() {
300
303
  const terminalPath = path.join(ROOT, 'out/headless/index.mjs')
301
304
  if (!fs.existsSync(terminalPath)) {
@@ -5471,7 +5471,7 @@ var require_package = __commonJS({
5471
5471
  "package.json"(exports, module) {
5472
5472
  module.exports = {
5473
5473
  name: "@phenx-inc/ctlsurf",
5474
- version: "0.3.9",
5474
+ version: "0.3.10",
5475
5475
  description: "Agent-agnostic terminal and desktop app for ctlsurf \u2014 run Claude Code, Codex, or any coding agent with live session logging and remote control",
5476
5476
  main: "out/main/index.js",
5477
5477
  bin: {
@@ -5548,6 +5548,19 @@ var require_package = __commonJS({
5548
5548
  }
5549
5549
  });
5550
5550
 
5551
+ // src/main/logger.ts
5552
+ var silent = false;
5553
+ function setSilent(value) {
5554
+ silent = value;
5555
+ }
5556
+ function log(...args) {
5557
+ if (silent) return;
5558
+ try {
5559
+ console.log(...args);
5560
+ } catch {
5561
+ }
5562
+ }
5563
+
5551
5564
  // src/main/orchestrator.ts
5552
5565
  import path from "path";
5553
5566
  import fs from "fs";
@@ -6051,12 +6064,6 @@ import os from "os";
6051
6064
  import crypto from "crypto";
6052
6065
  import WsModule from "ws";
6053
6066
  var WS = typeof WebSocket !== "undefined" ? WebSocket : WsModule;
6054
- function log(...args) {
6055
- try {
6056
- console.log(...args);
6057
- } catch {
6058
- }
6059
- }
6060
6067
  var HEARTBEAT_INTERVAL_MS = 3e4;
6061
6068
  var RECONNECT_DELAY_MS = 5e3;
6062
6069
  var MAX_RECONNECT_DELAY_MS = 6e4;
@@ -6220,7 +6227,7 @@ var WorkerWsClient = class {
6220
6227
  case "registered": {
6221
6228
  this.workerId = data.worker_id;
6222
6229
  const workerStatus = data.status;
6223
- console.log(`[worker-ws] Registered as ${this.workerId}, status: ${workerStatus}`);
6230
+ log(`[worker-ws] Registered as ${this.workerId}, status: ${workerStatus}`);
6224
6231
  if (workerStatus === "pending_approval") {
6225
6232
  this.setStatus("pending_approval");
6226
6233
  } else {
@@ -6246,7 +6253,7 @@ var WorkerWsClient = class {
6246
6253
  case "message": {
6247
6254
  const msg = data.message;
6248
6255
  if (msg) {
6249
- console.log(`[worker-ws] Received message: ${msg.id}`);
6256
+ log(`[worker-ws] Received message: ${msg.id}`);
6250
6257
  this.events.onMessage(msg);
6251
6258
  }
6252
6259
  break;
@@ -6261,7 +6268,7 @@ var WorkerWsClient = class {
6261
6268
  case "heartbeat_ack":
6262
6269
  break;
6263
6270
  default:
6264
- console.log(`[worker-ws] Unknown message type: ${msgType}`);
6271
+ log(`[worker-ws] Unknown message type: ${msgType}`);
6265
6272
  }
6266
6273
  }
6267
6274
  send(data) {
@@ -6283,7 +6290,7 @@ var WorkerWsClient = class {
6283
6290
  }
6284
6291
  scheduleReconnect() {
6285
6292
  if (!this.shouldReconnect) return;
6286
- console.log(`[worker-ws] Reconnecting in ${this.reconnectDelay / 1e3}s...`);
6293
+ log(`[worker-ws] Reconnecting in ${this.reconnectDelay / 1e3}s...`);
6287
6294
  this.reconnectTimer = setTimeout(() => {
6288
6295
  this.doConnect();
6289
6296
  }, this.reconnectDelay);
@@ -6601,12 +6608,6 @@ var TimeTracker = class {
6601
6608
  };
6602
6609
 
6603
6610
  // src/main/orchestrator.ts
6604
- function log3(...args) {
6605
- try {
6606
- console.log(...args);
6607
- } catch {
6608
- }
6609
- }
6610
6611
  var DEFAULT_IDLE_TIMEOUT_MIN = 15;
6611
6612
  var DEFAULT_PROFILES = {
6612
6613
  production: {
@@ -6645,11 +6646,11 @@ var Orchestrator = class {
6645
6646
  this.events = events;
6646
6647
  this.workerWs = new WorkerWsClient({
6647
6648
  onStatusChange: (status) => {
6648
- log3(`[worker-ws] Status: ${status}`);
6649
+ log(`[worker-ws] Status: ${status}`);
6649
6650
  events.onWorkerStatus(status);
6650
6651
  },
6651
6652
  onMessage: (message) => {
6652
- log3(`[worker-ws] Incoming message: ${message.id} (${message.type})`);
6653
+ log(`[worker-ws] Incoming message: ${message.id} (${message.type})`);
6653
6654
  events.onWorkerMessage(message);
6654
6655
  this.workerWs.sendAck(message.id);
6655
6656
  if (message.type === "prompt" || message.type === "task_dispatch") {
@@ -6661,7 +6662,7 @@ var Orchestrator = class {
6661
6662
  }
6662
6663
  },
6663
6664
  onRegistered: (data) => {
6664
- log3(`[worker-ws] Registered: worker_id=${data.worker_id}, folder_id=${data.folder_id}, status=${data.status}`);
6665
+ log(`[worker-ws] Registered: worker_id=${data.worker_id}, folder_id=${data.folder_id}, status=${data.status}`);
6665
6666
  events.onWorkerRegistered(data);
6666
6667
  if (!data.folder_id) {
6667
6668
  events.onWorkerStatus("no_project");
@@ -6719,7 +6720,7 @@ var Orchestrator = class {
6719
6720
  const baseUrl = profile.baseUrl || process.env.CTLSURF_BASE_URL || "https://app.ctlsurf.com";
6720
6721
  this.ctlsurfApi.setBaseUrl(baseUrl);
6721
6722
  this.workerWs.setBaseUrl(baseUrl);
6722
- log3(`[settings] Profile applied: ${profile.name} (${baseUrl})`);
6723
+ log(`[settings] Profile applied: ${profile.name} (${baseUrl})`);
6723
6724
  }
6724
6725
  loadSettings() {
6725
6726
  try {
@@ -6744,7 +6745,7 @@ var Orchestrator = class {
6744
6745
  logChat: !!raw.logChat
6745
6746
  };
6746
6747
  this.saveSettings();
6747
- log3("[settings] Migrated legacy settings to profiles");
6748
+ log("[settings] Migrated legacy settings to profiles");
6748
6749
  } else {
6749
6750
  this.settings = raw;
6750
6751
  if (!this.settings.profiles.production) {
@@ -6771,7 +6772,7 @@ var Orchestrator = class {
6771
6772
  fs.mkdirSync(this.settingsDir, { recursive: true });
6772
6773
  fs.writeFileSync(settingsPath, JSON.stringify(this.settings, null, 2));
6773
6774
  } catch (err) {
6774
- log3("[settings] Failed to save:", err.message);
6775
+ log("[settings] Failed to save:", err.message);
6775
6776
  }
6776
6777
  }
6777
6778
  overrideApiKey(key) {
@@ -6981,7 +6982,7 @@ var Orchestrator = class {
6981
6982
  const profile = this.getActiveProfile();
6982
6983
  const apiKey = profile.apiKey || process.env.CTLSURF_API_KEY;
6983
6984
  if (!apiKey) {
6984
- log3("[worker-ws] No API key, skipping WS connect");
6985
+ log("[worker-ws] No API key, skipping WS connect");
6985
6986
  return;
6986
6987
  }
6987
6988
  this.stopNoProjectPolling();
@@ -6995,7 +6996,7 @@ var Orchestrator = class {
6995
6996
  if (this.noProjectPollTimer && this.noProjectPollCwd === cwd) return;
6996
6997
  this.stopNoProjectPolling();
6997
6998
  this.noProjectPollCwd = cwd;
6998
- log3(`[worker-ws] Polling for project folder at ${cwd}`);
6999
+ log(`[worker-ws] Polling for project folder at ${cwd}`);
6999
7000
  this.noProjectPollTimer = setInterval(() => {
7000
7001
  void this.checkForProjectFolder(cwd);
7001
7002
  }, NO_PROJECT_POLL_MS);
@@ -7016,7 +7017,7 @@ var Orchestrator = class {
7016
7017
  try {
7017
7018
  const folder = await this.ctlsurfApi.findFolderByPath(cwd);
7018
7019
  if (folder?.id && this.currentCwd === cwd && this.currentAgent) {
7019
- log3(`[worker-ws] Project folder appeared (${folder.id}); reconnecting`);
7020
+ log(`[worker-ws] Project folder appeared (${folder.id}); reconnecting`);
7020
7021
  const agent = this.currentAgent;
7021
7022
  this.stopNoProjectPolling();
7022
7023
  this.workerWs.disconnect();
@@ -7393,6 +7394,7 @@ process.on("uncaughtException", (err) => {
7393
7394
  } catch {
7394
7395
  }
7395
7396
  });
7397
+ setSilent(true);
7396
7398
  function getCurrentVersion() {
7397
7399
  try {
7398
7400
  const pkg = require_package();