@phenx-inc/ctlsurf 0.3.8 → 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.8",
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: {
@@ -6619,6 +6620,7 @@ var DEFAULT_PROFILES = {
6619
6620
  }
6620
6621
  };
6621
6622
  var TERM_STREAM_INTERVAL_MS = 50;
6623
+ var NO_PROJECT_POLL_MS = 5e3;
6622
6624
  var Orchestrator = class {
6623
6625
  settingsDir;
6624
6626
  events;
@@ -6637,16 +6639,18 @@ var Orchestrator = class {
6637
6639
  profiles: { ...DEFAULT_PROFILES },
6638
6640
  logChat: false
6639
6641
  };
6642
+ noProjectPollTimer = null;
6643
+ noProjectPollCwd = null;
6640
6644
  constructor(settingsDir, events) {
6641
6645
  this.settingsDir = settingsDir;
6642
6646
  this.events = events;
6643
6647
  this.workerWs = new WorkerWsClient({
6644
6648
  onStatusChange: (status) => {
6645
- log3(`[worker-ws] Status: ${status}`);
6649
+ log(`[worker-ws] Status: ${status}`);
6646
6650
  events.onWorkerStatus(status);
6647
6651
  },
6648
6652
  onMessage: (message) => {
6649
- log3(`[worker-ws] Incoming message: ${message.id} (${message.type})`);
6653
+ log(`[worker-ws] Incoming message: ${message.id} (${message.type})`);
6650
6654
  events.onWorkerMessage(message);
6651
6655
  this.workerWs.sendAck(message.id);
6652
6656
  if (message.type === "prompt" || message.type === "task_dispatch") {
@@ -6658,10 +6662,15 @@ var Orchestrator = class {
6658
6662
  }
6659
6663
  },
6660
6664
  onRegistered: (data) => {
6661
- 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}`);
6662
6666
  events.onWorkerRegistered(data);
6663
6667
  if (!data.folder_id) {
6664
6668
  events.onWorkerStatus("no_project");
6669
+ if (this.currentCwd && data.status !== "pending_approval") {
6670
+ this.startNoProjectPolling(this.currentCwd);
6671
+ }
6672
+ } else {
6673
+ this.stopNoProjectPolling();
6665
6674
  }
6666
6675
  },
6667
6676
  onTerminalInput: (data) => {
@@ -6711,7 +6720,7 @@ var Orchestrator = class {
6711
6720
  const baseUrl = profile.baseUrl || process.env.CTLSURF_BASE_URL || "https://app.ctlsurf.com";
6712
6721
  this.ctlsurfApi.setBaseUrl(baseUrl);
6713
6722
  this.workerWs.setBaseUrl(baseUrl);
6714
- log3(`[settings] Profile applied: ${profile.name} (${baseUrl})`);
6723
+ log(`[settings] Profile applied: ${profile.name} (${baseUrl})`);
6715
6724
  }
6716
6725
  loadSettings() {
6717
6726
  try {
@@ -6736,7 +6745,7 @@ var Orchestrator = class {
6736
6745
  logChat: !!raw.logChat
6737
6746
  };
6738
6747
  this.saveSettings();
6739
- log3("[settings] Migrated legacy settings to profiles");
6748
+ log("[settings] Migrated legacy settings to profiles");
6740
6749
  } else {
6741
6750
  this.settings = raw;
6742
6751
  if (!this.settings.profiles.production) {
@@ -6763,7 +6772,7 @@ var Orchestrator = class {
6763
6772
  fs.mkdirSync(this.settingsDir, { recursive: true });
6764
6773
  fs.writeFileSync(settingsPath, JSON.stringify(this.settings, null, 2));
6765
6774
  } catch (err) {
6766
- log3("[settings] Failed to save:", err.message);
6775
+ log("[settings] Failed to save:", err.message);
6767
6776
  }
6768
6777
  }
6769
6778
  overrideApiKey(key) {
@@ -6901,6 +6910,7 @@ var Orchestrator = class {
6901
6910
  if (isCodingAgent(agent)) {
6902
6911
  this.connectWorkerWs(agent, cwd);
6903
6912
  } else {
6913
+ this.stopNoProjectPolling();
6904
6914
  this.workerWs.disconnect();
6905
6915
  this.checkProjectStatus(cwd);
6906
6916
  }
@@ -6972,15 +6982,50 @@ var Orchestrator = class {
6972
6982
  const profile = this.getActiveProfile();
6973
6983
  const apiKey = profile.apiKey || process.env.CTLSURF_API_KEY;
6974
6984
  if (!apiKey) {
6975
- log3("[worker-ws] No API key, skipping WS connect");
6985
+ log("[worker-ws] No API key, skipping WS connect");
6976
6986
  return;
6977
6987
  }
6988
+ this.stopNoProjectPolling();
6978
6989
  this.workerWs.connect({
6979
6990
  machine: os3.hostname(),
6980
6991
  cwd,
6981
6992
  agent: agent.name
6982
6993
  });
6983
6994
  }
6995
+ startNoProjectPolling(cwd) {
6996
+ if (this.noProjectPollTimer && this.noProjectPollCwd === cwd) return;
6997
+ this.stopNoProjectPolling();
6998
+ this.noProjectPollCwd = cwd;
6999
+ log(`[worker-ws] Polling for project folder at ${cwd}`);
7000
+ this.noProjectPollTimer = setInterval(() => {
7001
+ void this.checkForProjectFolder(cwd);
7002
+ }, NO_PROJECT_POLL_MS);
7003
+ }
7004
+ stopNoProjectPolling() {
7005
+ if (this.noProjectPollTimer) {
7006
+ clearInterval(this.noProjectPollTimer);
7007
+ this.noProjectPollTimer = null;
7008
+ this.noProjectPollCwd = null;
7009
+ }
7010
+ }
7011
+ async checkForProjectFolder(cwd) {
7012
+ if (this.currentCwd !== cwd || !this.currentAgent) {
7013
+ this.stopNoProjectPolling();
7014
+ return;
7015
+ }
7016
+ if (!this.ctlsurfApi.getApiKey()) return;
7017
+ try {
7018
+ const folder = await this.ctlsurfApi.findFolderByPath(cwd);
7019
+ if (folder?.id && this.currentCwd === cwd && this.currentAgent) {
7020
+ log(`[worker-ws] Project folder appeared (${folder.id}); reconnecting`);
7021
+ const agent = this.currentAgent;
7022
+ this.stopNoProjectPolling();
7023
+ this.workerWs.disconnect();
7024
+ this.connectWorkerWs(agent, cwd);
7025
+ }
7026
+ } catch {
7027
+ }
7028
+ }
6984
7029
  async checkProjectStatus(cwd) {
6985
7030
  if (!this.ctlsurfApi.getApiKey()) {
6986
7031
  this.events.onWorkerStatus("no_project");
@@ -7011,6 +7056,7 @@ var Orchestrator = class {
7011
7056
  }
7012
7057
  // ─── Shutdown ───────────────────────────────────
7013
7058
  async shutdown() {
7059
+ this.stopNoProjectPolling();
7014
7060
  this.bridge.endSession();
7015
7061
  await this.timeTracker.endAll();
7016
7062
  for (const [, tab] of this.tabs) {
@@ -7348,6 +7394,7 @@ process.on("uncaughtException", (err) => {
7348
7394
  } catch {
7349
7395
  }
7350
7396
  });
7397
+ setSilent(true);
7351
7398
  function getCurrentVersion() {
7352
7399
  try {
7353
7400
  const pkg = require_package();