@panorama-ai/gateway 2.24.100 → 2.24.108

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/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  import { createClient } from '@supabase/supabase-js';
2
3
  import { execFile, spawn } from 'node:child_process';
3
4
  import { randomUUID } from 'node:crypto';
@@ -53,18 +54,8 @@ const CLAUDE_ENV_ALLOWLIST = [
53
54
  'SSH_AUTH_SOCK',
54
55
  'SSH_AGENT_PID',
55
56
  ];
56
- const CONFIG_DIR = process.env.PANORAMA_GATEWAY_CONFIG_DIR || path.join(os.homedir(), '.panorama');
57
- const SUBAGENT_WORKDIR_ROOT = path.join(CONFIG_DIR, 'subagents');
58
- const CLAUDE_GATEWAY_HOME = process.env.PANORAMA_CLAUDE_HOME || null;
59
- const CONFIG_PATH = process.env.PANORAMA_GATEWAY_CONFIG_PATH || path.join(CONFIG_DIR, 'gateway.json');
60
- const LOG_PATH = process.env.PANORAMA_GATEWAY_LOG_PATH || path.join(path.dirname(CONFIG_PATH), 'gateway.log');
61
- const PID_PATH = process.env.PANORAMA_GATEWAY_PID_PATH || path.join(path.dirname(CONFIG_PATH), 'gateway.pid');
57
+ const DEFAULT_CONFIG_DIR = path.join(os.homedir(), '.panorama');
62
58
  const DEFAULT_CLAUDE_PATH = path.join(os.homedir(), '.claude', 'local', 'claude');
63
- const CLAUDE_COMMAND = process.env.PANORAMA_CLAUDE_CLI ||
64
- process.env.CLAUDE_CLI ||
65
- (fsSync.existsSync(DEFAULT_CLAUDE_PATH) ? DEFAULT_CLAUDE_PATH : 'claude');
66
- const CODEX_COMMAND = process.env.PANORAMA_CODEX_CLI || process.env.CODEX_CLI || 'codex';
67
- const GEMINI_COMMAND = process.env.PANORAMA_GEMINI_CLI || process.env.GEMINI_CLI || 'gemini';
68
59
  let CLAUDE_SUPPORT = null;
69
60
  let PROVIDER_CAPABILITIES = null;
70
61
  const GATEWAY_CONCURRENCY = (() => {
@@ -79,6 +70,7 @@ const GATEWAY_CONCURRENCY = (() => {
79
70
  const activeSubagentRuns = new Map();
80
71
  const pendingCancelByRunId = new Map();
81
72
  const pendingCancelBySubagentId = new Map();
73
+ let ACTIVE_OPTIONS = null;
82
74
  function parseArgs(argv) {
83
75
  const positional = [];
84
76
  const options = {};
@@ -112,7 +104,7 @@ function parseArgs(argv) {
112
104
  return { command, positional, options };
113
105
  }
114
106
  function printHelp() {
115
- console.log(`\nPanorama Gateway\n\nUsage:\n panorama-gateway pair <PAIRING_CODE> [--device-name \"My Mac\"] [--supabase-url URL] [--anon-key KEY]\n panorama-gateway start [--device-name \"My Mac\"] [--daemon]\n panorama-gateway stop\n panorama-gateway logs [--lines 200] [--no-follow]\n\nEnvironment options:\n --env <local|dev|test|stage|prod> Load .env.<env> from repo root (defaults to .env)\n --env-file <path> Load a specific env file\n PANORAMA_ENV Same as --env\n PANORAMA_ENV_FILE Same as --env-file\n\nEnvironment overrides:\n PANORAMA_SUPABASE_URL or SUPABASE_URL\n PANORAMA_SUPABASE_ANON_KEY or SUPABASE_ANON_KEY or SUPABASE_PUBLISHABLE_KEY\n PANORAMA_GATEWAY_CONFIG_PATH to override config path\n PANORAMA_GATEWAY_LOG_PATH to override log path\n PANORAMA_GATEWAY_PID_PATH to override pid path\n PANORAMA_CLAUDE_CLI or CLAUDE_CLI to override claude command\n`);
107
+ console.log(`\nPanorama Gateway\n\nUsage:\n panorama-gateway pair <PAIRING_CODE> [--device-name \"My Mac\"] [--supabase-url URL] [--anon-key KEY]\n panorama-gateway start [--device-name \"My Mac\"] [--foreground|--daemon]\n panorama-gateway stop\n panorama-gateway status\n panorama-gateway logs [--lines 200] [--no-follow]\n\nNotes:\n Start runs in the background by default for the built CLI. Use --foreground to keep it attached.\n\nEnvironment options:\n --env <local|dev|test|stage|prod> Load .env.<env> from repo root (defaults to .env)\n --env-file <path> Load a specific env file\n PANORAMA_ENV Same as --env\n PANORAMA_ENV_FILE Same as --env-file\n\nCLI overrides:\n --config-dir <path> Override config directory (default: ~/.panorama)\n --config-path <path> Override gateway config file\n --log-path <path> Override gateway log file\n --pid-path <path> Override gateway pid file\n --claude-cli <path> Override Claude CLI command\n --codex-cli <path> Override Codex CLI command\n --gemini-cli <path> Override Gemini CLI command\n --claude-home <path> Override Claude home directory\n\nEnvironment overrides:\n PANORAMA_SUPABASE_URL or SUPABASE_URL\n PANORAMA_SUPABASE_ANON_KEY or SUPABASE_ANON_KEY or SUPABASE_PUBLISHABLE_KEY\n PANORAMA_GATEWAY_CONFIG_DIR\n PANORAMA_GATEWAY_CONFIG_PATH\n PANORAMA_GATEWAY_LOG_PATH\n PANORAMA_GATEWAY_PID_PATH\n PANORAMA_CLAUDE_CLI or CLAUDE_CLI\n PANORAMA_CODEX_CLI or CODEX_CLI\n PANORAMA_GEMINI_CLI or GEMINI_CLI\n PANORAMA_CLAUDE_HOME\n`);
116
108
  }
117
109
  function getStringOption(options, key) {
118
110
  const value = options[key];
@@ -120,6 +112,87 @@ function getStringOption(options, key) {
120
112
  return value;
121
113
  return undefined;
122
114
  }
115
+ function getActiveOptions(options) {
116
+ return options ?? ACTIVE_OPTIONS ?? {};
117
+ }
118
+ function resolveGatewayPaths(options) {
119
+ const resolvedOptions = getActiveOptions(options);
120
+ const configDirRaw = getStringOption(resolvedOptions, 'config-dir') || process.env.PANORAMA_GATEWAY_CONFIG_DIR;
121
+ const configDir = configDirRaw ? path.resolve(configDirRaw) : DEFAULT_CONFIG_DIR;
122
+ const configPathRaw = getStringOption(resolvedOptions, 'config-path') || process.env.PANORAMA_GATEWAY_CONFIG_PATH;
123
+ const configPath = configPathRaw ? path.resolve(configPathRaw) : path.join(configDir, 'gateway.json');
124
+ const logPathRaw = getStringOption(resolvedOptions, 'log-path') || process.env.PANORAMA_GATEWAY_LOG_PATH;
125
+ const logPath = logPathRaw ? path.resolve(logPathRaw) : path.join(path.dirname(configPath), 'gateway.log');
126
+ const pidPathRaw = getStringOption(resolvedOptions, 'pid-path') || process.env.PANORAMA_GATEWAY_PID_PATH;
127
+ const pidPath = pidPathRaw ? path.resolve(pidPathRaw) : path.join(path.dirname(configPath), 'gateway.pid');
128
+ return { configDir, configPath, logPath, pidPath };
129
+ }
130
+ function resolveSubagentWorkdirRoot(options) {
131
+ return path.join(resolveGatewayPaths(options).configDir, 'subagents');
132
+ }
133
+ function resolveClaudeGatewayHome(options) {
134
+ const resolvedOptions = getActiveOptions(options);
135
+ const raw = getStringOption(resolvedOptions, 'claude-home') || process.env.PANORAMA_CLAUDE_HOME || null;
136
+ return raw ? path.resolve(raw) : null;
137
+ }
138
+ function resolveClaudeCommand(options) {
139
+ const resolvedOptions = getActiveOptions(options);
140
+ const override = getStringOption(resolvedOptions, 'claude-cli') ||
141
+ process.env.PANORAMA_CLAUDE_CLI ||
142
+ process.env.CLAUDE_CLI;
143
+ if (override)
144
+ return override;
145
+ return fsSync.existsSync(DEFAULT_CLAUDE_PATH) ? DEFAULT_CLAUDE_PATH : 'claude';
146
+ }
147
+ function resolveCodexCommand(options) {
148
+ const resolvedOptions = getActiveOptions(options);
149
+ return (getStringOption(resolvedOptions, 'codex-cli') ||
150
+ process.env.PANORAMA_CODEX_CLI ||
151
+ process.env.CODEX_CLI ||
152
+ 'codex');
153
+ }
154
+ function resolveGeminiCommand(options) {
155
+ const resolvedOptions = getActiveOptions(options);
156
+ return (getStringOption(resolvedOptions, 'gemini-cli') ||
157
+ process.env.PANORAMA_GEMINI_CLI ||
158
+ process.env.GEMINI_CLI ||
159
+ 'gemini');
160
+ }
161
+ function buildGatewayEnvOverrides(options) {
162
+ const resolvedOptions = getActiveOptions(options);
163
+ const env = {};
164
+ const configDir = getStringOption(resolvedOptions, 'config-dir');
165
+ const configPath = getStringOption(resolvedOptions, 'config-path');
166
+ const logPath = getStringOption(resolvedOptions, 'log-path');
167
+ const pidPath = getStringOption(resolvedOptions, 'pid-path');
168
+ const claudeCli = getStringOption(resolvedOptions, 'claude-cli');
169
+ const codexCli = getStringOption(resolvedOptions, 'codex-cli');
170
+ const geminiCli = getStringOption(resolvedOptions, 'gemini-cli');
171
+ const claudeHome = getStringOption(resolvedOptions, 'claude-home');
172
+ const supabaseUrl = getStringOption(resolvedOptions, 'supabase-url');
173
+ const supabaseAnonKey = getStringOption(resolvedOptions, 'anon-key');
174
+ if (configDir)
175
+ env.PANORAMA_GATEWAY_CONFIG_DIR = configDir;
176
+ if (configPath)
177
+ env.PANORAMA_GATEWAY_CONFIG_PATH = configPath;
178
+ if (logPath)
179
+ env.PANORAMA_GATEWAY_LOG_PATH = logPath;
180
+ if (pidPath)
181
+ env.PANORAMA_GATEWAY_PID_PATH = pidPath;
182
+ if (claudeCli)
183
+ env.PANORAMA_CLAUDE_CLI = claudeCli;
184
+ if (codexCli)
185
+ env.PANORAMA_CODEX_CLI = codexCli;
186
+ if (geminiCli)
187
+ env.PANORAMA_GEMINI_CLI = geminiCli;
188
+ if (claudeHome)
189
+ env.PANORAMA_CLAUDE_HOME = claudeHome;
190
+ if (supabaseUrl)
191
+ env.PANORAMA_SUPABASE_URL = supabaseUrl;
192
+ if (supabaseAnonKey)
193
+ env.PANORAMA_SUPABASE_ANON_KEY = supabaseAnonKey;
194
+ return env;
195
+ }
123
196
  function normalizeEnvName(raw) {
124
197
  if (!raw)
125
198
  return null;
@@ -178,13 +251,15 @@ function resolveSupabaseAnonKey(options, config) {
178
251
  config?.supabaseAnonKey ||
179
252
  '');
180
253
  }
181
- async function loadConfig() {
182
- const raw = await fs.readFile(CONFIG_PATH, 'utf-8');
254
+ async function loadConfig(options) {
255
+ const { configPath } = resolveGatewayPaths(options);
256
+ const raw = await fs.readFile(configPath, 'utf-8');
183
257
  return JSON.parse(raw);
184
258
  }
185
- async function saveConfig(config) {
186
- await fs.mkdir(path.dirname(CONFIG_PATH), { recursive: true });
187
- await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
259
+ async function saveConfig(config, options) {
260
+ const { configPath } = resolveGatewayPaths(options);
261
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
262
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
188
263
  }
189
264
  function logInfo(message, data) {
190
265
  if (data) {
@@ -202,9 +277,10 @@ function logError(message, data) {
202
277
  console.error(`[gateway] ${message}`);
203
278
  }
204
279
  }
205
- function readPidFile() {
280
+ function readPidFile(options) {
281
+ const { pidPath } = resolveGatewayPaths(options);
206
282
  try {
207
- const raw = fsSync.readFileSync(PID_PATH, 'utf-8').trim();
283
+ const raw = fsSync.readFileSync(pidPath, 'utf-8').trim();
208
284
  const pid = Number.parseInt(raw, 10);
209
285
  return Number.isFinite(pid) ? pid : null;
210
286
  }
@@ -212,13 +288,15 @@ function readPidFile() {
212
288
  return null;
213
289
  }
214
290
  }
215
- async function writePidFile(pid) {
216
- await fs.mkdir(path.dirname(PID_PATH), { recursive: true });
217
- await fs.writeFile(PID_PATH, `${pid}\n`);
291
+ async function writePidFile(pid, options) {
292
+ const { pidPath } = resolveGatewayPaths(options);
293
+ await fs.mkdir(path.dirname(pidPath), { recursive: true });
294
+ await fs.writeFile(pidPath, `${pid}\n`);
218
295
  }
219
- async function removePidFile() {
296
+ async function removePidFile(options) {
297
+ const { pidPath } = resolveGatewayPaths(options);
220
298
  try {
221
- await fs.unlink(PID_PATH);
299
+ await fs.unlink(pidPath);
222
300
  }
223
301
  catch {
224
302
  // ignore missing pid
@@ -278,17 +356,19 @@ async function pairGateway(code, options) {
278
356
  teamId: body.team_id,
279
357
  deviceName,
280
358
  };
281
- await saveConfig(config);
359
+ await saveConfig(config, options);
360
+ const { configPath } = resolveGatewayPaths(options);
282
361
  logInfo('Gateway paired successfully', {
283
362
  gatewayId: body.gateway_id,
284
363
  teamId: body.team_id,
285
- configPath: CONFIG_PATH,
364
+ configPath,
286
365
  });
287
366
  }
288
367
  async function execClaudeVersion() {
289
368
  const start = Date.now();
369
+ const command = resolveClaudeCommand();
290
370
  try {
291
- const { stdout, stderr } = await execFileAsync(CLAUDE_COMMAND, ['--version'], {
371
+ const { stdout, stderr } = await execFileAsync(command, ['--version'], {
292
372
  timeout: DEFAULT_CLAUDE_TIMEOUT_MS,
293
373
  });
294
374
  const durationMs = Date.now() - start;
@@ -317,8 +397,9 @@ async function execClaudeVersion() {
317
397
  }
318
398
  async function execClaudeHelp() {
319
399
  const start = Date.now();
400
+ const command = resolveClaudeCommand();
320
401
  try {
321
- const { stdout, stderr } = await execFileAsync(CLAUDE_COMMAND, ['--help'], {
402
+ const { stdout, stderr } = await execFileAsync(command, ['--help'], {
322
403
  timeout: DEFAULT_CLAUDE_TIMEOUT_MS,
323
404
  });
324
405
  const durationMs = Date.now() - start;
@@ -377,12 +458,13 @@ async function buildCapabilities() {
377
458
  if (claudeCapabilities?.supported_flags) {
378
459
  CLAUDE_SUPPORT = coerceClaudeSupport(claudeCapabilities.supported_flags);
379
460
  }
461
+ const resolvedClaudeCommand = resolveClaudeCommand();
380
462
  const claudeCliLegacy = claudeCapabilities
381
463
  ? {
382
464
  available: claudeCapabilities.available,
383
465
  version: claudeCapabilities.version,
384
466
  error: claudeCapabilities.error,
385
- command: `${CLAUDE_COMMAND} --version`,
467
+ command: `${resolvedClaudeCommand} --version`,
386
468
  supported_flags: claudeCapabilities.supported_flags,
387
469
  }
388
470
  : {
@@ -407,14 +489,15 @@ function appendClaudeIsolationArgs(args, support) {
407
489
  }
408
490
  }
409
491
  async function ensureClaudeGatewayHome() {
410
- if (!CLAUDE_GATEWAY_HOME)
492
+ const claudeHome = resolveClaudeGatewayHome();
493
+ if (!claudeHome)
411
494
  return;
412
- await fs.mkdir(CLAUDE_GATEWAY_HOME, { recursive: true });
413
- await fs.mkdir(path.join(CLAUDE_GATEWAY_HOME, 'xdg', 'config'), { recursive: true });
414
- await fs.mkdir(path.join(CLAUDE_GATEWAY_HOME, 'xdg', 'cache'), { recursive: true });
415
- await fs.mkdir(path.join(CLAUDE_GATEWAY_HOME, 'xdg', 'data'), { recursive: true });
495
+ await fs.mkdir(claudeHome, { recursive: true });
496
+ await fs.mkdir(path.join(claudeHome, 'xdg', 'config'), { recursive: true });
497
+ await fs.mkdir(path.join(claudeHome, 'xdg', 'cache'), { recursive: true });
498
+ await fs.mkdir(path.join(claudeHome, 'xdg', 'data'), { recursive: true });
416
499
  const sourceSessionEnv = path.join(os.homedir(), '.claude', 'session-env');
417
- const destSessionEnv = path.join(CLAUDE_GATEWAY_HOME, 'session-env');
500
+ const destSessionEnv = path.join(claudeHome, 'session-env');
418
501
  try {
419
502
  const srcStat = await fs.stat(sourceSessionEnv);
420
503
  if (srcStat.isDirectory()) {
@@ -438,11 +521,12 @@ function buildClaudeEnv() {
438
521
  env[key] = value;
439
522
  }
440
523
  }
441
- if (CLAUDE_GATEWAY_HOME) {
442
- env.HOME = CLAUDE_GATEWAY_HOME;
443
- env.XDG_CONFIG_HOME = path.join(CLAUDE_GATEWAY_HOME, 'xdg', 'config');
444
- env.XDG_CACHE_HOME = path.join(CLAUDE_GATEWAY_HOME, 'xdg', 'cache');
445
- env.XDG_DATA_HOME = path.join(CLAUDE_GATEWAY_HOME, 'xdg', 'data');
524
+ const claudeHome = resolveClaudeGatewayHome();
525
+ if (claudeHome) {
526
+ env.HOME = claudeHome;
527
+ env.XDG_CONFIG_HOME = path.join(claudeHome, 'xdg', 'config');
528
+ env.XDG_CACHE_HOME = path.join(claudeHome, 'xdg', 'cache');
529
+ env.XDG_DATA_HOME = path.join(claudeHome, 'xdg', 'data');
446
530
  }
447
531
  return env;
448
532
  }
@@ -671,7 +755,7 @@ function isUuid(value) {
671
755
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
672
756
  }
673
757
  async function ensureSubagentWorkDir(subagentId) {
674
- const dir = path.join(SUBAGENT_WORKDIR_ROOT, subagentId);
758
+ const dir = path.join(resolveSubagentWorkdirRoot(), subagentId);
675
759
  await fs.mkdir(dir, { recursive: true });
676
760
  return dir;
677
761
  }
@@ -1021,11 +1105,12 @@ async function updateJobDebug(supabase, jobId, debug) {
1021
1105
  }
1022
1106
  async function handleDiagnosticJob() {
1023
1107
  const output = await execClaudeVersion();
1108
+ const command = resolveClaudeCommand();
1024
1109
  if (!output.ok) {
1025
1110
  return {
1026
1111
  ok: false,
1027
1112
  result: {
1028
- command: `${CLAUDE_COMMAND} --version`,
1113
+ command: `${command} --version`,
1029
1114
  stdout: output.stdout,
1030
1115
  stderr: output.stderr,
1031
1116
  exit_code: output.exitCode,
@@ -1038,7 +1123,7 @@ async function handleDiagnosticJob() {
1038
1123
  return {
1039
1124
  ok: true,
1040
1125
  result: {
1041
- command: `${CLAUDE_COMMAND} --version`,
1126
+ command: `${command} --version`,
1042
1127
  stdout: output.stdout,
1043
1128
  stderr: output.stderr,
1044
1129
  exit_code: output.exitCode,
@@ -1351,10 +1436,10 @@ async function handleSubagentRunJob(supabase, job) {
1351
1436
  ? toFlagRecord(CLAUDE_SUPPORT ?? DEFAULT_CLAUDE_SUPPORT)
1352
1437
  : {});
1353
1438
  const command = adapterId === 'codex'
1354
- ? CODEX_COMMAND
1439
+ ? resolveCodexCommand()
1355
1440
  : adapterId === 'gemini'
1356
- ? GEMINI_COMMAND
1357
- : CLAUDE_COMMAND;
1441
+ ? resolveGeminiCommand()
1442
+ : resolveClaudeCommand();
1358
1443
  runPlan = adapter.buildRunPlan({
1359
1444
  prompt,
1360
1445
  config: configPayload,
@@ -1928,49 +2013,61 @@ async function processJob(supabase, config, job) {
1928
2013
  }, `Unsupported job type: ${claimed.job_type}`);
1929
2014
  }
1930
2015
  async function startGateway(options) {
1931
- const daemonRequested = options.daemon === true || options.background === true;
2016
+ const paths = resolveGatewayPaths(options);
1932
2017
  const foregroundRequested = options.foreground === true;
2018
+ const explicitDaemon = options.daemon === true || options.background === true;
2019
+ let daemonRequested = explicitDaemon || !foregroundRequested;
1933
2020
  if (daemonRequested && !foregroundRequested) {
1934
- const existingPid = readPidFile();
2021
+ const existingPid = readPidFile(options);
1935
2022
  if (existingPid && isProcessAlive(existingPid)) {
1936
2023
  logInfo('Gateway already running', { pid: existingPid });
1937
2024
  return;
1938
2025
  }
1939
2026
  const entryPath = process.argv[1] || '';
1940
2027
  if (entryPath.endsWith('.ts')) {
1941
- throw new Error('Daemon mode requires the built gateway. Run "pnpm gateway:start -- --daemon".');
2028
+ if (!explicitDaemon) {
2029
+ logInfo('Gateway running in foreground (dev mode detected)');
2030
+ daemonRequested = false;
2031
+ }
2032
+ else {
2033
+ throw new Error('Daemon mode requires the built gateway. Run "pnpm gateway:start -- --daemon".');
2034
+ }
1942
2035
  }
1943
- await fs.mkdir(path.dirname(LOG_PATH), { recursive: true });
1944
- const logHandle = await fs.open(LOG_PATH, 'a');
1945
- const childArgs = [entryPath, 'start', '--foreground'];
1946
- const deviceName = getStringOption(options, 'device-name') || process.env.PANORAMA_GATEWAY_DEVICE_NAME;
1947
- if (deviceName) {
1948
- childArgs.push('--device-name', deviceName);
2036
+ if (!daemonRequested) {
2037
+ // fall through to foreground startup
1949
2038
  }
1950
- const child = spawn(process.execPath, childArgs, {
1951
- detached: true,
1952
- stdio: ['ignore', logHandle.fd, logHandle.fd],
1953
- env: {
1954
- ...process.env,
1955
- PANORAMA_GATEWAY_LOG_PATH: LOG_PATH,
1956
- PANORAMA_GATEWAY_PID_PATH: PID_PATH,
1957
- },
1958
- });
1959
- child.unref();
1960
- await logHandle.close();
1961
- if (!child.pid) {
1962
- throw new Error('Failed to determine gateway process id');
2039
+ else {
2040
+ await fs.mkdir(path.dirname(paths.logPath), { recursive: true });
2041
+ const logHandle = await fs.open(paths.logPath, 'a');
2042
+ const childArgs = [entryPath, 'start', '--foreground'];
2043
+ const deviceName = getStringOption(options, 'device-name') || process.env.PANORAMA_GATEWAY_DEVICE_NAME;
2044
+ if (deviceName) {
2045
+ childArgs.push('--device-name', deviceName);
2046
+ }
2047
+ const child = spawn(process.execPath, childArgs, {
2048
+ detached: true,
2049
+ stdio: ['ignore', logHandle.fd, logHandle.fd],
2050
+ env: {
2051
+ ...process.env,
2052
+ ...buildGatewayEnvOverrides(options),
2053
+ },
2054
+ });
2055
+ child.unref();
2056
+ await logHandle.close();
2057
+ if (!child.pid) {
2058
+ throw new Error('Failed to determine gateway process id');
2059
+ }
2060
+ await writePidFile(child.pid, options);
2061
+ logInfo('Gateway started in background', { pid: child.pid, logPath: paths.logPath });
2062
+ return;
1963
2063
  }
1964
- await writePidFile(child.pid);
1965
- logInfo('Gateway started in background', { pid: child.pid, logPath: LOG_PATH });
1966
- return;
1967
2064
  }
1968
2065
  let config;
1969
2066
  try {
1970
- config = await loadConfig();
2067
+ config = await loadConfig(options);
1971
2068
  }
1972
2069
  catch (error) {
1973
- throw new Error(`Gateway config not found. Run "pair" first or set PANORAMA_GATEWAY_CONFIG_PATH. (${String(error)})`);
2070
+ throw new Error(`Gateway config not found. Run "pair" first or set --config-path / PANORAMA_GATEWAY_CONFIG_PATH. (${String(error)})`);
1974
2071
  }
1975
2072
  const supabaseUrl = resolveSupabaseUrl(options, config);
1976
2073
  const supabaseAnonKey = resolveSupabaseAnonKey(options, config);
@@ -1987,7 +2084,7 @@ async function startGateway(options) {
1987
2084
  supabaseAnonKey,
1988
2085
  deviceName,
1989
2086
  };
1990
- await saveConfig(config);
2087
+ await saveConfig(config, options);
1991
2088
  const supabase = createClient(supabaseUrl, supabaseAnonKey, {
1992
2089
  auth: {
1993
2090
  persistSession: false,
@@ -2009,14 +2106,14 @@ async function startGateway(options) {
2009
2106
  }
2010
2107
  config.accessToken = sessionData.session.access_token;
2011
2108
  config.refreshToken = sessionData.session.refresh_token;
2012
- await saveConfig(config);
2109
+ await saveConfig(config, options);
2013
2110
  supabase.auth.onAuthStateChange((event, session) => {
2014
2111
  if (!session)
2015
2112
  return;
2016
2113
  if (event === 'TOKEN_REFRESHED' || event === 'SIGNED_IN') {
2017
2114
  config.accessToken = session.access_token;
2018
2115
  config.refreshToken = session.refresh_token;
2019
- void saveConfig(config)
2116
+ void saveConfig(config, options)
2020
2117
  .then(() => logInfo('Gateway session refreshed'))
2021
2118
  .catch((error) => {
2022
2119
  logError('Failed to persist refreshed gateway session', {
@@ -2042,7 +2139,7 @@ async function startGateway(options) {
2042
2139
  });
2043
2140
  await sendHeartbeat(supabase, initialStatus, capabilities, deviceName);
2044
2141
  let heartbeatStatus = initialStatus;
2045
- await writePidFile(process.pid);
2142
+ await writePidFile(process.pid, options);
2046
2143
  const heartbeatTimer = setInterval(() => {
2047
2144
  void sendHeartbeat(supabase, heartbeatStatus, capabilities, deviceName);
2048
2145
  }, DEFAULT_HEARTBEAT_INTERVAL_MS);
@@ -2148,7 +2245,7 @@ async function startGateway(options) {
2148
2245
  heartbeatStatus = 'offline';
2149
2246
  await sendHeartbeat(supabase, 'offline', capabilities, deviceName);
2150
2247
  await channel.unsubscribe();
2151
- await removePidFile();
2248
+ await removePidFile(options);
2152
2249
  process.exit(0);
2153
2250
  };
2154
2251
  process.on('SIGINT', () => void shutdown('SIGINT'));
@@ -2161,15 +2258,16 @@ async function startGateway(options) {
2161
2258
  concurrency: GATEWAY_CONCURRENCY,
2162
2259
  });
2163
2260
  }
2164
- async function stopGateway() {
2165
- const pid = readPidFile();
2261
+ async function stopGateway(options) {
2262
+ const { pidPath } = resolveGatewayPaths(options);
2263
+ const pid = readPidFile(options);
2166
2264
  if (!pid) {
2167
- logInfo('No gateway pid file found', { pidPath: PID_PATH });
2265
+ logInfo('No gateway pid file found', { pidPath });
2168
2266
  return;
2169
2267
  }
2170
2268
  if (!isProcessAlive(pid)) {
2171
2269
  logInfo('Gateway not running, removing stale pid file', { pid });
2172
- await removePidFile();
2270
+ await removePidFile(options);
2173
2271
  return;
2174
2272
  }
2175
2273
  try {
@@ -2187,7 +2285,7 @@ async function stopGateway() {
2187
2285
  const start = Date.now();
2188
2286
  while (Date.now() - start < timeoutMs) {
2189
2287
  if (!isProcessAlive(pid)) {
2190
- await removePidFile();
2288
+ await removePidFile(options);
2191
2289
  logInfo('Gateway stopped', { pid });
2192
2290
  return;
2193
2291
  }
@@ -2195,19 +2293,45 @@ async function stopGateway() {
2195
2293
  }
2196
2294
  logInfo('Gateway stop timed out; process may still be running', { pid });
2197
2295
  }
2296
+ async function statusGateway(options) {
2297
+ const { pidPath, logPath, configPath } = resolveGatewayPaths(options);
2298
+ const pid = readPidFile(options);
2299
+ const configExists = fsSync.existsSync(configPath);
2300
+ if (!pid) {
2301
+ logInfo('Gateway status', {
2302
+ status: 'stopped',
2303
+ pidPath,
2304
+ logPath,
2305
+ configPath,
2306
+ configFound: configExists,
2307
+ });
2308
+ return;
2309
+ }
2310
+ const alive = isProcessAlive(pid);
2311
+ logInfo('Gateway status', {
2312
+ status: alive ? 'running' : 'stopped',
2313
+ pid,
2314
+ pidPath,
2315
+ logPath,
2316
+ configPath,
2317
+ configFound: configExists,
2318
+ stalePid: alive ? null : pid,
2319
+ });
2320
+ }
2198
2321
  async function showLogs(options) {
2199
2322
  const linesRaw = getStringOption(options, 'lines');
2200
2323
  const lines = linesRaw ? Number.parseInt(linesRaw, 10) : 200;
2201
2324
  const follow = options['no-follow'] !== true;
2325
+ const { logPath } = resolveGatewayPaths(options);
2202
2326
  try {
2203
- const content = await fs.readFile(LOG_PATH, 'utf-8');
2327
+ const content = await fs.readFile(logPath, 'utf-8');
2204
2328
  const allLines = content.split(/\r?\n/);
2205
2329
  const tail = Number.isFinite(lines) && lines > 0 ? allLines.slice(-lines) : allLines;
2206
2330
  process.stdout.write(`${tail.join('\n')}\n`);
2207
2331
  }
2208
2332
  catch (error) {
2209
2333
  logError('Unable to read gateway log file', {
2210
- logPath: LOG_PATH,
2334
+ logPath,
2211
2335
  error: error instanceof Error ? error.message : String(error),
2212
2336
  });
2213
2337
  return;
@@ -2216,22 +2340,22 @@ async function showLogs(options) {
2216
2340
  return;
2217
2341
  let position = 0;
2218
2342
  try {
2219
- const stat = await fs.stat(LOG_PATH);
2343
+ const stat = await fs.stat(logPath);
2220
2344
  position = stat.size;
2221
2345
  }
2222
2346
  catch {
2223
2347
  position = 0;
2224
2348
  }
2225
- logInfo('Tailing gateway logs', { logPath: LOG_PATH });
2226
- fsSync.watch(LOG_PATH, { persistent: true }, async (event) => {
2349
+ logInfo('Tailing gateway logs', { logPath });
2350
+ fsSync.watch(logPath, { persistent: true }, async (event) => {
2227
2351
  if (event !== 'change')
2228
2352
  return;
2229
2353
  try {
2230
- const stat = await fs.stat(LOG_PATH);
2354
+ const stat = await fs.stat(logPath);
2231
2355
  if (stat.size < position) {
2232
2356
  position = 0;
2233
2357
  }
2234
- const handle = await fs.open(LOG_PATH, 'r');
2358
+ const handle = await fs.open(logPath, 'r');
2235
2359
  const length = stat.size - position;
2236
2360
  if (length > 0) {
2237
2361
  const buffer = Buffer.alloc(length);
@@ -2243,7 +2367,7 @@ async function showLogs(options) {
2243
2367
  }
2244
2368
  catch (error) {
2245
2369
  logError('Failed to tail gateway logs', {
2246
- logPath: LOG_PATH,
2370
+ logPath,
2247
2371
  error: error instanceof Error ? error.message : String(error),
2248
2372
  });
2249
2373
  }
@@ -2252,6 +2376,7 @@ async function showLogs(options) {
2252
2376
  }
2253
2377
  async function run() {
2254
2378
  const parsed = parseArgs(process.argv.slice(2));
2379
+ ACTIVE_OPTIONS = parsed.options;
2255
2380
  if (parsed.options.h || parsed.options.help || parsed.command === null) {
2256
2381
  printHelp();
2257
2382
  return;
@@ -2271,7 +2396,11 @@ async function run() {
2271
2396
  return;
2272
2397
  }
2273
2398
  if (parsed.command === 'stop') {
2274
- await stopGateway();
2399
+ await stopGateway(parsed.options);
2400
+ return;
2401
+ }
2402
+ if (parsed.command === 'status') {
2403
+ await statusGateway(parsed.options);
2275
2404
  return;
2276
2405
  }
2277
2406
  if (parsed.command === 'logs') {