claude-simple 0.2.4 → 0.2.6

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 CHANGED
@@ -221,5 +221,6 @@ Legacy Node hub: `npm run gateway` → `gateway.js` (prefer the Go binary).
221
221
  | `npm start` / `npm run serve` | Production standalone |
222
222
  | `npm run build` | Bundle frontend into `dist/` |
223
223
  | `npm run client` | Edge worker (requires hub) |
224
+ | `claude-edge status\|start\|stop\|restart\|logs` | Supervised edge (launchd / systemd). Also `claude-simple-edge`, `edge-daemon` |
224
225
  | `npm run gateway` | Legacy Node hub |
225
226
  | `npm run gateway:build` | Build Go hub via `gateway/Makefile` |
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Manage a supervised Claude Simple edge:
4
+ * claude-edge status|start|stop|restart|logs
5
+ * claude-edge daemon <same>
6
+ *
7
+ * Detects launchd (macOS com.claude-simple.edge) or systemd (claude-simple-edge).
8
+ */
9
+ import { spawn, spawnSync } from 'child_process';
10
+ import fs from 'fs';
11
+ import os from 'os';
12
+ import path from 'path';
13
+ import { fileURLToPath } from 'url';
14
+
15
+ const LAUNCHD_LABEL = 'system/com.claude-simple.edge';
16
+ const LAUNCHD_PLIST = '/Library/LaunchDaemons/com.claude-simple.edge.plist';
17
+ const SYSTEMD_UNIT = 'claude-simple-edge.service';
18
+ const MAC_OUT = path.join(os.homedir(), '.claude-simple-edge', 'edge.out.log');
19
+ const MAC_ERR = path.join(os.homedir(), '.claude-simple-edge', 'edge.err.log');
20
+
21
+ function usage(code = 0) {
22
+ const text = `Usage: claude-edge <command>
23
+
24
+ status Show running state, pid, port, last log lines
25
+ start Start the edge daemon
26
+ stop Stop the edge daemon (stays down until start)
27
+ restart Restart the edge daemon
28
+ logs Last 80 lines of stdout (or journal on Linux)
29
+ logs -f Follow logs
30
+ logs err Stderr only (launchd)
31
+
32
+ Also installed as: claude-simple-edge, edge-daemon
33
+ `;
34
+ if (code) process.stderr.write(text);
35
+ else process.stdout.write(text);
36
+ process.exit(code);
37
+ }
38
+
39
+ function run(cmd, args, opts = {}) {
40
+ return spawnSync(cmd, args, {
41
+ encoding: 'utf8',
42
+ stdio: opts.stdio || ['ignore', 'pipe', 'pipe'],
43
+ env: process.env,
44
+ });
45
+ }
46
+
47
+ function hasCmd(cmd) {
48
+ const r = run(process.platform === 'win32' ? 'where' : 'which', [cmd]);
49
+ return r.status === 0;
50
+ }
51
+
52
+ function detectBackend() {
53
+ if (process.platform === 'darwin') {
54
+ if (fs.existsSync(LAUNCHD_PLIST)) return 'launchd';
55
+ const r = run('launchctl', ['print', LAUNCHD_LABEL]);
56
+ if (r.status === 0) return 'launchd';
57
+ }
58
+ if (hasCmd('systemctl')) {
59
+ const r = run('systemctl', ['cat', SYSTEMD_UNIT]);
60
+ if (r.status === 0) return 'systemd';
61
+ }
62
+ if (process.platform === 'darwin') return 'launchd';
63
+ if (process.platform === 'linux') return 'systemd';
64
+ return null;
65
+ }
66
+
67
+ function isRoot() {
68
+ return typeof process.getuid === 'function' && process.getuid() === 0;
69
+ }
70
+
71
+ /** Re-exec this CLI via sudo so launchctl/systemctl can touch the system unit. */
72
+ function ensureRoot() {
73
+ if (isRoot()) return;
74
+ if (!hasCmd('sudo')) return;
75
+ const script = fileURLToPath(import.meta.url);
76
+ const args = process.argv.slice(2);
77
+ const candidates = ['/usr/local/bin/claude-edge', process.argv[1], script].filter(Boolean);
78
+ const seen = new Set();
79
+ for (const entry of candidates) {
80
+ const resolved = path.resolve(entry);
81
+ if (seen.has(resolved) || !fs.existsSync(resolved)) continue;
82
+ seen.add(resolved);
83
+ const r = spawnSync('sudo', ['-n', resolved, ...args], { stdio: 'inherit' });
84
+ if (r.status === 0) process.exit(0);
85
+ }
86
+ const r = spawnSync('sudo', [process.execPath, script, ...args], { stdio: 'inherit' });
87
+ process.exit(r.status ?? 1);
88
+ }
89
+
90
+ function runRoot(cmd, args, opts = {}) {
91
+ return run(cmd, args, opts);
92
+ }
93
+
94
+ function fail(msg, extra) {
95
+ console.error(msg);
96
+ if (extra) process.stderr.write(String(extra).trimEnd() + '\n');
97
+ process.exit(1);
98
+ }
99
+
100
+ function parseLaunchdPrint(text) {
101
+ const pick = (re) => {
102
+ const m = text.match(re);
103
+ return m ? m[1] : '-';
104
+ };
105
+ return {
106
+ state: pick(/^\s*state = (\S+)/m),
107
+ pid: pick(/^\s*pid = (\S+)/m),
108
+ runs: pick(/^\s*runs = (\S+)/m),
109
+ };
110
+ }
111
+
112
+ function listenPort() {
113
+ const port = process.env.LOCAL_PORT || '13000';
114
+ const r = run('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN']);
115
+ if (r.status === 0 && r.stdout.trim()) return `127.0.0.1:${port}`;
116
+ const ss = run('ss', ['-lnt']);
117
+ if (ss.status === 0 && ss.stdout.includes(`:${port}`)) return `127.0.0.1:${port}`;
118
+ return 'down';
119
+ }
120
+
121
+ function tailFile(file, n) {
122
+ if (!fs.existsSync(file)) return '';
123
+ const r = run('tail', ['-n', String(n), file]);
124
+ return (r.stdout || '').trimEnd();
125
+ }
126
+
127
+ function cmdStatus(backend) {
128
+ ensureRoot();
129
+ if (backend === 'launchd') {
130
+ const r = runRoot('launchctl', ['print', LAUNCHD_LABEL]);
131
+ if (r.status !== 0) {
132
+ console.log(`service: ${LAUNCHD_LABEL}`);
133
+ console.log('state: not-loaded');
134
+ console.log(`plist: ${LAUNCHD_PLIST}`);
135
+ if (r.stderr) console.log(r.stderr.trim());
136
+ return;
137
+ }
138
+ const info = parseLaunchdPrint(r.stdout);
139
+ const machine = r.stdout.match(/MACHINE_ID => (\S+)/)?.[1] || process.env.MACHINE_ID || '-';
140
+ console.log(`service: ${LAUNCHD_LABEL}`);
141
+ console.log(`machine: ${machine}`);
142
+ console.log(`state: ${info.state}`);
143
+ console.log(`pid: ${info.pid}`);
144
+ console.log(`runs: ${info.runs}`);
145
+ console.log(`listen: ${listenPort()}`);
146
+ console.log(`plist: ${LAUNCHD_PLIST}`);
147
+ console.log(`stdout: ${MAC_OUT}`);
148
+ console.log(`stderr: ${MAC_ERR}`);
149
+ const last = tailFile(MAC_OUT, 8);
150
+ if (last) console.log(`\n--- last logs ---\n${last}`);
151
+ return;
152
+ }
153
+
154
+ const r = runRoot('systemctl', ['show', SYSTEMD_UNIT, '--no-pager',
155
+ '-p', 'ActiveState', '-p', 'SubState', '-p', 'MainPID', '-p', 'NRestarts', '-p', 'FragmentPath', '-p', 'Environment']);
156
+ if (r.status !== 0) fail(`cannot query ${SYSTEMD_UNIT}`, r.stderr || r.stdout);
157
+ const kv = Object.fromEntries(
158
+ r.stdout.split('\n').filter(Boolean).map((line) => {
159
+ const i = line.indexOf('=');
160
+ return [line.slice(0, i), line.slice(i + 1)];
161
+ }),
162
+ );
163
+ const machine = (kv.Environment || '').match(/(?:^|\s)MACHINE_ID=(\S+)/)?.[1]
164
+ || process.env.MACHINE_ID || '-';
165
+ console.log(`service: ${SYSTEMD_UNIT}`);
166
+ console.log(`machine: ${machine}`);
167
+ console.log(`state: ${kv.ActiveState || '-'} (${kv.SubState || '-'})`);
168
+ console.log(`pid: ${kv.MainPID || '-'}`);
169
+ console.log(`restarts: ${kv.NRestarts || '-'}`);
170
+ console.log(`listen: ${listenPort()}`);
171
+ console.log(`unit: ${kv.FragmentPath || '-'}`);
172
+ const logs = runRoot('journalctl', ['-u', SYSTEMD_UNIT, '-n', '8', '--no-pager']);
173
+ if (logs.stdout?.trim()) console.log(`\n--- last logs ---\n${logs.stdout.trimEnd()}`);
174
+ }
175
+
176
+ function cmdStart(backend) {
177
+ ensureRoot();
178
+ if (backend === 'launchd') {
179
+ runRoot('launchctl', ['enable', LAUNCHD_LABEL]);
180
+ const loaded = runRoot('launchctl', ['print', LAUNCHD_LABEL]);
181
+ if (loaded.status !== 0) {
182
+ const b = runRoot('launchctl', ['bootstrap', 'system', LAUNCHD_PLIST]);
183
+ if (b.status !== 0) fail('bootstrap failed', b.stderr || b.stdout);
184
+ }
185
+ const k = runRoot('launchctl', ['kickstart', '-k', LAUNCHD_LABEL]);
186
+ if (k.status !== 0) fail('start failed', k.stderr || k.stdout);
187
+ console.log('started');
188
+ return;
189
+ }
190
+ const r = runRoot('systemctl', ['enable', '--now', SYSTEMD_UNIT]);
191
+ if (r.status !== 0) fail('start failed', r.stderr || r.stdout);
192
+ console.log('started');
193
+ }
194
+
195
+ function cmdStop(backend) {
196
+ ensureRoot();
197
+ if (backend === 'launchd') {
198
+ runRoot('launchctl', ['disable', LAUNCHD_LABEL]);
199
+ runRoot('launchctl', ['bootout', LAUNCHD_LABEL]);
200
+ console.log('stopped');
201
+ return;
202
+ }
203
+ const r = runRoot('systemctl', ['disable', '--now', SYSTEMD_UNIT]);
204
+ if (r.status !== 0) fail('stop failed', r.stderr || r.stdout);
205
+ console.log('stopped');
206
+ }
207
+
208
+ function cmdRestart(backend) {
209
+ ensureRoot();
210
+ if (backend === 'launchd') {
211
+ runRoot('launchctl', ['enable', LAUNCHD_LABEL]);
212
+ const loaded = runRoot('launchctl', ['print', LAUNCHD_LABEL]);
213
+ if (loaded.status !== 0) {
214
+ const b = runRoot('launchctl', ['bootstrap', 'system', LAUNCHD_PLIST]);
215
+ if (b.status !== 0) fail('bootstrap failed', b.stderr || b.stdout);
216
+ }
217
+ const k = runRoot('launchctl', ['kickstart', '-k', LAUNCHD_LABEL]);
218
+ if (k.status !== 0) fail('restart failed', k.stderr || k.stdout);
219
+ console.log('restarted');
220
+ return;
221
+ }
222
+ const r = runRoot('systemctl', ['restart', SYSTEMD_UNIT]);
223
+ if (r.status !== 0) fail('restart failed', r.stderr || r.stdout);
224
+ console.log('restarted');
225
+ }
226
+
227
+ function cmdLogs(backend, mode) {
228
+ if (backend === 'systemd') ensureRoot();
229
+ if (backend === 'launchd') {
230
+ if (mode === '-f' || mode === '--follow') {
231
+ const child = spawn('tail', ['-n', '50', '-F', MAC_OUT, MAC_ERR], { stdio: 'inherit' });
232
+ child.on('exit', (c) => process.exit(c || 0));
233
+ return;
234
+ }
235
+ if (mode === 'err' || mode === 'error' || mode === 'stderr') {
236
+ process.stdout.write(tailFile(MAC_ERR, 80) + '\n');
237
+ return;
238
+ }
239
+ process.stdout.write(tailFile(MAC_OUT, 80) + '\n');
240
+ return;
241
+ }
242
+ const args = ['-u', SYSTEMD_UNIT, '--no-pager'];
243
+ if (mode === '-f' || mode === '--follow') {
244
+ const child = spawn('journalctl', ['-u', SYSTEMD_UNIT, '-f', '-n', '50'], { stdio: 'inherit' });
245
+ child.on('exit', (c) => process.exit(c || 0));
246
+ return;
247
+ }
248
+ args.push('-n', '80');
249
+ const r = runRoot('journalctl', args);
250
+ process.stdout.write((r.stdout || r.stderr || '') + (r.stdout?.endsWith('\n') ? '' : '\n'));
251
+ }
252
+
253
+ const raw = process.argv.slice(2);
254
+ if (raw[0] === 'daemon') raw.shift();
255
+ const cmd = raw[0] || 'status';
256
+ const rest = raw.slice(1);
257
+
258
+ if (['-h', '--help', 'help'].includes(cmd)) usage(0);
259
+
260
+ const backend = detectBackend();
261
+ if (!backend) fail('no launchd/systemd edge service found');
262
+
263
+ switch (cmd) {
264
+ case 'status':
265
+ cmdStatus(backend);
266
+ break;
267
+ case 'start':
268
+ cmdStart(backend);
269
+ break;
270
+ case 'stop':
271
+ cmdStop(backend);
272
+ break;
273
+ case 'restart':
274
+ cmdRestart(backend);
275
+ break;
276
+ case 'logs':
277
+ cmdLogs(backend, rest[0]);
278
+ break;
279
+ default:
280
+ console.error(`unknown command: ${cmd}`);
281
+ usage(2);
282
+ }