@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.
- package/bin/manager-for-ai.mjs +35 -2
- package/package.json +1 -1
package/bin/manager-for-ai.mjs
CHANGED
|
@@ -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