@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.
@@ -1,31 +1,22 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import fsAsync from "node:fs/promises";
3
- import path from "node:path";
4
- import { Finder } from "@forwardimpact/libutil";
5
1
  import {
6
2
  weeklyLogPath,
7
3
  rotateIfOverBudget,
8
4
  appendEntry,
9
5
  } from "../weekly-log.js";
10
6
  import { DECISION_HEADING } from "../constants.js";
11
- import { createDefaultIo } from "../io.js";
7
+ import { currentDayIso } from "../util/clock.js";
8
+ import { resolveWikiRoot } from "../util/wiki-dir.js";
12
9
 
13
- function projectRootForCommand(io) {
14
- const logger = { debug() {} };
15
- const finder = new Finder(fsAsync, logger, { cwd: io.cwd });
16
- return finder.findProjectRoot(io.cwd());
17
- }
18
-
19
- function commonContext(values, io) {
20
- const agent = values.agent || io.env.LIBEVAL_AGENT_PROFILE;
10
+ function commonContext(runtime, options) {
11
+ const agent = options.agent || runtime.proc.env.LIBEVAL_AGENT_PROFILE;
21
12
  if (!agent) {
22
- io.stderr("log requires --agent <name> or LIBEVAL_AGENT_PROFILE env var\n");
23
- io.exit(2);
24
- return null;
13
+ runtime.proc.stderr.write(
14
+ "log requires --agent <name> or LIBEVAL_AGENT_PROFILE env var\n",
15
+ );
16
+ return { error: { ok: false, code: 2 } };
25
17
  }
26
- const projectRoot = projectRootForCommand(io);
27
- const wikiRoot = values["wiki-root"] || path.join(projectRoot, "wiki");
28
- const today = values.today || io.today();
18
+ const wikiRoot = resolveWikiRoot(runtime, options);
19
+ const today = options.today || currentDayIso(runtime);
29
20
  return { agent, wikiRoot, today };
30
21
  }
31
22
 
@@ -39,14 +30,14 @@ function lastDateHeading(text) {
39
30
  return last;
40
31
  }
41
32
 
42
- function runDecision(values, io) {
43
- const ctx = commonContext(values, io);
44
- if (!ctx) return;
33
+ function runDecision(runtime, options) {
34
+ const ctx = commonContext(runtime, options);
35
+ if (ctx.error) return ctx.error;
45
36
  const { agent, wikiRoot, today } = ctx;
46
- const surveyed = values.surveyed || "—";
47
- const chosen = values.chosen || "—";
48
- const rationale = values.rationale || "—";
49
- const alternatives = values.alternatives || "—";
37
+ const surveyed = options.surveyed || "—";
38
+ const chosen = options.chosen || "—";
39
+ const rationale = options.rationale || "—";
40
+ const alternatives = options.alternatives || "—";
50
41
  const body = [
51
42
  `## ${today}`,
52
43
  "",
@@ -62,55 +53,70 @@ function runDecision(values, io) {
62
53
  "",
63
54
  ].join("\n");
64
55
  const lineCount = body.split("\n").length;
65
- rotateIfOverBudget(wikiRoot, agent, today, lineCount);
56
+ rotateIfOverBudget(wikiRoot, agent, today, lineCount, {}, runtime.fsSync);
66
57
  const target = weeklyLogPath(wikiRoot, agent, today);
67
- appendEntry(target, body, agent, today);
68
- io.stdout(`logged decision to ${target}\n`);
58
+ appendEntry(target, body, agent, today, runtime.fsSync);
59
+ runtime.proc.stdout.write(`logged decision to ${target}\n`);
60
+ return { ok: true };
69
61
  }
70
62
 
71
- function runNote(values, io) {
72
- const ctx = commonContext(values, io);
73
- if (!ctx) return;
63
+ function runNote(runtime, options) {
64
+ const ctx = commonContext(runtime, options);
65
+ if (ctx.error) return ctx.error;
74
66
  const { agent, wikiRoot, today } = ctx;
75
- if (!values.field || !values.body) {
76
- io.stderr("log note requires --field and --body\n");
77
- io.exit(2);
78
- return;
67
+ if (!options.field || !options.body) {
68
+ runtime.proc.stderr.write("log note requires --field and --body\n");
69
+ return { ok: false, code: 2 };
79
70
  }
80
- const fieldBlock = `### ${values.field}\n\n${values.body}\n`;
71
+ const fieldBlock = `### ${options.field}\n\n${options.body}\n`;
81
72
  // Conservative line budget: assume we'll prepend a date heading.
82
73
  const withHeading = `## ${today}\n\n${fieldBlock}`;
83
- rotateIfOverBudget(wikiRoot, agent, today, withHeading.split("\n").length);
74
+ rotateIfOverBudget(
75
+ wikiRoot,
76
+ agent,
77
+ today,
78
+ withHeading.split("\n").length,
79
+ {},
80
+ runtime.fsSync,
81
+ );
84
82
  const target = weeklyLogPath(wikiRoot, agent, today);
85
83
  // Append under the open entry if the file's last `## YYYY-MM-DD` is today;
86
84
  // otherwise open a new entry by prepending a date heading.
87
- const existing = existsSync(target) ? readFileSync(target, "utf-8") : "";
85
+ const existing = runtime.fsSync.existsSync(target)
86
+ ? runtime.fsSync.readFileSync(target, "utf-8")
87
+ : "";
88
88
  const body = lastDateHeading(existing) === today ? fieldBlock : withHeading;
89
- appendEntry(target, body, agent, today);
90
- io.stdout(`logged note to ${target}\n`);
89
+ appendEntry(target, body, agent, today, runtime.fsSync);
90
+ runtime.proc.stdout.write(`logged note to ${target}\n`);
91
+ return { ok: true };
91
92
  }
92
93
 
93
- function runDone(values, io) {
94
- const ctx = commonContext(values, io);
95
- if (!ctx) return;
94
+ function runDone(runtime, options) {
95
+ const ctx = commonContext(runtime, options);
96
+ if (ctx.error) return ctx.error;
96
97
  const { agent, wikiRoot, today } = ctx;
97
98
  const body = `### Closed\n\nRun closed ${today}.\n`;
98
99
  const lineCount = body.split("\n").length;
99
- rotateIfOverBudget(wikiRoot, agent, today, lineCount);
100
+ rotateIfOverBudget(wikiRoot, agent, today, lineCount, {}, runtime.fsSync);
100
101
  const target = weeklyLogPath(wikiRoot, agent, today);
101
- appendEntry(target, body, agent, today);
102
- io.stdout(`closed entry in ${target}\n`);
102
+ appendEntry(target, body, agent, today, runtime.fsSync);
103
+ runtime.proc.stdout.write(`closed entry in ${target}\n`);
104
+ return { ok: true };
103
105
  }
104
106
 
105
107
  const SUBS = { decision: runDecision, note: runNote, done: runDone };
106
108
 
107
109
  /** Dispatch `log {decision|note|done}` to the matching sub-handler. */
108
- export function runLogCommand(values, args, cli, io = createDefaultIo()) {
109
- const sub = args[0];
110
+ export function runLogCommand(ctx) {
111
+ const { runtime } = ctx.deps;
112
+ const sub = ctx.args.subcommand;
110
113
  const handler = SUBS[sub];
111
114
  if (!handler) {
112
- cli.usageError("log requires subcommand: decision | note | done");
113
- return io.exit(2);
115
+ return {
116
+ ok: false,
117
+ code: 2,
118
+ error: "log requires subcommand: decision | note | done",
119
+ };
114
120
  }
115
- handler(values, io);
121
+ return handler(runtime, ctx.options);
116
122
  }
@@ -1,18 +1,23 @@
1
- import { existsSync } from "node:fs";
2
- import fsAsync from "node:fs/promises";
3
1
  import path from "node:path";
4
- import { Finder } from "@forwardimpact/libutil";
5
2
  import { writeMemo } from "../memo-writer.js";
6
3
  import { listAgents } from "../agent-roster.js";
7
4
  import { BROADCAST_TARGET } from "../constants.js";
5
+ import { currentDayIso } from "../util/clock.js";
6
+ import { resolveProjectRoot } from "../util/wiki-dir.js";
8
7
 
9
- function writeAndCheck(summaryPath, sender, message, today) {
10
- const result = writeMemo({ summaryPath, sender, message, today });
8
+ function writeAndCheck(runtime, summaryPath, sender, message, today) {
9
+ const result = writeMemo(
10
+ { summaryPath, sender, message, today },
11
+ runtime.fsSync,
12
+ );
11
13
  if (!result.written) {
12
- process.stderr.write(`summary lacks memo:inbox marker: ${result.path}\n`);
13
- process.exit(2);
14
+ runtime.proc.stderr.write(
15
+ `summary lacks memo:inbox marker: ${result.path}\n`,
16
+ );
17
+ return { ok: false, code: 2 };
14
18
  }
15
- process.stdout.write(`wrote ${result.path}\n`);
19
+ runtime.proc.stdout.write(`wrote ${result.path}\n`);
20
+ return { ok: true };
16
21
  }
17
22
 
18
23
  function resolveTargetPath(wikiRoot, target) {
@@ -25,72 +30,76 @@ function resolveTargetPath(wikiRoot, target) {
25
30
  return { summaryPath, escapesRoot };
26
31
  }
27
32
 
28
- function writeSingleTarget({ wikiRoot, target, sender, message, today, cli }) {
33
+ function writeSingleTarget(
34
+ runtime,
35
+ { wikiRoot, target, sender, message, today },
36
+ ) {
29
37
  const { summaryPath, escapesRoot } = resolveTargetPath(wikiRoot, target);
30
38
  if (escapesRoot) {
31
- cli.usageError(`target escapes wiki root: ${target}`);
32
- process.exit(2);
39
+ return { ok: false, code: 2, error: `target escapes wiki root: ${target}` };
33
40
  }
34
- if (!existsSync(summaryPath)) {
35
- cli.usageError(`target summary not found: ${summaryPath}`);
36
- process.exit(2);
41
+ if (!runtime.fsSync.existsSync(summaryPath)) {
42
+ return {
43
+ ok: false,
44
+ code: 2,
45
+ error: `target summary not found: ${summaryPath}`,
46
+ };
37
47
  }
38
- writeAndCheck(summaryPath, sender, message, today);
48
+ return writeAndCheck(runtime, summaryPath, sender, message, today);
39
49
  }
40
50
 
41
- function writeBroadcast({ agentsDir, wikiRoot, sender, message, today }) {
42
- const agents = listAgents({ agentsDir, wikiRoot });
51
+ function writeBroadcast(
52
+ runtime,
53
+ { agentsDir, wikiRoot, sender, message, today },
54
+ ) {
55
+ const agents = listAgents({ agentsDir, wikiRoot }, runtime.fsSync);
43
56
  for (const { agent, summaryPath } of agents) {
44
57
  if (agent === sender) continue;
45
- writeAndCheck(summaryPath, sender, message, today);
58
+ const result = writeAndCheck(runtime, summaryPath, sender, message, today);
59
+ if (!result.ok) return result;
46
60
  }
61
+ return { ok: true };
47
62
  }
48
63
 
49
64
  /** Write a memo to a target agent's summary file (or broadcast to all except the sender); sender is --from or LIBEVAL_AGENT_PROFILE env var. */
50
- export function runMemoCommand(values, _args, cli) {
51
- const sender = values.from || process.env.LIBEVAL_AGENT_PROFILE;
65
+ export function runMemoCommand(ctx) {
66
+ const { runtime } = ctx.deps;
67
+ const options = ctx.options;
68
+ const sender = options.from || runtime.proc.env.LIBEVAL_AGENT_PROFILE;
52
69
 
53
70
  if (!sender) {
54
- cli.usageError(
55
- "memo requires --from <sender> or LIBEVAL_AGENT_PROFILE env var",
56
- );
57
- process.exit(2);
71
+ return {
72
+ ok: false,
73
+ code: 2,
74
+ error: "memo requires --from <sender> or LIBEVAL_AGENT_PROFILE env var",
75
+ };
58
76
  }
59
-
60
- if (!values.to) {
61
- cli.usageError("memo requires --to <target|all>");
62
- process.exit(2);
77
+ if (!options.to) {
78
+ return { ok: false, code: 2, error: "memo requires --to <target|all>" };
63
79
  }
64
-
65
- if (!values.message) {
66
- cli.usageError("memo requires --message <text>");
67
- process.exit(2);
80
+ if (!options.message) {
81
+ return { ok: false, code: 2, error: "memo requires --message <text>" };
68
82
  }
69
83
 
70
- const logger = { debug() {} };
71
- const finder = new Finder(fsAsync, logger, process);
72
- const projectRoot = finder.findProjectRoot(process.cwd());
73
-
74
- const wikiRoot = values["wiki-root"] || path.join(projectRoot, "wiki");
84
+ const projectRoot = resolveProjectRoot(runtime);
85
+ const wikiRoot = options["wiki-root"] || path.join(projectRoot, "wiki");
75
86
  const agentsDir = path.join(projectRoot, ".claude", "agents");
76
- const today = new Date().toISOString().slice(0, 10);
87
+ const today = currentDayIso(runtime);
77
88
 
78
- if (values.to === BROADCAST_TARGET) {
79
- writeBroadcast({
89
+ if (options.to === BROADCAST_TARGET) {
90
+ return writeBroadcast(runtime, {
80
91
  agentsDir,
81
92
  wikiRoot,
82
93
  sender,
83
- message: values.message,
84
- today,
85
- });
86
- } else {
87
- writeSingleTarget({
88
- wikiRoot,
89
- target: values.to,
90
- sender,
91
- message: values.message,
94
+ message: options.message,
92
95
  today,
93
- cli,
94
96
  });
95
97
  }
98
+ return writeSingleTarget(runtime, {
99
+ wikiRoot,
100
+ target: options.to,
101
+ sender,
102
+ message: options.message,
103
+ today,
104
+ });
96
105
  }
@@ -1,36 +1,33 @@
1
- import { readFileSync, writeFileSync } from "node:fs";
2
- import { spawnSync } from "node:child_process";
3
1
  import path from "node:path";
4
- import fsAsync from "node:fs/promises";
5
- import { Finder } from "@forwardimpact/libutil";
2
+ import { yearMonth } from "@forwardimpact/libutil";
6
3
  import { createScriptConfig } from "@forwardimpact/libconfig";
7
4
  import { scanMarkers } from "../marker-scanner.js";
8
5
  import { renderBlock, BlockRenderError } from "../block-renderer.js";
9
6
  import { renderIssueList, parseRepoSlug } from "../issue-list-renderer.js";
10
- import { createDefaultIo } from "../io.js";
7
+ import { currentDayIso } from "../util/clock.js";
8
+ import { resolveProjectRoot } from "../util/wiki-dir.js";
11
9
 
12
- function currentStoryboardPath(now = new Date()) {
13
- const yyyy = now.getFullYear();
14
- const mm = String(now.getMonth() + 1).padStart(2, "0");
15
- return `wiki/storyboard-${yyyy}-M${mm}.md`;
10
+ function currentStoryboardRelPath(runtime) {
11
+ return `wiki/storyboard-${yearMonth(currentDayIso(runtime))}.md`;
16
12
  }
17
13
 
18
- function deriveParentRepo(parentDir, env) {
14
+ async function deriveParentRepo(gitClient, parentDir, env) {
19
15
  if (env.FIT_GH_REPO) return env.FIT_GH_REPO;
20
- const r = spawnSync("git", ["-C", parentDir, "remote", "get-url", "origin"], {
21
- encoding: "utf-8",
22
- stdio: "pipe",
23
- });
24
- if (r.status !== 0) return null;
25
- return parseRepoSlug(r.stdout);
16
+ try {
17
+ const url = await gitClient.remoteGetUrl("origin", { cwd: parentDir });
18
+ return parseRepoSlug(url);
19
+ } catch {
20
+ return null;
21
+ }
26
22
  }
27
23
 
28
- function renderForBlock(block, projectRoot, ghContext) {
24
+ async function renderForBlock(block, projectRoot, ghContext, runtime) {
29
25
  if (block.kind === "xmr") {
30
26
  return renderBlock({
31
27
  metric: block.metric,
32
28
  csvPath: block.csvPath,
33
29
  projectRoot,
30
+ fs: runtime.fsSync,
34
31
  });
35
32
  }
36
33
  if (block.kind === "issue-list") {
@@ -41,6 +38,8 @@ function renderForBlock(block, projectRoot, ghContext) {
41
38
  cwd: ghContext.cwd,
42
39
  repo: ghContext.repo,
43
40
  token: ghContext.token,
41
+ today: currentDayIso(runtime),
42
+ runtime,
44
43
  });
45
44
  }
46
45
  return null;
@@ -55,23 +54,20 @@ function spliceBlock(lines, block, rendered) {
55
54
  }
56
55
 
57
56
  /** Re-render XmR chart blocks and issue-list blocks in a storyboard file. */
58
- export async function runRefreshCommand(
59
- values,
60
- args,
61
- _cli,
62
- io = createDefaultIo(),
63
- ) {
64
- const logger = { debug() {} };
65
- const finder = new Finder(fsAsync, logger, { cwd: io.cwd });
66
- const projectRoot = finder.findProjectRoot(io.cwd());
57
+ export async function runRefreshCommand(ctx) {
58
+ const { runtime, gitClient } = ctx.deps;
59
+ const options = ctx.options;
60
+ const projectRoot = resolveProjectRoot(runtime);
67
61
 
68
62
  const storyboardPath = path.resolve(
69
63
  projectRoot,
70
- args[0] || currentStoryboardPath(),
64
+ ctx.args["storyboard-path"] || currentStoryboardRelPath(runtime),
71
65
  );
72
- const text = readFileSync(storyboardPath, "utf-8");
73
- const blocks = scanMarkers(text);
74
- if (blocks.length === 0) return;
66
+ const text = runtime.fsSync.readFileSync(storyboardPath, "utf-8");
67
+ const blocks = scanMarkers(text, {
68
+ warn: (message) => runtime.proc.stderr.write(message),
69
+ });
70
+ if (blocks.length === 0) return { ok: true };
75
71
 
76
72
  const config = await createScriptConfig("wiki");
77
73
  let token = null;
@@ -89,7 +85,7 @@ export async function runRefreshCommand(
89
85
  // overrides the parsed origin.
90
86
  const ghContext = {
91
87
  cwd: projectRoot,
92
- repo: deriveParentRepo(projectRoot, io.env),
88
+ repo: await deriveParentRepo(gitClient, projectRoot, runtime.proc.env),
93
89
  token,
94
90
  };
95
91
 
@@ -99,20 +95,28 @@ export async function runRefreshCommand(
99
95
  for (let i = blocks.length - 1; i >= 0; i--) {
100
96
  const block = blocks[i];
101
97
  try {
102
- const rendered = renderForBlock(block, projectRoot, ghContext);
98
+ const rendered = await renderForBlock(
99
+ block,
100
+ projectRoot,
101
+ ghContext,
102
+ runtime,
103
+ );
103
104
  if (!rendered) continue;
104
105
  spliceBlock(lines, block, rendered);
105
106
  spliced = true;
106
107
  } catch (err) {
107
108
  if (!(err instanceof BlockRenderError)) throw err;
108
- io.stderr(
109
+ runtime.proc.stderr.write(
109
110
  `refresh-error ${storyboardPath}:${block.openLine + 1} ${err.message}\n`,
110
111
  );
111
112
  }
112
113
  }
113
114
 
114
- if (spliced) writeFileSync(storyboardPath, lines.join("\n"));
115
- if (values && values.format === "json") {
116
- io.stdout(JSON.stringify({ blocks: blocks.length, spliced }) + "\n");
115
+ if (spliced) runtime.fsSync.writeFileSync(storyboardPath, lines.join("\n"));
116
+ if (options.format === "json") {
117
+ runtime.proc.stdout.write(
118
+ JSON.stringify({ blocks: blocks.length, spliced }) + "\n",
119
+ );
117
120
  }
121
+ return { ok: true };
118
122
  }
@@ -1,25 +1,36 @@
1
- import fsAsync from "node:fs/promises";
2
- import path from "node:path";
3
- import { Finder } from "@forwardimpact/libutil";
4
1
  import { rotateIfOverBudget } from "../weekly-log.js";
2
+ import { currentDayIso } from "../util/clock.js";
3
+ import { resolveWikiRoot } from "../util/wiki-dir.js";
5
4
 
6
5
  /** Force-rotate the current weekly log to a sealed part file. */
7
- export function runRotateCommand(values, _args, cli) {
8
- const agent = values.agent || process.env.LIBEVAL_AGENT_PROFILE;
6
+ export function runRotateCommand(ctx) {
7
+ const { runtime } = ctx.deps;
8
+ const options = ctx.options;
9
+ const agent = options.agent || runtime.proc.env.LIBEVAL_AGENT_PROFILE;
9
10
  if (!agent) {
10
- cli.usageError("rotate requires --agent or LIBEVAL_AGENT_PROFILE");
11
- process.exit(2);
11
+ return {
12
+ ok: false,
13
+ code: 2,
14
+ error: "rotate requires --agent or LIBEVAL_AGENT_PROFILE",
15
+ };
12
16
  }
13
- const logger = { debug() {} };
14
- const finder = new Finder(fsAsync, logger, 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);
17
+ const wikiRoot = resolveWikiRoot(runtime, options);
18
+ const today = options.today || currentDayIso(runtime);
18
19
 
19
- const result = rotateIfOverBudget(wikiRoot, agent, today, 0, { force: true });
20
+ const result = rotateIfOverBudget(
21
+ wikiRoot,
22
+ agent,
23
+ today,
24
+ 0,
25
+ { force: true },
26
+ runtime.fsSync,
27
+ );
20
28
  if (result.rotated) {
21
- process.stdout.write(`rotated ${result.fromPath} → ${result.toPath}\n`);
29
+ runtime.proc.stdout.write(
30
+ `rotated ${result.fromPath} → ${result.toPath}\n`,
31
+ );
22
32
  } else {
23
- process.stdout.write(`no rotation needed for ${agent}\n`);
33
+ runtime.proc.stdout.write(`no rotation needed for ${agent}\n`);
24
34
  }
35
+ return { ok: true };
25
36
  }
@@ -1,33 +1,34 @@
1
- import { WikiPullConflict } from "../wiki-repo.js";
2
- import { buildRepo } from "../build-repo.js";
1
+ import { WikiPullConflict } from "../wiki-sync.js";
3
2
 
4
3
  /** Commit all wiki changes and push them to the remote wiki repository. */
5
- export async function runPushCommand(values, _args, cli) {
6
- const repo = await buildRepo(values);
7
- repo.inheritIdentity();
4
+ export async function runPushCommand(ctx) {
5
+ const { runtime, wikiSync } = ctx.deps;
6
+ await wikiSync.inheritIdentity();
8
7
 
9
- const result = repo.commitAndPush("wiki: update from session");
8
+ const result = await wikiSync.commitAndPush("wiki: update from session");
10
9
  if (result.pushed) {
11
- process.stdout.write("push: committed and pushed\n");
10
+ runtime.proc.stdout.write("push: committed and pushed\n");
12
11
  } else {
13
- process.stdout.write("push: nothing to push\n");
12
+ runtime.proc.stdout.write("push: nothing to push\n");
14
13
  }
14
+ return { ok: true };
15
15
  }
16
16
 
17
- /** Fetch and rebase the local wiki on origin/master; on rebase conflict, exit the process with code 1 and a message to resolve manually or push first. */
18
- export async function runPullCommand(values, _args, cli) {
19
- const repo = await buildRepo(values);
20
- repo.inheritIdentity();
17
+ /** Fetch and rebase the local wiki on origin/master; on rebase conflict, return a non-zero envelope with a message to resolve manually or push first. */
18
+ export async function runPullCommand(ctx) {
19
+ const { runtime, wikiSync } = ctx.deps;
20
+ await wikiSync.inheritIdentity();
21
21
 
22
22
  try {
23
- repo.pull();
24
- process.stdout.write("pull: up to date\n");
23
+ await wikiSync.pull();
24
+ runtime.proc.stdout.write("pull: up to date\n");
25
+ return { ok: true };
25
26
  } catch (err) {
26
27
  if (err instanceof WikiPullConflict) {
27
- process.stderr.write(
28
+ runtime.proc.stderr.write(
28
29
  "fit-wiki pull: rebase conflict — local divergence detected; resolve manually or push first\n",
29
30
  );
30
- process.exit(1);
31
+ return { ok: false, code: 1 };
31
32
  }
32
33
  throw err;
33
34
  }
package/src/index.js CHANGED
@@ -20,7 +20,7 @@ export {
20
20
  export { scanMarkers } from "./marker-scanner.js";
21
21
  export { renderBlock } from "./block-renderer.js";
22
22
  export { renderIssueList } from "./issue-list-renderer.js";
23
- export { WikiRepo } from "./wiki-repo.js";
23
+ export { WikiSync, WikiPullConflict } from "./wiki-sync.js";
24
24
  export { listSkills } from "./skill-roster.js";
25
25
  export {
26
26
  parseClaims,
@@ -1,22 +1,4 @@
1
- import { spawnSync } from "node:child_process";
2
-
3
- function defaultGh(args, options) {
4
- const env = options?.token
5
- ? { ...process.env, GH_TOKEN: options.token }
6
- : undefined;
7
- return spawnSync("gh", args, {
8
- encoding: "utf-8",
9
- stdio: ["ignore", "pipe", "pipe"],
10
- cwd: options?.cwd,
11
- env,
12
- });
13
- }
14
-
15
- function daysAgo(today, n) {
16
- const d = today instanceof Date ? new Date(today.getTime()) : new Date(today);
17
- d.setUTCDate(d.getUTCDate() - n);
18
- return d.toISOString().slice(0, 10);
19
- }
1
+ import { addDays } from "@forwardimpact/libutil";
20
2
 
21
3
  /** Parse `owner/repo` from a git origin URL. Tolerates http(s), ssh, and proxy-rewritten URLs (e.g. `http://host/git/owner/repo`) by taking the last two path segments after stripping `.git`. Returns null when nothing parseable is found. */
22
4
  export function parseRepoSlug(originUrl) {
@@ -27,16 +9,34 @@ export function parseRepoSlug(originUrl) {
27
9
  return `${match[1]}/${match[2]}`;
28
10
  }
29
11
 
30
- /** Render an issue-list block for an obstacles/experiments marker. Returns markdown lines. `cwd` should be the parent monorepo's project root so `gh` resolves the correct origin; `repo` is an explicit `owner/name` slug used when the origin remote is unparseable by `gh` (e.g. sandbox proxy URLs); `token` is the resolved GH token (e.g. via `Config.ghToken()`). */
31
- export function renderIssueList({
12
+ /**
13
+ * Render an issue-list block for an obstacles/experiments marker. Returns
14
+ * markdown lines. `cwd` should be the parent monorepo's project root so `gh`
15
+ * resolves the correct origin; `repo` is an explicit `owner/name` slug used when
16
+ * the origin remote is unparseable by `gh` (e.g. sandbox proxy URLs); `token`
17
+ * is the resolved GH token (e.g. via `Config.ghToken()`). The `gh` command runs
18
+ * through `runtime.subprocess`, and stderr warnings through `runtime.proc`.
19
+ *
20
+ * @param {object} options
21
+ * @param {string} options.topic
22
+ * @param {string} options.state
23
+ * @param {string|null} options.window
24
+ * @param {string} options.cwd
25
+ * @param {string} [options.repo]
26
+ * @param {string} [options.token]
27
+ * @param {string} options.today - ISO date string used for the closed-window cutoff.
28
+ * @param {import('@forwardimpact/libutil/runtime').Runtime} options.runtime
29
+ * @returns {Promise<string[]>}
30
+ */
31
+ export async function renderIssueList({
32
32
  topic,
33
33
  state,
34
34
  window,
35
35
  cwd,
36
36
  repo,
37
37
  token,
38
- today = new Date(),
39
- gh = defaultGh,
38
+ today,
39
+ runtime,
40
40
  }) {
41
41
  const ghState = state === "closed" ? "closed" : "open";
42
42
  const args = ["issue", "list"];
@@ -51,9 +51,10 @@ export function renderIssueList({
51
51
  "--limit",
52
52
  "100",
53
53
  );
54
- const result = gh(args, { cwd, token });
55
- if (result.status !== 0) {
56
- process.stderr.write(
54
+ const env = token ? { ...runtime.proc.env, GH_TOKEN: token } : undefined;
55
+ const result = await runtime.subprocess.run("gh", args, { cwd, env });
56
+ if (result.exitCode !== 0) {
57
+ runtime.proc.stderr.write(
57
58
  `refresh: gh issue list failed for ${topic}:${state}\n`,
58
59
  );
59
60
  return [];
@@ -62,7 +63,7 @@ export function renderIssueList({
62
63
  try {
63
64
  issues = JSON.parse(result.stdout || "[]");
64
65
  } catch {
65
- process.stderr.write(
66
+ runtime.proc.stderr.write(
66
67
  `refresh: gh issue list JSON parse failed for ${topic}:${state}\n`,
67
68
  );
68
69
  return [];
@@ -72,7 +73,7 @@ export function renderIssueList({
72
73
  const windowDays = window
73
74
  ? Number.parseInt(window.replace("d", ""), 10)
74
75
  : 7;
75
- const cutoff = daysAgo(today, windowDays);
76
+ const cutoff = addDays(today, -windowDays);
76
77
  issues = issues.filter(
77
78
  (i) => i.closedAt && i.closedAt.slice(0, 10) >= cutoff,
78
79
  );