@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/README.md +25 -4
- package/dist/database.types.d.ts +5365 -0
- package/dist/database.types.d.ts.map +1 -0
- package/dist/database.types.js +9 -0
- package/dist/database.types.js.map +1 -0
- package/dist/index.js +220 -92
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
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\
|
|
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
|
|
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
|
-
|
|
188
|
-
await fs.
|
|
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(
|
|
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
|
-
|
|
218
|
-
await fs.
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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: `${
|
|
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
|
-
|
|
492
|
+
const claudeHome = resolveClaudeGatewayHome();
|
|
493
|
+
if (!claudeHome)
|
|
412
494
|
return;
|
|
413
|
-
await fs.mkdir(
|
|
414
|
-
await fs.mkdir(path.join(
|
|
415
|
-
await fs.mkdir(path.join(
|
|
416
|
-
await fs.mkdir(path.join(
|
|
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(
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
env.
|
|
445
|
-
env.
|
|
446
|
-
env.
|
|
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(
|
|
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: `${
|
|
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: `${
|
|
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
|
-
?
|
|
1439
|
+
? resolveCodexCommand()
|
|
1356
1440
|
: adapterId === 'gemini'
|
|
1357
|
-
?
|
|
1358
|
-
:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1945
|
-
|
|
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
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
}
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
2227
|
-
fsSync.watch(
|
|
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(
|
|
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(
|
|
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
|
|
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') {
|