Skip to content

Bridge · #429

A command that actually runs

The intended CLI is a small script that reads the same JSON endpoints the integration pages print. Because it is a script rather than a package, the honest version is the file: download it, run it with Node, and point --base at any deployment.

Commands

  • node motif-cli.mjs listevery component with kind, size and both scores
  • node motif-cli.mjs list --kind animatedthe same table filtered by the kind field
  • node motif-cli.mjs tokenstokens as CSS custom properties
  • node motif-cli.mjs tokens --format jsonthe DTCG file, verbatim
  • node motif-cli.mjs badge tilt-cardMarkdown for that asset's badge
  • node motif-cli.mjs --helpthe usage text below

Not on npm — say it plainly

  • npx motif add tilt-card installs nothing, because there is no registry entry. The page documents what that command would do rather than printing it as if it works.
  • “Add” itself needs something to add: the catalog stores metadata, not component source, so the most a real command could do today is write the token file or print a badge.
  • The script needs network access to the deployment it points at, and it says which URL it read when a request fails, rather than exiting silently.

The script (3.5 KB, complete)

#!/usr/bin/env node
/*
 * Motif UI CLI — served from the site, run with node.
 *
 *   node motif-cli.mjs list [--kind animated]
 *   node motif-cli.mjs tokens [--format css|json]
 *   node motif-cli.mjs badge <slug>
 *   node motif-cli.mjs --help
 *
 * --base <url> points it at a deployment (default http://localhost:3139).
 * It is not published to npm: there is no registry entry, so `npx motif`
 * installs nothing. This file is the command.
 */

const args = process.argv.slice(2);
const baseIndex = args.indexOf("--base");
const base = (baseIndex >= 0 ? args[baseIndex + 1] : "http://localhost:3139").replace(/\/+$/, "");
const kindIndex = args.indexOf("--kind");
const kind = kindIndex >= 0 ? args[kindIndex + 1] : null;
const formatIndex = args.indexOf("--format");
const format = formatIndex >= 0 ? args[formatIndex + 1] : "css";
const positional = args.filter((a) => !a.startsWith("--") && a !== base && a !== kind && a !== format);
const command = positional[0] || "help";
const operand = positional[1];

async function json(path) {
  const url = base + path;
  let res;
  try {
    res = await fetch(url);
  } catch (err) {
    // Node reports "fetch failed" without the URL, which tells the reader
    // nothing. The command pr...
    throw new Error("could not reach " + url + " (" + (err && err.cause ? err.cause.code || err.cause.message : err.message) + ") — is the deployment running, and is --base right?");
  }
  if (!res.ok) throw new Error(url + " → HTTP " + res.status);
  return res.json();
}

function help() {
  console.log(`Motif UI CLI

  list [--kind <kind>]      components from /api/exports/catalog.json
  tokens [--format css|json] design tokens from /api/exports/tokens.json
  badge <slug>              Markdown for that asset's score badge
  --base <url>              deployment to read (default ${base})
  --help

Not published to npm — this file is the command.`);
}

async function list() {
  const data = await json("/api/exports/catalog.json");
  const rows = data.components.filter((c) => !kind || c.kind === kind);
  for (const c of rows) {
    console.log([c.slug.padEnd(26), c.kind.padEnd(9), (c.bundleKb + " KB").padStart(8), "a11y " + c.a11yScore, "Q " + c.qualityScore].join("  "));
  }
  console.log("\n" + rows.length + " of " + data.components.length + " components" + (kind ? " (kind: " + kind + ")" : ""));
}

async function tokens() {
  const data = await json("/api/exports/tokens.json");
  if (format === "json") { console.log(JSON.stringify(data, null, 2)); return; }
  for (const [name, token] of Object.entries(data.color)) console.log("  --color-" + name + ": " + token.$value + ";");
  for (const [name, token] of Object.entries(data.radius)) console.log("  --radius-" + name + ": " + token.$value + ";");
}

async function badge(slug) {
  if (!slug) { console.error("badge needs a slug: node motif-cli.mjs badge tilt-card"); process.exit(1); }
  const data = await json("/api/exports/catalog.json");
  const asset = data.components.find((c) => c.slug === slug);
  if (!asset) { console.error("no component called " + slug); process.exit(1); }
  console.log("![quality " + asset.qualityScore + "](" + base + "/api/badge/" + slug + ")");
  console.log("\n# " + asset.title + " — a11y " + asset.a11yScore + ", quality " + asset.qualityScore + ", " + asset.bundleKb + " KB, " + asset.license);
}

const run = { list, tokens, badge, help }[command] ?? help;
Promise.resolve(command === "badge" ? badge(operand) : run()).catch((err) => { console.error(err.message); process.exit(1); });

It uses fetch and process.argv only — no dependencies, so there is nothing to install before running it.