mobygate 0.3.0
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/CHANGELOG.md +207 -0
- package/LICENSE +21 -0
- package/README.md +429 -0
- package/bin/mobygate.js +443 -0
- package/index.html +805 -0
- package/launchd/ai.mobygate.auth-refresh.plist +83 -0
- package/lib/ascii.js +108 -0
- package/lib/config.js +131 -0
- package/lib/dashboard-bus.js +158 -0
- package/lib/platform.js +584 -0
- package/lib/session-store.js +112 -0
- package/mcp-inspect.mjs +186 -0
- package/package.json +62 -0
- package/scripts/auth-helper.js +198 -0
- package/scripts/auth-refresh.js +41 -0
- package/scripts/auth-status.js +36 -0
- package/server.js +1076 -0
package/bin/mobygate.js
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* mobygate CLI — install, run, and manage the proxy.
|
|
4
|
+
*
|
|
5
|
+
* Sub-commands:
|
|
6
|
+
* init Interactive setup: check prereqs, write config,
|
|
7
|
+
* install launchd/systemd/Task-Scheduler services,
|
|
8
|
+
* start the proxy, smoke-test.
|
|
9
|
+
* start Load the server service (or start the proxy directly
|
|
10
|
+
* if no service is installed).
|
|
11
|
+
* stop Unload the server service.
|
|
12
|
+
* restart Stop + start.
|
|
13
|
+
* status Show service state, auth state, /health probe.
|
|
14
|
+
* logs Tail the server log.
|
|
15
|
+
* auth Auth status + force-refresh probe.
|
|
16
|
+
* uninstall Remove installed services and the config directory.
|
|
17
|
+
*
|
|
18
|
+
* Run `mobygate <command> --help` for per-command flags.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
22
|
+
import { resolve, dirname, join } from 'path';
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
import { spawnSync } from 'child_process';
|
|
25
|
+
import { createInterface } from 'readline';
|
|
26
|
+
|
|
27
|
+
import { loadConfig, writeConfig, writeState, readState, CONFIG_DIR, CONFIG_PATH, LOGS_DIR } from '../lib/config.js';
|
|
28
|
+
import {
|
|
29
|
+
PLATFORM, IS_MAC, IS_LINUX, IS_WIN,
|
|
30
|
+
resolveNodeBin,
|
|
31
|
+
writeMacServerPlist, launchctlLoad, launchctlUnload,
|
|
32
|
+
plistPathForLabel, queryLaunchd, uninstallAllServices,
|
|
33
|
+
installWindowsServices, uninstallWindowsServices,
|
|
34
|
+
queryWindowsTask, startWindowsTask, stopWindowsTask, WIN_LABELS,
|
|
35
|
+
installLinuxServices, uninstallLinuxServices,
|
|
36
|
+
queryLinuxUnit, startLinuxUnit, stopLinuxUnit, LINUX_UNITS,
|
|
37
|
+
nonMacInstallInstructions,
|
|
38
|
+
} from '../lib/platform.js';
|
|
39
|
+
import { getAuthStatus, forceRefresh } from '../scripts/auth-helper.js';
|
|
40
|
+
import { banner, compactBanner } from '../lib/ascii.js';
|
|
41
|
+
|
|
42
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
43
|
+
const REPO_ROOT = resolve(dirname(__filename), '..');
|
|
44
|
+
const SERVER_LABEL = 'ai.mobygate.server';
|
|
45
|
+
const AUTH_LABEL = 'ai.mobygate.auth-refresh';
|
|
46
|
+
|
|
47
|
+
// ---------- tiny helpers ----------
|
|
48
|
+
|
|
49
|
+
const c = {
|
|
50
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
51
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
52
|
+
green:(s) => `\x1b[32m${s}\x1b[0m`,
|
|
53
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
54
|
+
yell: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
55
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function print(...args) { console.log(...args); }
|
|
59
|
+
function die(msg, code = 1) { console.error(c.red('✗ ' + msg)); process.exit(code); }
|
|
60
|
+
function ok(msg) { print(c.green('✓ ') + msg); }
|
|
61
|
+
function warn(msg) { print(c.yell('! ') + msg); }
|
|
62
|
+
function info(msg) { print(c.cyan('→ ') + msg); }
|
|
63
|
+
function section(title) { print('\n' + c.bold(title)); }
|
|
64
|
+
|
|
65
|
+
function prompt(q, def = '') {
|
|
66
|
+
return new Promise((res) => {
|
|
67
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
68
|
+
const defaultText = def ? c.dim(` [${def}]`) : '';
|
|
69
|
+
rl.question(`${q}${defaultText} `, (ans) => { rl.close(); res((ans || '').trim() || def); });
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function confirm(q, def = true) {
|
|
74
|
+
const tag = def ? 'Y/n' : 'y/N';
|
|
75
|
+
const ans = (await prompt(`${q} (${tag})`, '')).toLowerCase();
|
|
76
|
+
if (!ans) return def;
|
|
77
|
+
return ans.startsWith('y');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------- commands ----------
|
|
81
|
+
|
|
82
|
+
async function cmdInit() {
|
|
83
|
+
const pkg = JSON.parse(readFileSync(join(REPO_ROOT, 'package.json'), 'utf8'));
|
|
84
|
+
print(banner({ version: pkg.version }));
|
|
85
|
+
section('mobygate init');
|
|
86
|
+
print(c.dim(`Platform: ${PLATFORM} Install path: ${REPO_ROOT}`));
|
|
87
|
+
print(c.dim(`Config: ${CONFIG_PATH}`));
|
|
88
|
+
print('');
|
|
89
|
+
|
|
90
|
+
// ---- Prereq: node version ----
|
|
91
|
+
const major = parseInt(process.versions.node.split('.')[0], 10);
|
|
92
|
+
if (major < 18) die(`Node 18+ required (you have ${process.version}). Upgrade via fnm/nvm/volta.`);
|
|
93
|
+
ok(`Node ${process.version}`);
|
|
94
|
+
|
|
95
|
+
// ---- Prereq: claude CLI present ----
|
|
96
|
+
const claudeProbe = spawnSync(IS_WIN ? 'where' : 'which', ['claude'], { encoding: 'utf8' });
|
|
97
|
+
if (claudeProbe.status !== 0) {
|
|
98
|
+
warn('`claude` CLI not found on PATH.');
|
|
99
|
+
print(c.dim(' The proxy uses Claude Code\'s CLI to resolve OAuth credentials.'));
|
|
100
|
+
print(c.dim(' Install: https://docs.claude.com/en/docs/claude-code'));
|
|
101
|
+
const proceed = await confirm('Continue anyway? (you can set CLAUDE_BIN in config.yaml later)', false);
|
|
102
|
+
if (!proceed) die('Aborted.');
|
|
103
|
+
} else {
|
|
104
|
+
ok(`claude CLI at ${claudeProbe.stdout.trim().split('\n')[0]}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---- Prereq: claude auth status ----
|
|
108
|
+
try {
|
|
109
|
+
const authStatus = await getAuthStatus();
|
|
110
|
+
if (authStatus.ok && authStatus.loggedIn) {
|
|
111
|
+
ok(`Logged in as ${authStatus.email || '(email unknown)'} (${authStatus.subscriptionType || authStatus.authMethod})`);
|
|
112
|
+
} else {
|
|
113
|
+
warn('Not logged into Claude Max.');
|
|
114
|
+
print(c.dim(' Run `claude auth login` in another terminal, then re-run `mobygate init`.'));
|
|
115
|
+
const proceed = await confirm('Continue without login?', false);
|
|
116
|
+
if (!proceed) die('Aborted.');
|
|
117
|
+
}
|
|
118
|
+
} catch (e) {
|
|
119
|
+
warn(`Couldn't check auth status: ${e.message}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---- Config ----
|
|
123
|
+
section('Configuration');
|
|
124
|
+
const existing = loadConfig();
|
|
125
|
+
const port = await prompt('HTTP port', String(existing.port));
|
|
126
|
+
const defaultModel = await prompt('Default model', existing.default_model);
|
|
127
|
+
const sessionTtl = await prompt('Session TTL (minutes)', String(existing.session_ttl_minutes));
|
|
128
|
+
const claudeBin = claudeProbe.status === 0
|
|
129
|
+
? (await prompt('CLAUDE_BIN override (blank = PATH)', existing.claude_bin || ''))
|
|
130
|
+
: (await prompt('CLAUDE_BIN (absolute path to claude binary)', existing.claude_bin || ''));
|
|
131
|
+
|
|
132
|
+
const configPath = writeConfig({
|
|
133
|
+
port: parseInt(port, 10) || 3456,
|
|
134
|
+
default_model: defaultModel,
|
|
135
|
+
session_ttl_minutes: parseInt(sessionTtl, 10) || 60,
|
|
136
|
+
claude_bin: claudeBin,
|
|
137
|
+
});
|
|
138
|
+
ok(`Wrote ${configPath}`);
|
|
139
|
+
|
|
140
|
+
// ---- Logs dir (canonical per-user location, outside install dir) ----
|
|
141
|
+
const logsDir = LOGS_DIR;
|
|
142
|
+
if (!existsSync(logsDir)) mkdirSync(logsDir, { recursive: true });
|
|
143
|
+
ok(`Logs: ${logsDir}`);
|
|
144
|
+
|
|
145
|
+
// ---- Services ----
|
|
146
|
+
section('Services');
|
|
147
|
+
const nodeBin = resolveNodeBin();
|
|
148
|
+
const portNum = parseInt(port, 10) || 3456;
|
|
149
|
+
if (IS_MAC) {
|
|
150
|
+
// Server plist
|
|
151
|
+
const serverPlist = writeMacServerPlist({
|
|
152
|
+
installPath: REPO_ROOT,
|
|
153
|
+
nodeBin,
|
|
154
|
+
port: portNum,
|
|
155
|
+
logsDir,
|
|
156
|
+
});
|
|
157
|
+
launchctlLoad(serverPlist);
|
|
158
|
+
ok(`Installed ${SERVER_LABEL} (launchd)`);
|
|
159
|
+
|
|
160
|
+
// Auth refresh plist (we ship a template in launchd/ — copy + rewrite
|
|
161
|
+
// WorkingDirectory, node path, and log paths to match this install).
|
|
162
|
+
const authSrc = join(REPO_ROOT, 'launchd', 'ai.mobygate.auth-refresh.plist');
|
|
163
|
+
if (existsSync(authSrc)) {
|
|
164
|
+
const authDst = plistPathForLabel(AUTH_LABEL);
|
|
165
|
+
const tmpl = readFileSync(authSrc, 'utf8')
|
|
166
|
+
// WorkingDirectory + any path that referenced the repo root
|
|
167
|
+
.replace(/\/Users\/farhan\/openclaude\/claude-max-sdk-proxy\/logs/g, logsDir)
|
|
168
|
+
.replace(/\/Users\/farhan\/openclaude\/claude-max-sdk-proxy/g, REPO_ROOT)
|
|
169
|
+
// node binary baked into ProgramArguments
|
|
170
|
+
.replace(/\/Users\/farhan\/\.local\/share\/fnm\/aliases\/default\/bin\/node/g, nodeBin);
|
|
171
|
+
writeFileSync(authDst, tmpl);
|
|
172
|
+
launchctlLoad(authDst);
|
|
173
|
+
ok(`Installed ${AUTH_LABEL} (launchd, every ${existing.auth_refresh_interval_hours}h)`);
|
|
174
|
+
}
|
|
175
|
+
} else if (IS_WIN) {
|
|
176
|
+
// Register Task Scheduler entries and kick the server task now.
|
|
177
|
+
const r = installWindowsServices({
|
|
178
|
+
installPath: REPO_ROOT,
|
|
179
|
+
nodeBin,
|
|
180
|
+
port: portNum,
|
|
181
|
+
authRefreshHours: existing.auth_refresh_interval_hours,
|
|
182
|
+
});
|
|
183
|
+
for (const t of r.installed) ok(`Installed ${t} (Task Scheduler, user-level)`);
|
|
184
|
+
for (const e of r.errors) warn(`Failed to register ${e.task}: ${e.stderr || '(no error detail)'}`);
|
|
185
|
+
if (!r.ok) {
|
|
186
|
+
warn('Some tasks failed. You can retry manually — here is the PowerShell:');
|
|
187
|
+
print(nonMacInstallInstructions({ installPath: REPO_ROOT, nodeBin, port: portNum }));
|
|
188
|
+
}
|
|
189
|
+
} else if (IS_LINUX) {
|
|
190
|
+
// Install + enable systemd user units
|
|
191
|
+
const r = installLinuxServices({
|
|
192
|
+
installPath: REPO_ROOT,
|
|
193
|
+
nodeBin,
|
|
194
|
+
port: portNum,
|
|
195
|
+
authRefreshHours: existing.auth_refresh_interval_hours,
|
|
196
|
+
});
|
|
197
|
+
for (const u of r.installed) ok(`Installed ${u} (systemd user unit)`);
|
|
198
|
+
for (const e of r.errors) warn(`Failed on ${e.unit}: ${e.stderr || '(no error detail)'}`);
|
|
199
|
+
if (!r.ok) {
|
|
200
|
+
warn('Some units failed. You can retry manually:');
|
|
201
|
+
print(nonMacInstallInstructions({ installPath: REPO_ROOT, nodeBin, port: portNum }));
|
|
202
|
+
} else {
|
|
203
|
+
info('Tip: for the server to run when you are not logged in, enable linger:');
|
|
204
|
+
print(c.dim(` sudo loginctl enable-linger ${process.env.USER || process.env.USERNAME || 'your-user'}`));
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
warn(`${PLATFORM} — automated service install not supported.`);
|
|
208
|
+
print(nonMacInstallInstructions({ installPath: REPO_ROOT, nodeBin, port: portNum }));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
writeState({
|
|
212
|
+
install_path: REPO_ROOT,
|
|
213
|
+
installed_version: '0.1.0',
|
|
214
|
+
installed_at: new Date().toISOString(),
|
|
215
|
+
platform: PLATFORM,
|
|
216
|
+
node_bin: nodeBin,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// ---- Smoke ----
|
|
220
|
+
section('Smoke test');
|
|
221
|
+
if (IS_MAC || IS_WIN || IS_LINUX) {
|
|
222
|
+
// Service managers need a beat to actually launch the process
|
|
223
|
+
await new Promise((r) => setTimeout(r, 2500));
|
|
224
|
+
try {
|
|
225
|
+
const h = await fetch(`http://localhost:${portNum}/health`, { signal: AbortSignal.timeout(3000) });
|
|
226
|
+
if (h.ok) ok('Server responded on /health');
|
|
227
|
+
else warn(`Health check returned ${h.status}`);
|
|
228
|
+
} catch (e) {
|
|
229
|
+
warn(`Health check failed: ${e.message}`);
|
|
230
|
+
if (IS_WIN) print(c.dim(' Run `mobygate logs` or check Task Scheduler history for startup errors.'));
|
|
231
|
+
else if (IS_LINUX) print(c.dim(' Run `journalctl --user -u mobygate-server -n 50` or check logs/server.err.log.'));
|
|
232
|
+
else print(c.dim(' Check logs/server.err.log for startup errors.'));
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
info('Start the server manually (see instructions above) then run `mobygate status`.');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
section('Done');
|
|
239
|
+
print(`Dashboard: ${c.cyan(`http://localhost:${port}`)}`);
|
|
240
|
+
print(`Configure: ${c.cyan(CONFIG_PATH)}`);
|
|
241
|
+
print(`Try: ${c.cyan('mobygate status')} | ${c.cyan('mobygate logs')} | ${c.cyan('mobygate auth')}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function cmdStart() {
|
|
245
|
+
if (IS_MAC) {
|
|
246
|
+
const p = plistPathForLabel(SERVER_LABEL);
|
|
247
|
+
if (!existsSync(p)) return die(`${SERVER_LABEL} not installed — run \`mobygate init\` first.`);
|
|
248
|
+
launchctlLoad(p);
|
|
249
|
+
ok(`Loaded ${SERVER_LABEL}`);
|
|
250
|
+
} else if (IS_WIN) {
|
|
251
|
+
const r = startWindowsTask(WIN_LABELS.server);
|
|
252
|
+
if (!r.ok) return die(`Failed to start ${WIN_LABELS.server}: ${r.stderr || 'unknown'}`);
|
|
253
|
+
ok(`Started ${WIN_LABELS.server}`);
|
|
254
|
+
} else if (IS_LINUX) {
|
|
255
|
+
const r = startLinuxUnit(LINUX_UNITS.server);
|
|
256
|
+
if (!r.ok) return die(`Failed to start ${LINUX_UNITS.server}: ${r.stderr || 'unknown'}`);
|
|
257
|
+
ok(`Started ${LINUX_UNITS.server}`);
|
|
258
|
+
} else {
|
|
259
|
+
die('`mobygate start` not supported on this platform. Start manually: node server.js');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function cmdStop() {
|
|
264
|
+
if (IS_MAC) {
|
|
265
|
+
const p = plistPathForLabel(SERVER_LABEL);
|
|
266
|
+
launchctlUnload(p);
|
|
267
|
+
ok(`Unloaded ${SERVER_LABEL}`);
|
|
268
|
+
} else if (IS_WIN) {
|
|
269
|
+
const r = stopWindowsTask(WIN_LABELS.server);
|
|
270
|
+
if (!r.ok) return die(`Failed to stop ${WIN_LABELS.server}: ${r.stderr || 'unknown'}`);
|
|
271
|
+
ok(`Stopped ${WIN_LABELS.server}`);
|
|
272
|
+
} else if (IS_LINUX) {
|
|
273
|
+
const r = stopLinuxUnit(LINUX_UNITS.server);
|
|
274
|
+
if (!r.ok) return die(`Failed to stop ${LINUX_UNITS.server}: ${r.stderr || 'unknown'}`);
|
|
275
|
+
ok(`Stopped ${LINUX_UNITS.server}`);
|
|
276
|
+
} else {
|
|
277
|
+
die('`mobygate stop` not supported on this platform.');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function cmdRestart() {
|
|
282
|
+
cmdStop();
|
|
283
|
+
cmdStart();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function cmdStatus() {
|
|
287
|
+
const pkg = JSON.parse(readFileSync(join(REPO_ROOT, 'package.json'), 'utf8'));
|
|
288
|
+
print(compactBanner({ version: pkg.version }));
|
|
289
|
+
section('mobygate status');
|
|
290
|
+
const config = loadConfig();
|
|
291
|
+
print(c.dim(`Config: ${config.source}`));
|
|
292
|
+
print(c.dim(`Port: ${config.port}`));
|
|
293
|
+
print(c.dim(`Model: ${config.default_model}`));
|
|
294
|
+
|
|
295
|
+
if (IS_MAC) {
|
|
296
|
+
for (const label of [SERVER_LABEL, AUTH_LABEL]) {
|
|
297
|
+
const s = queryLaunchd(label);
|
|
298
|
+
if (!s.loaded) { warn(`${label}: not loaded`); continue; }
|
|
299
|
+
const pidPart = s.pid ? `pid ${s.pid}` : 'not running';
|
|
300
|
+
const exitPart = s.lastExit !== null && s.lastExit !== 0 ? ` last exit ${c.red(s.lastExit)}` : '';
|
|
301
|
+
ok(`${label}: ${pidPart}${exitPart}`);
|
|
302
|
+
}
|
|
303
|
+
} else if (IS_WIN) {
|
|
304
|
+
for (const name of [WIN_LABELS.server, WIN_LABELS.auth]) {
|
|
305
|
+
const s = queryWindowsTask(name);
|
|
306
|
+
if (!s.exists) { warn(`${name}: not installed`); continue; }
|
|
307
|
+
const resultPart = s.lastResult !== null && s.lastResult !== 0 ? ` last result ${c.red(s.lastResult)}` : '';
|
|
308
|
+
ok(`${name}: ${s.state || 'unknown'}${resultPart}`);
|
|
309
|
+
}
|
|
310
|
+
} else if (IS_LINUX) {
|
|
311
|
+
for (const unit of [LINUX_UNITS.server, LINUX_UNITS.timer]) {
|
|
312
|
+
const s = queryLinuxUnit(unit);
|
|
313
|
+
if (!s.exists) { warn(`${unit}: not installed`); continue; }
|
|
314
|
+
const pidPart = s.mainPid ? ` pid ${s.mainPid}` : '';
|
|
315
|
+
const state = s.active === 'failed' ? c.red(s.active) : s.active;
|
|
316
|
+
ok(`${unit}: ${state}${pidPart}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
const h = await fetch(`http://localhost:${config.port}/health`, { signal: AbortSignal.timeout(2000) });
|
|
322
|
+
if (h.ok) {
|
|
323
|
+
const j = await h.json();
|
|
324
|
+
ok(`/health OK — ${j.activeSessions} active session(s)`);
|
|
325
|
+
} else {
|
|
326
|
+
warn(`/health returned ${h.status}`);
|
|
327
|
+
}
|
|
328
|
+
} catch (e) {
|
|
329
|
+
warn(`/health unreachable: ${e.message}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
const auth = await getAuthStatus();
|
|
334
|
+
if (auth.ok && auth.loggedIn) ok(`auth: ${auth.email || 'logged in'} (${auth.subscriptionType || auth.authMethod})`);
|
|
335
|
+
else warn(`auth: not logged in`);
|
|
336
|
+
} catch (e) {
|
|
337
|
+
warn(`auth check failed: ${e.message}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function cmdLogs() {
|
|
342
|
+
const logFile = join(LOGS_DIR, 'server.log');
|
|
343
|
+
if (!existsSync(logFile)) return die(`No log file at ${logFile} — is the server running?`);
|
|
344
|
+
// Delegate to tail -f
|
|
345
|
+
const cmd = IS_WIN ? 'powershell' : 'tail';
|
|
346
|
+
const args = IS_WIN ? ['-Command', `Get-Content -Path "${logFile}" -Wait -Tail 50`] : ['-f', '-n', '50', logFile];
|
|
347
|
+
spawnSync(cmd, args, { stdio: 'inherit' });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function cmdAuth() {
|
|
351
|
+
section('mobygate auth');
|
|
352
|
+
const status = await getAuthStatus();
|
|
353
|
+
print(JSON.stringify(status, null, 2));
|
|
354
|
+
if (!status.ok || !status.loggedIn) {
|
|
355
|
+
warn('Not logged in — run `claude auth login`.');
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
info('Probing...');
|
|
359
|
+
const probe = await forceRefresh();
|
|
360
|
+
if (probe.ok) ok(`probe succeeded in ${probe.durationMs}ms`);
|
|
361
|
+
else die(`probe failed: ${probe.error}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function cmdUninstall() {
|
|
365
|
+
const y = await confirm('Remove installed services and leave the repo in place?', false);
|
|
366
|
+
if (!y) return info('Aborted.');
|
|
367
|
+
if (IS_MAC) {
|
|
368
|
+
const results = uninstallAllServices();
|
|
369
|
+
for (const r of results) {
|
|
370
|
+
if (r.removed) ok(`Removed ${r.label}`);
|
|
371
|
+
else info(`${r.label}: ${r.reason || r.error || 'n/a'}`);
|
|
372
|
+
}
|
|
373
|
+
} else if (IS_WIN) {
|
|
374
|
+
const results = uninstallWindowsServices({ installPath: REPO_ROOT });
|
|
375
|
+
for (const r of results) {
|
|
376
|
+
if (r.removed) ok(`Removed ${r.name}`);
|
|
377
|
+
else info(`${r.name}: ${r.reason || r.error || 'n/a'}`);
|
|
378
|
+
}
|
|
379
|
+
} else if (IS_LINUX) {
|
|
380
|
+
const results = uninstallLinuxServices();
|
|
381
|
+
for (const r of results) {
|
|
382
|
+
if (r.removed) ok(`Removed ${r.unit}`);
|
|
383
|
+
else info(`${r.unit}: ${r.stopped ? 'stopped' : 'not present'}`);
|
|
384
|
+
}
|
|
385
|
+
} else {
|
|
386
|
+
info('Uninstall for this platform not supported.');
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function cmdVersion() {
|
|
391
|
+
const pkg = JSON.parse(readFileSync(join(REPO_ROOT, 'package.json'), 'utf8'));
|
|
392
|
+
print(`mobygate v${pkg.version}`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function usage() {
|
|
396
|
+
print(`mobygate — OpenAI → Claude Max local gateway
|
|
397
|
+
|
|
398
|
+
Usage:
|
|
399
|
+
mobygate init Interactive setup: config, services, smoke test
|
|
400
|
+
mobygate start Start the proxy service
|
|
401
|
+
mobygate stop Stop the proxy service
|
|
402
|
+
mobygate restart Stop + start
|
|
403
|
+
mobygate status Show service + auth + /health state
|
|
404
|
+
mobygate logs Tail the server log
|
|
405
|
+
mobygate auth Show auth status + run a refresh probe
|
|
406
|
+
mobygate uninstall Remove installed services
|
|
407
|
+
mobygate version Print version
|
|
408
|
+
|
|
409
|
+
Config: ~/.mobygate/config.yaml (env vars override)
|
|
410
|
+
Repo: ${REPO_ROOT}
|
|
411
|
+
`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ---------- dispatch ----------
|
|
415
|
+
|
|
416
|
+
const cmd = process.argv[2];
|
|
417
|
+
const COMMANDS = {
|
|
418
|
+
init: cmdInit,
|
|
419
|
+
start: cmdStart,
|
|
420
|
+
stop: cmdStop,
|
|
421
|
+
restart: cmdRestart,
|
|
422
|
+
status: cmdStatus,
|
|
423
|
+
logs: cmdLogs,
|
|
424
|
+
auth: cmdAuth,
|
|
425
|
+
uninstall: cmdUninstall,
|
|
426
|
+
version: cmdVersion,
|
|
427
|
+
'--version': cmdVersion,
|
|
428
|
+
'-v': cmdVersion,
|
|
429
|
+
help: usage,
|
|
430
|
+
'--help': usage,
|
|
431
|
+
'-h': usage,
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
if (!cmd || !COMMANDS[cmd]) {
|
|
435
|
+
usage();
|
|
436
|
+
process.exit(cmd ? 1 : 0);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
await COMMANDS[cmd]();
|
|
441
|
+
} catch (e) {
|
|
442
|
+
die(`${e.message}\n${e.stack?.split('\n').slice(1, 4).join('\n') || ''}`);
|
|
443
|
+
}
|