@tokenoftrust/cli 1.3.1-rc.2 → 1.3.3-rc.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/cli",
3
- "version": "1.3.1-rc.2",
3
+ "version": "1.3.3-rc.0",
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",
@@ -36,6 +36,7 @@
36
36
  "registry": "https://registry.npmjs.org"
37
37
  },
38
38
  "scripts": {
39
- "test": "node --test"
39
+ "test": "node --test",
40
+ "prepublishOnly": "node ../../scripts/build/check-cli-clean.mjs ."
40
41
  }
41
42
  }
@@ -314,7 +314,8 @@ export function bootNative(runnerDir, workspace, port, url, args) {
314
314
  // Heartbeat the hosted cockpit (G1) with the CLI version + this live localhost
315
315
  // URL while the runner runs — CLI-side, using the SAME bridge credential the
316
316
  // runner's file-save path uses (no-op in --sample, which threads no credential).
317
- const stopHeartbeat = startHeartbeatFromEnv(bridgeEnv, { url });
317
+ // `cwd` (the checkout root) lets the cockpit show the FULL path to edit.
318
+ const stopHeartbeat = startHeartbeatFromEnv(bridgeEnv, { url, cwd: workspace });
318
319
  handle.done.finally(() => stopHeartbeat());
319
320
 
320
321
  // Auto-open the browser the moment the server answers (D). Non-blocking so
@@ -997,7 +998,7 @@ async function runContainer(workspace, args, ctx) {
997
998
  // Heartbeat the hosted cockpit (G1) CLI-side while the container runs — the
998
999
  // container reports file-saves via the threaded env, but the CLI owns the
999
1000
  // version + live URL. Same bridge credential (activityBridgeEnv), no-op absent.
1000
- const stopHeartbeat = startHeartbeatFromEnv(activityBridgeEnv(), { url: plan.url });
1001
+ const stopHeartbeat = startHeartbeatFromEnv(activityBridgeEnv(), { url: plan.url, cwd: plan.workspace });
1001
1002
  handle.done.finally(() => stopHeartbeat());
1002
1003
 
1003
1004
  // Auto-open the browser the moment the server answers (D). Non-blocking so
@@ -29,14 +29,22 @@ const HEARTBEAT_INTERVAL_MS = 10_000;
29
29
  * an activity URL and a bearer token. Never throws — a failed/offline hosted
30
30
  * worker just means the cockpit doesn't light up this beat.
31
31
  * @param {{ activityUrl?: string, token?: string, url?: string,
32
- * cliVersion?: string, runnerVersion?: string|null }} args
32
+ * cliVersion?: string, runnerVersion?: string|null, editor?: string|null }} args
33
33
  */
34
- export function postHeartbeat({ activityUrl, token, url, cliVersion, runnerVersion } = {}) {
34
+ export function postHeartbeat({ activityUrl, token, url, cliVersion, runnerVersion, editor, cwd } = {}) {
35
35
  if (!activityUrl || !token) return undefined;
36
36
  /** @type {Record<string, unknown>} */
37
37
  const body = { event: "heartbeat", cliVersion, at: Date.now() };
38
38
  if (runnerVersion) body.runnerVersion = runnerVersion;
39
39
  if (url) body.url = url;
40
+ // The developer's terminal $EDITOR (if set) — lets the cockpit suggest the exact
41
+ // "open this file" command (e.g. `code content/home.html`). Never a secret; a
42
+ // no-op when unset (the cockpit falls back to a generic hint).
43
+ if (editor) body.editor = editor;
44
+ // The project directory (the checkout root) — lets the cockpit show the FULL path
45
+ // to the file to edit so the command works from any directory. The dev's own local
46
+ // path (low sensitivity, like `url`); absent → the cockpit uses a relative path.
47
+ if (cwd) body.cwd = cwd;
40
48
  return fetch(`${String(activityUrl).replace(/\/+$/, "")}/api/dev/activity`, {
41
49
  method: "POST",
42
50
  headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
@@ -52,7 +60,8 @@ export function postHeartbeat({ activityUrl, token, url, cliVersion, runnerVersi
52
60
  * Returns a no-op stop() when there's no bridge credential (sample / not-signed-
53
61
  * in), so callers can wire it unconditionally.
54
62
  * @param {{ activityUrl?: string, token?: string, url?: string,
55
- * cliVersion?: string, runnerVersion?: string|null, intervalMs?: number }} [opts]
63
+ * cliVersion?: string, runnerVersion?: string|null, editor?: string|null,
64
+ * cwd?: string|null, intervalMs?: number }} [opts]
56
65
  * @returns {() => void} stop the heartbeat (idempotent).
57
66
  */
58
67
  export function startDevHeartbeat({
@@ -61,32 +70,52 @@ export function startDevHeartbeat({
61
70
  url,
62
71
  cliVersion = CLI_VERSION,
63
72
  runnerVersion,
73
+ editor,
74
+ cwd,
64
75
  intervalMs = HEARTBEAT_INTERVAL_MS,
65
76
  } = {}) {
66
77
  if (!activityUrl || !token) return () => {}; // no bridge / sample mode → no-op
67
- const beat = () => postHeartbeat({ activityUrl, token, url, cliVersion, runnerVersion });
78
+ const beat = () => postHeartbeat({ activityUrl, token, url, cliVersion, runnerVersion, editor, cwd });
68
79
  beat();
69
80
  const timer = setInterval(beat, intervalMs);
70
81
  if (typeof timer.unref === "function") timer.unref();
71
82
  return () => clearInterval(timer);
72
83
  }
73
84
 
85
+ /**
86
+ * Resolve the developer's terminal editor command for the "open this file"
87
+ * cockpit hint — VISUAL wins over EDITOR (the POSIX convention: VISUAL is the
88
+ * full-screen editor, EDITOR the line-editor fallback). Returns the trimmed
89
+ * command (which may carry flags, e.g. "code --wait") or undefined when neither
90
+ * is set — in which case the cockpit shows a generic hint instead of a wrong one.
91
+ * @param {NodeJS.ProcessEnv} [env]
92
+ * @returns {string|undefined}
93
+ */
94
+ export function resolveEditor(env = process.env) {
95
+ const e = String(env?.VISUAL || env?.EDITOR || "").trim();
96
+ return e || undefined;
97
+ }
98
+
74
99
  /**
75
100
  * Convenience wrapper for the runner-spawn sites: start a heartbeat from the
76
101
  * same bridge env that's threaded to the runner ({TOT_DEV_ACTIVITY_URL,
77
102
  * TOT_DEV_ACTIVITY_TOKEN}, or {} for the `--sample` path — which yields a no-op,
78
103
  * exactly the desired "never leak a real credential from sample" behavior). The
79
104
  * runner version defaults to whatever `tot dev` resolved this invocation
80
- * (clientPackages().runner — null until the runner is resolved).
105
+ * (clientPackages().runner — null until the runner is resolved). The editor comes
106
+ * from the CLI's own env (VISUAL/EDITOR) so the cockpit can suggest the exact
107
+ * open-file command.
81
108
  * @param {{ TOT_DEV_ACTIVITY_URL?: string, TOT_DEV_ACTIVITY_TOKEN?: string }} bridgeEnv
82
- * @param {{ url?: string, runnerVersion?: string|null }} [opts]
109
+ * @param {{ url?: string, runnerVersion?: string|null, cwd?: string|null, env?: NodeJS.ProcessEnv }} [opts]
83
110
  * @returns {() => void} stop the heartbeat.
84
111
  */
85
- export function startHeartbeatFromEnv(bridgeEnv, { url, runnerVersion } = {}) {
112
+ export function startHeartbeatFromEnv(bridgeEnv, { url, runnerVersion, cwd, env = process.env } = {}) {
86
113
  return startDevHeartbeat({
87
114
  activityUrl: bridgeEnv?.TOT_DEV_ACTIVITY_URL,
88
115
  token: bridgeEnv?.TOT_DEV_ACTIVITY_TOKEN,
89
116
  url,
90
117
  runnerVersion: runnerVersion ?? clientPackages().runner,
118
+ editor: resolveEditor(env),
119
+ cwd,
91
120
  });
92
121
  }
package/src/sample.mjs CHANGED
@@ -9,16 +9,19 @@
9
9
  * regulated tenant's identity so the runner's tenant registry resolves it with
10
10
  * its `compliance` block on.
11
11
  *
12
- * WHY GIANTVAPES (not a novel synthetic domain): compliance + catalog are
13
- * resolved by the RUNNER's tenant registry (apps/storefront/src/config/tenants.ts),
14
- * keyed by domain. `giantvapes.com` is a registered 21+/PACT vape store WITH a
15
- * `compliance` block and a 50-product catalog fixture baked into the runner, so
16
- * the sample lights up age-gate + nicotine warning + a populated catalog with
17
- * zero server. A truly novel domain (e.g. "sample.example") is unclaimed the
18
- * runner falls back to HOME_TENANT (siteType "marketing", NO compliance, empty
19
- * catalog), which would defeat the whole "the free taste still does compliance"
20
- * goalso we deliberately borrow giantvapes' identity for the sample. It's the
21
- * smallest of the vapor tenants (448K), so it keeps the npm tarball light.
12
+ * WHY sample-store.example (a FICTIONAL store, not a real merchant): compliance
13
+ * is resolved by the RUNNER's SANITIZED tenant registry
14
+ * (apps/storefront/src/config/tenants.ts rewritten at build time by
15
+ * scripts/build/build-runner.mjs), keyed by domain. The runner registers ONE
16
+ * generic sample tenant, `sample-store.example`, that carries a `compliance`
17
+ * block (minAge 21, nicotineWarning), so the sample lights up the age-gate +
18
+ * nicotine warning off a NON-merchant identity with zero server. The bundled
19
+ * template below (`template/sample-store/`) is neutral, fictional "Sample Vapor
20
+ * Co." content no real merchant's brand, copy, or assets ship here.
21
+ *
22
+ * (Earlier this borrowed a real vape merchant's identity + content to get a
23
+ * populated catalog; that leaked private merchant IP into public npm and was
24
+ * replaced — see the H1/H2 runner-sanitization work — with this generic sample.)
22
25
  *
23
26
  * Dependency-free (node:fs + node:path only).
24
27
  */
@@ -30,10 +33,10 @@ import { dirname, join, resolve } from "node:path";
30
33
 
31
34
  const here = dirname(fileURLToPath(import.meta.url)); // packages/cli/src
32
35
 
33
- /** The tenant identity the sample borrows so compliance + catalog resolve (see module doc). */
34
- export const SAMPLE_TENANT = "giantvapes";
36
+ /** The generic sample tenant the runner registers with a compliance block (see module doc). */
37
+ export const SAMPLE_TENANT = "sample-store.example";
35
38
  /** Dotted scope → the runner's path-prefix route + registry lookup (must contain a dot per context.mjs). */
36
- export const SAMPLE_SCOPE = "giantvapes.com";
39
+ export const SAMPLE_SCOPE = "sample-store.example";
37
40
  /** Default directory name scaffolded when the user doesn't name one. */
38
41
  export const SAMPLE_DIR_NAME = "sample-store";
39
42