@forwardimpact/libwiki 0.2.11 → 0.2.13

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/src/boot.js CHANGED
@@ -1,17 +1,15 @@
1
- import { readFileSync, existsSync } from "node:fs";
2
1
  import path from "node:path";
2
+ import { yearMonth } from "@forwardimpact/libutil";
3
3
  import { parseClaims, filterExpired } from "./active-claims.js";
4
4
  import { MEMO_INBOX_MARKER, PRIORITY_INDEX_HEADING } from "./constants.js";
5
5
 
6
- function readIfExists(filePath) {
7
- if (!existsSync(filePath)) return null;
8
- return readFileSync(filePath, "utf-8");
6
+ function readIfExists(fs, filePath) {
7
+ if (!fs.existsSync(filePath)) return null;
8
+ return fs.readFileSync(filePath, "utf-8");
9
9
  }
10
10
 
11
- function currentStoryboardPath(wikiRoot, date) {
12
- const yyyy = date.getUTCFullYear();
13
- const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
14
- return path.join(wikiRoot, `storyboard-${yyyy}-M${mm}.md`);
11
+ function currentStoryboardPath(wikiRoot, today) {
12
+ return path.join(wikiRoot, `storyboard-${yearMonth(today)}.md`);
15
13
  }
16
14
 
17
15
  function extractSummary(text) {
@@ -146,20 +144,22 @@ function mapClaim(c) {
146
144
  };
147
145
  }
148
146
 
149
- /** Build the boot digest JSON object. */
150
- export function buildDigest({ wikiRoot, agent, today, _fs, _gh }) {
151
- const date = today instanceof Date ? today : new Date(today);
152
- const todayStr = date.toISOString().slice(0, 10);
153
-
147
+ /**
148
+ * Build the boot digest JSON object.
149
+ * @param {{wikiRoot: string, agent: string, today: string, fs: object}} options
150
+ * `fs` is the sync filesystem surface (`runtime.fsSync`); `today` is an ISO
151
+ * date string.
152
+ */
153
+ export function buildDigest({ wikiRoot, agent, today, fs }) {
154
154
  const summaryPath = path.join(wikiRoot, `${agent}.md`);
155
155
  const memoryPath = path.join(wikiRoot, "MEMORY.md");
156
- const storyboardPath = currentStoryboardPath(wikiRoot, date);
156
+ const storyboardPath = currentStoryboardPath(wikiRoot, today);
157
157
 
158
- const summaryText = readIfExists(summaryPath);
159
- const memoryText = readIfExists(memoryPath);
160
- const storyboardText = readIfExists(storyboardPath);
158
+ const summaryText = readIfExists(fs, summaryPath);
159
+ const memoryText = readIfExists(fs, memoryPath);
160
+ const storyboardText = readIfExists(fs, storyboardPath);
161
161
 
162
- const { active } = filterExpired(parseClaims(memoryText ?? ""), todayStr);
162
+ const { active } = filterExpired(parseClaims(memoryText ?? ""), today);
163
163
  const { owned, cross } = splitPriorities(
164
164
  parsePriorityTable(memoryText ?? ""),
165
165
  agent,
@@ -172,7 +172,7 @@ export function buildDigest({ wikiRoot, agent, today, _fs, _gh }) {
172
172
  claims: active.map(mapClaim),
173
173
  storyboard_items: parseStoryboardItems(storyboardText ?? "", agent),
174
174
  inbox_count: countInbox(summaryText),
175
- storyboard_path: existsSync(storyboardPath)
175
+ storyboard_path: fs.existsSync(storyboardPath)
176
176
  ? path.relative(path.dirname(wikiRoot) || ".", storyboardPath)
177
177
  : "",
178
178
  };
@@ -0,0 +1,284 @@
1
+ import { runMemoCommand } from "./commands/memo.js";
2
+ import { runRefreshCommand } from "./commands/refresh.js";
3
+ import { runInitCommand } from "./commands/init.js";
4
+ import { runPushCommand, runPullCommand } from "./commands/sync.js";
5
+ import { runBootCommand } from "./commands/boot.js";
6
+ import { runLogCommand } from "./commands/log.js";
7
+ import { runClaimCommand, runReleaseCommand } from "./commands/claim.js";
8
+ import { runInboxCommand } from "./commands/inbox.js";
9
+ import { runRotateCommand } from "./commands/rotate.js";
10
+ import { runAuditCommand } from "./commands/audit.js";
11
+ import { runFixCommand } from "./commands/fix.js";
12
+
13
+ /**
14
+ * Build the `fit-wiki` libcli definition. The agent/sender defaults read the
15
+ * injected `env` rather than the ambient `process.env` so this module carries
16
+ * no ambient dependency; the bin shim passes `runtime.proc.env` and the
17
+ * package version. Each subcommand carries a `handler` and (for subcommand-
18
+ * bearing commands) `args`/`argsUsage` so `cli.dispatch` can route to the
19
+ * per-command handler with a frozen `ctx`.
20
+ *
21
+ * @param {Record<string, string>} env - The process env (`runtime.proc.env`).
22
+ * @param {string} version - The package version string.
23
+ * @returns {object} The libcli definition.
24
+ */
25
+ export function createDefinition(env, version) {
26
+ const wikiRootOpt = {
27
+ "wiki-root": {
28
+ type: "string",
29
+ description: "Override wiki root directory (default: wiki)",
30
+ },
31
+ };
32
+
33
+ const agentOpt = {
34
+ agent: {
35
+ type: "string",
36
+ description:
37
+ "Agent name (falls back to LIBEVAL_AGENT_PROFILE, then staff-engineer)",
38
+ default: env.LIBEVAL_AGENT_PROFILE || "staff-engineer",
39
+ },
40
+ };
41
+
42
+ const todayOpt = {
43
+ today: {
44
+ type: "string",
45
+ description: "Override today's ISO date (testing)",
46
+ },
47
+ };
48
+
49
+ return {
50
+ name: "fit-wiki",
51
+ version,
52
+ description: "Wiki lifecycle management for the Kata agent system",
53
+ commands: [
54
+ {
55
+ name: "boot",
56
+ description:
57
+ "Print on-boot digest (priorities, claims, storyboard items) as JSON",
58
+ handler: runBootCommand,
59
+ options: {
60
+ ...agentOpt,
61
+ ...wikiRootOpt,
62
+ ...todayOpt,
63
+ format: {
64
+ type: "string",
65
+ description: "Output format: json (default) or markdown",
66
+ },
67
+ },
68
+ },
69
+ {
70
+ name: "log",
71
+ description:
72
+ "Append a decision/note/done entry to the current weekly log",
73
+ args: ["subcommand"],
74
+ argsUsage: "[subcommand]",
75
+ handler: runLogCommand,
76
+ options: {
77
+ ...agentOpt,
78
+ ...wikiRootOpt,
79
+ ...todayOpt,
80
+ surveyed: {
81
+ type: "string",
82
+ description: "Decision: routing levels surveyed",
83
+ },
84
+ chosen: { type: "string", description: "Decision: chosen action" },
85
+ rationale: { type: "string", description: "Decision: rationale" },
86
+ alternatives: {
87
+ type: "string",
88
+ description: "Decision: alternatives",
89
+ },
90
+ field: { type: "string", description: "Note: field heading" },
91
+ body: { type: "string", description: "Note: field body" },
92
+ },
93
+ },
94
+ {
95
+ name: "claim",
96
+ description:
97
+ "Claim a target in MEMORY.md ## Active Claims (refuses duplicates)",
98
+ handler: runClaimCommand,
99
+ options: {
100
+ ...agentOpt,
101
+ ...wikiRootOpt,
102
+ ...todayOpt,
103
+ target: {
104
+ type: "string",
105
+ description: "What is being claimed (spec id, PR id, etc.)",
106
+ },
107
+ branch: { type: "string", description: "Branch carrying the work" },
108
+ pr: { type: "string", description: "Optional PR id" },
109
+ "expires-at": {
110
+ type: "string",
111
+ description: "Override expiry ISO date (default claim+7d)",
112
+ },
113
+ },
114
+ },
115
+ {
116
+ name: "release",
117
+ description: "Release a claim (or all expired claims with --expired)",
118
+ handler: runReleaseCommand,
119
+ options: {
120
+ ...agentOpt,
121
+ ...wikiRootOpt,
122
+ ...todayOpt,
123
+ target: { type: "string", description: "Target to release" },
124
+ expired: {
125
+ type: "boolean",
126
+ description: "Release every row past expires_at",
127
+ },
128
+ },
129
+ },
130
+ {
131
+ name: "inbox",
132
+ description: "Triage the agent's Message Inbox (list/ack/promote/drop)",
133
+ args: ["subcommand"],
134
+ argsUsage: "[subcommand]",
135
+ handler: runInboxCommand,
136
+ options: {
137
+ ...agentOpt,
138
+ ...wikiRootOpt,
139
+ ...todayOpt,
140
+ index: {
141
+ type: "string",
142
+ description: "Bullet index (0-based) for ack/promote/drop",
143
+ },
144
+ owner: {
145
+ type: "string",
146
+ description: "Owner field when promoting (default: --agent)",
147
+ },
148
+ },
149
+ },
150
+ {
151
+ name: "rotate",
152
+ description: "Force-rotate the current weekly log to a sealed part",
153
+ handler: runRotateCommand,
154
+ options: {
155
+ ...agentOpt,
156
+ ...wikiRootOpt,
157
+ ...todayOpt,
158
+ },
159
+ },
160
+ {
161
+ name: "audit",
162
+ description:
163
+ "Audit the wiki against the declarative rule catalogue (line and word budgets, headings, decision blocks, storyboards, claims)",
164
+ handler: runAuditCommand,
165
+ options: {
166
+ ...wikiRootOpt,
167
+ ...todayOpt,
168
+ format: {
169
+ type: "string",
170
+ description: "Output format: text (default) or json",
171
+ },
172
+ },
173
+ },
174
+ {
175
+ name: "fix",
176
+ description:
177
+ "Auto-fix wiki audit findings using an AI agent (technical-writer, Haiku)",
178
+ handler: runFixCommand,
179
+ options: {
180
+ ...wikiRootOpt,
181
+ ...todayOpt,
182
+ },
183
+ },
184
+ {
185
+ name: "memo",
186
+ description: "Send a cross-team memo into a teammate's Message Inbox",
187
+ handler: runMemoCommand,
188
+ options: {
189
+ from: {
190
+ type: "string",
191
+ description:
192
+ "Sender agent name (falls back to LIBEVAL_AGENT_PROFILE env var)",
193
+ default: env.LIBEVAL_AGENT_PROFILE,
194
+ },
195
+ to: {
196
+ type: "string",
197
+ description:
198
+ 'Target agent name, or "all" to broadcast (sender is skipped)',
199
+ },
200
+ message: {
201
+ type: "string",
202
+ description: "Memo text",
203
+ },
204
+ ...wikiRootOpt,
205
+ },
206
+ },
207
+ {
208
+ name: "refresh",
209
+ description:
210
+ "Regenerate XmR and obstacle/experiment marker blocks in a storyboard",
211
+ args: ["storyboard-path"],
212
+ argsUsage: "[storyboard-path]",
213
+ handler: runRefreshCommand,
214
+ options: {
215
+ format: {
216
+ type: "string",
217
+ description: "Output format: (default off) or json",
218
+ },
219
+ },
220
+ },
221
+ {
222
+ name: "init",
223
+ description: "Bootstrap a wiki working tree and scaffold Active Claims",
224
+ handler: runInitCommand,
225
+ options: {
226
+ ...wikiRootOpt,
227
+ "skills-dir": {
228
+ type: "string",
229
+ description: "Override skills directory (default: .claude/skills)",
230
+ },
231
+ },
232
+ },
233
+ {
234
+ name: "push",
235
+ description: "Commit and push local wiki changes to the remote",
236
+ handler: runPushCommand,
237
+ options: { ...wikiRootOpt },
238
+ },
239
+ {
240
+ name: "pull",
241
+ description: "Pull remote wiki changes into the local working tree",
242
+ handler: runPullCommand,
243
+ options: { ...wikiRootOpt },
244
+ },
245
+ ],
246
+ globalOptions: {
247
+ help: { type: "boolean", short: "h", description: "Show this help" },
248
+ version: { type: "boolean", description: "Show version" },
249
+ json: {
250
+ type: "boolean",
251
+ description: "Render --help output as JSON",
252
+ },
253
+ },
254
+ examples: [
255
+ "fit-wiki boot --agent staff-engineer",
256
+ 'fit-wiki log decision --agent staff-engineer --surveyed "..." --chosen "..." --rationale "..."',
257
+ "fit-wiki claim --agent staff-engineer --target spec-NNNN --branch claude/...",
258
+ "fit-wiki release --agent staff-engineer --target spec-NNNN",
259
+ "fit-wiki inbox list --agent staff-engineer",
260
+ "fit-wiki rotate --agent staff-engineer",
261
+ "fit-wiki audit",
262
+ "fit-wiki fix",
263
+ 'fit-wiki memo --from staff-engineer --to security-engineer --message "audit d642ff0c"',
264
+ "fit-wiki refresh",
265
+ "fit-wiki init",
266
+ "fit-wiki push",
267
+ "fit-wiki pull",
268
+ ],
269
+ documentation: [
270
+ {
271
+ title: "Operate a Predictable Agent Team",
272
+ url: "https://www.forwardimpact.team/docs/libraries/predictable-team/index.md",
273
+ description:
274
+ "End-to-end guide to wiki memory, XmR charts, and team coordination.",
275
+ },
276
+ {
277
+ title: "Send a Memo or Update a Storyboard",
278
+ url: "https://www.forwardimpact.team/docs/libraries/predictable-team/wiki-operations/index.md",
279
+ description:
280
+ "Send cross-team memos, refresh storyboard charts, and sync the wiki.",
281
+ },
282
+ ],
283
+ };
284
+ }
@@ -1,26 +1,27 @@
1
- import fsAsync from "node:fs/promises";
2
1
  import path from "node:path";
3
2
  import {
4
- Finder,
5
3
  emitFindingsJson,
6
4
  emitFindingsText,
7
5
  runRules,
8
6
  } from "@forwardimpact/libutil";
9
7
  import { RULES } from "../audit/rules.js";
10
8
  import { buildContext, resolveScope } from "../audit/scopes.js";
9
+ import { currentDayIso } from "../util/clock.js";
10
+ import { resolveProjectRoot } from "../util/wiki-dir.js";
11
11
 
12
12
  /** Run the wiki audit and emit findings. JSON via --format json. */
13
- export function runAuditCommand(values, _args, _cli) {
14
- const finder = new Finder(fsAsync, { debug() {} }, process);
15
- const projectRoot = finder.findProjectRoot(process.cwd());
16
- const wikiRoot = values["wiki-root"] || path.join(projectRoot, "wiki");
17
- const today = values.today || new Date().toISOString().slice(0, 10);
13
+ export function runAuditCommand(ctx) {
14
+ const { runtime } = ctx.deps;
15
+ const options = ctx.options;
16
+ const projectRoot = resolveProjectRoot(runtime);
17
+ const wikiRoot = options["wiki-root"] || path.join(projectRoot, "wiki");
18
+ const today = options.today || currentDayIso(runtime);
18
19
 
19
- const ctx = buildContext({ wikiRoot, today });
20
- const findings = runRules(RULES, ctx, { resolveScope });
20
+ const auditCtx = buildContext({ wikiRoot, today, fs: runtime.fsSync });
21
+ const findings = runRules(RULES, auditCtx, { resolveScope });
21
22
 
22
- process.stdout.write(
23
- values.format === "json"
23
+ runtime.proc.stdout.write(
24
+ options.format === "json"
24
25
  ? emitFindingsJson(findings)
25
26
  : emitFindingsText(findings, {
26
27
  cwd: projectRoot,
@@ -28,5 +29,6 @@ export function runAuditCommand(values, _args, _cli) {
28
29
  }),
29
30
  );
30
31
 
31
- if (findings.some((f) => f.level === "fail")) process.exit(1);
32
+ if (findings.some((f) => f.level === "fail")) return { ok: false, code: 1 };
33
+ return { ok: true };
32
34
  }
@@ -1,7 +1,6 @@
1
- import fsAsync from "node:fs/promises";
2
- import path from "node:path";
3
- import { Finder } from "@forwardimpact/libutil";
4
1
  import { buildDigest } from "../boot.js";
2
+ import { currentDayIso } from "../util/clock.js";
3
+ import { resolveWikiRoot } from "../util/wiki-dir.js";
5
4
 
6
5
  function renderMarkdown(digest) {
7
6
  const lines = [];
@@ -41,21 +40,21 @@ function renderMarkdown(digest) {
41
40
  }
42
41
 
43
42
  /** Print the on-boot digest for the calling agent. JSON by default; --format markdown renders prose. */
44
- export function runBootCommand(values, _args, cli) {
43
+ export function runBootCommand(ctx) {
44
+ const { runtime } = ctx.deps;
45
+ const options = ctx.options;
45
46
  const agent =
46
- values.agent || process.env.LIBEVAL_AGENT_PROFILE || "staff-engineer";
47
+ options.agent || runtime.proc.env.LIBEVAL_AGENT_PROFILE || "staff-engineer";
47
48
 
48
- const logger = { debug() {} };
49
- const finder = new Finder(fsAsync, logger, process);
50
- const projectRoot = finder.findProjectRoot(process.cwd());
51
- const wikiRoot = values["wiki-root"] || path.join(projectRoot, "wiki");
52
- const today = values.today || new Date().toISOString().slice(0, 10);
49
+ const wikiRoot = resolveWikiRoot(runtime, options);
50
+ const today = options.today || currentDayIso(runtime);
53
51
 
54
- const digest = buildDigest({ wikiRoot, agent, today });
52
+ const digest = buildDigest({ wikiRoot, agent, today, fs: runtime.fsSync });
55
53
 
56
- if ((values.format || "json") === "markdown") {
57
- process.stdout.write(renderMarkdown(digest) + "\n");
54
+ if ((options.format || "json") === "markdown") {
55
+ runtime.proc.stdout.write(renderMarkdown(digest) + "\n");
58
56
  } else {
59
- process.stdout.write(JSON.stringify(digest, null, 2) + "\n");
57
+ runtime.proc.stdout.write(JSON.stringify(digest, null, 2) + "\n");
60
58
  }
59
+ return { ok: true };
61
60
  }
@@ -1,102 +1,87 @@
1
- import { readFileSync, writeFileSync, existsSync } from "node:fs";
2
- import fsAsync from "node:fs/promises";
3
1
  import path from "node:path";
4
- import { Finder } from "@forwardimpact/libutil";
2
+ import { addDays } from "@forwardimpact/libutil";
5
3
  import {
6
4
  appendClaim,
7
5
  removeClaim,
8
6
  parseClaims,
9
7
  filterExpired,
10
8
  } from "../active-claims.js";
11
- import { createDefaultIo } from "../io.js";
12
- import { buildRepo } from "../build-repo.js";
9
+ import { currentDayIso } from "../util/clock.js";
10
+ import { resolveWikiRoot } from "../util/wiki-dir.js";
13
11
 
14
- function projectRoot(io) {
15
- const logger = { debug() {} };
16
- const finder = new Finder(fsAsync, logger, { cwd: io.cwd });
17
- return finder.findProjectRoot(io.cwd());
12
+ function readMemory(runtime, memPath) {
13
+ if (!runtime.fsSync.existsSync(memPath)) return "";
14
+ return runtime.fsSync.readFileSync(memPath, "utf-8");
18
15
  }
19
16
 
20
- function memoryPath(values, io) {
21
- const root = projectRoot(io);
22
- const wikiRoot = values["wiki-root"] || path.join(root, "wiki");
23
- return path.join(wikiRoot, "MEMORY.md");
17
+ function memoryPath(runtime, options) {
18
+ return path.join(resolveWikiRoot(runtime, options), "MEMORY.md");
24
19
  }
25
20
 
26
- function readMemory(memPath) {
27
- if (!existsSync(memPath)) return "";
28
- return readFileSync(memPath, "utf-8");
29
- }
30
-
31
- function addDays(today, n) {
32
- const d = new Date(today);
33
- d.setUTCDate(d.getUTCDate() + n);
34
- return d.toISOString().slice(0, 10);
35
- }
36
-
37
- async function pushWiki(repoFactory, values, io, message) {
38
- if (!repoFactory) return;
21
+ async function pushWiki(wikiSync, runtime, message) {
22
+ if (!wikiSync) return;
39
23
  try {
40
- const repo = await repoFactory(values, io.cwd());
41
- repo.inheritIdentity();
42
- const result = repo.commitAndPush(message);
43
- if (result.pushed) io.stdout("push: committed and pushed\n");
24
+ await wikiSync.inheritIdentity();
25
+ const result = await wikiSync.commitAndPush(message);
26
+ if (result.pushed)
27
+ runtime.proc.stdout.write("push: committed and pushed\n");
44
28
  } catch (err) {
45
- io.stderr(`push failed (saved locally): ${err.message}\n`);
29
+ runtime.proc.stderr.write(`push failed (saved locally): ${err.message}\n`);
46
30
  }
47
31
  }
48
32
 
49
33
  /** Insert a row into MEMORY.md `## Active Claims`. Refuses if (agent, target) already present. */
50
- export async function runClaimCommand(
51
- values,
52
- _args,
53
- cli,
54
- io = createDefaultIo(),
55
- repoFactory = buildRepo,
56
- ) {
57
- const agent = values.agent || io.env.LIBEVAL_AGENT_PROFILE;
34
+ export async function runClaimCommand(ctx) {
35
+ const { runtime, wikiSync } = ctx.deps;
36
+ const options = ctx.options;
37
+ const agent = options.agent || runtime.proc.env.LIBEVAL_AGENT_PROFILE;
58
38
  if (!agent) {
59
- cli.usageError("claim requires --agent or LIBEVAL_AGENT_PROFILE");
60
- return io.exit(2);
39
+ return {
40
+ ok: false,
41
+ code: 2,
42
+ error: "claim requires --agent or LIBEVAL_AGENT_PROFILE",
43
+ };
61
44
  }
62
- if (!values.target || !values.branch) {
63
- cli.usageError("claim requires --target and --branch");
64
- return io.exit(2);
45
+ if (!options.target || !options.branch) {
46
+ return {
47
+ ok: false,
48
+ code: 2,
49
+ error: "claim requires --target and --branch",
50
+ };
65
51
  }
66
- const today = values.today || io.today();
67
- const expires = values["expires-at"] || addDays(today, 7);
68
- const memPath = memoryPath(values, io);
69
- const text = readMemory(memPath);
52
+ const today = options.today || currentDayIso(runtime);
53
+ const expires = options["expires-at"] || addDays(today, 7);
54
+ const memPath = memoryPath(runtime, options);
55
+ const text = readMemory(runtime, memPath);
70
56
  const result = appendClaim(text, {
71
57
  agent,
72
- target: values.target,
73
- branch: values.branch,
74
- pr: values.pr || null,
58
+ target: options.target,
59
+ branch: options.branch,
60
+ pr: options.pr || null,
75
61
  claimed_at: today,
76
62
  expires_at: expires,
77
63
  });
78
64
  if (!result.inserted) {
79
- io.stderr(`claim already exists for ${agent}/${values.target}\n`);
80
- return io.exit(2);
65
+ runtime.proc.stderr.write(
66
+ `claim already exists for ${agent}/${options.target}\n`,
67
+ );
68
+ return { ok: false, code: 2 };
81
69
  }
82
- writeFileSync(memPath, result.text);
83
- io.stdout(`claimed ${values.target} (expires ${expires})\n`);
84
- await pushWiki(repoFactory, values, io, `wiki: claim ${values.target}`);
70
+ runtime.fsSync.writeFileSync(memPath, result.text);
71
+ runtime.proc.stdout.write(`claimed ${options.target} (expires ${expires})\n`);
72
+ await pushWiki(wikiSync, runtime, `wiki: claim ${options.target}`);
73
+ return { ok: true };
85
74
  }
86
75
 
87
76
  /** Remove a claim row. `--expired` cleans every row past expires_at. */
88
- export async function runReleaseCommand(
89
- values,
90
- _args,
91
- cli,
92
- io = createDefaultIo(),
93
- repoFactory = buildRepo,
94
- ) {
95
- const memPath = memoryPath(values, io);
96
- const text = readMemory(memPath);
77
+ export async function runReleaseCommand(ctx) {
78
+ const { runtime, wikiSync } = ctx.deps;
79
+ const options = ctx.options;
80
+ const memPath = memoryPath(runtime, options);
81
+ const text = readMemory(runtime, memPath);
97
82
 
98
- if (values.expired) {
99
- const today = values.today || io.today();
83
+ if (options.expired) {
84
+ const today = options.today || currentDayIso(runtime);
100
85
  const claims = parseClaims(text);
101
86
  const { expired } = filterExpired(claims, today);
102
87
  let current = text;
@@ -108,27 +93,36 @@ export async function runReleaseCommand(
108
93
  count++;
109
94
  }
110
95
  }
111
- writeFileSync(memPath, current);
112
- io.stdout(`released ${count} expired claim(s)\n`);
113
- await pushWiki(repoFactory, values, io, "wiki: release expired claims");
114
- return;
96
+ runtime.fsSync.writeFileSync(memPath, current);
97
+ runtime.proc.stdout.write(`released ${count} expired claim(s)\n`);
98
+ await pushWiki(wikiSync, runtime, "wiki: release expired claims");
99
+ return { ok: true };
115
100
  }
116
101
 
117
- const agent = values.agent || io.env.LIBEVAL_AGENT_PROFILE;
102
+ const agent = options.agent || runtime.proc.env.LIBEVAL_AGENT_PROFILE;
118
103
  if (!agent) {
119
- cli.usageError("release requires --agent or --expired");
120
- return io.exit(2);
104
+ return {
105
+ ok: false,
106
+ code: 2,
107
+ error: "release requires --agent or --expired",
108
+ };
121
109
  }
122
- if (!values.target) {
123
- cli.usageError("release requires --target (or --expired)");
124
- return io.exit(2);
110
+ if (!options.target) {
111
+ return {
112
+ ok: false,
113
+ code: 2,
114
+ error: "release requires --target (or --expired)",
115
+ };
125
116
  }
126
- const result = removeClaim(text, { agent, target: values.target });
127
- writeFileSync(memPath, result.text);
117
+ const result = removeClaim(text, { agent, target: options.target });
118
+ runtime.fsSync.writeFileSync(memPath, result.text);
128
119
  if (!result.removed) {
129
- io.stdout(`no matching claim for ${agent}/${values.target}\n`);
120
+ runtime.proc.stdout.write(
121
+ `no matching claim for ${agent}/${options.target}\n`,
122
+ );
130
123
  } else {
131
- io.stdout(`released ${values.target}\n`);
132
- await pushWiki(repoFactory, values, io, `wiki: release ${values.target}`);
124
+ runtime.proc.stdout.write(`released ${options.target}\n`);
125
+ await pushWiki(wikiSync, runtime, `wiki: release ${options.target}`);
133
126
  }
127
+ return { ok: true };
134
128
  }