groove-dev 0.27.207 → 0.27.209
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/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/axom-connector.js +50 -2
- package/node_modules/@groove-dev/daemon/src/axom-remote.js +16 -14
- package/node_modules/@groove-dev/daemon/src/axom-runtimes.js +498 -0
- package/node_modules/@groove-dev/daemon/src/axom-server.js +25 -4
- package/node_modules/@groove-dev/daemon/src/index.js +3 -1
- package/node_modules/@groove-dev/daemon/src/routes/axom.js +113 -0
- package/node_modules/@groove-dev/daemon/src/routes/watch.js +11 -0
- package/node_modules/@groove-dev/daemon/src/watcher.js +55 -2
- package/node_modules/@groove-dev/daemon/test/axom-connector.test.js +44 -1
- package/node_modules/@groove-dev/daemon/test/axom-runtimes.test.js +367 -0
- package/node_modules/@groove-dev/daemon/test/watcher.test.js +68 -2
- package/node_modules/@groove-dev/gui/dist/assets/{index-OzNfp6-Y.js → index-217ZVIOc.js} +242 -237
- package/node_modules/@groove-dev/gui/dist/assets/index-DdCadtGL.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/axom-connector.js +50 -2
- package/packages/daemon/src/axom-remote.js +16 -14
- package/packages/daemon/src/axom-runtimes.js +498 -0
- package/packages/daemon/src/axom-server.js +25 -4
- package/packages/daemon/src/index.js +3 -1
- package/packages/daemon/src/routes/axom.js +113 -0
- package/packages/daemon/src/routes/watch.js +11 -0
- package/packages/daemon/src/watcher.js +55 -2
- package/packages/gui/dist/assets/{index-OzNfp6-Y.js → index-217ZVIOc.js} +242 -237
- package/packages/gui/dist/assets/index-DdCadtGL.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-DG6yq4dB.css +0 -1
- package/packages/gui/dist/assets/index-DG6yq4dB.css +0 -1
|
@@ -186,6 +186,7 @@ export class AxomConnector {
|
|
|
186
186
|
live: !!info.live,
|
|
187
187
|
ws: null,
|
|
188
188
|
lastSeq: 0,
|
|
189
|
+
epoch: null,
|
|
189
190
|
ring: [],
|
|
190
191
|
overflow: 0,
|
|
191
192
|
unknownKinds: {},
|
|
@@ -209,14 +210,35 @@ export class AxomConnector {
|
|
|
209
210
|
|
|
210
211
|
_watchSession(ep, s) {
|
|
211
212
|
if (this.destroyed) return;
|
|
212
|
-
|
|
213
|
-
|
|
213
|
+
// §16.4: reconnects carry both cursor and epoch; a stale epoch voids the
|
|
214
|
+
// cursor runtime-side and we get a full replay instead of a silent gap.
|
|
215
|
+
const params = [];
|
|
216
|
+
if (s.lastSeq > 0) params.push(`since=ev-${String(s.lastSeq).padStart(6, '0')}`);
|
|
217
|
+
if (s.epoch) params.push(`epoch=${encodeURIComponent(s.epoch)}`);
|
|
218
|
+
const query = params.length ? `?${params.join('&')}` : '';
|
|
219
|
+
const wsUrl = `${ep.url.replace(/^http/, 'ws')}/ws/session/${encodeURIComponent(s.id)}${query}`;
|
|
214
220
|
const ws = new WebSocket(wsUrl);
|
|
215
221
|
s.ws = ws;
|
|
216
222
|
|
|
217
223
|
ws.on('message', (data) => {
|
|
218
224
|
let envelope;
|
|
219
225
|
try { envelope = JSON.parse(data.toString()); } catch { return; }
|
|
226
|
+
// §16.4 handshake frame — transport metadata, never a transcript event.
|
|
227
|
+
// A changed epoch means the runtime restarted and its event ids reset:
|
|
228
|
+
// our monotonic dedup would silently swallow the entire replay, so the
|
|
229
|
+
// cursor, ring, and GUI copy are all voided together.
|
|
230
|
+
if (envelope.kind === 'ws_hello') {
|
|
231
|
+
const epoch = envelope.payload?.epoch;
|
|
232
|
+
if (epoch && s.epoch && epoch !== s.epoch) {
|
|
233
|
+
s.lastSeq = 0;
|
|
234
|
+
s.ring = [];
|
|
235
|
+
s.overflow = 0;
|
|
236
|
+
s.unknownKinds = {};
|
|
237
|
+
this.daemon.broadcast({ type: 'axom:session:reset', endpoint: ep.name, session: s.id, epoch });
|
|
238
|
+
}
|
|
239
|
+
if (epoch) s.epoch = epoch;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
220
242
|
const seq = seqOf(envelope.id);
|
|
221
243
|
// Dedup on ring-buffer replay after reconnect — ids are monotonic.
|
|
222
244
|
if (seq !== null && seq <= s.lastSeq) return;
|
|
@@ -371,6 +393,32 @@ export class AxomConnector {
|
|
|
371
393
|
|
|
372
394
|
_broadcastStatus() {
|
|
373
395
|
this.daemon.broadcast({ type: 'axom:status', data: this.status() });
|
|
396
|
+
// Runtime-level state is derived from connector state — push it too so
|
|
397
|
+
// the GUI's runtime cards move on events, not on a poll.
|
|
398
|
+
this.daemon.axomRuntimes?.broadcastStatus?.();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Collapse the 'running but not connected' window: when a runtime is known
|
|
402
|
+
// to be answering /about, don't make the user wait out our backoff.
|
|
403
|
+
nudge(name) {
|
|
404
|
+
const ep = this.endpoints.get(name);
|
|
405
|
+
if (!ep || ep.status === 'connected' || this.destroyed) return;
|
|
406
|
+
if (ep.retryTimer) { clearTimeout(ep.retryTimer); ep.retryTimer = null; }
|
|
407
|
+
ep.backoffMs = this.backoffBaseMs;
|
|
408
|
+
this._handshake(ep);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// The inverse of nudge: after a deliberate stop, 'connected' is presumed
|
|
412
|
+
// stale — re-probe now so the endpoint transitions (and broadcasts) with the
|
|
413
|
+
// verb instead of lingering until the next scheduled poll fails.
|
|
414
|
+
recheck(name) {
|
|
415
|
+
const ep = this.endpoints.get(name);
|
|
416
|
+
if (!ep || this.destroyed) return;
|
|
417
|
+
if (ep.status === 'connected') {
|
|
418
|
+
this._pollSessions(ep).catch(() => this._endpointLost(ep));
|
|
419
|
+
} else {
|
|
420
|
+
this.nudge(name);
|
|
421
|
+
}
|
|
374
422
|
}
|
|
375
423
|
|
|
376
424
|
_teardownEndpoint(ep) {
|
|
@@ -55,8 +55,8 @@ export class AxomRemote {
|
|
|
55
55
|
return this.daemon.config?.axom?.remote || null;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
_ssh(command, timeoutMs = 30000) {
|
|
59
|
-
const cfg = this._config();
|
|
58
|
+
_ssh(command, timeoutMs = 30000, cfgOverride = null) {
|
|
59
|
+
const cfg = cfgOverride || this._config();
|
|
60
60
|
if (!cfg) return Promise.reject(new Error('no remote Axom host configured'));
|
|
61
61
|
const problem = validateRemote(cfg);
|
|
62
62
|
if (problem) return Promise.reject(new Error(`remote config invalid: ${problem}`));
|
|
@@ -71,14 +71,15 @@ export class AxomRemote {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// Is a runtime listening on the remote port right now?
|
|
74
|
-
async status() {
|
|
75
|
-
const cfg = this._config();
|
|
74
|
+
async status(cfgOverride = null) {
|
|
75
|
+
const cfg = cfgOverride || this._config();
|
|
76
76
|
if (!cfg) return { configured: false, running: null };
|
|
77
77
|
const port = cfg.port || AXOM_DEFAULT_PORT;
|
|
78
78
|
try {
|
|
79
79
|
const { stdout } = await this._ssh(
|
|
80
80
|
`curl -sf --max-time 4 http://127.0.0.1:${port}/about >/dev/null 2>&1 && echo UP || echo DOWN`,
|
|
81
81
|
15000,
|
|
82
|
+
cfg,
|
|
82
83
|
);
|
|
83
84
|
return {
|
|
84
85
|
configured: true,
|
|
@@ -94,11 +95,11 @@ export class AxomRemote {
|
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
async start() {
|
|
98
|
-
const cfg = this._config();
|
|
98
|
+
async start(cfgOverride = null) {
|
|
99
|
+
const cfg = cfgOverride || this._config();
|
|
99
100
|
if (!cfg) throw new Error('no remote Axom host configured');
|
|
100
101
|
const port = cfg.port || AXOM_DEFAULT_PORT;
|
|
101
|
-
const already = await this.status();
|
|
102
|
+
const already = await this.status(cfg);
|
|
102
103
|
if (already.running) return { started: false, alreadyRunning: true, port };
|
|
103
104
|
|
|
104
105
|
const command = cfg.command
|
|
@@ -112,12 +113,12 @@ export class AxomRemote {
|
|
|
112
113
|
// NOTHING while reporting success. Found by running it.
|
|
113
114
|
const log = cfg.logPath || '~/axom-serve/serve.log';
|
|
114
115
|
const quoted = `'${command.replace(/'/g, `'\\''`)}'`;
|
|
115
|
-
await this._ssh(`nohup bash -lc ${quoted} >> ${log} 2>&1 < /dev/null & disown; echo STARTED`, 30000);
|
|
116
|
+
await this._ssh(`nohup bash -lc ${quoted} >> ${log} 2>&1 < /dev/null & disown; echo STARTED`, 30000, cfg);
|
|
116
117
|
|
|
117
118
|
// Confirm it actually came up rather than reporting optimism.
|
|
118
119
|
for (let i = 0; i < 30; i++) {
|
|
119
120
|
await new Promise((r) => setTimeout(r, 2000));
|
|
120
|
-
const s = await this.status();
|
|
121
|
+
const s = await this.status(cfg);
|
|
121
122
|
if (s.running) {
|
|
122
123
|
this.daemon.audit.log('axom.remote.start', { host: cfg.host, port });
|
|
123
124
|
this._broadcast();
|
|
@@ -127,8 +128,8 @@ export class AxomRemote {
|
|
|
127
128
|
throw new Error(`started the command but nothing answered on port ${port} within 60s — check ${log} on ${cfg.host}`);
|
|
128
129
|
}
|
|
129
130
|
|
|
130
|
-
async stop({ force = false } = {}) {
|
|
131
|
-
const cfg = this._config();
|
|
131
|
+
async stop({ force = false } = {}, cfgOverride = null) {
|
|
132
|
+
const cfg = cfgOverride || this._config();
|
|
132
133
|
if (!cfg) throw new Error('no remote Axom host configured');
|
|
133
134
|
const port = cfg.port || AXOM_DEFAULT_PORT;
|
|
134
135
|
|
|
@@ -141,6 +142,7 @@ export class AxomRemote {
|
|
|
141
142
|
+ `-H 'Content-Type: application/json' -d '{"force":${force ? 'true' : 'false'}}' `
|
|
142
143
|
+ `http://127.0.0.1:${port}/shutdown`,
|
|
143
144
|
20000,
|
|
145
|
+
cfg,
|
|
144
146
|
);
|
|
145
147
|
const code = parseInt(stdout.trim().slice(-3), 10);
|
|
146
148
|
if (code === 202) {
|
|
@@ -156,7 +158,7 @@ export class AxomRemote {
|
|
|
156
158
|
|
|
157
159
|
// Pre-§14 runtime: signal the process that owns the port. Still the
|
|
158
160
|
// user's own machine, still an explicit action they asked for.
|
|
159
|
-
await this._ssh(`PID=$(lsof -t -i:${port} 2>/dev/null | head -1); [ -n "$PID" ] && kill $PID && echo KILLED || echo NOTFOUND`, 20000);
|
|
161
|
+
await this._ssh(`PID=$(lsof -t -i:${port} 2>/dev/null | head -1); [ -n "$PID" ] && kill $PID && echo KILLED || echo NOTFOUND`, 20000, cfg);
|
|
160
162
|
this.daemon.audit.log('axom.remote.stop', { host: cfg.host, port, via: 'signal' });
|
|
161
163
|
this._broadcast();
|
|
162
164
|
return { stopped: true, via: 'signal' };
|
|
@@ -181,8 +183,8 @@ export class AxomRemote {
|
|
|
181
183
|
}
|
|
182
184
|
}
|
|
183
185
|
|
|
184
|
-
async ensureTunnel() {
|
|
185
|
-
const cfg = this._config();
|
|
186
|
+
async ensureTunnel(cfgOverride = null) {
|
|
187
|
+
const cfg = cfgOverride || this._config();
|
|
186
188
|
if (!cfg) return { tunneled: false, reason: 'no remote configured' };
|
|
187
189
|
const problem = validateRemote(cfg);
|
|
188
190
|
if (problem) return { tunneled: false, reason: problem };
|