@scrthq/runlog 0.0.38 → 0.0.40

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.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/dist/runlog.js +1098 -65
  3. package/package.json +1 -1
package/dist/runlog.js CHANGED
@@ -7369,8 +7369,8 @@ var require_dist = __commonJS({
7369
7369
 
7370
7370
  // packages/cli/src/bin.ts
7371
7371
  var import_yaml4 = __toESM(require_dist(), 1);
7372
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "node:fs";
7373
- import { basename, dirname, resolve as resolve3 } from "node:path";
7372
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync3 } from "node:fs";
7373
+ import { basename as basename2, dirname, resolve as resolve4 } from "node:path";
7374
7374
  import { createHash } from "node:crypto";
7375
7375
 
7376
7376
  // node_modules/zod/v4/classic/external.js
@@ -17794,8 +17794,8 @@ var error43 = () => {
17794
17794
  case "not_multiple_of":
17795
17795
  return `N\xFAmero inv\xE1lido: deve ser m\xFAltiplo de ${issue2.divisor}`;
17796
17796
  case "unrecognized_keys": {
17797
- const plural = issue2.keys.length > 1 ? "s" : "";
17798
- return `Chave${plural} inv\xE1lida${plural}: ${joinValues(issue2.keys, ", ")}`;
17797
+ const plural2 = issue2.keys.length > 1 ? "s" : "";
17798
+ return `Chave${plural2} inv\xE1lida${plural2}: ${joinValues(issue2.keys, ", ")}`;
17799
17799
  }
17800
17800
  case "invalid_key":
17801
17801
  return `Entrada inv\xE1lida n${translateOriginWithArticle(issue2.origin, "definite")}`;
@@ -17937,8 +17937,8 @@ var error44 = () => {
17937
17937
  case "not_multiple_of":
17938
17938
  return `N\xFAmero inv\xE1lido: deve ser m\xFAltiplo de ${issue2.divisor}`;
17939
17939
  case "unrecognized_keys": {
17940
- const plural = issue2.keys.length > 1 ? "s" : "";
17941
- return `Chave${plural} inv\xE1lida${plural}: ${joinValues(issue2.keys, ", ")}`;
17940
+ const plural2 = issue2.keys.length > 1 ? "s" : "";
17941
+ return `Chave${plural2} inv\xE1lida${plural2}: ${joinValues(issue2.keys, ", ")}`;
17942
17942
  }
17943
17943
  case "invalid_key":
17944
17944
  return `Entrada inv\xE1lida n${translateOriginWithArticle(issue2.origin, "definite")}`;
@@ -20135,9 +20135,9 @@ function codePointLengthVar(doc, ctx, accessor, inDoubt) {
20135
20135
  doc.write(`const ${v} = typeof ${accessor} === "string" && ${inDoubt} ? ${cpLen}(${accessor}) : ${accessor}.length;`);
20136
20136
  return v;
20137
20137
  }
20138
- function numericOperand(value, label) {
20138
+ function numericOperand(value, label2) {
20139
20139
  if (typeof value !== "number" || !Number.isFinite(value)) {
20140
- throw new ZodCompileUnsupportedError(`${label} bound of type ${typeof value}`);
20140
+ throw new ZodCompileUnsupportedError(`${label2} bound of type ${typeof value}`);
20141
20141
  }
20142
20142
  return `${value}`;
20143
20143
  }
@@ -27524,13 +27524,13 @@ function lintPack(pack) {
27524
27524
  }
27525
27525
  }
27526
27526
  const claimed = new Set(pack.capabilities);
27527
- const requireCap = (cap, used, why) => {
27528
- if (used && !claimed.has(cap)) {
27527
+ const requireCap = (cap2, used, why) => {
27528
+ if (used && !claimed.has(cap2)) {
27529
27529
  d.push(
27530
27530
  warn(
27531
27531
  "capability/undeclared",
27532
27532
  "capabilities",
27533
- `pack ${why} but does not declare the "${cap}" capability`
27533
+ `pack ${why} but does not declare the "${cap2}" capability`
27534
27534
  )
27535
27535
  );
27536
27536
  }
@@ -27638,12 +27638,12 @@ function parsePack(input2) {
27638
27638
  const unsupported = pack.capabilities.filter(
27639
27639
  (c) => !IMPLEMENTED_CAPABILITIES.includes(c)
27640
27640
  );
27641
- for (const cap of unsupported) {
27641
+ for (const cap2 of unsupported) {
27642
27642
  diagnostics.push({
27643
27643
  level: "error",
27644
27644
  code: "pack/unsupported-capability",
27645
27645
  path: "capabilities",
27646
- message: `pack requires the "${cap}" capability, which this build does not implement`
27646
+ message: `pack requires the "${cap2}" capability, which this build does not implement`
27647
27647
  });
27648
27648
  }
27649
27649
  if (hasErrors(diagnostics)) return { ok: false, pack: null, diagnostics };
@@ -27885,6 +27885,830 @@ async function verifyPack(document) {
27885
27885
  }
27886
27886
  }
27887
27887
 
27888
+ // packages/rules-schema/src/describe.ts
27889
+ function entryKeys(table) {
27890
+ switch (table.resolution) {
27891
+ case "lookup":
27892
+ return table.entries.map((e) => e.range[0] === e.range[1] ? `${e.range[0]}` : `${e.range[0]}\u2013${e.range[1]}`);
27893
+ case "bands":
27894
+ return table.entries.map(
27895
+ (e) => [e.gte !== void 0 ? `\u2265${e.gte}` : "", e.lte !== void 0 ? `\u2264${e.lte}` : ""].filter(Boolean).join(" ")
27896
+ );
27897
+ case "opposed":
27898
+ return table.entries.map((e) => `beats ${e.beats}`);
27899
+ case "keyed":
27900
+ return table.entries.map((e) => e.key);
27901
+ }
27902
+ }
27903
+ function howConsulted(table) {
27904
+ switch (table.resolution) {
27905
+ case "lookup":
27906
+ return `Roll ${table.roll}.`;
27907
+ case "bands":
27908
+ return `Roll ${table.roll} and find the band it lands in.`;
27909
+ case "opposed":
27910
+ return `Roll ${table.action}${table.addResource ? ` and add ${table.addResource}` : ""} against ${table.challenge.count} \xD7 ${table.challenge.dice}; count how many of the challenge dice your total beats.`;
27911
+ case "keyed":
27912
+ return "Find the row whose key matches what was drawn.";
27913
+ }
27914
+ }
27915
+ function nouns(pack) {
27916
+ const v = pack.vocabulary;
27917
+ return {
27918
+ run: v.run.one,
27919
+ runs: v.run.many,
27920
+ unit: v.unit.one,
27921
+ units: v.unit.many,
27922
+ subject: v.subject.one,
27923
+ subjects: v.subject.many,
27924
+ finalize: v.finalize
27925
+ };
27926
+ }
27927
+ var label = (pack, kind, id) => {
27928
+ const entry = pack[kind]?.[id];
27929
+ return entry?.label ?? entry?.title ?? id;
27930
+ };
27931
+ function targetInWords(pack, to) {
27932
+ const n = nouns(pack);
27933
+ if (to === void 0) return `this ${n.subject}`;
27934
+ if (typeof to === "object") return `the ${n.subject} chosen as \u201C${to.var}\u201D`;
27935
+ switch (to) {
27936
+ case "thisSubject":
27937
+ return `this ${n.subject}`;
27938
+ case "targetSubject":
27939
+ return `the target`;
27940
+ case "allSubjects":
27941
+ return `every ${n.subject}`;
27942
+ case "allPriorSubjects":
27943
+ return `every earlier ${n.subject}`;
27944
+ case "run":
27945
+ return `the ${n.run}`;
27946
+ }
27947
+ }
27948
+ function boundInWords(pack, b) {
27949
+ const parts = [];
27950
+ if (b.eq !== void 0) parts.push(`exactly ${b.eq}`);
27951
+ if (b.gte !== void 0 && b.lte !== void 0) parts.push(`between ${b.gte} and ${b.lte}`);
27952
+ else {
27953
+ if (b.gte !== void 0) parts.push(`${b.gte} or more`);
27954
+ if (b.lte !== void 0) parts.push(`${b.lte} or fewer`);
27955
+ }
27956
+ if (b.gteCounter !== void 0) parts.push(`at least the ${label(pack, "counters", b.gteCounter)} tally`);
27957
+ if (b.lteCounter !== void 0) parts.push(`no more than the ${label(pack, "counters", b.lteCounter)} tally`);
27958
+ return parts.join(" and ");
27959
+ }
27960
+ function predicateInWords(pack, p) {
27961
+ const n = nouns(pack);
27962
+ if ("ask" in p) return `you answer yes to \u201C${p.ask}\u201D`;
27963
+ if ("unitIndex" in p) return p.unitIndex.eq !== void 0 ? `this is ${n.unit} ${p.unitIndex.eq}` : `the ${n.unit} number is ${boundInWords(pack, p.unitIndex)}`;
27964
+ if ("subjectCount" in p) return `there are ${boundInWords(pack, p.subjectCount)} ${n.subjects}`;
27965
+ if ("eligibleTargets" in p) return `there are ${boundInWords(pack, p.eligibleTargets)} ${n.subjects} that can be targeted`;
27966
+ if ("counter" in p) return `${label(pack, "counters", p.counter)} is ${boundInWords(pack, p.is)}`;
27967
+ if ("resource" in p) return `${label(pack, "resources", p.resource)} is ${boundInWords(pack, p.is)}`;
27968
+ if ("flag" in p) return `${p.flag} is ${p.is === false ? "not " : ""}set`;
27969
+ if ("subjectHasState" in p) return `${targetInWords(pack, p.of)} is ${label(pack, "states", p.subjectHasState)}`;
27970
+ if ("priorSubjectTagged" in p) return `an earlier ${n.subject} was tagged \u201C${p.priorSubjectTagged}\u201D`;
27971
+ if ("modeIs" in p) return `playing ${p.modeIs.map((m) => label(pack, "modes", m)).join(" or ")}`;
27972
+ if ("not" in p) return `not (${predicateInWords(pack, p.not)})`;
27973
+ if ("allOf" in p) return p.allOf.map((q) => predicateInWords(pack, q)).join(" and ");
27974
+ if ("anyOf" in p) return p.anyOf.map((q) => predicateInWords(pack, q)).join(" or ");
27975
+ return "";
27976
+ }
27977
+ function conditionsInWords(pack, preds, joiner = "all") {
27978
+ if (!preds || preds.length === 0) return "";
27979
+ return preds.map((p) => predicateInWords(pack, p)).join(joiner === "all" ? " and " : ", or ");
27980
+ }
27981
+ var signed = (by, set2) => set2 !== void 0 ? `set to ${set2}` : by === void 0 ? "+1" : by >= 0 ? `+${by}` : `\u2212${Math.abs(by)}`;
27982
+ function actionInWords(pack, a) {
27983
+ const n = nouns(pack);
27984
+ switch (a.do) {
27985
+ case "roll":
27986
+ return a.label ? `${a.label} (${a.dice})` : `roll ${a.dice}`;
27987
+ case "rollOn": {
27988
+ const t = label(pack, "tables", a.table);
27989
+ if (a.times && a.times > 1) return `roll ${a.times} times on ${t}${a.choose === "one" ? " and keep one" : ""}`;
27990
+ return `roll on ${t}`;
27991
+ }
27992
+ case "branch": {
27993
+ const cases = a.cases.map((c) => {
27994
+ const cond = c.in ? `on ${c.in.join(", ")}` : c.is ? boundInWords(pack, c.is) : "otherwise";
27995
+ return `${cond} \u2192 ${actionsInWords(pack, c.then)}`;
27996
+ });
27997
+ if (a.else) cases.push(`otherwise \u2192 ${actionsInWords(pack, a.else)}`);
27998
+ return `then, by the result: ${cases.join("; ")}`;
27999
+ }
28000
+ case "prompt":
28001
+ return a.label;
28002
+ case "resolveTarget":
28003
+ return `pick the target${a.from === "choice" ? " yourself" : a.from === "event" ? " by the pack's targeting rule" : " from the roll"}`;
28004
+ case "applyState":
28005
+ return `mark ${targetInWords(pack, a.to)} ${label(pack, "states", a.state)}`;
28006
+ case "removeState":
28007
+ return `clear ${label(pack, "states", a.state)} from ${targetInWords(pack, a.to)}`;
28008
+ case "removeSubject":
28009
+ return `remove ${targetInWords(pack, a.to)} from play`;
28010
+ case "ban":
28011
+ return a.label ?? (a.subjectType ? `no more ${a.subjectType} ${n.subjects} this ${n.run}` : `no more ${n.subjects} of ${targetInWords(pack, a.from)}'s kind this ${n.run}`);
28012
+ case "forceUnit": {
28013
+ const c = a.count ?? 1;
28014
+ return `add ${c} more ${c === 1 ? n.unit : n.units} before the ${n.run} may end`;
28015
+ }
28016
+ case "modCounter":
28017
+ return `${label(pack, "counters", a.counter)} ${signed(a.by, a.set)}`;
28018
+ case "modResource":
28019
+ return `${label(pack, "resources", a.resource)} ${signed(a.by, a.set)}`;
28020
+ case "grantCard": {
28021
+ const c = a.count ?? 1;
28022
+ return `draw ${c} from ${label(pack, "decks", a.deck)}`;
28023
+ }
28024
+ case "discardCard": {
28025
+ const c = a.count ?? 1;
28026
+ return `discard ${c} from ${label(pack, "decks", a.deck)}`;
28027
+ }
28028
+ case "startTimer":
28029
+ return a.label ? `${a.label} (${a.minutes} min)` : `start a ${a.minutes}-minute timer`;
28030
+ case "note":
28031
+ return a.text;
28032
+ case "setFlag":
28033
+ return a.value === false ? `unset ${a.flag}` : `set ${a.flag}`;
28034
+ case "endRunAttempt":
28035
+ return `try to end the ${n.run}`;
28036
+ case "when": {
28037
+ const then = `if ${conditionsInWords(pack, a.all)}: ${actionsInWords(pack, a.then)}`;
28038
+ return a.else ? `${then}; otherwise ${actionsInWords(pack, a.else)}` : then;
28039
+ }
28040
+ }
28041
+ }
28042
+ function actionsInWords(pack, actions) {
28043
+ return actions.map((a) => unstop(actionInWords(pack, a))).join("; ");
28044
+ }
28045
+ function unstop(s) {
28046
+ return s.replace(/\.\s*$/, "");
28047
+ }
28048
+ function sentence(s) {
28049
+ const t = s.trim();
28050
+ return /[.!?…]$/.test(t) ? t : `${t}.`;
28051
+ }
28052
+ function triggerPointInWords(pack, on) {
28053
+ const n = nouns(pack);
28054
+ switch (on) {
28055
+ case "immediately":
28056
+ return "Then";
28057
+ case "onEnterUnit":
28058
+ return `When a ${n.unit} begins`;
28059
+ case "onDeclareSubject":
28060
+ return `When the ${n.subject} is declared`;
28061
+ case "afterWork":
28062
+ return "After the work";
28063
+ case "onFinalize":
28064
+ return `When you ${n.finalize}`;
28065
+ case "onDeclareRunOver":
28066
+ return `When you declare the ${n.run} over`;
28067
+ case "onRunEnd":
28068
+ return `When the ${n.run} ends`;
28069
+ }
28070
+ }
28071
+ function triggerInWords(pack, t) {
28072
+ const when = conditionsInWords(pack, t.when);
28073
+ const head = t.label ? `${t.label} \u2014 ${triggerPointInWords(pack, t.on).toLowerCase()}` : triggerPointInWords(pack, t.on);
28074
+ return `${head}${when ? ` (if ${when})` : ""}: ${actionsInWords(pack, t.do)}`;
28075
+ }
28076
+ function modeLength(pack, mode) {
28077
+ const n = nouns(pack);
28078
+ const u = mode.units;
28079
+ if (u?.fixed !== void 0) return `${u.fixed} ${u.fixed === 1 ? n.unit : n.units}`;
28080
+ if (u?.roll) return `roll ${u.roll} for the number of ${n.units}`;
28081
+ if (u?.min !== void 0 && u?.max !== void 0) return `${u.min}\u2013${u.max} ${n.units}`;
28082
+ if (u?.max !== void 0) return `up to ${u.max} ${n.units}`;
28083
+ if (u?.min !== void 0) return `at least ${u.min} ${n.units}`;
28084
+ const max = pack.unit.max;
28085
+ return `as many ${n.units} as you like${max ? `, up to ${max}` : ""}`;
28086
+ }
28087
+ function modePlayers(mode) {
28088
+ const p = mode.players;
28089
+ if (!p || p.min <= 1 && p.max <= 1) return "solo";
28090
+ return p.min === p.max ? `${p.min} players` : `${p.min}\u2013${p.max} players`;
28091
+ }
28092
+ var diceOf = (expr) => {
28093
+ const m = /^(\d*)d(\d+)/.exec(expr);
28094
+ return m ? `d${m[2]}` : expr;
28095
+ };
28096
+ function diceNeeded(pack) {
28097
+ const found = /* @__PURE__ */ new Set();
28098
+ const seeActions = (actions) => {
28099
+ for (const a of actions ?? []) {
28100
+ if (a.do === "roll") found.add(diceOf(a.dice));
28101
+ if (a.do === "branch") {
28102
+ for (const c of a.cases) seeActions(c.then);
28103
+ seeActions(a.else);
28104
+ }
28105
+ if (a.do === "when") {
28106
+ seeActions(a.then);
28107
+ seeActions(a.else);
28108
+ }
28109
+ }
28110
+ };
28111
+ const seeTriggers = (triggers) => {
28112
+ for (const t of triggers ?? []) seeActions(t.do);
28113
+ };
28114
+ for (const table of Object.values(pack.tables)) {
28115
+ if (table.resolution === "lookup" || table.resolution === "bands") found.add(diceOf(table.roll));
28116
+ if (table.resolution === "opposed") {
28117
+ found.add(diceOf(table.action));
28118
+ found.add(diceOf(table.challenge.dice));
28119
+ }
28120
+ for (const e of table.entries) seeTriggers(e.triggers);
28121
+ }
28122
+ for (const deck of Object.values(pack.decks ?? {})) if (deck.kind === "cards") for (const c of deck.cards) seeTriggers(c.triggers);
28123
+ for (const c of Object.values(pack.counters ?? {})) for (const t of c.triggers ?? []) seeActions(t.do);
28124
+ for (const m of Object.values(pack.moves ?? {})) seeActions(m.do);
28125
+ for (const m of Object.values(pack.modes)) {
28126
+ if (m.units?.roll) found.add(diceOf(m.units.roll));
28127
+ for (const p of m.perUnit ?? []) seeActions(p.extra);
28128
+ }
28129
+ for (const p of pack.phases) for (const s of p.steps) if (s.kind === "actions") seeActions(s.do);
28130
+ seeTriggers(pack.triggers);
28131
+ if (pack.targeting?.strategy === "anchoredOffset" && pack.targeting.eventFallback) found.add(diceOf(pack.targeting.eventFallback.roll));
28132
+ return [...found].sort((a, b) => Number(a.slice(1)) - Number(b.slice(1)));
28133
+ }
28134
+ function diceInWords(dice) {
28135
+ const names = dice.map((d) => d === "d100" ? "percentile dice (d100)" : `a ${d}`);
28136
+ if (names.length === 0) return "";
28137
+ if (names.length === 1) return names[0];
28138
+ return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
28139
+ }
28140
+
28141
+ // packages/rules-schema/src/docs.ts
28142
+ var DOC_KINDS = [
28143
+ { kind: "summary", label: "Summary", what: "What a catalog shows: the shape of the game without its rules.", full: false },
28144
+ { kind: "rulebook", label: "Rulebook", what: "Everything, in reading order: setup, flow, every table, every rule.", full: true },
28145
+ { kind: "quickstart", label: "Quick start", what: "Enough to play the first time; the rest is on the reference card.", full: true },
28146
+ { kind: "reference", label: "Reference card", what: "Every table and the things people forget, compact enough to keep beside you.", full: true },
28147
+ { kind: "runlog", label: "Run log sheet", what: "A printable sheet to write a run on by hand.", full: true }
28148
+ ];
28149
+ var Builder = class {
28150
+ blocks = [];
28151
+ h(level, text) {
28152
+ this.blocks.push({ kind: "heading", level, text });
28153
+ return this;
28154
+ }
28155
+ p(text, tone) {
28156
+ if (text && text.trim()) this.blocks.push(tone ? { kind: "paragraph", text: text.trim(), tone } : { kind: "paragraph", text: text.trim() });
28157
+ return this;
28158
+ }
28159
+ list(items, ordered = false) {
28160
+ if (items.length > 0) this.blocks.push(ordered ? { kind: "list", items, ordered } : { kind: "list", items });
28161
+ return this;
28162
+ }
28163
+ table(columns, rows, compact = false) {
28164
+ if (rows.length > 0) this.blocks.push(compact ? { kind: "table", columns, rows, compact } : { kind: "table", columns, rows });
28165
+ return this;
28166
+ }
28167
+ terms(items) {
28168
+ if (items.length > 0) this.blocks.push({ kind: "terms", items });
28169
+ return this;
28170
+ }
28171
+ form(fields) {
28172
+ this.blocks.push({ kind: "form", fields });
28173
+ return this;
28174
+ }
28175
+ rule() {
28176
+ this.blocks.push({ kind: "rule" });
28177
+ return this;
28178
+ }
28179
+ page() {
28180
+ this.blocks.push({ kind: "pagebreak" });
28181
+ return this;
28182
+ }
28183
+ };
28184
+ var plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
28185
+ var cap = (s) => s ? s[0].toUpperCase() + s.slice(1) : s;
28186
+ function stepInWords(pack, step) {
28187
+ const n = nouns(pack);
28188
+ switch (step.kind) {
28189
+ case "rollTable":
28190
+ return `${step.label ?? `Roll on ${pack.tables[step.table]?.title ?? step.table}`}${step.optional ? " (optional)" : ""}`;
28191
+ case "declareSubject":
28192
+ return step.label ?? `Declare the ${n.subject}${step.constrainedBy ? `, within what ${pack.tables[step.constrainedBy]?.title ?? step.constrainedBy} allowed` : ""}`;
28193
+ case "manual":
28194
+ return step.label;
28195
+ case "actions":
28196
+ return sentence(cap(actionsInWords(pack, step.do)));
28197
+ case "finalizeUnit":
28198
+ return step.label ?? `${n.finalize} the ${n.unit}`;
28199
+ }
28200
+ }
28201
+ var checklistText = (item) => typeof item === "string" ? item : item.text;
28202
+ function stepDetail(pack, step) {
28203
+ const out = [];
28204
+ if (step.kind === "manual") {
28205
+ if (step.description) out.push(step.description);
28206
+ for (const c of step.checklist ?? []) out.push(`\u2610 ${checklistText(c)}`);
28207
+ }
28208
+ if (step.kind === "finalizeUnit") for (const c of step.confirm ?? []) out.push(`\u2610 ${checklistText(c)}`);
28209
+ if ("skipWhen" in step && step.skipWhen?.length) out.push(`Skipped when ${conditionsInWords(pack, step.skipWhen, "any")}.`);
28210
+ return out;
28211
+ }
28212
+ function whatYouNeed(pack) {
28213
+ const n = nouns(pack);
28214
+ const items = [];
28215
+ const dice = diceInWords(diceNeeded(pack));
28216
+ if (dice) items.push(cap(dice) + ".");
28217
+ for (const deck of Object.values(pack.decks ?? {})) {
28218
+ if (deck.kind === "standard52") items.push(`A standard deck of playing cards${deck.includeJokers ? " with the jokers in" : ""}.`);
28219
+ else items.push(`${plural(deck.cards.length, "card", "cards")} for ${deck.title} \u2014 write them out, or use the app.`);
28220
+ }
28221
+ if (pack.capabilities.includes("timers")) items.push("A timer.");
28222
+ items.push(pack.journal?.enabled === false ? `Somewhere to keep the ${n.run} log.` : `Somewhere to keep the ${n.run} log \u2014 the run log sheet, or the app.`);
28223
+ return items;
28224
+ }
28225
+ function flowSteps(pack, opts) {
28226
+ const items = [];
28227
+ for (const phase of pack.phases) {
28228
+ const head = phase.steps.length === 1 ? `${phase.label}: ${stepInWords(pack, phase.steps[0])}` : phase.label;
28229
+ const notes = [];
28230
+ if (opts.detail && phase.description) notes.push(phase.description);
28231
+ if (opts.detail && phase.skipWhen?.length) notes.push(`Skipped when ${conditionsInWords(pack, phase.skipWhen, "any")}.`);
28232
+ if (phase.steps.length > 1) for (const s of phase.steps) notes.push(`${stepInWords(pack, s)}${opts.detail ? stepDetail(pack, s).map((d) => ` \u2014 ${d}`).join("") : ""}`);
28233
+ else if (opts.detail) notes.push(...stepDetail(pack, phase.steps[0]));
28234
+ items.push([head, ...notes.map((t) => ` ${t}`)].join("\n"));
28235
+ }
28236
+ return items;
28237
+ }
28238
+ function modeRows(pack, opts) {
28239
+ return Object.entries(pack.modes).map(([id, m]) => [
28240
+ `${m.label}${id === pack.defaultMode ? " (default)" : ""}`,
28241
+ modeLength(pack, m),
28242
+ modePlayers(m) + (m.seeded ? ", seeded" : ""),
28243
+ [m.description ?? "", ...opts.notes ? modeExtras(pack, m) : []].filter(Boolean).join(" ")
28244
+ ]);
28245
+ }
28246
+ function modeExtras(pack, m) {
28247
+ const n = nouns(pack);
28248
+ const out = [];
28249
+ for (const note of m.notes ?? []) out.push(note);
28250
+ const off = [
28251
+ ...(m.disable?.tables ?? []).map((t) => pack.tables[t]?.title ?? t),
28252
+ ...(m.disable?.decks ?? []).map((d) => pack.decks?.[d]?.title ?? d),
28253
+ ...(m.disable?.counters ?? []).map((c) => pack.counters?.[c]?.label ?? c),
28254
+ ...(m.disable?.phases ?? []).map((p) => pack.phases.find((x) => x.id === p)?.label ?? p)
28255
+ ];
28256
+ if (off.length) out.push(`Without ${off.join(", ")}.`);
28257
+ for (const p of m.perUnit ?? []) {
28258
+ const which = p.unit === "all" ? `Every ${n.unit}` : `${cap(n.unit)} ${p.unit}`;
28259
+ const skip = (p.skipPhases ?? []).map((id) => pack.phases.find((x) => x.id === id)?.label ?? id);
28260
+ const parts = [skip.length ? `skips ${skip.join(", ")}` : "", p.extra?.length ? actionsInWords(pack, p.extra) : ""].filter(Boolean);
28261
+ if (parts.length) out.push(sentence(`${which}: ${parts.join("; ")}`));
28262
+ }
28263
+ for (const r of m.players?.roles ?? []) out.push(`${r.label}: ${r.description ?? ""}`.trim());
28264
+ return out;
28265
+ }
28266
+ function tableRows(pack, table, opts) {
28267
+ const keys = entryKeys(table);
28268
+ return table.entries.map((e, i) => {
28269
+ const extras = [];
28270
+ if (opts.full) {
28271
+ for (const g of e.grants ?? []) extras.push(`\u2192 ${pack.states?.[g]?.label ?? g}`);
28272
+ for (const t of e.triggers ?? []) extras.push(sentence(triggerInWords(pack, t)));
28273
+ if (e.requires?.length) extras.push(`Only if ${conditionsInWords(pack, e.requires)}; otherwise roll again.`);
28274
+ }
28275
+ const text = [e.title ? `${e.title}. ` : "", sentence(e.text), ...extras.map((x) => ` ${x}`)].join("");
28276
+ return [keys[i] ?? "", text];
28277
+ });
28278
+ }
28279
+ function license(pack) {
28280
+ const l = pack.license;
28281
+ const who = l.holder ? ` \xA9 ${l.holder}` : "";
28282
+ return `${l.id}${who}${l.redistributable ? "" : " \u2014 private; not for redistribution"}${l.notice ? `. ${l.notice}` : ""}${l.url ? ` (${l.url})` : ""}`;
28283
+ }
28284
+ function byline(pack) {
28285
+ return [pack.author ? `by ${pack.author}` : "", `version ${pack.version}`].filter(Boolean).join(" \xB7 ");
28286
+ }
28287
+ function summaryDoc(pack) {
28288
+ const n = nouns(pack);
28289
+ const b = new Builder();
28290
+ b.p(pack.description);
28291
+ b.p(license(pack), "muted");
28292
+ b.h(2, "How it plays");
28293
+ const unitRange = `${pack.unit.min === pack.unit.max ? pack.unit.min : `${pack.unit.min}\u2013${pack.unit.max}`}`;
28294
+ b.p(
28295
+ `A ${n.run} is a series of ${n.units}${pack.unit.createsSubject ? `, each producing a ${n.subject}` : ""}; ${unitRange} of them, depending on the mode. Each ${n.unit} moves through ${plural(pack.phases.length, "phase", "phases")}: ${pack.phases.map((p) => p.label).join(", ")}.`
28296
+ );
28297
+ b.table(["Mode", "Length", "Players", "About"], modeRows(pack, { notes: false }));
28298
+ b.h(2, "What is inside");
28299
+ const tables = Object.values(pack.tables);
28300
+ const decks = Object.values(pack.decks ?? {});
28301
+ const parts = [];
28302
+ if (tables.length) parts.push(`${plural(tables.length, "table", "tables")}: ${tables.map((t) => `${t.title} (${howConsulted(t).replace(/\.$/, "").toLowerCase()}, ${plural(t.entries.length, "entry", "entries")})`).join("; ")}.`);
28303
+ if (decks.length) parts.push(`${plural(decks.length, "deck", "decks")}: ${decks.map((d) => d.kind === "cards" ? `${d.title} (${plural(d.cards.length, "card", "cards")})` : `${d.title} (a standard deck)`).join("; ")}.`);
28304
+ const states = Object.values(pack.states ?? {});
28305
+ if (states.length) parts.push(`${plural(states.length, "state", "states")} a ${n.subject} or ${n.run} can carry: ${states.map((s) => s.label).join(", ")}.`);
28306
+ const counters = Object.values(pack.counters ?? {}).filter((c) => !c.hidden);
28307
+ if (counters.length) parts.push(`${plural(counters.length, "tally", "tallies")}: ${counters.map((c) => c.label).join(", ")}.`);
28308
+ const resources = Object.values(pack.resources ?? {});
28309
+ if (resources.length) parts.push(`${plural(resources.length, "track", "tracks")}: ${resources.map((r) => r.label).join(", ")}.`);
28310
+ const moves = Object.values(pack.moves ?? {});
28311
+ if (moves.length) parts.push(`${plural(moves.length, "move", "moves")} you may make: ${moves.map((m) => m.label).join(", ")}.`);
28312
+ if (pack.endings?.length) parts.push(`${plural(pack.endings.length, "ending", "endings")}: ${pack.endings.map((e) => e.label).join(", ")}.`);
28313
+ if (pack.targeting && pack.targeting.strategy !== "none") parts.push(`Consequences can reach back to earlier ${n.subjects}.`);
28314
+ if (pack.journal?.enabled !== false) parts.push(`A journal line per ${n.unit}${pack.journal?.required ? ", required" : ""}.`);
28315
+ b.list(parts);
28316
+ b.h(2, "What you need");
28317
+ b.list(whatYouNeed(pack));
28318
+ if (pack.homepage) b.p(`More at ${pack.homepage}`, "muted");
28319
+ return { kind: "summary", layout: "book", title: pack.title, subtitle: byline(pack), blocks: b.blocks };
28320
+ }
28321
+ function rulebookDoc(pack) {
28322
+ const n = nouns(pack);
28323
+ const b = new Builder();
28324
+ b.p(pack.description);
28325
+ b.p(license(pack), "muted");
28326
+ b.h(2, "What you need");
28327
+ b.list(whatYouNeed(pack));
28328
+ b.h(2, "Terms");
28329
+ b.terms([
28330
+ { term: cap(n.run), text: `One session of play. A ${n.run} is a series of ${n.units}, and ends when you say so or when the game does.` },
28331
+ { term: cap(n.unit), text: `One round. Every ${n.unit} passes through the same phases${pack.unit.createsSubject ? ` and produces one ${n.subject}` : ""}.` },
28332
+ ...pack.unit.createsSubject ? [{ term: cap(n.subject), text: `The thing a ${n.unit} makes. States attach to it, and consequences can land on it later.` }] : [],
28333
+ { term: cap(n.finalize), text: `Closing a ${n.unit}. Once ${n.finalize.toLowerCase()}d, what it made is fixed unless a rule says otherwise.` },
28334
+ ...Object.entries(pack.vocabulary.terms ?? {}).map(([term, text]) => ({ term, text }))
28335
+ ]);
28336
+ if (pack.hierarchy?.length) b.p(`When rules contradict each other, the more specific wins, in this order: ${pack.hierarchy.join(" > ")}.`, "note");
28337
+ b.h(2, `A ${n.unit}, step by step`);
28338
+ b.list(flowSteps(pack, { detail: true }), true);
28339
+ if (pack.journal?.enabled !== false) b.p(`${pack.journal?.prompt ? `After each ${n.unit}: \u201C${pack.journal.prompt}\u201D` : `Keep a line of notes per ${n.unit}.`}${pack.journal?.required ? " A note is required before moving on." : ""}`);
28340
+ b.h(2, "Modes");
28341
+ b.table(["Mode", "Length", "Players", "About"], modeRows(pack, { notes: true }));
28342
+ b.h(2, "Tables");
28343
+ for (const [id, table] of Object.entries(pack.tables)) {
28344
+ b.h(3, table.title);
28345
+ b.p([howConsulted(table), table.description ?? ""].filter(Boolean).join(" "));
28346
+ b.table([table.resolution === "keyed" ? "Key" : "Roll", "Result"], tableRows(pack, table, { full: true }));
28347
+ void id;
28348
+ }
28349
+ const decks = Object.entries(pack.decks ?? {});
28350
+ if (decks.length) {
28351
+ b.h(2, "Cards");
28352
+ for (const [, deck] of decks) {
28353
+ b.h(3, deck.title);
28354
+ if (deck.kind === "standard52") {
28355
+ b.p([deck.description, `A standard deck${deck.includeJokers ? " with jokers" : ""}; ${deck.drawAtStart ? `draw ${deck.drawAtStart} at the start. ` : ""}Each draw is read by ${deck.resolveBy}${deck.resolveOn ? ` on ${pack.tables[deck.resolveOn]?.title ?? deck.resolveOn}` : ""}.`].filter(Boolean).join(" "));
28356
+ } else {
28357
+ b.p([deck.description, deck.drawAtStart ? `Draw ${deck.drawAtStart} at the start.` : "", deck.unique ? "" : "Cards may repeat.", deck.carriesOver ? `Unused cards carry over to the next ${n.run}.` : ""].filter(Boolean).join(" "));
28358
+ b.table(
28359
+ ["Card", "Effect"],
28360
+ deck.cards.map((c) => [c.title, [c.text, ...(c.triggers ?? []).map((t) => triggerInWords(pack, t)), c.requires?.length ? `Only if ${conditionsInWords(pack, c.requires)}.` : ""].filter(Boolean).join(" ")])
28361
+ );
28362
+ }
28363
+ }
28364
+ }
28365
+ const states = Object.entries(pack.states ?? {});
28366
+ if (states.length) {
28367
+ b.h(2, "States");
28368
+ b.table(
28369
+ ["State", "On", "Means"],
28370
+ states.map(([, s]) => [
28371
+ s.short ? `${s.label} (${s.short})` : s.label,
28372
+ s.scope === "run" ? `the ${n.run}` : `a ${n.subject}`,
28373
+ [s.description ?? "", ...(s.semantics ?? []).map(semanticInWords)].filter(Boolean).join(" ")
28374
+ ])
28375
+ );
28376
+ }
28377
+ const counters = Object.entries(pack.counters ?? {}).filter(([, c]) => !c.hidden);
28378
+ const resources = Object.entries(pack.resources ?? {});
28379
+ if (counters.length || resources.length) {
28380
+ b.h(2, "Tallies and tracks");
28381
+ b.table(
28382
+ ["Name", "Starts at", "Rule"],
28383
+ [
28384
+ ...counters.map(([, c]) => [
28385
+ c.label,
28386
+ String(c.initial ?? 0),
28387
+ [
28388
+ c.min !== void 0 && c.max !== void 0 ? `Stays within ${c.min}\u2013${c.max}.` : c.max !== void 0 ? `Never above ${c.max}.` : c.min !== void 0 && c.min !== 0 ? `Never below ${c.min}.` : "",
28389
+ ...(c.triggers ?? []).map((t) => sentence(`${t.label ? `${t.label} \u2014 ` : ""}at ${boundText(t.when)}${t.oncePerRun ? `, once per ${n.run}` : ""}: ${actionsInWords(pack, t.do)}`))
28390
+ ].filter(Boolean).join(" ")
28391
+ ]),
28392
+ ...resources.map(([, r]) => [r.label, String(r.initial ?? 0), [r.description ?? "", `From ${r.min ?? 0}${r.max !== void 0 ? ` to ${r.max}` : ""}, in steps of ${r.step ?? 1}.`].filter(Boolean).join(" ")])
28393
+ ]
28394
+ );
28395
+ }
28396
+ const moves = Object.entries(pack.moves ?? {});
28397
+ if (moves.length) {
28398
+ b.h(2, "Moves");
28399
+ b.table(
28400
+ ["Move", "When", "What happens"],
28401
+ moves.map(([, m]) => [
28402
+ m.label,
28403
+ [whenInWords(m.when, n), m.oncePerRun ? `once per ${n.run}` : "", m.available?.length ? `if ${conditionsInWords(pack, m.available)}` : ""].filter(Boolean).join("; "),
28404
+ [m.description ?? "", sentence(cap(actionsInWords(pack, m.do)))].filter(Boolean).join(" ")
28405
+ ])
28406
+ );
28407
+ }
28408
+ if (pack.targeting && pack.targeting.strategy !== "none") {
28409
+ b.h(2, "Reaching back");
28410
+ b.p(targetingInWords(pack));
28411
+ }
28412
+ if (pack.triggers?.length) {
28413
+ b.h(2, "Always in effect");
28414
+ b.list(pack.triggers.map((t) => sentence(cap(triggerInWords(pack, t)))));
28415
+ }
28416
+ if (pack.endings?.length) {
28417
+ b.h(2, `Ending a ${n.run}`);
28418
+ b.terms(pack.endings.map((e) => ({ term: e.label, text: [e.text ?? "", e.requires?.length ? `Available when ${conditionsInWords(pack, e.requires)}.` : ""].filter(Boolean).join(" ") })));
28419
+ }
28420
+ return { kind: "rulebook", layout: "book", title: pack.title, subtitle: byline(pack), blocks: b.blocks };
28421
+ }
28422
+ function quickstartDoc(pack) {
28423
+ const n = nouns(pack);
28424
+ const b = new Builder();
28425
+ b.p(pack.description);
28426
+ b.h(2, "What you need");
28427
+ b.list(whatYouNeed(pack));
28428
+ b.h(2, "The words");
28429
+ b.p(`A ${n.run} is one session. It is made of ${n.units}${pack.unit.createsSubject ? `, and each ${n.unit} makes one ${n.subject}` : ""}. Closing a ${n.unit} is called \u201C${n.finalize}\u201D.`);
28430
+ b.h(2, `Each ${n.unit}`);
28431
+ b.list(flowSteps(pack, { detail: false }), true);
28432
+ const d = pack.modes[pack.defaultMode];
28433
+ if (d) {
28434
+ b.h(2, `Start with ${d.label}`);
28435
+ b.p([d.description ?? "", `${cap(modeLength(pack, d))}, ${modePlayers(d)}.`, ...modeExtras(pack, d)].filter(Boolean).join(" "));
28436
+ const others = Object.entries(pack.modes).filter(([id]) => id !== pack.defaultMode);
28437
+ if (others.length) b.p(`Other ways to play: ${others.map(([, m]) => m.label).join(", ")}. They are in the rulebook.`, "muted");
28438
+ }
28439
+ if (pack.endings?.length) {
28440
+ b.h(2, "When you stop");
28441
+ b.terms(pack.endings.map((e) => ({ term: e.label, text: e.text ?? "" })));
28442
+ }
28443
+ b.p("Every table is on the reference card. Everything else is in the rulebook.", "muted");
28444
+ return { kind: "quickstart", layout: "book", title: pack.title, subtitle: "Quick start", blocks: b.blocks };
28445
+ }
28446
+ function referenceDoc(pack) {
28447
+ const n = nouns(pack);
28448
+ const b = new Builder();
28449
+ b.h(2, `Each ${n.unit}`);
28450
+ b.list(flowSteps(pack, { detail: false }), true);
28451
+ for (const [, table] of Object.entries(pack.tables)) {
28452
+ b.h(2, `${table.title} \u2014 ${howConsulted(table).replace(/\.$/, "")}`);
28453
+ b.table([table.resolution === "keyed" ? "Key" : "Roll", ""], tableRows(pack, table, { full: true }), true);
28454
+ }
28455
+ const states = Object.values(pack.states ?? {});
28456
+ if (states.length) {
28457
+ b.h(2, "States");
28458
+ b.table(["", "State"], states.map((s) => [s.short ?? "", `${s.label}${s.description ? ` \u2014 ${s.description}` : ""}`]), true);
28459
+ }
28460
+ const moves = Object.values(pack.moves ?? {});
28461
+ if (moves.length) {
28462
+ b.h(2, "Moves");
28463
+ b.list(moves.map((m) => sentence(`${m.label} (${whenInWords(m.when, n)}${m.oncePerRun ? `, once per ${n.run}` : ""}): ${actionsInWords(pack, m.do)}`)));
28464
+ }
28465
+ const remember = [];
28466
+ for (const t of pack.triggers ?? []) remember.push(sentence(cap(triggerInWords(pack, t))));
28467
+ for (const c of Object.values(pack.counters ?? {})) for (const t of c.triggers ?? []) remember.push(sentence(`${c.label} at ${boundText(t.when)}: ${actionsInWords(pack, t.do)}`));
28468
+ for (const m of Object.values(pack.modes)) for (const note of m.notes ?? []) remember.push(`${m.label}: ${note}`);
28469
+ if (pack.hierarchy?.length) remember.push(`Precedence: ${pack.hierarchy.join(" > ")}.`);
28470
+ if (remember.length) {
28471
+ b.h(2, "Frequently forgotten");
28472
+ b.list(remember);
28473
+ }
28474
+ if (pack.endings?.length) {
28475
+ b.h(2, "Endings");
28476
+ b.list(pack.endings.map((e) => `${e.label}${e.text ? `: ${e.text}` : ""}`));
28477
+ }
28478
+ return { kind: "reference", layout: "card", title: pack.title, subtitle: `Reference \xB7 v${pack.version}`, blocks: b.blocks };
28479
+ }
28480
+ function runlogDoc(pack) {
28481
+ const n = nouns(pack);
28482
+ const b = new Builder();
28483
+ const head = [{ label: "Date", width: "short" }, { label: "Mode", width: "short" }];
28484
+ if (Object.values(pack.modes).some((m) => m.seeded)) head.push({ label: "Seed", width: "short" });
28485
+ if (Object.values(pack.modes).some((m) => (m.players?.max ?? 1) > 1)) head.push({ label: "Players", width: "long" });
28486
+ for (const deck of Object.values(pack.decks ?? {})) if (deck.kind === "cards" && deck.drawAtStart) head.push({ label: deck.title, width: "short" });
28487
+ for (const c of Object.values(pack.counters ?? {}).filter((c2) => !c2.hidden)) head.push({ label: c.label, width: "short", box: true });
28488
+ for (const r of Object.values(pack.resources ?? {})) head.push({ label: r.label, width: "short", box: true });
28489
+ head.push({ label: "Page", width: "short" });
28490
+ b.form(head);
28491
+ b.rule();
28492
+ const unitFields = [];
28493
+ unitFields.push({ label: `${cap(n.unit)} \u2116`, width: "short" });
28494
+ if (pack.unit.createsSubject) unitFields.push({ label: `${cap(n.subject)} / states`, width: "long" });
28495
+ for (const phase of pack.phases) {
28496
+ for (const step of phase.steps) {
28497
+ if (step.kind === "rollTable") unitFields.push({ label: pack.tables[step.table]?.title ?? step.table, width: "full", lines: 2 });
28498
+ }
28499
+ }
28500
+ if (pack.journal?.enabled !== false) unitFields.push({ label: "Notes", width: "full", lines: 2 });
28501
+ for (let i = 0; i < 6; i++) b.form(unitFields);
28502
+ b.rule();
28503
+ b.form([{ label: `${cap(n.run)} states`, width: "full", lines: 2 }, { label: "Ending", width: "long" }]);
28504
+ return { kind: "runlog", layout: "sheet", title: pack.title, subtitle: `${cap(n.run)} log`, blocks: b.blocks };
28505
+ }
28506
+ function semanticInWords(s) {
28507
+ switch (s) {
28508
+ case "blocksEdit":
28509
+ return "Nothing about it may be changed.";
28510
+ case "makesUntargetable":
28511
+ return "Consequences cannot land on it.";
28512
+ case "excludesFromResult":
28513
+ return "It does not count toward the result.";
28514
+ case "locksValue":
28515
+ return "Its value is fixed.";
28516
+ case "removesFromPlay":
28517
+ return "It is out of play.";
28518
+ default:
28519
+ return s;
28520
+ }
28521
+ }
28522
+ function whenInWords(when, n) {
28523
+ switch (when) {
28524
+ case "betweenUnits":
28525
+ return `between ${n.units}`;
28526
+ case "beforeEnding":
28527
+ return `before ending the ${n.run}`;
28528
+ default:
28529
+ return "any time";
28530
+ }
28531
+ }
28532
+ function boundText(b) {
28533
+ if (b.eq !== void 0) return String(b.eq);
28534
+ if (b.gte !== void 0 && b.lte !== void 0) return `${b.gte}\u2013${b.lte}`;
28535
+ if (b.gte !== void 0) return `${b.gte}+`;
28536
+ if (b.lte !== void 0) return `\u2264${b.lte}`;
28537
+ return "";
28538
+ }
28539
+ function targetingInWords(pack) {
28540
+ const n = nouns(pack);
28541
+ const t = pack.targeting;
28542
+ if (!t) return "";
28543
+ switch (t.strategy) {
28544
+ case "none":
28545
+ return "";
28546
+ case "playerChoice":
28547
+ return `When a consequence reaches back, you choose which earlier ${n.subject} it lands on.`;
28548
+ case "random":
28549
+ return `When a consequence reaches back, pick an earlier ${n.subject} at random.`;
28550
+ case "oldest":
28551
+ return `When a consequence reaches back, it lands on the oldest ${n.subject} still in play.`;
28552
+ case "newest":
28553
+ return `When a consequence reaches back, it lands on the newest ${n.subject} still in play.`;
28554
+ case "anchoredOffset": {
28555
+ const bands = t.bands.map((b) => {
28556
+ const anchor2 = b.anchor === "playerChoice" ? "you choose" : `count ${b.direction ?? "before"} from the ${b.anchor}`;
28557
+ return `${b.range[0]}\u2013${b.range[1]}: ${anchor2}`;
28558
+ });
28559
+ return `When a roll reaches back, its ones digit says how far: ${bands.join("; ")}. Counting ${t.wraparound === false ? "stops at the end" : "wraps around"}${t.skipIneligible === false ? "" : ` and skips any ${n.subject} that cannot be targeted`}. A 0 lands on the anchor itself.${t.eventFallback ? ` When something other than a roll reaches back, roll ${t.eventFallback.roll} and read it the same way${t.eventFallback.missOn ? `; a ${t.eventFallback.missOn} misses` : ""}.` : ""}`;
28560
+ }
28561
+ }
28562
+ }
28563
+ function generateDoc(pack, kind) {
28564
+ switch (kind) {
28565
+ case "summary":
28566
+ return summaryDoc(pack);
28567
+ case "rulebook":
28568
+ return rulebookDoc(pack);
28569
+ case "quickstart":
28570
+ return quickstartDoc(pack);
28571
+ case "reference":
28572
+ return referenceDoc(pack);
28573
+ case "runlog":
28574
+ return runlogDoc(pack);
28575
+ }
28576
+ }
28577
+ var mdEscape = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
28578
+ function toMarkdown(doc) {
28579
+ const out = [`# ${doc.title}`];
28580
+ if (doc.subtitle) out.push(`*${doc.subtitle}*`);
28581
+ for (const block of doc.blocks) {
28582
+ switch (block.kind) {
28583
+ case "heading":
28584
+ out.push(`${"#".repeat(block.level)} ${block.text}`);
28585
+ break;
28586
+ case "paragraph":
28587
+ out.push(block.tone === "note" ? `> ${block.text}` : block.text);
28588
+ break;
28589
+ case "list":
28590
+ out.push(block.items.map((item, i) => `${block.ordered ? `${i + 1}.` : "-"} ${item.replace(/\n\s*/g, " \n ")}`).join("\n"));
28591
+ break;
28592
+ case "table":
28593
+ out.push([`| ${block.columns.map(mdEscape).join(" | ")} |`, `| ${block.columns.map(() => "---").join(" | ")} |`, ...block.rows.map((r) => `| ${r.map(mdEscape).join(" | ")} |`)].join("\n"));
28594
+ break;
28595
+ case "terms":
28596
+ out.push(block.items.map((t) => `**${t.term}** \u2014 ${t.text}`).join("\n\n"));
28597
+ break;
28598
+ case "form":
28599
+ out.push(block.fields.map((f) => `${f.label}: ${f.box ? "[ ]" : "_".repeat(f.width === "full" ? 40 : f.width === "long" ? 24 : 12)}`).join(" \n"));
28600
+ break;
28601
+ case "rule":
28602
+ out.push("---");
28603
+ break;
28604
+ case "pagebreak":
28605
+ out.push("");
28606
+ break;
28607
+ }
28608
+ }
28609
+ return `${out.join("\n\n")}
28610
+ `;
28611
+ }
28612
+ var esc2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
28613
+ var multiline = (s) => esc2(s).replace(/\n/g, "<br>");
28614
+ var CSS = `
28615
+ :root { color-scheme: light; }
28616
+ body { margin: 0; background: #fff; color: #161513; font: 11pt/1.45 Georgia, "Times New Roman", serif; }
28617
+ main { max-width: 42em; margin: 0 auto; padding: 2.5rem 1.5rem; }
28618
+ h1 { font-size: 2rem; margin: 0 0 .25rem; line-height: 1.1; }
28619
+ .subtitle { margin: 0 0 2rem; color: #5b5751; font-style: italic; }
28620
+ h2 { font-size: 1.25rem; margin: 2rem 0 .5rem; border-bottom: 1px solid #d9d4cb; padding-bottom: .2rem; }
28621
+ h3 { font-size: 1.05rem; margin: 1.4rem 0 .35rem; }
28622
+ p { margin: 0 0 .75rem; }
28623
+ p.muted { color: #5b5751; font-size: .92em; }
28624
+ p.note { border-left: 3px solid #b9b2a5; padding-left: .75rem; color: #3f3b36; }
28625
+ ol, ul { margin: 0 0 .9rem; padding-left: 1.4rem; }
28626
+ li { margin: .2rem 0; white-space: pre-line; }
28627
+ table { border-collapse: collapse; width: 100%; margin: 0 0 1rem; font-size: .95em; }
28628
+ th, td { text-align: left; vertical-align: top; padding: .3rem .5rem; border-bottom: 1px solid #e4dfd6; }
28629
+ th { font-family: system-ui, sans-serif; font-size: .75em; letter-spacing: .04em; text-transform: uppercase; color: #5b5751; }
28630
+ td:first-child { white-space: nowrap; font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: .9em; }
28631
+ table.compact td { padding: .12rem .35rem; font-size: .85em; }
28632
+ dl { margin: 0 0 1rem; }
28633
+ dt { font-weight: 700; margin-top: .5rem; }
28634
+ dd { margin: 0; }
28635
+ hr { border: 0; border-top: 1px solid #d9d4cb; margin: 1.5rem 0; }
28636
+ .page { break-after: page; }
28637
+ .form { display: flex; flex-wrap: wrap; gap: .5rem 1.25rem; margin: .6rem 0 .9rem; font-family: system-ui, sans-serif; font-size: .85em; }
28638
+ .field { display: flex; align-items: flex-end; gap: .4rem; }
28639
+ .field .line { display: inline-block; border-bottom: 1px solid #161513; height: 1.3em; }
28640
+ .field.short .line { width: 7em; } .field.long .line { width: 16em; } .field.full { flex: 1 1 100%; }
28641
+ .field.full .line { flex: 1; }
28642
+ .field .lines { flex: 1; display: grid; gap: .55em; }
28643
+ .field .lines .line { width: 100%; }
28644
+ .field .box { display: inline-block; width: 2.6em; height: 1.6em; border: 1px solid #161513; }
28645
+ .layout-card main { max-width: none; column-width: 17em; column-gap: 2rem; padding: 1.25rem; font-size: 9pt; }
28646
+ .layout-card h1 { column-span: all; font-size: 1.4rem; }
28647
+ .layout-card .subtitle { column-span: all; margin-bottom: 1rem; }
28648
+ .layout-card h2 { margin-top: 1rem; font-size: .95rem; break-after: avoid; }
28649
+ .layout-card table { break-inside: auto; }
28650
+ .layout-card tr { break-inside: avoid; }
28651
+ .layout-sheet main { max-width: none; padding: 1.25rem; }
28652
+ .layout-sheet .form + .form { border-top: 1px dashed #d9d4cb; padding-top: .6rem; }
28653
+ @media print { main { padding: 0; } @page { margin: 1.6cm; } .layout-card @page { margin: 1cm; } }
28654
+ `;
28655
+ function toHtml(doc) {
28656
+ const body = [];
28657
+ for (const block of doc.blocks) {
28658
+ switch (block.kind) {
28659
+ case "heading":
28660
+ body.push(`<h${block.level}>${esc2(block.text)}</h${block.level}>`);
28661
+ break;
28662
+ case "paragraph":
28663
+ body.push(`<p${block.tone ? ` class="${block.tone}"` : ""}>${multiline(block.text)}</p>`);
28664
+ break;
28665
+ case "list":
28666
+ body.push(`<${block.ordered ? "ol" : "ul"}>${block.items.map((i) => `<li>${esc2(i)}</li>`).join("")}</${block.ordered ? "ol" : "ul"}>`);
28667
+ break;
28668
+ case "table":
28669
+ body.push(
28670
+ `<table${block.compact ? ' class="compact"' : ""}><thead><tr>${block.columns.map((c) => `<th>${esc2(c)}</th>`).join("")}</tr></thead><tbody>${block.rows.map((r) => `<tr>${r.map((c) => `<td>${multiline(c)}</td>`).join("")}</tr>`).join("")}</tbody></table>`
28671
+ );
28672
+ break;
28673
+ case "terms":
28674
+ body.push(`<dl>${block.items.map((t) => `<dt>${esc2(t.term)}</dt><dd>${multiline(t.text)}</dd>`).join("")}</dl>`);
28675
+ break;
28676
+ case "form":
28677
+ body.push(
28678
+ `<div class="form">${block.fields.map((f) => {
28679
+ const width = f.width ?? "short";
28680
+ const line = f.box ? `<span class="box"></span>` : f.lines && f.lines > 1 ? `<span class="lines">${'<span class="line"></span>'.repeat(f.lines)}</span>` : `<span class="line"></span>`;
28681
+ return `<span class="field ${width}"><span class="label">${esc2(f.label)}</span>${line}</span>`;
28682
+ }).join("")}</div>`
28683
+ );
28684
+ break;
28685
+ case "rule":
28686
+ body.push("<hr>");
28687
+ break;
28688
+ case "pagebreak":
28689
+ body.push('<div class="page"></div>');
28690
+ break;
28691
+ }
28692
+ }
28693
+ return `<!doctype html>
28694
+ <html lang="en" class="layout-${doc.layout}">
28695
+ <head>
28696
+ <meta charset="utf-8">
28697
+ <meta name="viewport" content="width=device-width, initial-scale=1">
28698
+ <title>${esc2(doc.title)}${doc.subtitle ? ` \u2014 ${esc2(doc.subtitle)}` : ""}</title>
28699
+ <style>${CSS}</style>
28700
+ </head>
28701
+ <body>
28702
+ <main>
28703
+ <h1>${esc2(doc.title)}</h1>
28704
+ ${doc.subtitle ? `<p class="subtitle">${esc2(doc.subtitle)}</p>` : ""}
28705
+ ${body.join("\n")}
28706
+ </main>
28707
+ </body>
28708
+ </html>
28709
+ `;
28710
+ }
28711
+
27888
28712
  // packages/engine/src/log.ts
27889
28713
  function voidedIds(events) {
27890
28714
  const out = /* @__PURE__ */ new Set();
@@ -28214,11 +29038,14 @@ import { resolve as resolve2 } from "node:path";
28214
29038
 
28215
29039
  // packages/cli/src/account.ts
28216
29040
  var import_yaml2 = __toESM(require_dist(), 1);
29041
+ import { spawn } from "node:child_process";
28217
29042
  import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
28218
29043
  import { homedir } from "node:os";
28219
29044
  import { join, resolve } from "node:path";
28220
29045
  import { createInterface } from "node:readline";
28221
29046
  var DEFAULT_API = "https://runlog.scrthq.com/api";
29047
+ var RENEW_MARGIN_MS = 6e4;
29048
+ var DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
28222
29049
  function configDir() {
28223
29050
  const base = process.env["RUNLOG_CONFIG_DIR"] ?? (process.platform === "win32" ? join(process.env["APPDATA"] ?? join(homedir(), "AppData", "Roaming"), "runlog") : join(process.env["XDG_CONFIG_HOME"] ?? join(homedir(), ".config"), "runlog"));
28224
29051
  return base;
@@ -28229,12 +29056,31 @@ function credentials() {
28229
29056
  if (fromEnv) return { api: process.env["RUNLOG_API"] ?? DEFAULT_API, key: fromEnv, savedAt: "" };
28230
29057
  try {
28231
29058
  const raw = JSON.parse(readFileSync(credentialsPath(), "utf8"));
28232
- if (typeof raw.key !== "string" || typeof raw.api !== "string") return null;
28233
- return { api: raw.api, key: raw.key, savedAt: raw.savedAt ?? "" };
29059
+ if (typeof raw.api !== "string") return null;
29060
+ if (typeof raw.key === "string") return { api: raw.api, key: raw.key, savedAt: raw.savedAt ?? "" };
29061
+ const s = raw.session;
29062
+ if (s && typeof s.accessToken === "string" && typeof s.refreshToken === "string" && typeof s.clientId === "string") {
29063
+ return {
29064
+ api: raw.api,
29065
+ session: { clientId: s.clientId, issuer: s.issuer ?? WORKOS, accessToken: s.accessToken, refreshToken: s.refreshToken, expiresAt: s.expiresAt ?? "" },
29066
+ savedAt: raw.savedAt ?? ""
29067
+ };
29068
+ }
29069
+ return null;
28234
29070
  } catch {
28235
29071
  return null;
28236
29072
  }
28237
29073
  }
29074
+ function save(creds) {
29075
+ const dir = configDir();
29076
+ mkdirSync(dir, { recursive: true });
29077
+ writeFileSync(credentialsPath(), `${JSON.stringify(creds, null, 2)}
29078
+ `, "utf8");
29079
+ try {
29080
+ chmodSync(credentialsPath(), 384);
29081
+ } catch {
29082
+ }
29083
+ }
28238
29084
  async function ask(prompt, hidden = false) {
28239
29085
  const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
28240
29086
  return new Promise((done) => {
@@ -28260,60 +29106,182 @@ async function ask(prompt, hidden = false) {
28260
29106
  }
28261
29107
  });
28262
29108
  }
28263
- async function api(method, path, body) {
28264
- const creds = credentials();
28265
- if (!creds) throw new Error("not signed in: run `runlog login` with a key from your profile page");
28266
- const response = await fetch(`${creds.api.replace(/\/$/, "")}${path}`, {
28267
- method,
28268
- headers: { authorization: `Bearer ${creds.key}`, ...body !== void 0 ? { "content-type": "application/json" } : {} },
28269
- ...body !== void 0 ? { body: JSON.stringify(body) } : {}
29109
+ var WORKOS = "https://api.workos.com";
29110
+ var realDeps = {
29111
+ fetch: (input2, init) => fetch(input2, init),
29112
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
29113
+ say: (line) => console.log(line),
29114
+ open: (url2) => {
29115
+ try {
29116
+ const [cmd, args] = process.platform === "win32" ? ["cmd", ["/c", "start", "", url2]] : process.platform === "darwin" ? ["open", [url2]] : ["xdg-open", [url2]];
29117
+ spawn(cmd, args, { detached: true, stdio: "ignore" }).on("error", () => {
29118
+ }).unref();
29119
+ } catch {
29120
+ }
29121
+ },
29122
+ now: () => Date.now()
29123
+ };
29124
+ async function postForm(deps, url2, form) {
29125
+ const response = await deps.fetch(url2, {
29126
+ method: "POST",
29127
+ headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
29128
+ body: new URLSearchParams(form).toString()
28270
29129
  });
28271
29130
  const text = await response.text();
28272
- let parsed = null;
28273
29131
  try {
28274
- parsed = JSON.parse(text);
29132
+ return { status: response.status, body: JSON.parse(text) };
28275
29133
  } catch {
28276
- throw new Error(`the API at ${creds.api} did not answer as expected (${response.status})`);
29134
+ throw new Error(`WorkOS did not answer as expected (${response.status})`);
28277
29135
  }
28278
- if (response.status === 401) throw new Error("that key is not accepted; make a new one on your profile page and `runlog login` again");
28279
- if (response.status >= 400) {
28280
- const said = parsed?.error;
28281
- throw new Error(said ?? `the API said ${response.status}`);
29136
+ }
29137
+ function expiryOf(accessToken, now) {
29138
+ try {
29139
+ const payload = JSON.parse(Buffer.from(accessToken.split(".")[1] ?? "", "base64url").toString("utf8"));
29140
+ if (typeof payload.exp === "number") return new Date(payload.exp * 1e3).toISOString();
29141
+ } catch {
28282
29142
  }
28283
- return parsed;
29143
+ return new Date(now + 5 * 6e4).toISOString();
29144
+ }
29145
+ async function deviceFlow(clientId, issuer, deps = realDeps) {
29146
+ const start = await postForm(deps, `${issuer}/user_management/authorize/device`, { client_id: clientId });
29147
+ const s = start.body;
29148
+ if (start.status >= 400 || !s.device_code || !s.user_code || !s.verification_uri) {
29149
+ throw new Error(s.error_description ?? `WorkOS would not start a sign-in (${start.status})`);
29150
+ }
29151
+ deps.say("");
29152
+ deps.say(` Open ${s.verification_uri}`);
29153
+ deps.say(` Code ${s.user_code}`);
29154
+ deps.say("");
29155
+ deps.say("Waiting for you to confirm it there. Ctrl-C gives up.");
29156
+ if (s.verification_uri_complete) deps.open(s.verification_uri_complete);
29157
+ let interval = Math.max(1, s.interval ?? 5);
29158
+ const deadline = deps.now() + (s.expires_in ?? 300) * 1e3;
29159
+ while (deps.now() < deadline) {
29160
+ await deps.sleep(interval * 1e3);
29161
+ const poll = await postForm(deps, `${issuer}/user_management/authenticate`, { grant_type: DEVICE_GRANT, device_code: s.device_code, client_id: clientId });
29162
+ const a = poll.body;
29163
+ if (a.access_token && a.refresh_token) {
29164
+ return { clientId, issuer, accessToken: a.access_token, refreshToken: a.refresh_token, expiresAt: expiryOf(a.access_token, deps.now()) };
29165
+ }
29166
+ switch (a.error) {
29167
+ case "authorization_pending":
29168
+ continue;
29169
+ case "slow_down":
29170
+ interval += 1;
29171
+ continue;
29172
+ case "access_denied":
29173
+ throw new Error("the sign-in was refused in the browser");
29174
+ case "expired_token":
29175
+ throw new Error("the code expired before it was confirmed; run `runlog login` again");
29176
+ default:
29177
+ throw new Error(a.error_description ?? a.error ?? `WorkOS answered ${poll.status}`);
29178
+ }
29179
+ }
29180
+ throw new Error("the code expired before it was confirmed; run `runlog login` again");
29181
+ }
29182
+ async function renew(session, deps = realDeps) {
29183
+ const answer = await postForm(deps, `${session.issuer}/user_management/authenticate`, {
29184
+ grant_type: "refresh_token",
29185
+ refresh_token: session.refreshToken,
29186
+ client_id: session.clientId
29187
+ });
29188
+ const a = answer.body;
29189
+ if (!a.access_token || !a.refresh_token) {
29190
+ throw new Error("your sign-in has lapsed; run `runlog login` again");
29191
+ }
29192
+ return { ...session, accessToken: a.access_token, refreshToken: a.refresh_token, expiresAt: expiryOf(a.access_token, deps.now()) };
29193
+ }
29194
+ async function bearer(creds, force = false, deps = realDeps) {
29195
+ if (creds.key) return creds.key;
29196
+ if (!creds.session) throw new Error("not signed in: run `runlog login`");
29197
+ const lapsing = !creds.session.expiresAt || Date.parse(creds.session.expiresAt) - deps.now() < RENEW_MARGIN_MS;
29198
+ if (!force && !lapsing) return creds.session.accessToken;
29199
+ const session = await renew(creds.session, deps);
29200
+ creds.session = session;
29201
+ if (!process.env["RUNLOG_API_KEY"]) save(creds);
29202
+ return session.accessToken;
29203
+ }
29204
+ async function api(method, path, body) {
29205
+ const creds = credentials();
29206
+ if (!creds) throw new Error("not signed in: run `runlog login`");
29207
+ const once = async (token) => {
29208
+ const response = await fetch(`${creds.api.replace(/\/$/, "")}${path}`, {
29209
+ method,
29210
+ headers: { authorization: `Bearer ${token}`, ...body !== void 0 ? { "content-type": "application/json" } : {} },
29211
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
29212
+ });
29213
+ const text = await response.text();
29214
+ let parsed = null;
29215
+ try {
29216
+ parsed = JSON.parse(text);
29217
+ } catch {
29218
+ throw new Error(`the API at ${creds.api} did not answer as expected (${response.status})`);
29219
+ }
29220
+ return { status: response.status, parsed };
29221
+ };
29222
+ let answer = await once(await bearer(creds));
29223
+ if (answer.status === 401 && creds.session) answer = await once(await bearer(creds, true));
29224
+ if (answer.status === 401) {
29225
+ throw new Error(creds.key ? "that key is not accepted; make a new one on your profile page and `runlog login --key` again" : "your sign-in has lapsed; run `runlog login` again");
29226
+ }
29227
+ if (answer.status >= 400) {
29228
+ const said = answer.parsed?.error;
29229
+ throw new Error(said ?? `the API said ${answer.status}`);
29230
+ }
29231
+ return answer.parsed;
29232
+ }
29233
+ async function greet(apiUrl) {
29234
+ const me = await api("GET", "/me");
29235
+ const who = me.profile?.name ?? me.profile?.email ?? "you";
29236
+ console.log(`signed in as ${who} at ${apiUrl}`);
28284
29237
  }
28285
- async function cmdLogin(args) {
28286
- const apiUrl = flag(args, "--api") ?? process.env["RUNLOG_API"] ?? DEFAULT_API;
28287
- console.log("Paste a command-line key from your profile page in Runlog. It is not shown as you type.");
28288
- const key = await ask("key: ", true);
28289
- if (!key.startsWith("rl_")) {
28290
- console.error("that does not look like a Runlog key; they begin with rl_");
29238
+ async function cmdLogin(args, deps = realDeps) {
29239
+ const apiUrl = (flag(args, "--api") ?? process.env["RUNLOG_API"] ?? DEFAULT_API).replace(/\/$/, "");
29240
+ if (process.env["RUNLOG_API_KEY"]) {
29241
+ console.error("RUNLOG_API_KEY is set, so that is what every command will use; unset it to sign in as yourself");
28291
29242
  return 1;
28292
29243
  }
28293
- const dir = configDir();
28294
- mkdirSync(dir, { recursive: true });
28295
- const saved = { api: apiUrl, key, savedAt: (/* @__PURE__ */ new Date()).toISOString() };
28296
- writeFileSync(credentialsPath(), `${JSON.stringify(saved, null, 2)}
28297
- `, "utf8");
28298
- try {
28299
- chmodSync(credentialsPath(), 384);
28300
- } catch {
29244
+ if (args.includes("--key")) {
29245
+ console.log("Paste a command-line key from your profile page in Runlog. It is not shown as you type.");
29246
+ const key = await ask("key: ", true);
29247
+ if (!key.startsWith("rl_")) {
29248
+ console.error("that does not look like a Runlog key; they begin with rl_");
29249
+ return 1;
29250
+ }
29251
+ save({ api: apiUrl, key, savedAt: (/* @__PURE__ */ new Date()).toISOString() });
29252
+ try {
29253
+ await greet(apiUrl);
29254
+ return 0;
29255
+ } catch (error61) {
29256
+ unlinkSync(credentialsPath());
29257
+ console.error(error61 instanceof Error ? error61.message : String(error61));
29258
+ return 1;
29259
+ }
28301
29260
  }
28302
29261
  try {
28303
- const me = await api("GET", "/me");
28304
- const who = me.profile?.name ?? me.profile?.email ?? "you";
28305
- console.log(`signed in as ${who} at ${apiUrl}`);
29262
+ const response = await deps.fetch(`${apiUrl}/auth/cli`, { headers: { accept: "application/json" } });
29263
+ const text = await response.text();
29264
+ let about = {};
29265
+ try {
29266
+ about = JSON.parse(text);
29267
+ } catch {
29268
+ throw new Error(`the API at ${apiUrl} did not answer as expected (${response.status})`);
29269
+ }
29270
+ if (!about.clientId) throw new Error(`the API at ${apiUrl} cannot sign in a terminal yet; use \`runlog login --key\` with a key from your profile page`);
29271
+ const session = await deviceFlow(about.clientId, about.issuer ?? WORKOS, deps);
29272
+ save({ api: apiUrl, session, savedAt: (/* @__PURE__ */ new Date()).toISOString() });
29273
+ await greet(apiUrl);
28306
29274
  return 0;
28307
29275
  } catch (error61) {
28308
- unlinkSync(credentialsPath());
28309
29276
  console.error(error61 instanceof Error ? error61.message : String(error61));
28310
29277
  return 1;
28311
29278
  }
28312
29279
  }
28313
29280
  function cmdLogout() {
29281
+ const creds = credentials();
28314
29282
  if (existsSync(credentialsPath())) {
28315
29283
  unlinkSync(credentialsPath());
28316
- console.log("signed out; the key is still valid until you revoke it on your profile page");
29284
+ console.log(creds?.key ? "signed out; the key is still valid until you revoke it on your profile page" : "signed out");
28317
29285
  } else {
28318
29286
  console.log("not signed in");
28319
29287
  }
@@ -28321,9 +29289,12 @@ function cmdLogout() {
28321
29289
  }
28322
29290
  async function cmdWhoami() {
28323
29291
  try {
29292
+ const creds = credentials();
28324
29293
  const me = await api("GET", "/me");
28325
29294
  const claims = await api("GET", "/claims");
29295
+ const how = process.env["RUNLOG_API_KEY"] ? "with RUNLOG_API_KEY" : creds?.key ? "with a key from the profile page" : "from the browser";
28326
29296
  console.log(`${me.profile?.name ?? "(no name)"} <${me.profile?.email ?? "?"}> ${me.sub}`);
29297
+ console.log(`signed in ${how} at ${creds?.api ?? DEFAULT_API}`);
28327
29298
  console.log(claims.claims.length > 0 ? `claimed signing keys: ${claims.claims.map((c) => c.fingerprint).join(", ")}` : "no claimed signing keys yet: `runlog claim key.json`");
28328
29299
  return 0;
28329
29300
  } catch (error61) {
@@ -28475,11 +29446,11 @@ async function cmdSign(args) {
28475
29446
  const format = detectFormat(path);
28476
29447
  const document = import_yaml3.default.parse(text);
28477
29448
  const signature = await signPack(document, key.privateKey, flag2(args, "--as"));
28478
- const signed = { ...document, signature };
29449
+ const signed2 = { ...document, signature };
28479
29450
  writeFileSync2(
28480
29451
  path,
28481
- format === "json" ? `${JSON.stringify(signed, null, 2)}
28482
- ` : import_yaml3.default.stringify(signed, { lineWidth: 90 }),
29452
+ format === "json" ? `${JSON.stringify(signed2, null, 2)}
29453
+ ` : import_yaml3.default.stringify(signed2, { lineWidth: 90 }),
28483
29454
  "utf8"
28484
29455
  );
28485
29456
  console.log(`${paint(GREEN, "signed")} ${input2}`);
@@ -28620,6 +29591,62 @@ async function reportSignature(document) {
28620
29591
  );
28621
29592
  }
28622
29593
 
29594
+ // packages/cli/src/docs.ts
29595
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "node:fs";
29596
+ import { basename, join as join2, resolve as resolve3 } from "node:path";
29597
+ import { readFileSync as readFileSync3 } from "node:fs";
29598
+ function cmdDocs(args) {
29599
+ const files = args.filter((a) => !a.startsWith("-"));
29600
+ const input2 = files[0];
29601
+ if (!input2) {
29602
+ console.error("usage: runlog docs <pack.yaml> [-o dir] [--only summary,rulebook,quickstart,reference,runlog] [--md | --html]");
29603
+ return 2;
29604
+ }
29605
+ const outIndex = args.findIndex((a) => a === "-o" || a === "--out");
29606
+ const onlyIndex = args.findIndex((a) => a === "--only");
29607
+ const wanted = new Set(
29608
+ onlyIndex >= 0 && args[onlyIndex + 1] ? args[onlyIndex + 1].split(",").map((k) => k.trim()) : DOC_KINDS.map((k) => k.kind)
29609
+ );
29610
+ for (const k of wanted) {
29611
+ if (!DOC_KINDS.some((d) => d.kind === k)) {
29612
+ console.error(`no such document: ${k}. Choose from ${DOC_KINDS.map((d) => d.kind).join(", ")}.`);
29613
+ return 2;
29614
+ }
29615
+ }
29616
+ const formats = args.includes("--md") && !args.includes("--html") ? ["md"] : args.includes("--html") && !args.includes("--md") ? ["html"] : ["html", "md"];
29617
+ const path = resolve3(input2);
29618
+ let text;
29619
+ try {
29620
+ text = readFileSync3(path, "utf8");
29621
+ } catch {
29622
+ console.error(`cannot read ${input2}`);
29623
+ return 1;
29624
+ }
29625
+ const loaded = loadPackText(text, detectFormat(path));
29626
+ if (!loaded.ok) {
29627
+ console.error(`${basename(input2)} does not load; run \`runlog validate\` to see why`);
29628
+ return 1;
29629
+ }
29630
+ const { pack } = loaded;
29631
+ const dir = outIndex >= 0 && args[outIndex + 1] ? resolve3(args[outIndex + 1]) : resolve3(`${pack.id}-docs`);
29632
+ mkdirSync2(dir, { recursive: true });
29633
+ for (const kind of DOC_KINDS) {
29634
+ if (!wanted.has(kind.kind)) continue;
29635
+ const doc = generateDoc(pack, kind.kind);
29636
+ for (const format of formats) {
29637
+ const file2 = join2(dir, `${kind.kind}.${format}`);
29638
+ writeFileSync3(file2, format === "md" ? toMarkdown(doc) : toHtml(doc), "utf8");
29639
+ console.log(`${kind.label.padEnd(14)} ${file2}`);
29640
+ }
29641
+ }
29642
+ if (!pack.license.redistributable) {
29643
+ console.log("note: this pack is marked non-redistributable. The summary is safe to show anyone; keep the rest as private as the pack.");
29644
+ } else {
29645
+ console.log("open an .html in a browser and print it for a PDF; the reference card lays out in columns and the run log as a sheet.");
29646
+ }
29647
+ return 0;
29648
+ }
29649
+
28623
29650
  // packages/cli/src/bin.ts
28624
29651
  var RESET2 = "\x1B[0m";
28625
29652
  var RED2 = "\x1B[31m";
@@ -28647,17 +29674,17 @@ function printDiagnostics(diagnostics, file2) {
28647
29674
  console.error(` ${parts.join(", ")} in ${file2}`);
28648
29675
  }
28649
29676
  function read(path) {
28650
- const abs = resolve3(path);
29677
+ const abs = resolve4(path);
28651
29678
  if (!existsSync3(abs)) {
28652
29679
  console.error(paint2(RED2, `no such file: ${path}`));
28653
29680
  process.exit(2);
28654
29681
  }
28655
- const text = readFileSync3(abs, "utf8");
29682
+ const text = readFileSync4(abs, "utf8");
28656
29683
  const result = loadPackText(text, detectFormat(abs));
28657
29684
  return { text, pack: result.pack, diagnostics: result.diagnostics };
28658
29685
  }
28659
29686
  function report(path, diagnostics, strict) {
28660
- printDiagnostics(diagnostics, basename(path));
29687
+ printDiagnostics(diagnostics, basename2(path));
28661
29688
  if (hasErrors(diagnostics)) return 1;
28662
29689
  if (strict && diagnostics.length > 0) {
28663
29690
  console.error(paint2(YELLOW2, " --strict: treating warnings as failures"));
@@ -28683,7 +29710,7 @@ async function cmdValidate(args) {
28683
29710
  console.log(
28684
29711
  `${paint2(GREEN2, "ok")} ${paint2(BOLD, pack.title)} ${paint2(DIM2, `v${pack.version}`)} \u2014 ${tables} table${tables === 1 ? "" : "s"}, ${entries} entries, ${Object.keys(pack.modes).length} mode(s)` + (diagnostics.length ? paint2(YELLOW2, `, ${diagnostics.length} warning(s)`) : "")
28685
29712
  );
28686
- await reportSignature(import_yaml4.default.parse(readFileSync3(resolve3(file2), "utf8")));
29713
+ await reportSignature(import_yaml4.default.parse(readFileSync4(resolve4(file2), "utf8")));
28687
29714
  }
28688
29715
  }
28689
29716
  return worst;
@@ -28703,14 +29730,14 @@ function cmdBundle(args) {
28703
29730
  }
28704
29731
  printDiagnostics(
28705
29732
  diagnostics.filter((d) => d.level === "warning"),
28706
- basename(input2)
29733
+ basename2(input2)
28707
29734
  );
28708
29735
  const body = JSON.stringify(pack, null, 2);
28709
29736
  const hash2 = createHash("sha256").update(body).digest("hex").slice(0, 16);
28710
29737
  const bundled = { ...pack, bundledAt: (/* @__PURE__ */ new Date()).toISOString(), contentHash: hash2 };
28711
- const out = outIndex >= 0 && args[outIndex + 1] ? resolve3(args[outIndex + 1]) : resolve3(`${pack.id}-${pack.version}.pack.json`);
28712
- mkdirSync2(dirname(out), { recursive: true });
28713
- writeFileSync3(out, `${JSON.stringify(bundled, null, 2)}
29738
+ const out = outIndex >= 0 && args[outIndex + 1] ? resolve4(args[outIndex + 1]) : resolve4(`${pack.id}-${pack.version}.pack.json`);
29739
+ mkdirSync3(dirname(out), { recursive: true });
29740
+ writeFileSync4(out, `${JSON.stringify(bundled, null, 2)}
28714
29741
  `, "utf8");
28715
29742
  console.log(`${paint2(GREEN2, "bundled")} ${out} ${paint2(DIM2, `sha256:${hash2}`)}`);
28716
29743
  if (!pack.license.redistributable) {
@@ -28763,12 +29790,12 @@ function cmdTest(args) {
28763
29790
  }
28764
29791
  function cmdInit(args) {
28765
29792
  const name = args.filter((a) => !a.startsWith("-"))[0] ?? "my-pack";
28766
- const out = resolve3(`${name}.yaml`);
29793
+ const out = resolve4(`${name}.yaml`);
28767
29794
  if (existsSync3(out)) {
28768
29795
  console.error(paint2(RED2, `refusing to overwrite ${out}`));
28769
29796
  return 1;
28770
29797
  }
28771
- writeFileSync3(out, SKELETON.replaceAll("__NAME__", name), "utf8");
29798
+ writeFileSync4(out, SKELETON.replaceAll("__NAME__", name), "utf8");
28772
29799
  console.log(`${paint2(GREEN2, "created")} ${out}`);
28773
29800
  console.log(paint2(DIM2, ` next: runlog validate ${name}.yaml`));
28774
29801
  return 0;
@@ -28830,19 +29857,23 @@ usage:
28830
29857
  runlog bundle <pack> [-o out.json] normalize to a distributable JSON
28831
29858
  runlog test <pack> replay the pack's own fixtures
28832
29859
  runlog init [name] scaffold a new pack
29860
+ runlog docs <pack> [-o dir] write its rulebook, quick start, reference card,
29861
+ [--only kinds] run log sheet and catalog summary (HTML and Markdown)
28833
29862
  runlog keygen [-o key.json] make a signing key for your packs
28834
29863
  runlog sign <pack> --key key.json sign a pack, proving you wrote it
28835
29864
  runlog issue <pack> --to "Name" stamp a copy with a buyer's name and sign it
28836
29865
  [--seal] \u2026and seal it, so it needs a license key to open
28837
29866
 
28838
- runlog login [--api URL] paste a key from your profile page, once
29867
+ runlog login [--api URL] sign in: a code to confirm in your browser
29868
+ [--key] \u2026or paste a key from your profile page, for a machine with no browser
28839
29869
  runlog whoami who the command line is acting as
28840
29870
  runlog claim key.json prove a signing key is yours; the app then names you
28841
29871
  runlog publish <pack> put a pack in your library, on every device
28842
- runlog logout forget the saved key
29872
+ runlog logout forget the sign-in
28843
29873
 
28844
29874
  --strict makes warnings fail, which is what you want in CI. In CI, set
28845
- RUNLOG_API_KEY instead of running login.
29875
+ RUNLOG_API_KEY to a key from your profile page instead of running login;
29876
+ nobody is there to confirm a code.
28846
29877
 
28847
29878
  Signing proves authorship. It does not restrict copying and cannot: the app
28848
29879
  has to read every word of a pack to play it. What it gives you is that an
@@ -28857,6 +29888,8 @@ async function main(argv) {
28857
29888
  return cmdValidate(args);
28858
29889
  case "bundle":
28859
29890
  return cmdBundle(args);
29891
+ case "docs":
29892
+ return cmdDocs(args);
28860
29893
  case "test":
28861
29894
  return cmdTest(args);
28862
29895
  case "keygen":