baesian 0.0.0-stage → 1.0.1
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/LICENSE.md +105 -0
- package/README.md +217 -2
- package/dist/agents.js +249 -0
- package/dist/api.js +13 -0
- package/dist/bin.js +14 -0
- package/dist/ci.js +82 -0
- package/dist/cite.js +66 -0
- package/dist/cli.js +831 -0
- package/dist/client/assets/alegreya-cyrillic-ext-wght-italic-DzWRXZb0.woff2 +0 -0
- package/dist/client/assets/alegreya-cyrillic-ext-wght-normal-CN0-BfWD.woff2 +0 -0
- package/dist/client/assets/alegreya-cyrillic-wght-italic-Dhrv2xfk.woff2 +0 -0
- package/dist/client/assets/alegreya-cyrillic-wght-normal-CU5lb38f.woff2 +0 -0
- package/dist/client/assets/alegreya-greek-ext-wght-italic-CoUI9EEu.woff2 +0 -0
- package/dist/client/assets/alegreya-greek-ext-wght-normal-C8PPRmwJ.woff2 +0 -0
- package/dist/client/assets/alegreya-greek-wght-italic-CVt9-Jr4.woff2 +0 -0
- package/dist/client/assets/alegreya-greek-wght-normal-BPx8yWcZ.woff2 +0 -0
- package/dist/client/assets/alegreya-latin-ext-wght-italic-BIuo_2sR.woff2 +0 -0
- package/dist/client/assets/alegreya-latin-ext-wght-normal-o8axeAGu.woff2 +0 -0
- package/dist/client/assets/alegreya-latin-wght-italic-Bxkcgmge.woff2 +0 -0
- package/dist/client/assets/alegreya-latin-wght-normal-BG9rTJo4.woff2 +0 -0
- package/dist/client/assets/alegreya-vietnamese-wght-italic-BAPuWgnQ.woff2 +0 -0
- package/dist/client/assets/alegreya-vietnamese-wght-normal-DOV9JYup.woff2 +0 -0
- package/dist/client/assets/archivo-latin-ext-wdth-normal-7khWdh9v.woff2 +0 -0
- package/dist/client/assets/archivo-latin-wdth-normal-DY7AcnAa.woff2 +0 -0
- package/dist/client/assets/archivo-vietnamese-wdth-normal-rJmnGBSt.woff2 +0 -0
- package/dist/client/assets/index-9yf70Vqv.css +1 -0
- package/dist/client/assets/index-Dwg9e0YR.js +15 -0
- package/dist/client/index.html +15 -0
- package/dist/facts.js +278 -0
- package/dist/folder-storage.js +71 -0
- package/dist/frontmatter.js +38 -0
- package/dist/gate-paths.js +77 -0
- package/dist/gate.js +237 -0
- package/dist/git.js +100 -0
- package/dist/globs.js +120 -0
- package/dist/hookblock.js +146 -0
- package/dist/hooks.js +911 -0
- package/dist/ids.js +23 -0
- package/dist/login.js +301 -0
- package/dist/mcp.js +303 -0
- package/dist/merges.js +849 -0
- package/dist/modes.js +245 -0
- package/dist/notify.js +91 -0
- package/dist/ops.js +381 -0
- package/dist/passkey.js +86 -0
- package/dist/personal.js +163 -0
- package/dist/practice-answers.js +42 -0
- package/dist/proposals.js +197 -0
- package/dist/quote.js +28 -0
- package/dist/report.js +261 -0
- package/dist/routing.js +411 -0
- package/dist/schema.js +1178 -0
- package/dist/server.js +514 -0
- package/dist/similarity.js +45 -0
- package/dist/sshsig.js +159 -0
- package/dist/state.js +135 -0
- package/dist/storage.js +61 -0
- package/dist/store.js +2259 -0
- package/dist/style.js +408 -0
- package/dist/sync.js +797 -0
- package/dist/trailers.js +121 -0
- package/dist/trust.js +222 -0
- package/dist/usage-fields.js +161 -0
- package/dist/vault.js +849 -0
- package/dist/verify.js +443 -0
- package/dist/version.js +70 -0
- package/dist/wait.js +449 -0
- package/dist/wikilinks.js +25 -0
- package/package.json +71 -4
package/dist/modes.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { claudeMdMissesAgents, writeAgentsBlock } from "./agents.js";
|
|
4
|
+
import { cite } from "./cite.js";
|
|
5
|
+
import { installHooks, installPersonalHooks, uninstallPersonalHooks } from "./hooks.js";
|
|
6
|
+
import { CONFIG_PATH } from "./facts.js";
|
|
7
|
+
import { linkAck } from "./login.js";
|
|
8
|
+
import { excludeRecord, includeRecord, sharedAlready } from "./personal.js";
|
|
9
|
+
import { PERSONAL_CONFIG, RECORD_DIR, ValidationError } from "./store.js";
|
|
10
|
+
import { attention, cmd, fields, head, next, note, warning } from "./style.js";
|
|
11
|
+
import { initVault, vaultGuidance } from "./vault.js";
|
|
12
|
+
/** A path as people read it: relative to the worktree when it's inside it. */
|
|
13
|
+
function shown(store, path) {
|
|
14
|
+
const rel = relative(store.repo.toplevel, path);
|
|
15
|
+
return rel && !rel.startsWith("..") ? rel : path;
|
|
16
|
+
}
|
|
17
|
+
/** "a, b and c". */
|
|
18
|
+
const listed = (items) => (items.length < 2 ? items.join("") : `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`);
|
|
19
|
+
/** The hooks a setup wrote, by what it did to each: "pre-commit and commit-msg", or "pre-commit created; commit-msg updated". */
|
|
20
|
+
function hooksWritten(files) {
|
|
21
|
+
const changed = Object.entries(files).filter(([, v]) => v !== "unchanged");
|
|
22
|
+
const by = new Map();
|
|
23
|
+
for (const [name, what] of changed)
|
|
24
|
+
by.set(what, [...(by.get(what) ?? []), name]);
|
|
25
|
+
if (by.size === 1 && by.has("created"))
|
|
26
|
+
return listed(by.get("created"));
|
|
27
|
+
return [...by].map(([what, names]) => `${listed(names)} ${what}`).join("; ");
|
|
28
|
+
}
|
|
29
|
+
/** The preferences field: none yet means the client's setup survey suggests them (B-5na1k); only the client writes them (D147). */
|
|
30
|
+
function preferences(s) {
|
|
31
|
+
if (s.inForce().name)
|
|
32
|
+
return undefined;
|
|
33
|
+
return { label: "preferences", value: attention("none yet"), why: "the client suggests them, from a few example decisions" };
|
|
34
|
+
}
|
|
35
|
+
const SERVE = next("baesian serve", "open the client, and choose how much should reach you");
|
|
36
|
+
/** Shared mode's setup, as `baesian init` has always done it: the live layer, the record's config, the vault, the hooks' block and AGENTS.md. */
|
|
37
|
+
export function setUpShared(s, o) {
|
|
38
|
+
const out = [];
|
|
39
|
+
const notes = [];
|
|
40
|
+
const paths = [];
|
|
41
|
+
const created = s.init();
|
|
42
|
+
if (created.length) {
|
|
43
|
+
out.push({ label: "live layer", value: `${shown(s, s.live)}/`, why: "shared by every worktree, never committed" });
|
|
44
|
+
out.push({ label: "record", value: `${shown(s, s.record)}/`, why: "committed with the code" });
|
|
45
|
+
paths.push(RECORD_DIR);
|
|
46
|
+
}
|
|
47
|
+
// M6: the vault adapter. An existing config gains the vault; Bases is turned on, and the Decision Log gets its query.
|
|
48
|
+
if (o.vault !== undefined) {
|
|
49
|
+
const v = initVault(s, o.vault);
|
|
50
|
+
out.push(v.field);
|
|
51
|
+
notes.push(...v.notes);
|
|
52
|
+
if (v.changed && !paths.includes(RECORD_DIR))
|
|
53
|
+
paths.push(RECORD_DIR);
|
|
54
|
+
}
|
|
55
|
+
if (o.hooks) {
|
|
56
|
+
const r = installHooks(s, o.fallback);
|
|
57
|
+
const changed = Object.values(r.files).some((v) => v !== "unchanged");
|
|
58
|
+
const where = shown(s, r.dir);
|
|
59
|
+
if (changed || r.setHooksPath) {
|
|
60
|
+
const written = hooksWritten(r.files);
|
|
61
|
+
const why = [written];
|
|
62
|
+
if (r.setHooksPath)
|
|
63
|
+
why.push(written ? "; " : "", "core.hooksPath points here, so each clone runs ", cmd("baesian init"), " once");
|
|
64
|
+
out.push({ label: "git gate", value: `${where}/`, why });
|
|
65
|
+
}
|
|
66
|
+
if (!r.versioned)
|
|
67
|
+
notes.push(note(`${where} isn't versioned, so other clones won't get the gate; CI still checks.`));
|
|
68
|
+
else if (changed)
|
|
69
|
+
paths.push(where);
|
|
70
|
+
}
|
|
71
|
+
if (o.agents) {
|
|
72
|
+
const result = writeAgentsBlock(s.repo.toplevel, vaultGuidance(s));
|
|
73
|
+
if (result !== "unchanged") {
|
|
74
|
+
out.push({ label: "agents", value: "AGENTS.md", why: `instructions ${result === "created" ? "added" : result}` });
|
|
75
|
+
paths.push("AGENTS.md");
|
|
76
|
+
}
|
|
77
|
+
if (claudeMdMissesAgents(s.repo.toplevel)) {
|
|
78
|
+
notes.push(note("CLAUDE.md doesn't import AGENTS.md. Add a line ", cmd("@AGENTS.md"), " so Claude Code reads the instructions."));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Anything set up now (the layers, the hooks, AGENTS.md) makes it a setup; nothing, and it was set up already.
|
|
82
|
+
return { fresh: created.length > 0 || paths.length > 0 || out.some((f) => f.label === "git gate"), fields: out, notes, paths };
|
|
83
|
+
}
|
|
84
|
+
/** `baesian init`, shared mode: what it set up, the client, and the commit to make. */
|
|
85
|
+
export function initSharedMode(s, o) {
|
|
86
|
+
const r = setUpShared(s, o);
|
|
87
|
+
const out = [head("done", r.fresh ? "set up Baesian in this repo, in shared mode" : "Baesian is set up in this repo already, in shared mode")];
|
|
88
|
+
out.push(...fields([...r.fields, preferences(s)]), ...r.notes);
|
|
89
|
+
if (!s.inForce().name)
|
|
90
|
+
out.push(SERVE);
|
|
91
|
+
if (r.paths.length)
|
|
92
|
+
out.push(next(`git add ${r.paths.join(" ")} && git commit -m "Use Baesian"`));
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* `baesian init --personal` (B-4rwyz): the live layer with the clone's config in it, `/.baesian/` in the clone's
|
|
97
|
+
* `info/exclude`, and the clone's own hooks, which run Baesian's gate and then the repo's, named by a config file of
|
|
98
|
+
* Baesian's that the clone's `.git/config` includes last. Nothing in the worktree changes. Refused where the repo is
|
|
99
|
+
* shared already, and with a vault, whose notes are committed files.
|
|
100
|
+
*/
|
|
101
|
+
export function setUpPersonal(s, o) {
|
|
102
|
+
if (o.vault !== undefined) {
|
|
103
|
+
throw new ValidationError(`--vault: the vault's notes are committed files, and personal mode commits nothing${cite("B-4rwyz")}`, [
|
|
104
|
+
{ command: "baesian init --shared --vault <path>", why: "give the vault when the repo switches to shared mode" },
|
|
105
|
+
]);
|
|
106
|
+
}
|
|
107
|
+
const was = s.personal;
|
|
108
|
+
if (!was) {
|
|
109
|
+
const why = sharedAlready(s);
|
|
110
|
+
if (why)
|
|
111
|
+
throw new ValidationError(why);
|
|
112
|
+
}
|
|
113
|
+
const out = [];
|
|
114
|
+
const created = s.init({}, { personal: true });
|
|
115
|
+
if (created.length)
|
|
116
|
+
out.push({ label: "live layer", value: `${shown(s, s.live)}/`, why: "this clone's cards, answers, ledger and config, in place of a committed record" });
|
|
117
|
+
if (excludeRecord(s))
|
|
118
|
+
out.push({ label: "git ignores", value: `${RECORD_DIR}/`, why: "in this clone only (.git/info/exclude), so a stray record is never committed" });
|
|
119
|
+
const notes = [];
|
|
120
|
+
if (o.hooks) {
|
|
121
|
+
const r = installPersonalHooks(s, o.fallback);
|
|
122
|
+
const dir = `${shown(s, r.dir)}/`;
|
|
123
|
+
const why = [];
|
|
124
|
+
if (r.moved)
|
|
125
|
+
why.push(`moved out of the clone's core.hooksPath, which names the repo's own hooks again (${r.repo.shown}), into ${shown(s, r.config)}, so a setup script that sets core.hooksPath no longer turns it off`);
|
|
126
|
+
if (r.wasOff)
|
|
127
|
+
why.push(`on again (it was off: ${r.wasOff})`);
|
|
128
|
+
if (r.pointed) {
|
|
129
|
+
why.push(`this clone's own hooks: Baesian's gate, then the repo's own hooks from ${r.repo.shown}/, which are unchanged. Git finds them through ${shown(s, r.config)}, which the clone's .git/config includes last, so a setup script that sets core.hooksPath leaves the gate on. Commits keep no Baesian trailers: the gate takes them out of the message, into this clone's ledger`);
|
|
130
|
+
}
|
|
131
|
+
else
|
|
132
|
+
why.push(`this clone's own hooks, refreshed, then the repo's from ${r.repo.shown}/`);
|
|
133
|
+
// A field each: what moved, that it's on again, and what the hooks are.
|
|
134
|
+
for (const w of why)
|
|
135
|
+
out.push({ label: "git gate", value: dir, why: w });
|
|
136
|
+
if (r.stillOff)
|
|
137
|
+
notes.push(warning(`git doesn't run them here yet: ${r.stillOff}. Take that setting out, and the gate is on.`));
|
|
138
|
+
}
|
|
139
|
+
if (!was)
|
|
140
|
+
out.push({ label: "agents", value: "AGENTS.md", why: "left as it is: agents hear of Baesian from its plugin, for Claude Code or Codex, at the start of each session" });
|
|
141
|
+
const lines = [
|
|
142
|
+
head("done", was
|
|
143
|
+
? `Baesian is set up in this clone already, in personal mode${cite("B-4rwyz")}: nothing of Baesian's is committed from this clone`
|
|
144
|
+
: `set up Baesian in this clone alone, in personal mode${cite("B-4rwyz")}: nothing of Baesian's is committed, so the repo's collaborators see no change`),
|
|
145
|
+
...fields([...out, preferences(s)]),
|
|
146
|
+
...notes,
|
|
147
|
+
];
|
|
148
|
+
if (!s.inForce().name)
|
|
149
|
+
lines.push(SERVE);
|
|
150
|
+
if (!was)
|
|
151
|
+
lines.push(next("baesian init --shared", "to share it later: writes this branch's record, the hooks' block and AGENTS.md into the repo, for you to commit"));
|
|
152
|
+
return lines;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The cards a branch's record holds when a personal clone switches to shared mode (B-4rwyz): those filed on it or
|
|
156
|
+
* attached to it, and those its merges brought (D181), through the branches merged into it in turn.
|
|
157
|
+
*/
|
|
158
|
+
export function cardsForBranch(s, branch = s.repo.branch) {
|
|
159
|
+
const ids = new Set(s.branchCards(branch).map((c) => c.meta.id));
|
|
160
|
+
const merges = s.merges();
|
|
161
|
+
const into = new Set([branch]);
|
|
162
|
+
for (let grew = true; grew;) {
|
|
163
|
+
grew = false;
|
|
164
|
+
for (const m of merges) {
|
|
165
|
+
if (!into.has(m.into))
|
|
166
|
+
continue;
|
|
167
|
+
for (const id of m.cards)
|
|
168
|
+
ids.add(id);
|
|
169
|
+
if (!into.has(m.branch)) {
|
|
170
|
+
into.add(m.branch);
|
|
171
|
+
grew = true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return [...ids].sort();
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* `baesian init --shared` (B-4rwyz). In a personal clone, the switch: the clone's config becomes the record's, this
|
|
179
|
+
* branch's record is written from the live layer, the clone's own hooks go (with the include of Baesian's setting in the
|
|
180
|
+
* clone's `.git/config`, so `core.hooksPath` is the repo's again) and Baesian's block goes into the repo's, AGENTS.md
|
|
181
|
+
* gets its block, and `info/exclude` no longer hides `.baesian/`.
|
|
182
|
+
* Nothing is committed: the lines end with the commit to make, and the gate facts it raises. Elsewhere, shared mode's
|
|
183
|
+
* setup, as `baesian init` does it, and this branch's cards into its record when they aren't there.
|
|
184
|
+
*/
|
|
185
|
+
export function initShared(open, o) {
|
|
186
|
+
let s = open();
|
|
187
|
+
const switching = s.personal;
|
|
188
|
+
const config = switching ? s.config() : {};
|
|
189
|
+
if (switching) {
|
|
190
|
+
if (existsSync(s.record)) {
|
|
191
|
+
throw new ValidationError(`this worktree has a ${RECORD_DIR}/ already, which personal mode never writes: delete it, then run \`baesian init --shared\` again, which writes the record from this clone's live layer`);
|
|
192
|
+
}
|
|
193
|
+
// The clone's config becomes the record's, before anything else changes: it holds the link.
|
|
194
|
+
mkdirSync(s.record, { recursive: true });
|
|
195
|
+
copyFileSync(join(s.live, PERSONAL_CONFIG), join(s.record, "config.json"));
|
|
196
|
+
rmSync(join(s.live, PERSONAL_CONFIG));
|
|
197
|
+
uninstallPersonalHooks(s);
|
|
198
|
+
includeRecord(s);
|
|
199
|
+
s = open();
|
|
200
|
+
}
|
|
201
|
+
const r = setUpShared(s, o);
|
|
202
|
+
const rows = [...r.fields];
|
|
203
|
+
if (!o.hooks && switching)
|
|
204
|
+
rows.push({ label: "git gate", value: "none", why: "with --no-hooks, this clone's own hooks are gone and none were put in the repo's" });
|
|
205
|
+
// This branch's record, from the live layer: what personal mode kept there in its place.
|
|
206
|
+
const ids = cardsForBranch(s);
|
|
207
|
+
const added = s.syncRecord(ids);
|
|
208
|
+
if (added.length || switching) {
|
|
209
|
+
const record = { label: "record", value: `${shown(s, s.record)}/`, why: `${ids.length} card${ids.length === 1 ? "" : "s"} of this branch's, ${added.length} file${added.length === 1 ? "" : "s"} written` };
|
|
210
|
+
const at = rows.findIndex((f) => f.label === "record");
|
|
211
|
+
if (at >= 0)
|
|
212
|
+
rows[at] = record;
|
|
213
|
+
else
|
|
214
|
+
rows.push(record);
|
|
215
|
+
}
|
|
216
|
+
const notes = [...r.notes];
|
|
217
|
+
const others = s.cards().filter((c) => !ids.includes(c.meta.id));
|
|
218
|
+
if (others.length) {
|
|
219
|
+
const branches = [...new Set(others.map((c) => c.meta.source_branch))].sort();
|
|
220
|
+
notes.push(note(`${others.length === 1 ? "1 card" : `${others.length} cards`} filed on ${branches.length === 1 ? "another branch" : "other branches"} (${branches.join(", ")}) ${others.length === 1 ? "stays" : "stay"} in the live layer: run `, cmd("baesian init --shared"), ` on ${branches.length === 1 ? "that branch" : "each"} to write ${others.length === 1 ? "it" : "them"} into its record`));
|
|
221
|
+
}
|
|
222
|
+
const out = [
|
|
223
|
+
head("done", switching
|
|
224
|
+
? `switched this clone to shared mode${cite("B-4rwyz")}: its Baesian goes into the repo, for you to commit`
|
|
225
|
+
: r.fresh
|
|
226
|
+
? "set up Baesian in this repo, in shared mode"
|
|
227
|
+
: "Baesian is set up in this repo already, in shared mode"),
|
|
228
|
+
...fields([...rows, preferences(s)]),
|
|
229
|
+
...notes,
|
|
230
|
+
];
|
|
231
|
+
if (!s.inForce().name)
|
|
232
|
+
out.push(SERVE);
|
|
233
|
+
if (switching) {
|
|
234
|
+
const paths = [RECORD_DIR, ...r.paths.filter((p) => p !== RECORD_DIR)];
|
|
235
|
+
const ack = config.service && config.project ? linkAck(config.service, config.project) : undefined;
|
|
236
|
+
out.push(note(`nothing is committed yet. The commit gate ${ack ? `asks an ack for the link, infra:${CONFIG_PATH}${cite("B-w4g6c")}` : "sees no decision in it"}: Baesian's own block in the repo's hooks isn't one${cite("B-g6ywj")}.`));
|
|
237
|
+
out.push(next(`git add ${paths.join(" ")} && git commit -m "Use Baesian in this repo"${ack ? ` -m "${ack}"` : ""}`));
|
|
238
|
+
}
|
|
239
|
+
else if (added.length && !r.paths.includes(RECORD_DIR))
|
|
240
|
+
out.push(next(`git add ${RECORD_DIR} && git commit -m "This branch's decisions"`));
|
|
241
|
+
else if (r.paths.length)
|
|
242
|
+
out.push(next(`git add ${r.paths.join(" ")} && git commit -m "Use Baesian"`));
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=modes.js.map
|
package/dist/notify.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { UserConfig, describeError, upgradeFile } from "./schema.js";
|
|
6
|
+
/** `$BAESIAN_CONFIG`, else `$XDG_CONFIG_HOME/baesian/config.json`, else `~/.config/baesian/config.json`. */
|
|
7
|
+
export function userConfigPath() {
|
|
8
|
+
if (process.env.BAESIAN_CONFIG)
|
|
9
|
+
return process.env.BAESIAN_CONFIG;
|
|
10
|
+
const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
11
|
+
return join(base, "baesian", "config.json");
|
|
12
|
+
}
|
|
13
|
+
export function userConfig(path = userConfigPath()) {
|
|
14
|
+
if (!existsSync(path))
|
|
15
|
+
return {};
|
|
16
|
+
const result = UserConfig.safeParse(upgradeFile("user-config", JSON.parse(readFileSync(path, "utf8")), path));
|
|
17
|
+
if (!result.success)
|
|
18
|
+
throw new Error(`${path} is not valid:\n${describeError(result.error)}`);
|
|
19
|
+
return result.data;
|
|
20
|
+
}
|
|
21
|
+
export function payloadFor(card) {
|
|
22
|
+
const m = card.meta;
|
|
23
|
+
return { id: m.id, title: m.title, question: m.question, level: m.level, proposal: m.proposal, deck: m.deck, branch: m.source_branch };
|
|
24
|
+
}
|
|
25
|
+
/** Only cards that make an agent wait interrupt the reviewer (D40). */
|
|
26
|
+
export function shouldNotify(card) {
|
|
27
|
+
return card.meta.level === "blocking" || card.meta.level === "hard";
|
|
28
|
+
}
|
|
29
|
+
function osNotifyEnabled(config) {
|
|
30
|
+
if (process.env.BAESIAN_OS_NOTIFY === "0")
|
|
31
|
+
return false;
|
|
32
|
+
return config.os_notify ?? true;
|
|
33
|
+
}
|
|
34
|
+
/** Start a process and forget it. Notification failures must never fail the filing. */
|
|
35
|
+
function fire(command, args, input, env) {
|
|
36
|
+
try {
|
|
37
|
+
const child = spawn(command, args, { detached: true, stdio: [input ? "pipe" : "ignore", "ignore", "ignore"], env });
|
|
38
|
+
child.on("error", () => { });
|
|
39
|
+
if (input && child.stdin) {
|
|
40
|
+
child.stdin.on("error", () => { });
|
|
41
|
+
child.stdin.end(input);
|
|
42
|
+
}
|
|
43
|
+
child.unref();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// ignore
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function osNotification(p) {
|
|
50
|
+
const title = `Baesian: ${p.level} card ${p.id}`;
|
|
51
|
+
const message = p.question;
|
|
52
|
+
if (process.platform === "darwin") {
|
|
53
|
+
// Text goes in as arguments, never interpolated into the script.
|
|
54
|
+
fire("osascript", [
|
|
55
|
+
"-e",
|
|
56
|
+
"on run argv",
|
|
57
|
+
"-e",
|
|
58
|
+
"display notification (item 2 of argv) with title (item 1 of argv)",
|
|
59
|
+
"-e",
|
|
60
|
+
"end run",
|
|
61
|
+
title,
|
|
62
|
+
message,
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
else if (process.platform === "linux") {
|
|
66
|
+
// `--` first: a question an agent wrote that starts with "-" must not be read as an option.
|
|
67
|
+
fire("notify-send", ["--", title, message]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Tell the reviewer a blocking or hard card is waiting: an OS notification, and the user's `notify`
|
|
72
|
+
* command with the card as JSON on stdin. The link to the card comes with the client (M4).
|
|
73
|
+
*/
|
|
74
|
+
export function notifyFiled(card, config = userConfig()) {
|
|
75
|
+
if (!shouldNotify(card))
|
|
76
|
+
return { os: false, command: false };
|
|
77
|
+
const payload = payloadFor(card);
|
|
78
|
+
const os = osNotifyEnabled(config);
|
|
79
|
+
if (os)
|
|
80
|
+
osNotification(payload);
|
|
81
|
+
if (config.notify) {
|
|
82
|
+
fire("/bin/sh", ["-c", config.notify], `${JSON.stringify(payload)}\n`, {
|
|
83
|
+
...process.env,
|
|
84
|
+
BAESIAN_CARD_ID: payload.id,
|
|
85
|
+
BAESIAN_CARD_LEVEL: payload.level,
|
|
86
|
+
BAESIAN_CARD_TITLE: payload.title,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return { os, command: Boolean(config.notify) };
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=notify.js.map
|