runPublish

tcmts.cli.run.runPublish · #r6lwbakt · function · runs on the server

uses node:url

Runs tcm publish: a standardSite(…) definition's records written as its account (only what changed, and its posts' documents it no longer has removed), and whether the site says it's that publication.

import { atprotoSession } from "#codebase/tcmts/cli/atproto";
import { routePublish } from "#codebase/tcmts/cli/commands";
import { execute } from "#codebase/tcmts/cli/main";
import { PUBLISH_SCOPE, publishSite } from "#codebase/tcmts/cli/publish";
import type { Context } from "#codebase/tcmts/cli/run";
import { bold, dim, green, red, yellow } from "#codebase/tcmts/cli/style";
import { chooseName, UsageError } from "#codebase/tcmts/cli/ui";
import type { StandardSite } from "#codebase/tcmts/content";
import { exportId } from "#codebase/tcmts/core/hash";
import { materialize } from "#codebase/tcmts/loader";
import { parse } from "@bomb.sh/router";
import { pathToFileURL } from "node:url";

export async function runPublish({ cb, cmd, rest }: Context): Promise<number> {
  const m = execute(cmd, parse(routePublish(), { argv: rest })).model;
  const name =
    m.name ??
    (await chooseName(cb, "publish <name> [--as <account>] [--dry-run]", "Publish what?", "value"));
  const h = cb.lookupOne(name, "value");
  const rec = cb.store.getDef(h)!;
  if (rec.kind === "type") throw new UsageError(`${name} is a type`);
  // its value, here: the records, as its code makes them
  materialize([h], cb.source(), cb.lay);
  const mod = (await import(pathToFileURL(cb.modulePath(rec.componentHash)).href)) as Record<
    string,
    unknown
  >;
  const site = mod[exportId(h)] as StandardSite | undefined;
  if (
    !site ||
    typeof site !== "object" ||
    site.publication?.collection !== "site.standard.publication" ||
    !Array.isArray(site.documents)
  )
    throw new UsageError(
      `${name} isn't a standard.site publication: make it with standardSite(…), the content library's (\`tcm lib add tcm content\`)`,
    );
  const session = await atprotoSession({
    who: m.as ?? site.did,
    scope: PUBLISH_SCOPE,
    why: `publishing ${name} writes records to your AT Protocol repository`,
  });
  if (session.did !== site.did)
    throw new UsageError(`${name}'s records are ${site.did}'s: publish it as that account (--as)`);
  const who = session.handle ? `@${session.handle}` : session.did;
  const dry = m["dry-run"];
  const c = await publishSite(session, site, {
    dryRun: dry,
    tell: (what, uri) => console.log(`  ${what === "removed" ? red(what) : green(what)}  ${uri}`),
  });
  console.log(
    `${dry ? "would publish" : "published"} ${bold(name)} as ${who}: ${c.added} new, ${c.updated} updated, ${c.removed} removed, ${c.same} as they were`,
  );
  console.log(dim(`  ${site.publication.uri}`));
  // (readers believe a publication its site says is its: asked as they would)
  const url = new URL(site.wellKnown.path, String(site.publication.record.url)).href;
  const says = await fetch(url, { signal: AbortSignal.timeout(10_000) })
    .then((r) => (r.ok ? r.text() : `${r.status}`))
    .catch((e: unknown) => (e as Error).message);
  if (says.trim() !== site.wellKnown.text)
    console.log(
      yellow(
        `  ${url} doesn't say it's this publication (${says.trim().slice(0, 80) || "nothing"}): serve ${site.wellKnown.text} there, and each post's page its <link rel="site.standard.document">`,
      ),
    );
  return 0;
}

uses

used by