@afokapu/atdd-bun 0.10.31 → 0.10.33
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/README.md +14 -120
- package/conventions/_profiles/architecture.yaml +190 -0
- package/conventions/_profiles/coder.yaml +804 -0
- package/conventions/_profiles/design.yaml +219 -0
- package/conventions/_profiles/docs.yaml +136 -0
- package/conventions/_profiles/htmx.yaml +436 -0
- package/conventions/_profiles/interlocking.yaml +248 -0
- package/conventions/_profiles/metrics.yaml +80 -0
- package/conventions/_profiles/planner.yaml +789 -0
- package/conventions/_profiles/runtime.yaml +62 -0
- package/conventions/_profiles/security.yaml +70 -0
- package/conventions/_profiles/telemetry.yaml +196 -0
- package/conventions/_profiles/tester.yaml +734 -0
- package/conventions/_profiles/topology.yaml +192 -0
- package/conventions/_profiles/traceability.yaml +94 -0
- package/conventions/atdd-bun.profiles/atdd-bun.profiles.presets.convention.yaml +21 -3
- package/conventions/planner.docs/planner.docs.area-index-required.convention.yaml +2 -2
- package/conventions/planner.docs/planner.docs.artifact-path-shape.convention.yaml +1 -1
- package/conventions/planner.docs/planner.docs.asciidoc-only.convention.yaml +0 -1
- package/conventions/planner.docs/planner.docs.identity-required.convention.yaml +1 -1
- package/conventions/planner.docs/planner.docs.undeclared-change.convention.yaml +1 -2
- package/detectors/bun_interlocking_coverage/checks/interlocking_train_sequence.mjs +7 -5
- package/detectors/planner_docs_capability/README.md +1 -1
- package/integrity.json +36 -50
- package/package.json +1 -1
- package/relationships.yaml +0 -151
- package/src/agent.ts +15 -31
- package/src/cli.ts +7 -8
- package/src/docs-capability.ts +6 -24
- package/src/enforce.ts +8 -9
- package/src/hooks.ts +4 -8
- package/src/index.ts +1 -2
- package/src/integrity.ts +3 -11
- package/src/profile-registry.ts +74 -0
- package/src/setup.ts +3 -5
- package/templates/agents/AGENTS.block.md +4 -8
- package/templates/github/atdd-bun.yml +0 -5
- package/conventions/delivery/delivery.approved-sha-resolves.convention.yaml +0 -33
- package/conventions/delivery/delivery.board.convention.yaml +0 -60
- package/conventions/delivery/delivery.config-schema.convention.yaml +0 -49
- package/conventions/delivery/delivery.evidence-schema.convention.yaml +0 -34
- package/conventions/delivery/delivery.findings-resolved.convention.yaml +0 -42
- package/conventions/delivery/delivery.merge-gate.convention.yaml +0 -46
- package/conventions/delivery/delivery.model-allowed.convention.yaml +0 -29
- package/conventions/delivery/delivery.operating-model.convention.yaml +0 -97
- package/conventions/delivery/delivery.review.convention.yaml +0 -42
- package/conventions/delivery/delivery.reviewer-independent.convention.yaml +0 -34
- package/conventions/delivery/delivery.stages-complete.convention.yaml +0 -33
- package/detectors/delivery_evidence/atdd.implementation.yaml +0 -33
- package/detectors/delivery_evidence/detect.mjs +0 -9
- package/detectors/delivery_evidence/fixtures/clean/atdd-bun.yaml +0 -8
- package/detectors/delivery_evidence/fixtures/clean/docs/delivery/tranches/api/evidence.yaml +0 -54
- package/detectors/delivery_evidence/fixtures/dirty/atdd-bun.yaml +0 -3
- package/detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/api/evidence.yaml +0 -31
- package/detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/data/evidence.yaml +0 -8
- package/detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/orphan/README.md +0 -1
- package/detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/ui/evidence.yaml +0 -5
- package/detectors/planner_docs_capability/fixtures/dirty_missing_index/docs/delivery/release.adoc +0 -10
- package/detectors/planner_docs_capability/fixtures/dirty_missing_index/docs/purpose/index.adoc +0 -5
- package/planner-schemas/delivery-config.schema.json +0 -95
- package/planner-schemas/delivery-evidence.schema.json +0 -103
- package/src/board-ui.ts +0 -91
- package/src/board.ts +0 -222
- package/src/delivery.ts +0 -567
- package/templates/agents/atdd/SKILL.md +0 -20
- package/templates/agents/delivery/SKILL.md +0 -15
package/src/board.ts
DELETED
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from "node:fs";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { readFile } from "node:fs/promises";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { runUi } from "./board-ui";
|
|
7
|
-
|
|
8
|
-
/** The delivery board: a local ntfy server the coordinator, drivers, writers and reviewers talk through
|
|
9
|
-
* (the delivery.board convention). One topic per level (program, tranche, review conversation); every message
|
|
10
|
-
* carries a header naming its sender, recipients and subject. The board is where the work is visible; the
|
|
11
|
-
* evidence record stays the only thing the delivery rules judge. */
|
|
12
|
-
|
|
13
|
-
export const DEFAULT_BOARD_URL = "http://127.0.0.1:2586";
|
|
14
|
-
const LOCAL_HOSTS = new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
|
|
15
|
-
const TOPIC_LIMIT = 64;
|
|
16
|
-
/** The board's directory: one message per topic, its name, posted with the topic's first message. */
|
|
17
|
-
export const DIRECTORY = "atdd-topics";
|
|
18
|
-
|
|
19
|
-
/** The board is opt-in: on only when atdd-bun.yaml names delivery.board. Its URL is $ATDD_BOARD_URL, else
|
|
20
|
-
* delivery.board.url, else the default; the environment moves the board on one machine but never switches it on. Only a
|
|
21
|
-
* local address is accepted, so a wrong setting can never send a message to a public server. */
|
|
22
|
-
export async function boardUrl(root = process.cwd(), env: Record<string, string | undefined> = process.env): Promise<string> {
|
|
23
|
-
let board: unknown;
|
|
24
|
-
const file = join(root, "atdd-bun.yaml");
|
|
25
|
-
if (existsSync(file)) try { board = (Bun.YAML.parse(await readFile(file, "utf8")) as { delivery?: { board?: unknown } } | null)?.delivery?.board; } catch { /* reported by delivery.config-schema */ }
|
|
26
|
-
if (!board || typeof board !== "object" || Array.isArray(board)) throw new Error("the board is not enabled; add delivery.board to atdd-bun.yaml (for example board: { url: http://127.0.0.1:2586 }) to have agents talk through it");
|
|
27
|
-
const configured = (board as { url?: unknown }).url;
|
|
28
|
-
return localUrl(env.ATDD_BOARD_URL || (typeof configured === "string" ? configured : DEFAULT_BOARD_URL));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function localUrl(raw: string): string {
|
|
32
|
-
let url: URL;
|
|
33
|
-
try { url = new URL(raw); } catch { throw new Error(`board URL ${raw} is not a URL`); }
|
|
34
|
-
if (!/^https?:$/.test(url.protocol) || !LOCAL_HOSTS.has(url.hostname)) throw new Error(`board URL ${raw} is not local; the board runs on this machine (127.0.0.1, localhost or [::1])`);
|
|
35
|
-
return `${url.protocol}//${url.host}`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** A topic name derived from its level: atdd-<program>[-<tranche>[-<stage>-<round>]]. Parts are lowercased and
|
|
39
|
-
* reduced to [a-z0-9-]; a name longer than ntfy's 64 characters keeps its start and a hash of the whole. */
|
|
40
|
-
export function topicName(program: string, tranche?: string, stage?: string, round?: string | number): string {
|
|
41
|
-
const parts = [program, tranche, stage, round === undefined ? undefined : String(round)].filter((part): part is string => part !== undefined && part !== "");
|
|
42
|
-
if (!parts.length) throw new Error("a topic needs at least the program name");
|
|
43
|
-
if (stage !== undefined && round === undefined) throw new Error("a review conversation topic needs its stage and round");
|
|
44
|
-
const slug = (part: string) => part.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
45
|
-
const name = ["atdd", ...parts.map(slug)].join("-");
|
|
46
|
-
if (name.length <= TOPIC_LIMIT) return name;
|
|
47
|
-
const hash = createHash("sha256").update(name).digest("hex").slice(0, 8);
|
|
48
|
-
return `${name.slice(0, TOPIC_LIMIT - 9).replace(/-+$/, "")}-${hash}`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Topics this agent may use, from $ATDD_TOPICS (comma-separated), given by whoever launched it. */
|
|
52
|
-
export function allowedTopics(env: Record<string, string | undefined> = process.env): string[] {
|
|
53
|
-
return (env.ATDD_TOPICS ?? "").split(",").map(topic => topic.trim()).filter(Boolean);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function checkTopic(topic: string, env: Record<string, string | undefined> = process.env): void {
|
|
57
|
-
if (!/^[A-Za-z0-9_-]{1,64}$/.test(topic)) throw new Error(`${topic} is not a topic name; derive one with atdd-bun chat topic`);
|
|
58
|
-
const allowed = allowedTopics(env);
|
|
59
|
-
if (!allowed.includes(topic)) throw new Error(`${topic} is not one of this agent's topics (ATDD_TOPICS: ${allowed.join(", ") || "none"}); an agent reads and writes only the topics it was launched with`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export const HEADER_FIELDS = ["kind", "stage", "reply-to", "repo", "branch", "worktree", "goal", "base", "sha"] as const;
|
|
63
|
-
export type Header = { from: string; to: string[]; participants: string[]; conversation: string } & Partial<Record<(typeof HEADER_FIELDS)[number], string>>;
|
|
64
|
-
export type Message = { id: string; time: number; topic: string; header: Partial<Header>; body: string; raw: string };
|
|
65
|
-
|
|
66
|
-
export function envelope(header: Header, body: string): string {
|
|
67
|
-
const value = (v: string | string[]) => Array.isArray(v) ? `[${v.join(", ")}]` : JSON.stringify(v);
|
|
68
|
-
return `---\n${Object.entries(header).filter(([, v]) => v !== undefined).map(([k, v]) => `${k}: ${value(v as string | string[])}`).join("\n")}\n---\n${body.trim()}`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function parseEnvelope(text: string): { header: Partial<Header>; body: string } {
|
|
72
|
-
const match = text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
73
|
-
if (!match) return { header: {}, body: text };
|
|
74
|
-
try { return { header: (Bun.YAML.parse(match[1]) ?? {}) as Partial<Header>, body: match[2] }; } catch { return { header: {}, body: text }; }
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const addressedTo = (message: Message, agent: string) => [message.header.to ?? []].flat().includes(agent);
|
|
78
|
-
|
|
79
|
-
/** Posts with a few retries (a busy or restarting board), then fails loudly: a message is never silently lost. The first
|
|
80
|
-
* message on a topic also names the topic in the directory, so the board's view can list it. */
|
|
81
|
-
export async function post(url: string, topic: string, header: Header, body: string): Promise<string> {
|
|
82
|
-
const first = await fetch(`${url}/${topic}/json?poll=1&since=latest`).then(res => res.ok ? res.text() : "x").then(text => !text.trim()).catch(() => false);
|
|
83
|
-
let error = "";
|
|
84
|
-
for (let attempt = 0; attempt < 5; attempt++) {
|
|
85
|
-
try {
|
|
86
|
-
const res = await fetch(`${url}/${topic}`, { method: "POST", body: envelope(header, body), headers: { Title: `${header.from} -> ${header.to.join(", ")}` } });
|
|
87
|
-
if (res.ok) {
|
|
88
|
-
const id = (await res.json() as { id: string }).id;
|
|
89
|
-
if (first) await fetch(`${url}/${DIRECTORY}`, { method: "POST", body: topic }).catch(() => undefined);
|
|
90
|
-
return id;
|
|
91
|
-
}
|
|
92
|
-
error = `${res.status} ${await res.text()}`;
|
|
93
|
-
} catch (e) { error = String(e); }
|
|
94
|
-
await Bun.sleep(250 * 2 ** attempt);
|
|
95
|
-
}
|
|
96
|
-
throw new Error(`post to ${topic} failed: ${error}`);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** Every message on the topics after `since` (a message id, or "all"). */
|
|
100
|
-
export async function read(url: string, topics: string[], since = "all", signal?: AbortSignal): Promise<Message[]> {
|
|
101
|
-
const res = await fetch(`${url}/${topics.join(",")}/json?poll=1&since=${since}`, { signal });
|
|
102
|
-
if (!res.ok) throw new Error(`reading ${topics.join(", ")} failed: ${res.status} ${await res.text()}`);
|
|
103
|
-
return (await res.text()).split("\n").filter(Boolean).map(line => JSON.parse(line)).filter(m => m.event === "message")
|
|
104
|
-
.map(m => ({ id: m.id, time: m.time, topic: m.topic, raw: m.message, ...parseEnvelope(m.message) }));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Waits for the next message addressed to `agent` on any of `topics` after `since`, passing over the kinds in `skip`.
|
|
108
|
-
* It polls rather than streams: a live stream can drop a message published while it connects. ntfy reads `since` (a
|
|
109
|
-
* message id) as that message's time on every topic, so one cursor serves several topics. Returns null after
|
|
110
|
-
* `timeoutSeconds`, so an agent's tool call stays bounded. */
|
|
111
|
-
/** The longest one poll may take. ntfy answers a poll at once, so only a stalled request ever reaches it. */
|
|
112
|
-
const POLL_LIMIT_MS = 10_000;
|
|
113
|
-
|
|
114
|
-
/** What a listener prints when nothing has arrived for a while: the owner's check-in, for the agent to act on (move
|
|
115
|
-
* the work, or tell whoever it waits on), never to answer with a "still here" post. */
|
|
116
|
-
export function checkIn(agent: string, quietSeconds: number, rearm = false): string {
|
|
117
|
-
const quiet = quietSeconds <= 0 ? "" : quietSeconds < 60 ? ` for ${quietSeconds} s` : ` for ${Math.round(quietSeconds / 60)} min`;
|
|
118
|
-
return `CHECK-IN for ${agent}: no message${quiet}. Where are you? Are you moving forward, or blocked on someone, and do they know? Is your stakeholder informed? Are you avoiding ceremony and rabbit holes, effectiveness first, then efficiency? Act on it: move the work, or post once to whoever you wait on; never post just to say you are still here.${rearm ? " Then re-arm this listener." : ""}`;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export async function waitFor(url: string, topics: string[], agent: string, since = "all", timeoutSeconds = 0, skip: string[] = [], intervalMs = 500, seen: (id: string) => void = () => {}): Promise<Message | null> {
|
|
122
|
-
const deadline = timeoutSeconds > 0 ? Date.now() + timeoutSeconds * 1000 : Infinity;
|
|
123
|
-
for (;;) {
|
|
124
|
-
let messages: Message[] = [];
|
|
125
|
-
// Each poll is bounded: a request that never answers (a half-open connection after the board restarts or the
|
|
126
|
-
// machine sleeps) would otherwise hold the wait past its deadline and deliver nothing. It counts as unavailable.
|
|
127
|
-
const bound = AbortSignal.timeout(Math.max(1, Math.min(POLL_LIMIT_MS, deadline - Date.now())));
|
|
128
|
-
try { messages = await read(url, topics, since, bound); } catch { /* the board is briefly unavailable: keep waiting */ }
|
|
129
|
-
for (const message of messages) { since = message.id; seen(since); if (addressedTo(message, agent) && !skip.includes(message.header.kind ?? "")) return message; }
|
|
130
|
-
if (Date.now() >= deadline) return null;
|
|
131
|
-
await Bun.sleep(intervalMs);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** Where `wait` keeps its place, per identity and topic set, so restarting a watcher is always the same command.
|
|
136
|
-
* Best effort: where the file cannot be read or written, wait behaves as if it had no saved place. */
|
|
137
|
-
function cursorFile(agent: string, topics: string[], env: Record<string, string | undefined>): string {
|
|
138
|
-
const key = `${agent}__${[...topics].sort().join("+")}`.replace(/[^A-Za-z0-9@+_.-]/g, "_");
|
|
139
|
-
return join(env.ATDD_BOARD_STATE || join(homedir(), ".atdd-board", "cursors"), key);
|
|
140
|
-
}
|
|
141
|
-
const loadCursor = (file: string) => { try { return readFileSync(file, "utf8").trim() || undefined; } catch { return undefined; } };
|
|
142
|
-
/** The place to start from on a topic set this identity has never listened to: its OLDEST saved place on any other set,
|
|
143
|
-
* so nothing unread is skipped and nothing older is replayed. Without one, the start of the board, as before. */
|
|
144
|
-
function seedCursor(agent: string, env: Record<string, string | undefined>): string | undefined {
|
|
145
|
-
const dir = env.ATDD_BOARD_STATE || join(homedir(), ".atdd-board", "cursors"), prefix = `${agent}__`.replace(/[^A-Za-z0-9@+_.-]/g, "_");
|
|
146
|
-
try {
|
|
147
|
-
const saved = readdirSync(dir).filter(name => name.startsWith(prefix)).map(name => ({ file: join(dir, name), at: statSync(join(dir, name)).mtimeMs })).sort((a, b) => a.at - b.at);
|
|
148
|
-
for (const { file } of saved) { const id = loadCursor(file); if (id) return id; }
|
|
149
|
-
} catch { /* no saved places */ }
|
|
150
|
-
return undefined;
|
|
151
|
-
}
|
|
152
|
-
const saveCursor = (file: string, id: string) => { try { mkdirSync(join(file, ".."), { recursive: true }); writeFileSync(file, `${id}\n`); } catch { /* best effort */ } };
|
|
153
|
-
|
|
154
|
-
export function format(message: Message, withTopic = false): string {
|
|
155
|
-
const time = new Date(message.time * 1000).toTimeString().slice(0, 8), h = message.header;
|
|
156
|
-
const route = `${h.from ?? "?"} -> ${[h.to ?? []].flat().join(", ") || "?"}`, tags = [h.kind, h.stage].filter(Boolean).join(" · ");
|
|
157
|
-
return `${withTopic ? `[${message.topic}] ` : ""}${time} ${route}${tags ? ` (${tags})` : ""} #${message.id}\n${message.body.trim()}\n`;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** `atdd-bun chat` opens the board's terminal view; `atdd-bun chat <topic|post|read|wait> …` is for agents.
|
|
161
|
-
* Returns the exit code: 0 done, 1 error, 2 wait timed out. */
|
|
162
|
-
export async function chat(args: string[], env: Record<string, string | undefined> = process.env, root = process.cwd()): Promise<number> {
|
|
163
|
-
const [command, ...rest] = args;
|
|
164
|
-
const flag = (name: string) => { const i = rest.indexOf(`--${name}`); return i >= 0 ? rest[i + 1] : undefined; };
|
|
165
|
-
const list = (value?: string) => (value ?? "").split(",").map(item => item.trim()).filter(Boolean);
|
|
166
|
-
const positional = rest.filter((arg, i) => !arg.startsWith("--") && !(i > 0 && rest[i - 1].startsWith("--") && rest[i - 1] !== "--mine"));
|
|
167
|
-
const agent = () => { if (!env.ATDD_AGENT) throw new Error("ATDD_AGENT is not set: every agent is launched with its identity"); return env.ATDD_AGENT; };
|
|
168
|
-
try {
|
|
169
|
-
if (command === "topic") { console.log(topicName(positional[0], positional[1], positional[2], positional[3])); return 0; }
|
|
170
|
-
const url = await boardUrl(root, env);
|
|
171
|
-
if (command === undefined) return await runUi(url, env);
|
|
172
|
-
const topics = list(positional[0]);
|
|
173
|
-
if (!topics.length) throw new Error(`usage: atdd-bun chat ${command} <topic> …`);
|
|
174
|
-
const me = agent();
|
|
175
|
-
topics.forEach(topic => checkTopic(topic, env));
|
|
176
|
-
if (command === "wait") {
|
|
177
|
-
// Without --since, continue from where this identity last stopped on these topics; either way, remember the place.
|
|
178
|
-
const file = cursorFile(me, topics, env);
|
|
179
|
-
const message = await waitFor(url, topics, me, flag("since") || loadCursor(file) || seedCursor(me, env) || "all", Number(flag("timeout") ?? 0), list(flag("skip")), 500, id => saveCursor(file, id));
|
|
180
|
-
if (!message) { console.log(checkIn(me, Number(flag("timeout") ?? 0), true)); return 2; }
|
|
181
|
-
console.log(format(message, topics.length > 1));
|
|
182
|
-
return 0;
|
|
183
|
-
}
|
|
184
|
-
if (command === "follow") {
|
|
185
|
-
// The listener for a host that wakes its agent on each line a running process prints: every message addressed to
|
|
186
|
-
// this identity, as it arrives, until --timeout (seconds; none = until stopped). Same place-keeping as wait.
|
|
187
|
-
const file = cursorFile(me, topics, env), skip = list(flag("skip")), total = Number(flag("timeout") ?? 0);
|
|
188
|
-
const deadline = total > 0 ? Date.now() + total * 1000 : Infinity;
|
|
189
|
-
let since = flag("since") || loadCursor(file) || seedCursor(me, env) || "all";
|
|
190
|
-
// After --check-in seconds without a message (default 20 minutes; 0 = never), the agent is asked to check itself.
|
|
191
|
-
const quietLimit = Number(flag("check-in") ?? 1200) * 1000;
|
|
192
|
-
let quietSince = Date.now();
|
|
193
|
-
for (;;) {
|
|
194
|
-
const left = Math.ceil((deadline - Date.now()) / 1000);
|
|
195
|
-
if (left <= 0) return 0;
|
|
196
|
-
const message = await waitFor(url, topics, me, since, Math.min(60, left, quietLimit > 0 ? Math.max(1, Math.ceil((quietSince + quietLimit - Date.now()) / 1000)) : 60), skip, 500, id => { since = id; saveCursor(file, id); });
|
|
197
|
-
if (message) { console.log(format(message, topics.length > 1)); quietSince = Date.now(); }
|
|
198
|
-
else if (quietLimit > 0 && Date.now() - quietSince >= quietLimit) { console.log(checkIn(me, Math.round((Date.now() - quietSince) / 1000))); quietSince = Date.now(); }
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
if (topics.length !== 1) throw new Error(`${command} takes one topic`);
|
|
202
|
-
const topic = topics[0];
|
|
203
|
-
if (command === "post") {
|
|
204
|
-
const to = list(flag("to"));
|
|
205
|
-
if (!to.length) throw new Error("--to is required: every message names its recipients");
|
|
206
|
-
const body = await new Response(Bun.stdin.stream()).text();
|
|
207
|
-
if (!body.trim()) throw new Error("the message body is read from standard input and is empty");
|
|
208
|
-
const header: Header = { from: me, to, participants: [...new Set([me, ...to, ...list(flag("participants"))])], conversation: topic };
|
|
209
|
-
for (const field of HEADER_FIELDS) if (flag(field)) header[field] = flag(field);
|
|
210
|
-
console.log(await post(url, topic, header, body));
|
|
211
|
-
return 0;
|
|
212
|
-
}
|
|
213
|
-
if (command === "read") {
|
|
214
|
-
for (const message of await read(url, [topic], flag("since") || "all")) if (!rest.includes("--mine") || addressedTo(message, me)) console.log(format(message));
|
|
215
|
-
return 0;
|
|
216
|
-
}
|
|
217
|
-
throw new Error(`unknown chat command ${command}; run atdd-bun chat alone for the board, or topic, post, read, wait`);
|
|
218
|
-
} catch (error) {
|
|
219
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
220
|
-
return 1;
|
|
221
|
-
}
|
|
222
|
-
}
|