@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/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.d.ts +1 -0
- package/dist/index.js +221 -92
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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
|
|
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\
|
|
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
|
|
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
|
-
|
|
187
|
-
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));
|
|
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(
|
|
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
|
-
|
|
217
|
-
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`);
|
|
218
295
|
}
|
|
219
|
-
async function removePidFile() {
|
|
296
|
+
async function removePidFile(options) {
|
|
297
|
+
const { pidPath } = resolveGatewayPaths(options);
|
|
220
298
|
try {
|
|
221
|
-
await fs.unlink(
|
|
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
|
|
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(
|
|
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(
|
|
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: `${
|
|
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
|
-
|
|
492
|
+
const claudeHome = resolveClaudeGatewayHome();
|
|
493
|
+
if (!claudeHome)
|
|
411
494
|
return;
|
|
412
|
-
await fs.mkdir(
|
|
413
|
-
await fs.mkdir(path.join(
|
|
414
|
-
await fs.mkdir(path.join(
|
|
415
|
-
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 });
|
|
416
499
|
const sourceSessionEnv = path.join(os.homedir(), '.claude', 'session-env');
|
|
417
|
-
const destSessionEnv = path.join(
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
env.
|
|
444
|
-
env.
|
|
445
|
-
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');
|
|
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(
|
|
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: `${
|
|
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: `${
|
|
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
|
-
?
|
|
1439
|
+
? resolveCodexCommand()
|
|
1355
1440
|
: adapterId === 'gemini'
|
|
1356
|
-
?
|
|
1357
|
-
:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1944
|
-
|
|
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
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
}
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
2226
|
-
fsSync.watch(
|
|
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(
|
|
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(
|
|
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
|
|
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') {
|