@tokenoftrust/cli 2.2.16 → 2.2.17

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": "2.2.16",
3
+ "version": "2.2.17",
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",
@@ -286,9 +286,12 @@ function runMonorepo(ctx, argv) {
286
286
  */
287
287
  export class NativeArtifactUnavailableError extends Error {}
288
288
 
289
- /** Same URL shape buildContainerPlan uses — factored out so native + Docker agree. */
289
+ /**
290
+ * The local landing URL, shared by the native and Docker paths. The store's path is its canonical
291
+ * id, the config's `tenant` — the same id `tot dev` grafts the checkout under.
292
+ */
290
293
  export function deriveUrl(cfg, port) {
291
- const domain = typeof cfg.scope === "string" && cfg.scope.includes(".") ? cfg.scope : null;
294
+ const domain = typeof cfg.tenant === "string" && cfg.tenant.includes(".") ? cfg.tenant : null;
292
295
  return { domain, url: `http://localhost:${port}/${domain ? `${domain}/` : ""}` };
293
296
  }
294
297
 
@@ -1835,8 +1838,7 @@ export function buildContainerPlan(workspace, args, _ctx) {
1835
1838
 
1836
1839
  const image = resolveDevImage(args);
1837
1840
  const port = String(args.port || "4321");
1838
- const domain = typeof cfg.scope === "string" && cfg.scope.includes(".") ? cfg.scope : null;
1839
- const url = `http://localhost:${port}/${domain ? `${domain}/` : ""}`;
1841
+ const { domain, url } = deriveUrl(cfg, port);
1840
1842
 
1841
1843
  // docker run equivalent of scripts/dev/docker-compose.pull.yml: bind-mount the
1842
1844
  // checkout, map the port, force polling (reliable HMR across the mount), pull
@@ -233,10 +233,14 @@ function resolveStore(args, ctx) {
233
233
  return null;
234
234
  }
235
235
 
236
- /** @param {string} dir */
236
+ /**
237
+ * The store's id from its `.tot/config.json` — the id its `/tenants/<id>/` assets bind to. Never the
238
+ * directory's name.
239
+ * @param {string} dir
240
+ */
237
241
  function storeTenant(dir) {
238
242
  const config = readJson(join(dir, ".tot", "config.json"));
239
- return typeof config?.tenant === "string" && config.tenant ? config.tenant : basename(dir);
243
+ return typeof config?.tenant === "string" && config.tenant ? config.tenant : null;
240
244
  }
241
245
 
242
246
  /**
@@ -272,7 +276,14 @@ export function run(argv, ctx, io = {}) {
272
276
  return 2;
273
277
  }
274
278
  const storeExists = existsSync(store);
275
- const tenant = args.tenant ?? (storeExists ? storeTenant(store) : basename(store));
279
+ const tenant = args.tenant ?? (storeExists ? storeTenant(store) : null);
280
+ if (!tenant) {
281
+ err(`${fail(
282
+ `${store} names no store id`,
283
+ "set \"tenant\" in .tot/config.json to your store's id (the one `tot whoami` lists), or pass --tenant <id>",
284
+ )}\n`);
285
+ return 2;
286
+ }
276
287
  const lint = storeExists ? validateTenant(store, { tenantId: tenant, mode: args.workspace ? "workspace" : "monorepo" }) : null;
277
288
  const evidence = evidenceFromDir({ pagesDir, stylePath: args.style ? resolve(args.style) : null, lint });
278
289
  if (!evidence.measured) {
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * Read-only closure checks for the optional visual-parity migration module.
3
- * The tenant checkout remains portable: this reads only content/, public/, and
4
- * .tot/visual-parity.json, never a preview host or a delivery provider.
3
+ * The tenant checkout remains portable: this reads only content/, public/ and theme.json (where
4
+ * `.tot/config.json` maps them) and .tot/visual-parity.json, never a preview host or a delivery
5
+ * provider.
5
6
  */
6
7
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
7
8
  import { spawnSync } from "node:child_process";
8
9
  import { extname, join, relative } from "node:path";
10
+ import { workspacePathForTenantRelative } from "./tot-repo-mappings.mjs";
9
11
 
10
12
  const ERROR = "error";
11
13
  // Public routes implemented by the renderer. Authentication and cart URLs are
@@ -59,10 +61,10 @@ function parseConfig(tenantDir) {
59
61
  }
60
62
  }
61
63
 
62
- function tenantFromConfig(tenantDir) {
64
+ /** The checkout's `.tot/config.json`, or null when absent or unreadable. */
65
+ function readTotConfig(tenantDir) {
63
66
  try {
64
- const value = JSON.parse(readFileSync(join(tenantDir, ".tot", "config.json"), "utf8"));
65
- return typeof value?.tenant === "string" ? value.tenant : null;
67
+ return JSON.parse(readFileSync(join(tenantDir, ".tot", "config.json"), "utf8"));
66
68
  } catch { return null; }
67
69
  }
68
70
 
@@ -278,7 +280,11 @@ export function validateVisualParityClosure(tenantDir, options = {}) {
278
280
  const findings = [];
279
281
  const { value: config, findings: configFindings } = parseConfig(tenantDir);
280
282
  findings.push(...configFindings);
281
- const pagesDir = join(tenantDir, "content", "pages");
283
+ // Files are found where the checkout's `.tot/config.json` mappings declare them, as `tot validate`
284
+ // and the hosted reconcile find them.
285
+ const totConfig = readTotConfig(tenantDir);
286
+ const located = (rel) => join(tenantDir, workspacePathForTenantRelative(totConfig, rel));
287
+ const pagesDir = located("content/pages");
282
288
  const files = walk(pagesDir).filter((file) => file.endsWith(".json")).sort();
283
289
  if (files.length === 0) findings.push(finding("parity-pages-missing", "content/pages/", "no page JSON files define the visual-parity route scope", "add the paired authored page records under content/pages/ before closing parity"));
284
290
  const pages = [];
@@ -297,17 +303,17 @@ export function validateVisualParityClosure(tenantDir, options = {}) {
297
303
  const routes = new Set(["/"]);
298
304
  for (const page of pages) routes.add(normalizeRoute(page.value.slug));
299
305
  /** @type {{findings:ParityFinding[], tenantId:string|null, publicDir:string, routes:Set<string>, allowlist:string[], decodeAssets:DecodeAsset[]}} */
300
- const context = { findings, tenantId: tenantId ?? tenantFromConfig(tenantDir), publicDir: join(tenantDir, "public"), routes, allowlist: Array.isArray(config.externalNavigationAllowlist) ? config.externalNavigationAllowlist : [], decodeAssets: [] };
306
+ const context = { findings, tenantId: tenantId ?? (typeof totConfig?.tenant === "string" ? totConfig.tenant : null), publicDir: located("public"), routes, allowlist: Array.isArray(config.externalNavigationAllowlist) ? config.externalNavigationAllowlist : [], decodeAssets: [] };
301
307
  if (!context.tenantId) findings.push(finding("parity-tenant-id", ".tot/config.json", "tenant id is required to verify /tenants/<tenant>/ assets", "set tenant in .tot/config.json or pass a resolved tenant id"));
302
308
  for (const page of pages) inspectValue(page.value, page.file, [], context);
303
309
  for (const name of ["home.json", "chrome.json"]) {
304
- const path = join(tenantDir, "content", name);
310
+ const path = located(`content/${name}`);
305
311
  if (existsSync(path)) {
306
312
  const value = readStructured(path, `content/${name}`, findings);
307
313
  if (value != null) inspectValue(value, `content/${name}`, [], context);
308
314
  }
309
315
  }
310
- const themePath = join(tenantDir, "theme.json");
316
+ const themePath = located("theme.json");
311
317
  if (existsSync(themePath)) {
312
318
  const value = readStructured(themePath, "theme.json", findings);
313
319
  if (value != null) inspectValue(value, "theme.json", [], context);