@tokenoftrust/cli 1.3.1-rc.1 → 1.3.1-rc.2
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/package.json +1 -1
- package/src/commands/dev.mjs +15 -1
- package/src/commands/start.mjs +18 -5
- package/src/dev-heartbeat.mjs +92 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/cli",
|
|
3
|
-
"version": "1.3.1-rc.
|
|
3
|
+
"version": "1.3.1-rc.2",
|
|
4
4
|
"description": "Token of Trust developer CLI — check out a tenant store, run it locally with save→reload, and submit it for preview. Installs the `tot` command.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Token of Trust",
|
package/src/commands/dev.mjs
CHANGED
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
scaffoldSample, isSampleCheckout, sampleConfig,
|
|
50
50
|
resolveRendererSource as resolveLocalRendererSource, newestCachedRunner, SAMPLE_DIR_NAME,
|
|
51
51
|
} from "../sample.mjs";
|
|
52
|
+
import { startHeartbeatFromEnv } from "../dev-heartbeat.mjs";
|
|
52
53
|
|
|
53
54
|
/** The published runner image (--docker fallback). Override with --image / TOT_DEV_IMAGE. */
|
|
54
55
|
const DEFAULT_DEV_IMAGE =
|
|
@@ -307,7 +308,14 @@ export function bootNativeEnv(args) {
|
|
|
307
308
|
}
|
|
308
309
|
|
|
309
310
|
export function bootNative(runnerDir, workspace, port, url, args) {
|
|
310
|
-
const
|
|
311
|
+
const bridgeEnv = bootNativeEnv(args);
|
|
312
|
+
const handle = spawnNativeDev(runnerDir, workspace, port, { stdio: "inherit", env: bridgeEnv });
|
|
313
|
+
|
|
314
|
+
// Heartbeat the hosted cockpit (G1) with the CLI version + this live localhost
|
|
315
|
+
// URL while the runner runs — CLI-side, using the SAME bridge credential the
|
|
316
|
+
// runner's file-save path uses (no-op in --sample, which threads no credential).
|
|
317
|
+
const stopHeartbeat = startHeartbeatFromEnv(bridgeEnv, { url });
|
|
318
|
+
handle.done.finally(() => stopHeartbeat());
|
|
311
319
|
|
|
312
320
|
// Auto-open the browser the moment the server answers (D). Non-blocking so
|
|
313
321
|
// Ctrl-C / logs are unaffected; --no-open suppresses it.
|
|
@@ -986,6 +994,12 @@ async function runContainer(workspace, args, ctx) {
|
|
|
986
994
|
|
|
987
995
|
const handle = await spawnDevContainer(plan, args, { stdio: "inherit" });
|
|
988
996
|
|
|
997
|
+
// Heartbeat the hosted cockpit (G1) CLI-side while the container runs — the
|
|
998
|
+
// container reports file-saves via the threaded env, but the CLI owns the
|
|
999
|
+
// version + live URL. Same bridge credential (activityBridgeEnv), no-op absent.
|
|
1000
|
+
const stopHeartbeat = startHeartbeatFromEnv(activityBridgeEnv(), { url: plan.url });
|
|
1001
|
+
handle.done.finally(() => stopHeartbeat());
|
|
1002
|
+
|
|
989
1003
|
// Auto-open the browser the moment the server answers (D). Non-blocking so
|
|
990
1004
|
// Ctrl-C / logs are unaffected; --no-open suppresses it.
|
|
991
1005
|
if (!args.noOpen) {
|
package/src/commands/start.mjs
CHANGED
|
@@ -64,6 +64,7 @@ import {
|
|
|
64
64
|
activityBridgeEnv, NativeArtifactUnavailableError,
|
|
65
65
|
} from "./dev.mjs";
|
|
66
66
|
import { scaffoldSample, isSampleCheckout, sampleConfig, SAMPLE_DIR_NAME } from "../sample.mjs";
|
|
67
|
+
import { startHeartbeatFromEnv } from "../dev-heartbeat.mjs";
|
|
67
68
|
import { IDEAS } from "./ideas.mjs";
|
|
68
69
|
|
|
69
70
|
const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
|
|
@@ -226,6 +227,7 @@ export async function run(argv, ctx) {
|
|
|
226
227
|
// above; wait for the server, open the browser (C/D).
|
|
227
228
|
const ctxDev = detectContext(dir);
|
|
228
229
|
let url, handle;
|
|
230
|
+
const bridgeEnv = activityBridgeEnv(env);
|
|
229
231
|
if (runtime.useDocker) {
|
|
230
232
|
const plan = buildContainerPlan(dir, devArgs, ctxDev);
|
|
231
233
|
url = plan.url;
|
|
@@ -234,9 +236,15 @@ export async function run(argv, ctx) {
|
|
|
234
236
|
} else {
|
|
235
237
|
url = deriveUrl(ctxDev.config || {}, devArgs.port).url;
|
|
236
238
|
console.log(` → starting dev … ${url}`);
|
|
237
|
-
handle = spawnNativeDev(runtime.runnerDir, dir, devArgs.port, { stdio: "piped", env:
|
|
239
|
+
handle = spawnNativeDev(runtime.runnerDir, dir, devArgs.port, { stdio: "piped", env: bridgeEnv });
|
|
238
240
|
}
|
|
239
241
|
|
|
242
|
+
// Heartbeat the hosted cockpit (G1) with the CLI version + this live localhost
|
|
243
|
+
// URL for the life of the run — CLI-side, using the cached bridge credential
|
|
244
|
+
// (no-op when none is cached). The runner keeps reporting file-saves itself.
|
|
245
|
+
const stopHeartbeat = startHeartbeatFromEnv(bridgeEnv, { url });
|
|
246
|
+
handle.done.finally(() => stopHeartbeat());
|
|
247
|
+
|
|
240
248
|
// The runner's stdio is "piped" (its logs are held until the aha), so this
|
|
241
249
|
// boot would otherwise be a silent 5–60s gap. Tick a spinner over it.
|
|
242
250
|
const up = await waitForBoot(url, handle);
|
|
@@ -666,23 +674,28 @@ function connectClaude() {
|
|
|
666
674
|
* hidden. Ctrl-C still tears the server down (the child owns the TTY signals).
|
|
667
675
|
*/
|
|
668
676
|
function streamDevLogs(child) {
|
|
669
|
-
// Startup churn + tool internals — never user-facing.
|
|
677
|
+
// Startup churn + tool internals — never user-facing. Matched AFTER stripping
|
|
678
|
+
// the runner/Vite "HH:MM:SS " timestamp prefix (see `body` below), so a
|
|
679
|
+
// timestamped internal line like "10:50:17 [vite] connected" is still dropped.
|
|
670
680
|
const NOISE =
|
|
671
|
-
|
|
681
|
+
/^(\[vite\]|\[types\]|\[@astrojs|\[WARN\]|▲|┃|astro\s+v[\d.]|(Local|Network)\s+http|watching for file changes|Scope: all \d|copy-tenant-assets:|.*dependency optimized|.*optimized dependencies changed|.*program reload|\d+ deprecated|Packages:\s*\+|Progress:\s*resolved|Downloading @|node_modules\/|devDependencies:|\+\s+\w+@|Done in \d)/i;
|
|
672
682
|
// A real save-triggered reload (not startup "program reload" churn).
|
|
673
683
|
const RELOAD = /(hmr update|page reload)/i;
|
|
674
684
|
let reloadPending = null;
|
|
675
685
|
const emit = (line) => {
|
|
676
686
|
const t = line.replace(/\s+$/, "");
|
|
677
687
|
if (!t) return;
|
|
678
|
-
|
|
688
|
+
// The runner/Vite prefix most lines with an "HH:MM:SS " (or ".mmm ")
|
|
689
|
+
// timestamp — strip it before matching so the filters catch them.
|
|
690
|
+
const body = t.replace(/^\d{1,2}:\d{2}:\d{2}(\.\d+)?\s+/, "").replace(/^\s+/, "");
|
|
691
|
+
if (RELOAD.test(body)) {
|
|
679
692
|
if (reloadPending) return; // debounce a burst into one line
|
|
680
693
|
reloadPending = setTimeout(() => { reloadPending = null; }, 1000);
|
|
681
694
|
if (reloadPending.unref) reloadPending.unref();
|
|
682
695
|
process.stdout.write(" ↻ your store reloaded\n");
|
|
683
696
|
return;
|
|
684
697
|
}
|
|
685
|
-
if (NOISE.test(
|
|
698
|
+
if (NOISE.test(body)) return;
|
|
686
699
|
process.stdout.write(` ${t}\n`);
|
|
687
700
|
};
|
|
688
701
|
lineStream(child.stdout, emit);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-side heartbeat for the local→hosted activity bridge (G1).
|
|
3
|
+
*
|
|
4
|
+
* While `tot dev` / `tot start` is running, the CLI (which knows its own
|
|
5
|
+
* version, the resolved runner version, the port + tenant, and the cached
|
|
6
|
+
* activity-bridge credential) POSTs a periodic heartbeat to the hosted
|
|
7
|
+
* `/api/dev/activity` endpoint. The hosted cockpit turns the latest heartbeat
|
|
8
|
+
* into a LIVE, clickable local-dev link + the running CLI version.
|
|
9
|
+
*
|
|
10
|
+
* It emits CLI-side (not from the runner) because the CLI is the one process
|
|
11
|
+
* that holds ALL of {version, port, tenant, bridge credential} — the runner
|
|
12
|
+
* keeps emitting file-save events as before. Same bearer + same endpoint as the
|
|
13
|
+
* file-save path; no new secret.
|
|
14
|
+
*
|
|
15
|
+
* Contract (identical posture to the runner's file-save bridge): best-effort,
|
|
16
|
+
* NEVER throws or blocks, the interval timer is unref'd so it can't hold the
|
|
17
|
+
* process open, and with no bridge credential (a bare `tot login`, an older
|
|
18
|
+
* session, or the zero-login `--sample` path) it's a silent no-op.
|
|
19
|
+
* Dependency-free (global fetch, Node 20+).
|
|
20
|
+
*/
|
|
21
|
+
import { CLI_VERSION, clientPackages } from "./mcp.mjs";
|
|
22
|
+
|
|
23
|
+
/** ~10s between beats — frequent enough that the cockpit's ~30s live window
|
|
24
|
+
* tolerates a missed beat without the badge flapping, cheap enough to ignore. */
|
|
25
|
+
const HEARTBEAT_INTERVAL_MS = 10_000;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* POST one heartbeat body, best-effort. No-op (returns undefined) without both
|
|
29
|
+
* an activity URL and a bearer token. Never throws — a failed/offline hosted
|
|
30
|
+
* worker just means the cockpit doesn't light up this beat.
|
|
31
|
+
* @param {{ activityUrl?: string, token?: string, url?: string,
|
|
32
|
+
* cliVersion?: string, runnerVersion?: string|null }} args
|
|
33
|
+
*/
|
|
34
|
+
export function postHeartbeat({ activityUrl, token, url, cliVersion, runnerVersion } = {}) {
|
|
35
|
+
if (!activityUrl || !token) return undefined;
|
|
36
|
+
/** @type {Record<string, unknown>} */
|
|
37
|
+
const body = { event: "heartbeat", cliVersion, at: Date.now() };
|
|
38
|
+
if (runnerVersion) body.runnerVersion = runnerVersion;
|
|
39
|
+
if (url) body.url = url;
|
|
40
|
+
return fetch(`${String(activityUrl).replace(/\/+$/, "")}/api/dev/activity`, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
43
|
+
body: JSON.stringify(body),
|
|
44
|
+
}).catch(() => {
|
|
45
|
+
/* best-effort — hosted panel just won't show this beat */
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Start beating every `intervalMs` until the returned stop() is called. Fires
|
|
51
|
+
* one beat IMMEDIATELY so the cockpit lights up without waiting a full interval.
|
|
52
|
+
* Returns a no-op stop() when there's no bridge credential (sample / not-signed-
|
|
53
|
+
* in), so callers can wire it unconditionally.
|
|
54
|
+
* @param {{ activityUrl?: string, token?: string, url?: string,
|
|
55
|
+
* cliVersion?: string, runnerVersion?: string|null, intervalMs?: number }} [opts]
|
|
56
|
+
* @returns {() => void} stop the heartbeat (idempotent).
|
|
57
|
+
*/
|
|
58
|
+
export function startDevHeartbeat({
|
|
59
|
+
activityUrl,
|
|
60
|
+
token,
|
|
61
|
+
url,
|
|
62
|
+
cliVersion = CLI_VERSION,
|
|
63
|
+
runnerVersion,
|
|
64
|
+
intervalMs = HEARTBEAT_INTERVAL_MS,
|
|
65
|
+
} = {}) {
|
|
66
|
+
if (!activityUrl || !token) return () => {}; // no bridge / sample mode → no-op
|
|
67
|
+
const beat = () => postHeartbeat({ activityUrl, token, url, cliVersion, runnerVersion });
|
|
68
|
+
beat();
|
|
69
|
+
const timer = setInterval(beat, intervalMs);
|
|
70
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
71
|
+
return () => clearInterval(timer);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Convenience wrapper for the runner-spawn sites: start a heartbeat from the
|
|
76
|
+
* same bridge env that's threaded to the runner ({TOT_DEV_ACTIVITY_URL,
|
|
77
|
+
* TOT_DEV_ACTIVITY_TOKEN}, or {} for the `--sample` path — which yields a no-op,
|
|
78
|
+
* exactly the desired "never leak a real credential from sample" behavior). The
|
|
79
|
+
* runner version defaults to whatever `tot dev` resolved this invocation
|
|
80
|
+
* (clientPackages().runner — null until the runner is resolved).
|
|
81
|
+
* @param {{ TOT_DEV_ACTIVITY_URL?: string, TOT_DEV_ACTIVITY_TOKEN?: string }} bridgeEnv
|
|
82
|
+
* @param {{ url?: string, runnerVersion?: string|null }} [opts]
|
|
83
|
+
* @returns {() => void} stop the heartbeat.
|
|
84
|
+
*/
|
|
85
|
+
export function startHeartbeatFromEnv(bridgeEnv, { url, runnerVersion } = {}) {
|
|
86
|
+
return startDevHeartbeat({
|
|
87
|
+
activityUrl: bridgeEnv?.TOT_DEV_ACTIVITY_URL,
|
|
88
|
+
token: bridgeEnv?.TOT_DEV_ACTIVITY_TOKEN,
|
|
89
|
+
url,
|
|
90
|
+
runnerVersion: runnerVersion ?? clientPackages().runner,
|
|
91
|
+
});
|
|
92
|
+
}
|