@tokenoftrust/cli 1.3.4-rc.3 → 1.3.4-rc.4
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/bin/tot.cjs +44 -16
- package/package.json +1 -1
- package/src/commands/checkout.mjs +5 -0
- package/src/commands/dev.mjs +5 -2
- package/src/obstacle-beacon.cjs +111 -0
- package/src/obstacle.mjs +35 -0
package/bin/tot.cjs
CHANGED
|
@@ -40,21 +40,49 @@ if (!meets) {
|
|
|
40
40
|
" → next: install Node " + RECOMMENDED_NODE + " (LTS) — nvm: `nvm install " + RECOMMENDED_NODE +
|
|
41
41
|
" && nvm use " + RECOMMENDED_NODE + "`, or https://nodejs.org/ — then re-run the same command.\n"
|
|
42
42
|
);
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// Beacon the hosted cockpit that this machine dead-ended on the Node floor, then
|
|
44
|
+
// exit. This is THE headline obstacle: an invited dev whose `tot login` (the
|
|
45
|
+
// pasted setup command) can't even run because their Node is too old. We're
|
|
46
|
+
// pre-login, pre-ESM, maybe pre-`fetch` — so the beacon reads the bridge
|
|
47
|
+
// credential straight from the pasted flags and posts over require("https").
|
|
48
|
+
// Best-effort and time-boxed: the ✗ message is already printed; we only linger
|
|
49
|
+
// (≤ the beacon's own timeout) to deliver telemetry before exit(1).
|
|
50
|
+
beaconNodeTooOld(nodeVersion, function () { process.exit(1); });
|
|
51
|
+
} else {
|
|
52
|
+
// Supported Node from here on. Hand off to the ESM CLI; pathToFileURL keeps
|
|
53
|
+
// the import specifier correct on Windows drive-letter paths too.
|
|
54
|
+
var path = require("path");
|
|
55
|
+
var pathToFileURL = require("url").pathToFileURL;
|
|
56
|
+
var entry = pathToFileURL(path.join(__dirname, "tot.mjs")).href;
|
|
45
57
|
|
|
46
|
-
//
|
|
47
|
-
// the
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
// new Function with a CONSTANT body ("return import(u)") — nothing is ever
|
|
59
|
+
// interpolated into the code string; the entry URL travels as an argument. This
|
|
60
|
+
// indirection exists only so pre-import() parsers never see the import syntax.
|
|
61
|
+
new Function("u", "return import(u)")(entry).catch(function (e) {
|
|
62
|
+
// tot.mjs formats + exits on its own errors; landing here means the CLI
|
|
63
|
+
// itself failed to LOAD on a supported Node — a packaging bug, worth the detail.
|
|
64
|
+
process.stderr.write("✗ tot failed to start: " + ((e && e.message) || e) + "\n");
|
|
65
|
+
process.exit(1);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
51
68
|
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
// Fire the node-too-old obstacle beacon, then always call `done` (exactly once,
|
|
70
|
+
// bounded by the beacon's timeout). Isolated + fully guarded: any hiccup —
|
|
71
|
+
// missing flags, an old Node that can't require a .cjs, a network stall — just
|
|
72
|
+
// falls through to `done`, so the exit path is never blocked or altered. The
|
|
73
|
+
// heavy lifting lives in the shared ES5 helper so the wire shape can't drift.
|
|
74
|
+
function beaconNodeTooOld(have, done) {
|
|
75
|
+
try {
|
|
76
|
+
var mod = require("../src/obstacle-beacon.cjs");
|
|
77
|
+
var act = mod.parseActivityArgs(process.argv);
|
|
78
|
+
if (!act.url || !act.token) { done(); return; }
|
|
79
|
+
var cliVersion;
|
|
80
|
+
try { cliVersion = require("../package.json").version; } catch (e) {}
|
|
81
|
+
mod.beacon(
|
|
82
|
+
{ url: act.url, token: act.token, kind: "node-too-old", have: have, need: MIN_NODE, cliVersion: cliVersion },
|
|
83
|
+
done
|
|
84
|
+
);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
done();
|
|
87
|
+
}
|
|
88
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/cli",
|
|
3
|
-
"version": "1.3.4-rc.
|
|
3
|
+
"version": "1.3.4-rc.4",
|
|
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",
|
|
@@ -24,6 +24,7 @@ import { createMcpClient } from "../mcp.mjs";
|
|
|
24
24
|
import { establishSession, AuthUnavailableError } from "../auth.mjs";
|
|
25
25
|
import { CliError, fail, formatError } from "../errors.mjs";
|
|
26
26
|
import { writeNvmrc } from "../sample.mjs";
|
|
27
|
+
import { emitObstacle } from "../obstacle.mjs";
|
|
27
28
|
|
|
28
29
|
const execFileP = promisify(execFile);
|
|
29
30
|
|
|
@@ -202,6 +203,10 @@ async function cloneRepo(gitRemote, dir, redact) {
|
|
|
202
203
|
try {
|
|
203
204
|
await git(["clone", gitRemote, dir]);
|
|
204
205
|
} catch (e) {
|
|
206
|
+
// Beacon the cockpit before we surface the error — covers this path for both
|
|
207
|
+
// `tot checkout` and `tot start` (which clones through here). Awaited so the
|
|
208
|
+
// packet lands before the process prints + exits; swallowed either way.
|
|
209
|
+
await emitObstacle("clone-failed");
|
|
205
210
|
throw new CliError(`clone failed: ${redact(String(e.stderr || e.message || e))}`, {
|
|
206
211
|
next: `check the target dir is empty and you can reach the remote, then re-run`,
|
|
207
212
|
});
|
package/src/commands/dev.mjs
CHANGED
|
@@ -42,6 +42,7 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
42
42
|
import { createMcpClient, CLI_VERSION, setRunnerVersion, versionStamp } from "../mcp.mjs";
|
|
43
43
|
import { establishSession } from "../auth.mjs";
|
|
44
44
|
import { defaultCredentialsPath, readCredentials } from "../token-store.mjs";
|
|
45
|
+
import { emitObstacle } from "../obstacle.mjs";
|
|
45
46
|
import { CliError, fail, formatError } from "../errors.mjs";
|
|
46
47
|
import { openBrowser, waitForServer, firstFreePort } from "../open.mjs";
|
|
47
48
|
import { startProgress } from "../progress.mjs";
|
|
@@ -928,7 +929,7 @@ export async function installRunnerTarball({ source, version, isUrl = true, stri
|
|
|
928
929
|
stages: [{ afterMs: 20000, text: "still setting up the preview engine (first run only)…" }],
|
|
929
930
|
});
|
|
930
931
|
try {
|
|
931
|
-
runPnpmInstall(stagingDir, { logPath: installLog });
|
|
932
|
+
await runPnpmInstall(stagingDir, { logPath: installLog });
|
|
932
933
|
} finally {
|
|
933
934
|
installSpin.stop();
|
|
934
935
|
}
|
|
@@ -1022,7 +1023,7 @@ function ensureCorepackPnpm(runnerDir, { logPath } = {}) {
|
|
|
1022
1023
|
* caller's spinner owns the screen. On failure we surface a clean, business-
|
|
1023
1024
|
* readable message + the log path — never the raw node/pnpm firehose.
|
|
1024
1025
|
*/
|
|
1025
|
-
function runPnpmInstall(runnerDir, { logPath } = {}) {
|
|
1026
|
+
async function runPnpmInstall(runnerDir, { logPath } = {}) {
|
|
1026
1027
|
const fd = logPath ? openSync(logPath, "a") : null;
|
|
1027
1028
|
const installArgs = ["install", "--config.dangerouslyAllowAllBuilds=true"];
|
|
1028
1029
|
// npm FIRST: it ships with EVERY Node (including 25+, where corepack is no
|
|
@@ -1055,6 +1056,7 @@ function runPnpmInstall(runnerDir, { logPath } = {}) {
|
|
|
1055
1056
|
continue;
|
|
1056
1057
|
}
|
|
1057
1058
|
// The launcher ran; the install itself failed. That's the actionable error.
|
|
1059
|
+
await emitObstacle("install-failed");
|
|
1058
1060
|
throw new CliError(
|
|
1059
1061
|
`couldn't set up the store preview engine${pnpmFailureHint(logPath)}` +
|
|
1060
1062
|
(logPath ? `\n details: ${logPath}` : ""),
|
|
@@ -1064,6 +1066,7 @@ function runPnpmInstall(runnerDir, { logPath } = {}) {
|
|
|
1064
1066
|
// Every launcher ENOENT'd → there's no pnpm on this machine and corepack
|
|
1065
1067
|
// couldn't provide one (corepack isn't bundled on Node 25+). npm ships with
|
|
1066
1068
|
// every Node, so `npm i -g pnpm` is the escape hatch that always exists.
|
|
1069
|
+
await emitObstacle("pnpm-missing");
|
|
1067
1070
|
throw new CliError(
|
|
1068
1071
|
"couldn't set up the store preview engine — pnpm isn't available on this machine" +
|
|
1069
1072
|
(logPath ? `\n details: ${logPath}` : ""),
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The obstacle beacon — a fire-and-forget POST that tells the hosted cockpit a
|
|
3
|
+
* `tot start` / `tot checkout` failed, so it can show the exact fix in the bridge
|
|
4
|
+
* strip (obstacle lane, server side already shipped). Best-effort telemetry that
|
|
5
|
+
* rides ALONGSIDE the house-style `✗ … → next:` error; it must NEVER change,
|
|
6
|
+
* delay past its timeout, or fail that error path.
|
|
7
|
+
*
|
|
8
|
+
* KEEP THIS FILE ES5/CommonJS: var, string concat, function expressions — no
|
|
9
|
+
* arrow functions, template literals, optional chaining, or const/let. The ES5
|
|
10
|
+
* launcher (bin/tot.cjs) require()s this on a machine that FAILED the Node
|
|
11
|
+
* floor (Node < 22.12, no global `fetch`, no ESM) to beacon `node-too-old`
|
|
12
|
+
* pre-login — the exact old-Node dead-end the whole obstacle lane exists for.
|
|
13
|
+
* So this uses require("https"/"http"), and a test keeps it parseable by
|
|
14
|
+
* ancient Nodes. The ESM side (src/obstacle.mjs) imports beaconAsync from here
|
|
15
|
+
* too, so the wire shape + endpoint live in ONE place and can't drift.
|
|
16
|
+
*
|
|
17
|
+
* The event contract is DECIDED (owned by the server's rail-obstacle-lane):
|
|
18
|
+
* POST <activity-url>/api/dev/activity Authorization: Bearer <token>
|
|
19
|
+
* { event:"obstacle", kind, have?, need?, cliVersion?, at? }
|
|
20
|
+
* Send ONLY these machine-readable fields — the human remediation copy is composed
|
|
21
|
+
* SERVER-side per kind, so guidance changes without a CLI release. The server
|
|
22
|
+
* stamps its own reportedAt (skew-proof) and a later heartbeat CLEARS the obstacle.
|
|
23
|
+
*/
|
|
24
|
+
"use strict";
|
|
25
|
+
|
|
26
|
+
var VALID_KINDS = { "node-too-old": 1, "pnpm-missing": 1, "install-failed": 1, "clone-failed": 1 };
|
|
27
|
+
var TIMEOUT_MS = 2000;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Pull --activity-url / --activity-token straight out of argv. The launcher runs
|
|
31
|
+
* BEFORE any arg framework (and before login has cached anything), so on the
|
|
32
|
+
* node-too-old path this is the only source of the bridge credential — the pasted
|
|
33
|
+
* setup command carries both flags. Returns {} for whatever is absent.
|
|
34
|
+
*/
|
|
35
|
+
function parseActivityArgs(argv) {
|
|
36
|
+
var out = { url: undefined, token: undefined };
|
|
37
|
+
if (!argv) return out;
|
|
38
|
+
for (var i = 0; i < argv.length; i++) {
|
|
39
|
+
if (argv[i] === "--activity-url") out.url = argv[i + 1];
|
|
40
|
+
else if (argv[i] === "--activity-token") out.token = argv[i + 1];
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Fire one obstacle beacon. Everything is wrapped so it can never throw into the
|
|
47
|
+
* caller's error path; `done` is invoked EXACTLY once (on success, error, timeout,
|
|
48
|
+
* or a skip) — the launcher passes process.exit as `done` so the process lingers
|
|
49
|
+
* only as long as the beacon (≤ TIMEOUT_MS) before exiting.
|
|
50
|
+
* opts: { url, token, kind, have?, need?, cliVersion?, at? }
|
|
51
|
+
*/
|
|
52
|
+
function beacon(opts, done) {
|
|
53
|
+
var finished = false;
|
|
54
|
+
function finish() {
|
|
55
|
+
if (finished) return;
|
|
56
|
+
finished = true;
|
|
57
|
+
try { if (timer) clearTimeout(timer); } catch (e) {}
|
|
58
|
+
if (typeof done === "function") { try { done(); } catch (e) {} }
|
|
59
|
+
}
|
|
60
|
+
var timer = null;
|
|
61
|
+
try {
|
|
62
|
+
opts = opts || {};
|
|
63
|
+
if (!opts.url || !opts.token || !VALID_KINDS[opts.kind]) { finish(); return; }
|
|
64
|
+
|
|
65
|
+
var body = { event: "obstacle", kind: opts.kind };
|
|
66
|
+
if (opts.have) body.have = opts.have;
|
|
67
|
+
if (opts.need) body.need = opts.need;
|
|
68
|
+
if (opts.cliVersion) body.cliVersion = opts.cliVersion;
|
|
69
|
+
body.at = opts.at || Date.now();
|
|
70
|
+
var payload = JSON.stringify(body);
|
|
71
|
+
|
|
72
|
+
var endpoint = String(opts.url).replace(/\/+$/, "") + "/api/dev/activity";
|
|
73
|
+
var lib = endpoint.indexOf("http://") === 0 ? require("http") : require("https");
|
|
74
|
+
|
|
75
|
+
// A hard timeout guarantees the caller's exit (or await) is bounded even if
|
|
76
|
+
// the socket hangs — telemetry never holds a failing developer hostage.
|
|
77
|
+
timer = setTimeout(finish, TIMEOUT_MS);
|
|
78
|
+
|
|
79
|
+
var req = lib.request(
|
|
80
|
+
endpoint,
|
|
81
|
+
{
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: {
|
|
84
|
+
"content-type": "application/json",
|
|
85
|
+
authorization: "Bearer " + opts.token,
|
|
86
|
+
"content-length": Buffer.byteLength(payload),
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
function (res) {
|
|
90
|
+
// Drain and finish — we don't care about the status, only that it was sent.
|
|
91
|
+
res.on("data", function () {});
|
|
92
|
+
res.on("end", finish);
|
|
93
|
+
res.on("error", finish);
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
req.on("error", finish);
|
|
97
|
+
req.write(payload);
|
|
98
|
+
req.end();
|
|
99
|
+
} catch (e) {
|
|
100
|
+
finish();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Promise wrapper for the ESM side (src/obstacle.mjs) so a failure path can await delivery. */
|
|
105
|
+
function beaconAsync(opts) {
|
|
106
|
+
return new Promise(function (resolve) {
|
|
107
|
+
try { beacon(opts, resolve); } catch (e) { resolve(); }
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { parseActivityArgs: parseActivityArgs, beacon: beacon, beaconAsync: beaconAsync };
|
package/src/obstacle.mjs
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESM front door to the obstacle beacon (src/obstacle-beacon.cjs). The post-login
|
|
3
|
+
* failure paths — `pnpm-missing`, `install-failed`, `clone-failed` — run on a
|
|
4
|
+
* supported Node where the bridge credential is already cached, so they resolve
|
|
5
|
+
* {url, token} from ~/.tot/credentials.json (the same source activityBridgeEnv +
|
|
6
|
+
* the heartbeat use) rather than argv. `node-too-old` does NOT come through here:
|
|
7
|
+
* it fires from the ES5 launcher pre-login, where creds may not exist yet.
|
|
8
|
+
*
|
|
9
|
+
* emitObstacle is awaited at the failure site so the packet is delivered before
|
|
10
|
+
* the process prints its `✗ … → next:` and exits — but it is fully swallowed, so
|
|
11
|
+
* telemetry can never disrupt or delay (past the beacon's own ~2s timeout) the
|
|
12
|
+
* error the developer actually needs to see.
|
|
13
|
+
*/
|
|
14
|
+
import beaconCore from "./obstacle-beacon.cjs";
|
|
15
|
+
import { CLI_VERSION } from "./mcp.mjs";
|
|
16
|
+
import { defaultCredentialsPath, readCredentials } from "./token-store.mjs";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Best-effort obstacle beacon for a post-login failure. No-op (silent) when no
|
|
20
|
+
* bridge credential is cached — the developer signed in with a build that didn't
|
|
21
|
+
* carry the activity flags, or ran a bare `tot login`.
|
|
22
|
+
* @param {"pnpm-missing"|"install-failed"|"clone-failed"} kind
|
|
23
|
+
* @param {{ have?: string, need?: string, env?: NodeJS.ProcessEnv }} [opts]
|
|
24
|
+
*/
|
|
25
|
+
export async function emitObstacle(kind, { have, need, env = process.env } = {}) {
|
|
26
|
+
try {
|
|
27
|
+
const creds = readCredentials(defaultCredentialsPath(env));
|
|
28
|
+
const url = creds?.activityUrl;
|
|
29
|
+
const token = creds?.activityToken;
|
|
30
|
+
if (!url || !token) return;
|
|
31
|
+
await beaconCore.beaconAsync({ url, token, kind, have, need, cliVersion: CLI_VERSION });
|
|
32
|
+
} catch {
|
|
33
|
+
/* best-effort telemetry — never disrupts the error path */
|
|
34
|
+
}
|
|
35
|
+
}
|