@tokenoftrust/cli 4.4.0 → 4.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/cli",
3
- "version": "4.4.0",
3
+ "version": "4.4.2",
4
4
  "description": "Token of Trust developer CLI — clone 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 @@ import {
36
36
  import { createMcpClient } from "../mcp.mjs";
37
37
  import { establishSession, AuthUnavailableError } from "../auth.mjs";
38
38
  import { fail } from "../errors.mjs";
39
+ import { previewPrUrl } from "../preview-url.mjs";
39
40
  import { scopeToTenant } from "../tenant-scope.mjs";
40
41
  import { repoNameFromRemote, currentBranch } from "./submit.mjs";
41
42
  // `tot pr list` reuses `tot ship --pr`'s operator-secret transport helpers verbatim
@@ -53,7 +54,7 @@ const DEFAULT_STOREFRONT_URL = "https://storefront.tokenoftrust.store";
53
54
  const SUBCOMMANDS = ["list", "view", "close"];
54
55
 
55
56
  /**
56
- * The storefront-owned, shareable `/preview/<tenant>/pr/<N>` link — NEVER the
57
+ * The storefront-owned, shareable `/<tenant>/preview/pr/<N>` link — NEVER the
57
58
  * forge/Gitea `url` (2026-08-18 incident: a raw forge PR URL reached an
58
59
  * owner). `candidate_status` (the local-checkout MCP tool) has no
59
60
  * `previewUrl` field at all, unlike the operator `GET /api/changes` path — so
@@ -66,8 +67,7 @@ const SUBCOMMANDS = ["list", "view", "close"];
66
67
  */
67
68
  export function buildPreviewUrl(storefrontUrl, tenant, prNumber) {
68
69
  if (typeof prNumber !== "number" || !tenant) return null;
69
- const base = (storefrontUrl || DEFAULT_STOREFRONT_URL).trim().replace(/\/+$/, "");
70
- return `${base}/preview/${tenant}/pr/${prNumber}`;
70
+ return previewPrUrl(storefrontUrl || DEFAULT_STOREFRONT_URL, tenant, prNumber);
71
71
  }
72
72
 
73
73
  const USAGE = `tot pr — see and manage candidate PRs
@@ -30,6 +30,7 @@
30
30
  */
31
31
  import { execFileSync } from "node:child_process";
32
32
  import { planForAction, printPlanAndConfirm } from "../plan.mjs";
33
+ import { previewPrUrl } from "../preview-url.mjs";
33
34
 
34
35
  const DEFAULT_STOREFRONT_URL = "https://storefront.tokenoftrust.store";
35
36
 
@@ -147,7 +148,7 @@ export async function run(argv, ctx) {
147
148
  return 2;
148
149
  }
149
150
 
150
- const previewUrl = pr != null ? `${base}/preview/${encodeURIComponent(tenant)}/pr/${pr}` : null;
151
+ const previewUrl = pr != null ? previewPrUrl(base, tenant, pr) : null;
151
152
 
152
153
  // Self-declaring: state the EXACT plan before acting (the shared plan module).
153
154
  const planLines = planForAction({
@@ -39,6 +39,7 @@ import { fail } from "../errors.mjs";
39
39
  import { scopeToTenant } from "../tenant-scope.mjs";
40
40
  import { isInteractive, promptYesNo } from "../prompt.mjs";
41
41
  import { startProgress } from "../progress.mjs";
42
+ import { previewRevUrl } from "../preview-url.mjs";
42
43
  import { resolveViewerTransport } from "../viewer-session.mjs";
43
44
 
44
45
  const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
@@ -145,7 +146,7 @@ export function normalizeRollbackResult(r) {
145
146
  // ─── Revision links (pure) ───────────────────────────────────────────────
146
147
 
147
148
  /**
148
- * The immutable `/preview/<tenant>/rev/<sha>/` deep-link for a promoted
149
+ * The immutable `/<tenant>/preview/rev/<sha>/` deep-link for a promoted
149
150
  * `versionId` — this route, and per `gitea-only-tenant-content-authority` the
150
151
  * `versionId` recorded by a promote IS the tenant Gitea sha, so no extra lookup
151
152
  * is needed to build it. `null` when there's no `base` (storefront origin) or no
@@ -158,7 +159,7 @@ export function normalizeRollbackResult(r) {
158
159
  */
159
160
  export function revisionUrl(base, tenant, versionId) {
160
161
  if (!base || !versionId) return null;
161
- return `${String(base).replace(/\/+$/, "")}/preview/${encodeURIComponent(tenant)}/rev/${encodeURIComponent(versionId)}/`;
162
+ return previewRevUrl(base, tenant, versionId);
162
163
  }
163
164
 
164
165
  // ─── Candidate selection + rendering (pure) ──────────────────────────────────────
@@ -57,6 +57,7 @@ import { startProgress } from "../progress.mjs";
57
57
  import { CliError, fail } from "../errors.mjs";
58
58
  import { isBrokerUnavailable, waitOutBrokerOutage } from "../broker-outage.mjs";
59
59
  import { emitActivity } from "../activity.mjs";
60
+ import { previewPrUrl } from "../preview-url.mjs";
60
61
  import {
61
62
  diagnosticReference,
62
63
  invocationDegradedResult,
@@ -2096,7 +2097,7 @@ function reportComplianceCheck(c) {
2096
2097
  * formatShareableUrlBlock's job, above — the two are deliberately redundant:
2097
2098
  * this one is available immediately but "building", that one is authoritative
2098
2099
  * once reconcile actually lands). Same route shape as the server-minted one
2099
- * (`/preview/<tenant>/pr/<N>`) by construction — see
2100
+ * (`/<tenant>/preview/pr/<N>`) by construction — see
2100
2101
  * docs/architecture/preview-candidate-workflow.md — so the link doesn't change
2101
2102
  * out from under the reviewer once the build completes; it just starts
2102
2103
  * resolving.
@@ -2108,7 +2109,7 @@ function reportComplianceCheck(c) {
2108
2109
  * @returns {string}
2109
2110
  */
2110
2111
  export function shareablePrUrl(base, tenant, prNumber) {
2111
- return `${String(base).replace(/\/+$/, "")}/preview/${tenant}/pr/${prNumber}`;
2112
+ return previewPrUrl(base, tenant, prNumber) ?? "";
2112
2113
  }
2113
2114
 
2114
2115
  /**
@@ -68,6 +68,9 @@ export interface NicotineWarningPresentation {
68
68
  textTransform: "uppercase" | "none";
69
69
  fontWeight: number;
70
70
  fontSizePx: number;
71
+ borderWidthPx?: number;
72
+ paddingYPx?: number;
73
+ letterSpacingPx?: number;
71
74
  }
72
75
  export interface ResolvedNicotineWarning {
73
76
  enabled: boolean;
@@ -79,6 +82,8 @@ export interface ResolvedNicotineWarning {
79
82
  export const NICOTINE_WARNING_TEXT: string;
80
83
  export const CONSPICUITY_FLOOR: Readonly<{ contrast: number; fontSizePx: number; fontWeight: number }>;
81
84
  export const DEFAULT_NICOTINE_WARNING_PRESET: string;
85
+ export const NICOTINE_WARNING_BOX_BOUNDS: Readonly<Record<"borderWidthPx" | "paddingYPx" | "letterSpacingPx", Readonly<{ min: number; max: number; integer: boolean }>>>;
86
+ export const MEDIUM_WEIGHT_REQUIREMENTS: Readonly<{ fontSizePx: number; contrast: number; borderWidthPx: number }>;
82
87
  export const NICOTINE_WARNING_PRESETS: Readonly<Record<string, Readonly<NicotineWarningPresentation>>>;
83
88
  export const NICOTINE_WARNING_KEYS: readonly string[];
84
89
  export const NICOTINE_WARNING_STYLE_KEYS: readonly string[];
@@ -782,6 +782,16 @@ export const CONSPICUITY_FLOOR = Object.freeze({ contrast: 7, fontSizePx: 12, fo
782
782
  /** Bounds on the size a custom style may declare, so the bar cannot swallow the header. */
783
783
  const MAX_FONT_SIZE_PX = 24;
784
784
 
785
+ /** Bounds on the optional box and spacing keys; an unset key renders the preset's own frame. */
786
+ export const NICOTINE_WARNING_BOX_BOUNDS = Object.freeze({
787
+ borderWidthPx: Object.freeze({ min: 0, max: 8, integer: true }),
788
+ paddingYPx: Object.freeze({ min: 0, max: 24, integer: true }),
789
+ letterSpacingPx: Object.freeze({ min: 0, max: 4, integer: false }),
790
+ });
791
+
792
+ /** Weight 500 is allowed only on a boxed bar that is at least this large, contrasty and bordered. */
793
+ export const MEDIUM_WEIGHT_REQUIREMENTS = Object.freeze({ fontSizePx: 16, contrast: 7, borderWidthPx: 2 });
794
+
785
795
  export const DEFAULT_NICOTINE_WARNING_PRESET = "dark";
786
796
 
787
797
  /**
@@ -816,6 +826,9 @@ export const NICOTINE_WARNING_PRESETS = Object.freeze({
816
826
  * textTransform: "uppercase" | "none",
817
827
  * fontWeight: number,
818
828
  * fontSizePx: number,
829
+ * borderWidthPx?: number,
830
+ * paddingYPx?: number,
831
+ * letterSpacingPx?: number,
819
832
  * }} NicotineWarningPresentation
820
833
  */
821
834
 
@@ -824,9 +837,10 @@ export const NICOTINE_WARNING_KEYS = Object.freeze(["preset", "style", "checkWid
824
837
 
825
838
  export const NICOTINE_WARNING_STYLE_KEYS = Object.freeze([
826
839
  "background", "text", "border", "textTransform", "fontWeight", "fontSizePx",
840
+ "borderWidthPx", "paddingYPx", "letterSpacingPx",
827
841
  ]);
828
842
  const TEXT_TRANSFORMS = ["uppercase", "none"];
829
- const FONT_WEIGHTS = [600, 700, 800, 900];
843
+ const FONT_WEIGHTS = [500, 600, 700, 800, 900];
830
844
  const HEX = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
831
845
 
832
846
  /** `#abc` / `#AABBCC` → `#aabbcc`. */
@@ -865,7 +879,15 @@ export function conspicuityProblems(p) {
865
879
  out.push(`font size ${p.fontSizePx}px is below the ${CONSPICUITY_FLOOR.fontSizePx}px floor`);
866
880
  }
867
881
  if (p.fontWeight < CONSPICUITY_FLOOR.fontWeight) {
868
- out.push(`font weight ${p.fontWeight} is below the ${CONSPICUITY_FLOOR.fontWeight} floor`);
882
+ const need = MEDIUM_WEIGHT_REQUIREMENTS;
883
+ const medium = p.fontWeight === 500 && p.fontSizePx >= need.fontSizePx && ratio >= need.contrast
884
+ && (p.borderWidthPx ?? 0) >= need.borderWidthPx;
885
+ if (!medium) {
886
+ out.push(`font weight ${p.fontWeight} is below the ${CONSPICUITY_FLOOR.fontWeight} floor` +
887
+ (p.fontWeight === 500
888
+ ? ` (500 needs font size >= ${need.fontSizePx}px, contrast >= ${need.contrast}:1 and borderWidthPx >= ${need.borderWidthPx})`
889
+ : ""));
890
+ }
869
891
  }
870
892
  return out;
871
893
  }
@@ -885,6 +907,14 @@ function styleValueProblem(key, value) {
885
907
  return typeof value === "number" && Number.isFinite(value) && value >= CONSPICUITY_FLOOR.fontSizePx && value <= MAX_FONT_SIZE_PX
886
908
  ? null
887
909
  : `\`fontSizePx\` must be a number from ${CONSPICUITY_FLOOR.fontSizePx} to ${MAX_FONT_SIZE_PX}`;
910
+ case "borderWidthPx":
911
+ case "paddingYPx":
912
+ case "letterSpacingPx": {
913
+ const { min, max, integer } = NICOTINE_WARNING_BOX_BOUNDS[key];
914
+ return typeof value === "number" && Number.isFinite(value) && value >= min && value <= max && (!integer || Number.isInteger(value))
915
+ ? null
916
+ : `\`${key}\` must be ${integer ? "a whole number" : "a number"} from ${min} to ${max}`;
917
+ }
888
918
  default:
889
919
  return `\`${key}\` is not a nicotine-warning style key (allowed: ${NICOTINE_WARNING_STYLE_KEYS.join(", ")})`;
890
920
  }
@@ -993,7 +1023,10 @@ export function nicotineWarningCheckWidths(declaration) {
993
1023
  */
994
1024
  export function describeNicotineWarningPresentation(p) {
995
1025
  return `background ${p.background}, text ${p.text}, border ${p.border}, ` +
996
- `${p.textTransform === "uppercase" ? "uppercase" : "normal case"}, weight ${p.fontWeight}, ${p.fontSizePx}px`;
1026
+ `${p.textTransform === "uppercase" ? "uppercase" : "normal case"}, weight ${p.fontWeight}, ${p.fontSizePx}px` +
1027
+ (p.borderWidthPx === undefined ? "" : `, border width ${p.borderWidthPx}px`) +
1028
+ (p.paddingYPx === undefined ? "" : `, vertical padding ${p.paddingYPx}px`) +
1029
+ (p.letterSpacingPx === undefined ? "" : `, letter spacing ${p.letterSpacingPx}px`);
997
1030
  }
998
1031
 
999
1032
  /**
@@ -1017,11 +1050,12 @@ export function describeNicotineWarningPresentation(p) {
1017
1050
  */
1018
1051
  export function nicotineWarningHtml(declaration, variant) {
1019
1052
  const { presentation, preset } = resolveNicotineWarning(declaration);
1053
+ const barPadding = presentation.paddingYPx === undefined ? "0.5rem" : `${presentation.paddingYPx}px`;
1020
1054
  const attrs = `style="${nicotineWarningStyle(presentation, variant)}" role="note" aria-label="Health warning" ` +
1021
1055
  `data-compliance="nicotine-warning" data-nicotine-warning-preset="${preset ?? "custom"}"`;
1022
1056
  const text = NICOTINE_WARNING_TEXT_LOCK;
1023
1057
  return variant === "bar"
1024
- ? `<div ${attrs}><p class="container-prose" style="margin-block:0;padding-block:0.5rem;text-align:center;${text}">${NICOTINE_WARNING_TEXT}</p></div>`
1058
+ ? `<div ${attrs}><p class="container-prose" style="margin-block:0;padding-block:${barPadding};text-align:center;${text}">${NICOTINE_WARNING_TEXT}</p></div>`
1025
1059
  : `<div ${attrs}><p style="margin:0;${text}">${NICOTINE_WARNING_TEXT}</p></div>`;
1026
1060
  }
1027
1061
 
@@ -1061,15 +1095,17 @@ const NICOTINE_WARNING_TEXT_LOCK = [
1061
1095
  */
1062
1096
  export function nicotineWarningStyle(p, variant) {
1063
1097
  const size = variant === "block" ? Math.max(p.fontSizePx, 14) : p.fontSizePx;
1098
+ const boxed = `${p.borderWidthPx ?? 2}px`;
1064
1099
  const frame = variant === "bar"
1065
- ? [`border-bottom:2px solid ${p.border}`]
1066
- : [`border:2px solid ${p.border}`, "border-radius:var(--radius-md)", "padding:0.75rem 1rem"];
1100
+ ? [p.borderWidthPx === undefined ? `border-bottom:2px solid ${p.border}` : `border:${boxed} solid ${p.border}`]
1101
+ : [`border:${boxed} solid ${p.border}`, "border-radius:var(--radius-md)",
1102
+ `padding:${p.paddingYPx === undefined ? "0.75rem" : `${p.paddingYPx}px`} 1rem`];
1067
1103
  return [
1068
1104
  ...frame,
1069
1105
  `background-color:${p.background}`,
1070
1106
  `color:${p.text}`,
1071
1107
  `text-transform:${p.textTransform}`,
1072
- `letter-spacing:${p.textTransform === "uppercase" ? "0.06em" : "normal"}`,
1108
+ `letter-spacing:${p.letterSpacingPx !== undefined ? `${p.letterSpacingPx}px` : p.textTransform === "uppercase" ? "0.06em" : "normal"}`,
1073
1109
  `font-weight:${p.fontWeight}`,
1074
1110
  `font-size:${size}px`,
1075
1111
  "line-height:1.4",
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The one builder of preview URLs. A store's preview lives under the store, never under a
3
+ * `/preview/<store>` prefix:
4
+ *
5
+ * <origin>/<store>/preview the store's current shared preview
6
+ * <origin>/<store>/preview/pr/<n> candidate PR n
7
+ * <origin>/<store>/preview/rev/<sha>/ an immutable revision
8
+ *
9
+ * Every producer (Worker, CLI) calls these; `apps/storefront/src/pages/preview/preview-url-routes.test.ts`
10
+ * resolves each shape against the page route table. `packages/cli/src/preview-url.mjs` is a
11
+ * byte-identical copy (a CLI test pins it).
12
+ */
13
+
14
+ /** The canonical shapes, for messages that name the right URL. */
15
+ export const PREVIEW_URL_SHAPE = "/<store>/preview[/pr/<n>]";
16
+
17
+ function trimOrigin(origin) {
18
+ return String(origin).trim().replace(/\/+$/, "");
19
+ }
20
+
21
+ /** `/<store>/preview` with an optional sub-path (`/pr/5`). */
22
+ export function previewPath(store, subPath = "") {
23
+ return `/${encodeURIComponent(store)}/preview${subPath}`;
24
+ }
25
+
26
+ export function previewUrl(origin, store) {
27
+ return `${trimOrigin(origin)}${previewPath(store)}`;
28
+ }
29
+
30
+ /** `null` when `prNumber` is not a positive integer. */
31
+ export function previewPrUrl(origin, store, prNumber) {
32
+ const n = Number(prNumber);
33
+ if (!Number.isInteger(n) || n < 1) return null;
34
+ return `${trimOrigin(origin)}${previewPath(store, `/pr/${n}`)}`;
35
+ }
36
+
37
+ export function previewRevUrl(origin, store, versionId) {
38
+ return `${trimOrigin(origin)}${previewPath(store, `/rev/${encodeURIComponent(versionId)}/`)}`;
39
+ }
40
+
41
+ /**
42
+ * The canonical location for a legacy `/preview/<store>[/pr/<n>[/...]]` request, or `null` when
43
+ * the path is not that shape. `isStore` decides whether the first segment names a store
44
+ * (the caller owns what a store id looks like), so `/preview/pr/5`, `/preview/rev/...` and the
45
+ * other first-segment routes are never read as a store.
46
+ */
47
+ export function legacyPreviewLocation(pathname, search, isStore) {
48
+ const m = /^\/preview\/([^/]+)(\/.*)?$/.exec(pathname);
49
+ if (!m) return null;
50
+ const store = m[1];
51
+ if (!/^[A-Za-z0-9.-]+$/.test(store) || !isStore(store)) return null;
52
+ const rest = (m[2] ?? "").replace(/\/+$/, "");
53
+ return `/${store}/preview${rest}${search ?? ""}`;
54
+ }