@panorama-ai/gateway 2.24.102 → 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
@@ -54,18 +54,8 @@ const CLAUDE_ENV_ALLOWLIST = [
54
54
  'SSH_AUTH_SOCK',
55
55
  'SSH_AGENT_PID',
56
56
  ];
57
- const CONFIG_DIR = process.env.PANORAMA_GATEWAY_CONFIG_DIR || path.join(os.homedir(), '.panorama');
58
- const SUBAGENT_WORKDIR_ROOT = path.join(CONFIG_DIR, 'subagents');
59
- const CLAUDE_GATEWAY_HOME = process.env.PANORAMA_CLAUDE_HOME || null;
60
- const CONFIG_PATH = process.env.PANORAMA_GATEWAY_CONFIG_PATH || path.join(CONFIG_DIR, 'gateway.json');
61
- const LOG_PATH = process.env.PANORAMA_GATEWAY_LOG_PATH || path.join(path.dirname(CONFIG_PATH), 'gateway.log');
62
- 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');
63
58
  const DEFAULT_CLAUDE_PATH = path.join(os.homedir(), '.claude', 'local', 'claude');
64
- const CLAUDE_COMMAND = process.env.PANORAMA_CLAUDE_CLI ||
65
- process.env.CLAUDE_CLI ||
66
- (fsSync.existsSync(DEFAULT_CLAUDE_PATH) ? DEFAULT_CLAUDE_PATH : 'claude');
67
- const CODEX_COMMAND = process.env.PANORAMA_CODEX_CLI || process.env.CODEX_CLI || 'codex';
68
- const GEMINI_COMMAND = process.env.PANORAMA_GEMINI_CLI || process.env.GEMINI_CLI || 'gemini';
69
59
  let CLAUDE_SUPPORT = null;
70
60
  let PROVIDER_CAPABILITIES = null;
71
61
  const GATEWAY_CONCURRENCY = (() => {
@@ -80,6 +70,7 @@ const GATEWAY_CONCURRENCY = (() => {
80
70
  const activeSubagentRuns = new Map();
81
71
  const pendingCancelByRunId = new Map();
82
72
  const pendingCancelBySubagentId = new Map();
73
+ let ACTIVE_OPTIONS = null;
83
74
  function parseArgs(argv) {
84
75
  const positional = [];
85
76
  const options = {};
@@ -113,7 +104,7 @@ function parseArgs(argv) {
113
104
  return { command, positional, options };
114
105
  }
115
106
  function printHelp() {
116
- 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`);
117
108
  }
118
109
  function getStringOption(options, key) {
119
110
  const value = options[key];
@@ -121,6 +112,87 @@ function getStringOption(options, key) {
121
112
  return value;
122
113
  return undefined;
123
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
+ }
124
196
  function normalizeEnvName(raw) {
125
197
  if (!raw)
126
198
  return null;
@@ -179,13 +251,15 @@ function resolveSupabaseAnonKey(options, config) {
179
251
  config?.supabaseAnonKey ||
180
252
  '');
181
253
  }
182
- async function loadConfig() {
183
- 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');
184
257
  return JSON.parse(raw);
185
258
  }
186
- async function saveConfig(config) {
187
- await fs.mkdir(path.dirname(CONFIG_PATH), { recursive: true });
188
- 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));
189
263
  }
190
264
  function logInfo(message, data) {
191
265
  if (data) {
@@ -203,9 +277,10 @@ function logError(message, data) {
203
277
  console.error(`[gateway] ${message}`);
204
278
  }
205
279
  }
206
- function readPidFile() {
280
+ function readPidFile(options) {
281
+ const { pidPath } = resolveGatewayPaths(options);
207
282
  try {
208
- const raw = fsSync.readFileSync(PID_PATH, 'utf-8').trim();
283
+ const raw = fsSync.readFileSync(pidPath, 'utf-8').trim();
209
284
  const pid = Number.parseInt(raw, 10);
210
285
  return Number.isFinite(pid) ? pid : null;
211
286
  }
@@ -213,13 +288,15 @@ function readPidFile() {
213
288
  return null;
214
289
  }
215
290
  }
216
- async function writePidFile(pid) {
217
- await fs.mkdir(path.dirname(PID_PATH), { recursive: true });
218
- 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`);
219
295
  }
220
- async function removePidFile() {
296
+ async function removePidFile(options) {
297
+ const { pidPath } = resolveGatewayPaths(options);
221
298
  try {
222
- await fs.unlink(PID_PATH);
299
+ await fs.unlink(pidPath);
223
300
  }
224
301
  catch {
225
302
  // ignore missing pid
@@ -279,17 +356,19 @@ async function pairGateway(code, options) {
279
356
  teamId: body.team_id,
280
357
  deviceName,
281
358
  };
282
- await saveConfig(config);
359
+ await saveConfig(config, options);
360
+ const { configPath } = resolveGatewayPaths(options);
283
361
  logInfo('Gateway paired successfully', {
284
362
  gatewayId: body.gateway_id,
285
363
  teamId: body.team_id,
286
- configPath: CONFIG_PATH,
364
+ configPath,
287
365
  });
288
366
  }
289
367
  async function execClaudeVersion() {
290
368
  const start = Date.now();
369
+ const command = resolveClaudeCommand();
291
370
  try {
292
- const { stdout, stderr } = await execFileAsync(CLAUDE_COMMAND, ['--version'], {
371
+ const { stdout, stderr } = await execFileAsync(command, ['--version'], {
293
372
  timeout: DEFAULT_CLAUDE_TIMEOUT_MS,
294
373
  });
295
374
  const durationMs = Date.now() - start;
@@ -318,8 +397,9 @@ async function execClaudeVersion() {
318
397
  }
319
398
  async function execClaudeHelp() {
320
399
  const start = Date.now();
400
+ const command = resolveClaudeCommand();
321
401
  try {
322
- const { stdout, stderr } = await execFileAsync(CLAUDE_COMMAND, ['--help'], {
402
+ const { stdout, stderr } = await execFileAsync(command, ['--help'], {
323
403
  timeout: DEFAULT_CLAUDE_TIMEOUT_MS,
324
404
  });
325
405
  const durationMs = Date.now() - start;
@@ -378,12 +458,13 @@ async function buildCapabilities() {
378
458
  if (claudeCapabilities?.supported_flags) {
379
459
  CLAUDE_SUPPORT = coerceClaudeSupport(claudeCapabilities.supported_flags);
380
460
  }
461
+ const resolvedClaudeCommand = resolveClaudeCommand();
381
462
  const claudeCliLegacy = claudeCapabilities
382
463
  ? {
383
464
  available: claudeCapabilities.available,
384
465
  version: claudeCapabilities.version,
385
466
  error: claudeCapabilities.error,
386
- command: `${CLAUDE_COMMAND} --version`,
467
+ command: `${resolvedClaudeCommand} --version`,
387
468
  supported_flags: claudeCapabilities.supported_flags,
388
469
  }
389
470
  : {
@@ -408,14 +489,15 @@ function appendClaudeIsolationArgs(args, support) {
408
489
  }
409
490
  }
410
491
  async function ensureClaudeGatewayHome() {
411
- if (!CLAUDE_GATEWAY_HOME)
492
+ const claudeHome = resolveClaudeGatewayHome();
493
+ if (!claudeHome)
412
494
  return;
413
- await fs.mkdir(CLAUDE_GATEWAY_HOME, { recursive: true });
414
- await fs.mkdir(path.join(CLAUDE_GATEWAY_HOME, 'xdg', 'config'), { recursive: true });
415
- await fs.mkdir(path.join(CLAUDE_GATEWAY_HOME, 'xdg', 'cache'), { recursive: true });
416
- 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 });
417
499
  const sourceSessionEnv = path.join(os.homedir(), '.claude', 'session-env');
418
- const destSessionEnv = path.join(CLAUDE_GATEWAY_HOME, 'session-env');
500
+ const destSessionEnv = path.join(claudeHome, 'session-env');
419
501
  try {
420
502
  const srcStat = await fs.stat(sourceSessionEnv);
421
503
  if (srcStat.isDirectory()) {
@@ -439,11 +521,12 @@ function buildClaudeEnv() {
439
521
  env[key] = value;
440
522
  }
441
523
  }
442
- if (CLAUDE_GATEWAY_HOME) {
443
- env.HOME = CLAUDE_GATEWAY_HOME;
444
- env.XDG_CONFIG_HOME = path.join(CLAUDE_GATEWAY_HOME, 'xdg', 'config');
445
- env.XDG_CACHE_HOME = path.join(CLAUDE_GATEWAY_HOME, 'xdg', 'cache');
446
- 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');
447
530
  }
448
531
  return env;
449
532
  }
@@ -672,7 +755,7 @@ function isUuid(value) {
672
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);
673
756
  }
674
757
  async function ensureSubagentWorkDir(subagentId) {
675
- const dir = path.join(SUBAGENT_WORKDIR_ROOT, subagentId);
758
+ const dir = path.join(resolveSubagentWorkdirRoot(), subagentId);
676
759
  await fs.mkdir(dir, { recursive: true });
677
760
  return dir;
678
761
  }
@@ -1022,11 +1105,12 @@ async function updateJobDebug(supabase, jobId, debug) {
1022
1105
  }
1023
1106
  async function handleDiagnosticJob() {
1024
1107
  const output = await execClaudeVersion();
1108
+ const command = resolveClaudeCommand();
1025
1109
  if (!output.ok) {
1026
1110
  return {
1027
1111
  ok: false,
1028
1112
  result: {
1029
- command: `${CLAUDE_COMMAND} --version`,
1113
+ command: `${command} --version`,
1030
1114
  stdout: output.stdout,
1031
1115
  stderr: output.stderr,
1032
1116
  exit_code: output.exitCode,
@@ -1039,7 +1123,7 @@ async function handleDiagnosticJob() {
1039
1123
  return {
1040
1124
  ok: true,
1041
1125
  result: {
1042
- command: `${CLAUDE_COMMAND} --version`,
1126
+ command: `${command} --version`,
1043
1127
  stdout: output.stdout,
1044
1128
  stderr: output.stderr,
1045
1129
  exit_code: output.exitCode,
@@ -1352,10 +1436,10 @@ async function handleSubagentRunJob(supabase, job) {
1352
1436
  ? toFlagRecord(CLAUDE_SUPPORT ?? DEFAULT_CLAUDE_SUPPORT)
1353
1437
  : {});
1354
1438
  const command = adapterId === 'codex'
1355
- ? CODEX_COMMAND
1439
+ ? resolveCodexCommand()
1356
1440
  : adapterId === 'gemini'
1357
- ? GEMINI_COMMAND
1358
- : CLAUDE_COMMAND;
1441
+ ? resolveGeminiCommand()
1442
+ : resolveClaudeCommand();
1359
1443
  runPlan = adapter.buildRunPlan({
1360
1444
  prompt,
1361
1445
  config: configPayload,
@@ -1929,49 +2013,61 @@ async function processJob(supabase, config, job) {
1929
2013
  }, `Unsupported job type: ${claimed.job_type}`);
1930
2014
  }
1931
2015
  async function startGateway(options) {
1932
- const daemonRequested = options.daemon === true || options.background === true;
2016
+ const paths = resolveGatewayPaths(options);
1933
2017
  const foregroundRequested = options.foreground === true;
2018
+ const explicitDaemon = options.daemon === true || options.background === true;
2019
+ let daemonRequested = explicitDaemon || !foregroundRequested;
1934
2020
  if (daemonRequested && !foregroundRequested) {
1935
- const existingPid = readPidFile();
2021
+ const existingPid = readPidFile(options);
1936
2022
  if (existingPid && isProcessAlive(existingPid)) {
1937
2023
  logInfo('Gateway already running', { pid: existingPid });
1938
2024
  return;
1939
2025
  }
1940
2026
  const entryPath = process.argv[1] || '';
1941
2027
  if (entryPath.endsWith('.ts')) {
1942
- 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
+ }
1943
2035
  }
1944
- await fs.mkdir(path.dirname(LOG_PATH), { recursive: true });
1945
- const logHandle = await fs.open(LOG_PATH, 'a');
1946
- const childArgs = [entryPath, 'start', '--foreground'];
1947
- const deviceName = getStringOption(options, 'device-name') || process.env.PANORAMA_GATEWAY_DEVICE_NAME;
1948
- if (deviceName) {
1949
- childArgs.push('--device-name', deviceName);
2036
+ if (!daemonRequested) {
2037
+ // fall through to foreground startup
1950
2038
  }
1951
- const child = spawn(process.execPath, childArgs, {
1952
- detached: true,
1953
- stdio: ['ignore', logHandle.fd, logHandle.fd],
1954
- env: {
1955
- ...process.env,
1956
- PANORAMA_GATEWAY_LOG_PATH: LOG_PATH,
1957
- PANORAMA_GATEWAY_PID_PATH: PID_PATH,
1958
- },
1959
- });
1960
- child.unref();
1961
- await logHandle.close();
1962
- if (!child.pid) {
1963
- 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;
1964
2063
  }
1965
- await writePidFile(child.pid);
1966
- logInfo('Gateway started in background', { pid: child.pid, logPath: LOG_PATH });
1967
- return;
1968
2064
  }
1969
2065
  let config;
1970
2066
  try {
1971
- config = await loadConfig();
2067
+ config = await loadConfig(options);
1972
2068
  }
1973
2069
  catch (error) {
1974
- 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)})`);
1975
2071
  }
1976
2072
  const supabaseUrl = resolveSupabaseUrl(options, config);
1977
2073
  const supabaseAnonKey = resolveSupabaseAnonKey(options, config);
@@ -1988,7 +2084,7 @@ async function startGateway(options) {
1988
2084
  supabaseAnonKey,
1989
2085
  deviceName,
1990
2086
  };
1991
- await saveConfig(config);
2087
+ await saveConfig(config, options);
1992
2088
  const supabase = createClient(supabaseUrl, supabaseAnonKey, {
1993
2089
  auth: {
1994
2090
  persistSession: false,
@@ -2010,14 +2106,14 @@ async function startGateway(options) {
2010
2106
  }
2011
2107
  config.accessToken = sessionData.session.access_token;
2012
2108
  config.refreshToken = sessionData.session.refresh_token;
2013
- await saveConfig(config);
2109
+ await saveConfig(config, options);
2014
2110
  supabase.auth.onAuthStateChange((event, session) => {
2015
2111
  if (!session)
2016
2112
  return;
2017
2113
  if (event === 'TOKEN_REFRESHED' || event === 'SIGNED_IN') {
2018
2114
  config.accessToken = session.access_token;
2019
2115
  config.refreshToken = session.refresh_token;
2020
- void saveConfig(config)
2116
+ void saveConfig(config, options)
2021
2117
  .then(() => logInfo('Gateway session refreshed'))
2022
2118
  .catch((error) => {
2023
2119
  logError('Failed to persist refreshed gateway session', {
@@ -2043,7 +2139,7 @@ async function startGateway(options) {
2043
2139
  });
2044
2140
  await sendHeartbeat(supabase, initialStatus, capabilities, deviceName);
2045
2141
  let heartbeatStatus = initialStatus;
2046
- await writePidFile(process.pid);
2142
+ await writePidFile(process.pid, options);
2047
2143
  const heartbeatTimer = setInterval(() => {
2048
2144
  void sendHeartbeat(supabase, heartbeatStatus, capabilities, deviceName);
2049
2145
  }, DEFAULT_HEARTBEAT_INTERVAL_MS);
@@ -2149,7 +2245,7 @@ async function startGateway(options) {
2149
2245
  heartbeatStatus = 'offline';
2150
2246
  await sendHeartbeat(supabase, 'offline', capabilities, deviceName);
2151
2247
  await channel.unsubscribe();
2152
- await removePidFile();
2248
+ await removePidFile(options);
2153
2249
  process.exit(0);
2154
2250
  };
2155
2251
  process.on('SIGINT', () => void shutdown('SIGINT'));
@@ -2162,15 +2258,16 @@ async function startGateway(options) {
2162
2258
  concurrency: GATEWAY_CONCURRENCY,
2163
2259
  });
2164
2260
  }
2165
- async function stopGateway() {
2166
- const pid = readPidFile();
2261
+ async function stopGateway(options) {
2262
+ const { pidPath } = resolveGatewayPaths(options);
2263
+ const pid = readPidFile(options);
2167
2264
  if (!pid) {
2168
- logInfo('No gateway pid file found', { pidPath: PID_PATH });
2265
+ logInfo('No gateway pid file found', { pidPath });
2169
2266
  return;
2170
2267
  }
2171
2268
  if (!isProcessAlive(pid)) {
2172
2269
  logInfo('Gateway not running, removing stale pid file', { pid });
2173
- await removePidFile();
2270
+ await removePidFile(options);
2174
2271
  return;
2175
2272
  }
2176
2273
  try {
@@ -2188,7 +2285,7 @@ async function stopGateway() {
2188
2285
  const start = Date.now();
2189
2286
  while (Date.now() - start < timeoutMs) {
2190
2287
  if (!isProcessAlive(pid)) {
2191
- await removePidFile();
2288
+ await removePidFile(options);
2192
2289
  logInfo('Gateway stopped', { pid });
2193
2290
  return;
2194
2291
  }
@@ -2196,19 +2293,45 @@ async function stopGateway() {
2196
2293
  }
2197
2294
  logInfo('Gateway stop timed out; process may still be running', { pid });
2198
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
+ }
2199
2321
  async function showLogs(options) {
2200
2322
  const linesRaw = getStringOption(options, 'lines');
2201
2323
  const lines = linesRaw ? Number.parseInt(linesRaw, 10) : 200;
2202
2324
  const follow = options['no-follow'] !== true;
2325
+ const { logPath } = resolveGatewayPaths(options);
2203
2326
  try {
2204
- const content = await fs.readFile(LOG_PATH, 'utf-8');
2327
+ const content = await fs.readFile(logPath, 'utf-8');
2205
2328
  const allLines = content.split(/\r?\n/);
2206
2329
  const tail = Number.isFinite(lines) && lines > 0 ? allLines.slice(-lines) : allLines;
2207
2330
  process.stdout.write(`${tail.join('\n')}\n`);
2208
2331
  }
2209
2332
  catch (error) {
2210
2333
  logError('Unable to read gateway log file', {
2211
- logPath: LOG_PATH,
2334
+ logPath,
2212
2335
  error: error instanceof Error ? error.message : String(error),
2213
2336
  });
2214
2337
  return;
@@ -2217,22 +2340,22 @@ async function showLogs(options) {
2217
2340
  return;
2218
2341
  let position = 0;
2219
2342
  try {
2220
- const stat = await fs.stat(LOG_PATH);
2343
+ const stat = await fs.stat(logPath);
2221
2344
  position = stat.size;
2222
2345
  }
2223
2346
  catch {
2224
2347
  position = 0;
2225
2348
  }
2226
- logInfo('Tailing gateway logs', { logPath: LOG_PATH });
2227
- fsSync.watch(LOG_PATH, { persistent: true }, async (event) => {
2349
+ logInfo('Tailing gateway logs', { logPath });
2350
+ fsSync.watch(logPath, { persistent: true }, async (event) => {
2228
2351
  if (event !== 'change')
2229
2352
  return;
2230
2353
  try {
2231
- const stat = await fs.stat(LOG_PATH);
2354
+ const stat = await fs.stat(logPath);
2232
2355
  if (stat.size < position) {
2233
2356
  position = 0;
2234
2357
  }
2235
- const handle = await fs.open(LOG_PATH, 'r');
2358
+ const handle = await fs.open(logPath, 'r');
2236
2359
  const length = stat.size - position;
2237
2360
  if (length > 0) {
2238
2361
  const buffer = Buffer.alloc(length);
@@ -2244,7 +2367,7 @@ async function showLogs(options) {
2244
2367
  }
2245
2368
  catch (error) {
2246
2369
  logError('Failed to tail gateway logs', {
2247
- logPath: LOG_PATH,
2370
+ logPath,
2248
2371
  error: error instanceof Error ? error.message : String(error),
2249
2372
  });
2250
2373
  }
@@ -2253,6 +2376,7 @@ async function showLogs(options) {
2253
2376
  }
2254
2377
  async function run() {
2255
2378
  const parsed = parseArgs(process.argv.slice(2));
2379
+ ACTIVE_OPTIONS = parsed.options;
2256
2380
  if (parsed.options.h || parsed.options.help || parsed.command === null) {
2257
2381
  printHelp();
2258
2382
  return;
@@ -2272,7 +2396,11 @@ async function run() {
2272
2396
  return;
2273
2397
  }
2274
2398
  if (parsed.command === 'stop') {
2275
- await stopGateway();
2399
+ await stopGateway(parsed.options);
2400
+ return;
2401
+ }
2402
+ if (parsed.command === 'status') {
2403
+ await statusGateway(parsed.options);
2276
2404
  return;
2277
2405
  }
2278
2406
  if (parsed.command === 'logs') {