@galda/cli 0.10.45 → 0.10.46

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.
@@ -7,11 +7,12 @@
7
7
  // keyed URL. State lives in ~/.manager-for-ai (never in the npx cache).
8
8
 
9
9
  import { spawnSync } from 'node:child_process';
10
- import { existsSync } from 'node:fs';
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { homedir } from 'node:os';
11
12
  import { join, resolve, dirname } from 'node:path';
12
13
  import { fileURLToPath, pathToFileURL } from 'node:url';
13
14
  import { createServer } from 'node:net';
14
- import { isCommandOnPath, resolveConnectedAgents, resolveOpenBrowserDefault } from '../engine/lib.mjs';
15
+ import { isCommandOnPath, resolveConnectedAgents, resolveOpenBrowserDefault, licenseTokenIdentity, classifyRelayClientConflict } from '../engine/lib.mjs';
15
16
 
16
17
  const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
17
18
  const say = (m) => console.log(`[galda] ${m}`);
@@ -103,6 +104,38 @@ process.env.MANAGER_APP_URL = process.env.MANAGER_APP_URL ?? 'https://app.galda.
103
104
  // exists — the way to switch to a different account (a stale token otherwise
104
105
  // binds you to the previous account).
105
106
  if (process.argv.includes('--signin') || process.argv.includes('--login')) process.env.MANAGER_FORCE_SIGNIN = '1';
107
+ // Duplicate-instance guard (2026-07-24). A SECOND `npx @galda/cli` for the SAME
108
+ // account — while a first is already connected to the relay on this machine —
109
+ // used to boot a doomed second manager on a fallback port (4401): its relay-client
110
+ // hits the single-instance PID lock and exits, the supervisor restarts it, it
111
+ // refuses again → an endless "connect / run npx" loop in that instance's UI, since
112
+ // the first instance holds the one relay connection. The PID lock (relay-client.mjs,
113
+ // #5) correctly PREVENTS two relay-clients, but nothing stopped the second SERVER
114
+ // from booting and looping (dogfood 2026-07-24). Catch it here, before we pick a
115
+ // port: if the relay lock is held by a LIVE process for the SAME identity, don't
116
+ // start a second copy — point at the one already running and exit clean (0, not an
117
+ // error: "already running" is not a fault). Skipped when there's no relay
118
+ // (RELAY_URL empty = local-only, no contention), on an explicit account switch
119
+ // (--signin), or the deliberate-second-instance hatch (RELAY_CLIENT_FORCE=1). A
120
+ // DIFFERENT account is NOT caught here — that's a legitimate switch the relay-client
121
+ // offers to take over (classifyRelayClientConflict → offer-switch).
122
+ if (process.env.RELAY_URL && !process.env.MANAGER_FORCE_SIGNIN && process.env.RELAY_CLIENT_FORCE !== '1') {
123
+ const DATA_DIR = process.env.MANAGER_HOME
124
+ ?? (existsSync(join(ROOT, '.git')) ? join(ROOT, 'engine') : join(homedir(), '.manager-for-ai'));
125
+ let lock = null;
126
+ try { lock = JSON.parse(readFileSync(join(DATA_DIR, 'relay-client.lock'), 'utf8')); } catch { /* no lock yet */ }
127
+ let alive = false;
128
+ if (lock && Number.isFinite(lock.pid)) { try { process.kill(lock.pid, 0); alive = true; } catch { alive = false; } }
129
+ let identity = '';
130
+ try { identity = licenseTokenIdentity(readFileSync(join(DATA_DIR, 'license.token'), 'utf8').trim()); } catch { /* not signed in */ }
131
+ const conflict = classifyRelayClientConflict({ existingLock: lock, alive, newIdentity: identity });
132
+ if (conflict.action === 'refuse' && conflict.reason === 'same-account-already-connected') {
133
+ say(`Galda is already running for ${conflict.holderIdentity} on this machine (pid ${conflict.holderPid}).`);
134
+ say(`Open it at ${process.env.MANAGER_APP_URL || 'https://app.galda.app'} — not starting a second copy.`);
135
+ say('Switch accounts with `npx @galda/cli --signin`, or set MANAGER_HOME to a different folder to run a separate instance.');
136
+ process.exit(0);
137
+ }
138
+ }
106
139
  // Choose a free port now and pin it for BOTH the server and the relay-client, so
107
140
  // a busy 4400 (another manager) no longer stops onboarding — no MANAGER_PORT by hand.
108
141
  const wantedPort = Number(process.env.MANAGER_PORT ?? 4400);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.45",
3
+ "version": "0.10.46",
4
4
  "type": "module",
5
5
  "description": "Galda - hand off work to Claude Code or Codex, get proof back. Runs on your existing subscription, no extra API cost.",
6
6
  "scripts": {