@forwardimpact/libwiki 0.1.1 → 0.1.2
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/bin/fit-wiki.js +53 -1
- package/package.json +11 -5
- package/src/block-renderer.js +65 -0
- package/src/commands/init.js +55 -0
- package/src/commands/refresh.js +55 -0
- package/src/commands/sync.js +44 -0
- package/src/index.js +4 -0
- package/src/marker-scanner.js +42 -0
- package/src/skill-roster.js +18 -0
- package/src/wiki-repo.js +121 -0
package/bin/fit-wiki.js
CHANGED
|
@@ -4,6 +4,9 @@ import { readFileSync } from "node:fs";
|
|
|
4
4
|
import { createCli } from "@forwardimpact/libcli";
|
|
5
5
|
|
|
6
6
|
import { runMemoCommand } from "../src/commands/memo.js";
|
|
7
|
+
import { runRefreshCommand } from "../src/commands/refresh.js";
|
|
8
|
+
import { runInitCommand } from "../src/commands/init.js";
|
|
9
|
+
import { runPushCommand, runPullCommand } from "../src/commands/sync.js";
|
|
7
10
|
|
|
8
11
|
const { version: VERSION } = JSON.parse(
|
|
9
12
|
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
@@ -38,6 +41,46 @@ const definition = {
|
|
|
38
41
|
},
|
|
39
42
|
},
|
|
40
43
|
},
|
|
44
|
+
{
|
|
45
|
+
name: "refresh",
|
|
46
|
+
description:
|
|
47
|
+
"Regenerate XmR chart blocks inside a storyboard markdown file",
|
|
48
|
+
args: "[storyboard-path]",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "init",
|
|
52
|
+
description: "Bootstrap a wiki working tree for a Kata installation",
|
|
53
|
+
options: {
|
|
54
|
+
"wiki-root": {
|
|
55
|
+
type: "string",
|
|
56
|
+
description: "Override wiki root directory (default: wiki)",
|
|
57
|
+
},
|
|
58
|
+
"skills-dir": {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "Override skills directory (default: .claude/skills)",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "push",
|
|
66
|
+
description: "Commit and push local wiki changes to the remote",
|
|
67
|
+
options: {
|
|
68
|
+
"wiki-root": {
|
|
69
|
+
type: "string",
|
|
70
|
+
description: "Override wiki root directory (default: wiki)",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "pull",
|
|
76
|
+
description: "Pull remote wiki changes into the local working tree",
|
|
77
|
+
options: {
|
|
78
|
+
"wiki-root": {
|
|
79
|
+
type: "string",
|
|
80
|
+
description: "Override wiki root directory (default: wiki)",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
41
84
|
],
|
|
42
85
|
globalOptions: {
|
|
43
86
|
help: { type: "boolean", short: "h", description: "Show this help" },
|
|
@@ -50,13 +93,18 @@ const definition = {
|
|
|
50
93
|
examples: [
|
|
51
94
|
'fit-wiki memo --from staff-engineer --to security-engineer --message "audit d642ff0c"',
|
|
52
95
|
'fit-wiki memo --from technical-writer --to all --message "new XmR baseline"',
|
|
96
|
+
"fit-wiki refresh",
|
|
97
|
+
"fit-wiki refresh wiki/storyboard-2026-M05.md",
|
|
98
|
+
"fit-wiki init",
|
|
99
|
+
"fit-wiki push",
|
|
100
|
+
"fit-wiki pull",
|
|
53
101
|
],
|
|
54
102
|
documentation: [
|
|
55
103
|
{
|
|
56
104
|
title: "Wiki Operations",
|
|
57
105
|
url: "https://www.forwardimpact.team/docs/libraries/wiki-operations/index.md",
|
|
58
106
|
description:
|
|
59
|
-
"Send cross-team memos,
|
|
107
|
+
"Send cross-team memos, refresh storyboard charts, and sync the wiki.",
|
|
60
108
|
},
|
|
61
109
|
],
|
|
62
110
|
};
|
|
@@ -65,6 +113,10 @@ const cli = createCli(definition);
|
|
|
65
113
|
|
|
66
114
|
const COMMANDS = {
|
|
67
115
|
memo: runMemoCommand,
|
|
116
|
+
refresh: runRefreshCommand,
|
|
117
|
+
init: runInitCommand,
|
|
118
|
+
push: runPushCommand,
|
|
119
|
+
pull: runPullCommand,
|
|
68
120
|
};
|
|
69
121
|
|
|
70
122
|
function main() {
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forwardimpact/libwiki",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Wiki lifecycle primitives for the Kata agent system: cross-team memos,
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Wiki lifecycle primitives for the Kata agent system: cross-team memos, storyboard XmR chart refresh, wiki bootstrap, and git sync.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wiki",
|
|
7
7
|
"memo",
|
|
8
|
-
"
|
|
8
|
+
"storyboard",
|
|
9
|
+
"xmr",
|
|
9
10
|
"agent"
|
|
10
11
|
],
|
|
11
12
|
"homepage": "https://www.forwardimpact.team",
|
|
@@ -19,7 +20,11 @@
|
|
|
19
20
|
"forwardimpact": {
|
|
20
21
|
"capability": "agent-self-improvement",
|
|
21
22
|
"needs": [
|
|
22
|
-
"Send a cross-team memo to a teammate's wiki inbox"
|
|
23
|
+
"Send a cross-team memo to a teammate's wiki inbox",
|
|
24
|
+
"Refresh XmR chart blocks inside a storyboard markdown file",
|
|
25
|
+
"Bootstrap a wiki working tree for a Kata installation",
|
|
26
|
+
"Push agent-authored wiki changes to the remote",
|
|
27
|
+
"Pull remote wiki changes into the local working tree"
|
|
23
28
|
]
|
|
24
29
|
},
|
|
25
30
|
"type": "module",
|
|
@@ -41,7 +46,8 @@
|
|
|
41
46
|
},
|
|
42
47
|
"dependencies": {
|
|
43
48
|
"@forwardimpact/libcli": "^0.1.0",
|
|
44
|
-
"@forwardimpact/libutil": "^0.1.0"
|
|
49
|
+
"@forwardimpact/libutil": "^0.1.0",
|
|
50
|
+
"@forwardimpact/libxmr": "^1.1.0"
|
|
45
51
|
},
|
|
46
52
|
"devDependencies": {
|
|
47
53
|
"@forwardimpact/libharness": "^0.1.5"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { analyze, renderChart, MIN_POINTS } from "@forwardimpact/libxmr";
|
|
4
|
+
|
|
5
|
+
export class BlockRenderError extends Error {
|
|
6
|
+
constructor(reason) {
|
|
7
|
+
super(reason);
|
|
8
|
+
this.name = "BlockRenderError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function renderBlock({
|
|
13
|
+
metric,
|
|
14
|
+
csvPath,
|
|
15
|
+
projectRoot,
|
|
16
|
+
fs = { readFileSync },
|
|
17
|
+
}) {
|
|
18
|
+
const fullPath = path.resolve(projectRoot, csvPath);
|
|
19
|
+
let csvText;
|
|
20
|
+
try {
|
|
21
|
+
csvText = fs.readFileSync(fullPath, "utf-8");
|
|
22
|
+
} catch {
|
|
23
|
+
throw new BlockRenderError(`csv-not-found: ${csvPath}`);
|
|
24
|
+
}
|
|
25
|
+
const report = analyze(csvText);
|
|
26
|
+
|
|
27
|
+
const m = report.metrics.find((entry) => entry.metric === metric);
|
|
28
|
+
if (!m) {
|
|
29
|
+
throw new BlockRenderError(`metric-not-found: ${metric}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const latestValue = m.latest?.value ?? m.values[m.values.length - 1] ?? "—";
|
|
33
|
+
const status = m.status;
|
|
34
|
+
|
|
35
|
+
let chartLines;
|
|
36
|
+
if (status === "insufficient_data") {
|
|
37
|
+
chartLines = [
|
|
38
|
+
`Insufficient data: ${m.n} points (need at least ${MIN_POINTS}).`,
|
|
39
|
+
];
|
|
40
|
+
} else {
|
|
41
|
+
const chartText = renderChart(m.values, m.stats, m.signals);
|
|
42
|
+
chartLines = chartText.split("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const signalLine = formatSignals(m.signals);
|
|
46
|
+
|
|
47
|
+
return [
|
|
48
|
+
`**Latest:** ${latestValue} · **Status:** ${status}`,
|
|
49
|
+
"",
|
|
50
|
+
"```",
|
|
51
|
+
...chartLines,
|
|
52
|
+
"```",
|
|
53
|
+
"",
|
|
54
|
+
`**Signals:** ${signalLine}`,
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatSignals(signals) {
|
|
59
|
+
if (!signals) return "—";
|
|
60
|
+
const fired = [];
|
|
61
|
+
for (const rule of ["xRule1", "xRule2", "xRule3", "mrRule1"]) {
|
|
62
|
+
if (signals[rule]?.length > 0) fired.push(rule);
|
|
63
|
+
}
|
|
64
|
+
return fired.length > 0 ? fired.join(", ") : "—";
|
|
65
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import fsAsync from "node:fs/promises";
|
|
5
|
+
import { Finder } from "@forwardimpact/libutil";
|
|
6
|
+
import { WikiRepo } from "../wiki-repo.js";
|
|
7
|
+
import { listSkills } from "../skill-roster.js";
|
|
8
|
+
|
|
9
|
+
function deriveWikiUrl(parentDir) {
|
|
10
|
+
const r = spawnSync("git", ["-C", parentDir, "remote", "get-url", "origin"], {
|
|
11
|
+
encoding: "utf-8",
|
|
12
|
+
stdio: "pipe",
|
|
13
|
+
});
|
|
14
|
+
if (r.status !== 0) return null;
|
|
15
|
+
const origin = r.stdout.trim();
|
|
16
|
+
const base = origin.replace(/\.git$/, "");
|
|
17
|
+
return base + ".wiki.git";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function runInitCommand(values, _args, cli) {
|
|
21
|
+
const logger = { debug() {} };
|
|
22
|
+
const finder = new Finder(fsAsync, logger, process);
|
|
23
|
+
const projectRoot = finder.findProjectRoot(process.cwd());
|
|
24
|
+
|
|
25
|
+
const wikiDir = path.resolve(projectRoot, values["wiki-root"] ?? "wiki");
|
|
26
|
+
const skillsDir = path.resolve(
|
|
27
|
+
projectRoot,
|
|
28
|
+
values["skills-dir"] ?? path.join(".claude", "skills"),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const wikiUrl = deriveWikiUrl(projectRoot);
|
|
32
|
+
if (!wikiUrl) {
|
|
33
|
+
process.stderr.write(
|
|
34
|
+
"init: could not determine wiki URL from origin remote\n",
|
|
35
|
+
);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const repo = new WikiRepo({ wikiDir, parentDir: projectRoot });
|
|
40
|
+
|
|
41
|
+
const cloneResult = repo.ensureCloned(wikiUrl);
|
|
42
|
+
if (!cloneResult.cloned) {
|
|
43
|
+
process.stderr.write("init: could not clone wiki, skipping\n");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
repo.inheritIdentity();
|
|
48
|
+
|
|
49
|
+
const skills = listSkills({ skillsDir });
|
|
50
|
+
for (const slug of skills) {
|
|
51
|
+
mkdirSync(path.join(wikiDir, "metrics", slug), { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
process.stdout.write(`init: wiki ready at ${wikiDir}\n`);
|
|
55
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fsAsync from "node:fs/promises";
|
|
4
|
+
import { Finder } from "@forwardimpact/libutil";
|
|
5
|
+
import { scanMarkers } from "../marker-scanner.js";
|
|
6
|
+
import { renderBlock, BlockRenderError } from "../block-renderer.js";
|
|
7
|
+
|
|
8
|
+
function currentStoryboardPath() {
|
|
9
|
+
const now = new Date();
|
|
10
|
+
const yyyy = now.getFullYear();
|
|
11
|
+
const mm = String(now.getMonth() + 1).padStart(2, "0");
|
|
12
|
+
return `wiki/storyboard-${yyyy}-M${mm}.md`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function runRefreshCommand(values, args, cli) {
|
|
16
|
+
const logger = { debug() {} };
|
|
17
|
+
const finder = new Finder(fsAsync, logger, process);
|
|
18
|
+
const projectRoot = finder.findProjectRoot(process.cwd());
|
|
19
|
+
|
|
20
|
+
const storyboardPath = path.resolve(
|
|
21
|
+
projectRoot,
|
|
22
|
+
args[0] || currentStoryboardPath(),
|
|
23
|
+
);
|
|
24
|
+
const text = readFileSync(storyboardPath, "utf-8");
|
|
25
|
+
const blocks = scanMarkers(text);
|
|
26
|
+
|
|
27
|
+
if (blocks.length === 0) return;
|
|
28
|
+
|
|
29
|
+
const lines = text.split("\n");
|
|
30
|
+
let spliced = false;
|
|
31
|
+
|
|
32
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
33
|
+
const block = blocks[i];
|
|
34
|
+
try {
|
|
35
|
+
const rendered = renderBlock({
|
|
36
|
+
metric: block.metric,
|
|
37
|
+
csvPath: block.csvPath,
|
|
38
|
+
projectRoot,
|
|
39
|
+
});
|
|
40
|
+
lines.splice(
|
|
41
|
+
block.openLine + 1,
|
|
42
|
+
block.closeLine - block.openLine - 1,
|
|
43
|
+
...rendered,
|
|
44
|
+
);
|
|
45
|
+
spliced = true;
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (!(err instanceof BlockRenderError)) throw err;
|
|
48
|
+
process.stderr.write(
|
|
49
|
+
`refresh-error ${storyboardPath}:${block.openLine + 1} ${err.message}\n`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (spliced) writeFileSync(storyboardPath, lines.join("\n"));
|
|
55
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fsAsync from "node:fs/promises";
|
|
3
|
+
import { Finder } from "@forwardimpact/libutil";
|
|
4
|
+
import { WikiRepo, WikiPullConflict } from "../wiki-repo.js";
|
|
5
|
+
|
|
6
|
+
export function runPushCommand(values, _args, cli) {
|
|
7
|
+
const logger = { debug() {} };
|
|
8
|
+
const finder = new Finder(fsAsync, logger, process);
|
|
9
|
+
const projectRoot = finder.findProjectRoot(process.cwd());
|
|
10
|
+
const wikiDir = path.resolve(projectRoot, values["wiki-root"] ?? "wiki");
|
|
11
|
+
|
|
12
|
+
const repo = new WikiRepo({ wikiDir, parentDir: projectRoot });
|
|
13
|
+
repo.inheritIdentity();
|
|
14
|
+
|
|
15
|
+
const result = repo.commitAndPush("wiki: update from session");
|
|
16
|
+
if (result.pushed) {
|
|
17
|
+
process.stdout.write("push: committed and pushed\n");
|
|
18
|
+
} else {
|
|
19
|
+
process.stdout.write("push: nothing to push\n");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function runPullCommand(values, _args, cli) {
|
|
24
|
+
const logger = { debug() {} };
|
|
25
|
+
const finder = new Finder(fsAsync, logger, process);
|
|
26
|
+
const projectRoot = finder.findProjectRoot(process.cwd());
|
|
27
|
+
const wikiDir = path.resolve(projectRoot, values["wiki-root"] ?? "wiki");
|
|
28
|
+
|
|
29
|
+
const repo = new WikiRepo({ wikiDir, parentDir: projectRoot });
|
|
30
|
+
repo.inheritIdentity();
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
repo.pull();
|
|
34
|
+
process.stdout.write("pull: up to date\n");
|
|
35
|
+
} catch (err) {
|
|
36
|
+
if (err instanceof WikiPullConflict) {
|
|
37
|
+
process.stderr.write(
|
|
38
|
+
"fit-wiki pull: rebase conflict — local divergence detected; resolve manually or push first\n",
|
|
39
|
+
);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/index.js
CHANGED
|
@@ -6,3 +6,7 @@ export {
|
|
|
6
6
|
INBOX_HEADING,
|
|
7
7
|
BROADCAST_TARGET,
|
|
8
8
|
} from "./constants.js";
|
|
9
|
+
export { scanMarkers } from "./marker-scanner.js";
|
|
10
|
+
export { renderBlock } from "./block-renderer.js";
|
|
11
|
+
export { WikiRepo } from "./wiki-repo.js";
|
|
12
|
+
export { listSkills } from "./skill-roster.js";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const OPEN_RE = /^<!--\s*xmr:([^:\s]+):([^\s]+)\s*-->\s*$/;
|
|
2
|
+
const CLOSE_RE = /^<!--\s*\/xmr\s*-->\s*$/;
|
|
3
|
+
|
|
4
|
+
export function scanMarkers(text) {
|
|
5
|
+
const lines = text.split("\n");
|
|
6
|
+
const pairs = [];
|
|
7
|
+
let open = null;
|
|
8
|
+
|
|
9
|
+
for (let i = 0; i < lines.length; i++) {
|
|
10
|
+
const openMatch = lines[i].match(OPEN_RE);
|
|
11
|
+
if (openMatch) {
|
|
12
|
+
if (open) {
|
|
13
|
+
process.stderr.write(
|
|
14
|
+
`dangling-marker ${open.metric} at line ${open.openLine + 1}\n`,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
open = { metric: openMatch[1], csvPath: openMatch[2], openLine: i };
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (CLOSE_RE.test(lines[i])) {
|
|
22
|
+
if (open) {
|
|
23
|
+
pairs.push({
|
|
24
|
+
metric: open.metric,
|
|
25
|
+
csvPath: open.csvPath,
|
|
26
|
+
openLine: open.openLine,
|
|
27
|
+
closeLine: i,
|
|
28
|
+
});
|
|
29
|
+
open = null;
|
|
30
|
+
}
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (open) {
|
|
36
|
+
process.stderr.write(
|
|
37
|
+
`dangling-marker ${open.metric} at line ${open.openLine + 1}\n`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return pairs;
|
|
42
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readdirSync, statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function listSkills({ skillsDir }, fs = { readdirSync, statSync }) {
|
|
5
|
+
const entries = fs.readdirSync(skillsDir);
|
|
6
|
+
const skills = [];
|
|
7
|
+
|
|
8
|
+
for (const entry of entries) {
|
|
9
|
+
if (entry.startsWith(".")) continue;
|
|
10
|
+
if (!entry.startsWith("kata-")) continue;
|
|
11
|
+
const fullPath = path.join(skillsDir, entry);
|
|
12
|
+
const stat = fs.statSync(fullPath);
|
|
13
|
+
if (!stat.isDirectory()) continue;
|
|
14
|
+
skills.push(entry);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return skills.sort();
|
|
18
|
+
}
|
package/src/wiki-repo.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const CREDENTIAL_HELPER_BODY =
|
|
4
|
+
'!f() { echo username=x-access-token; echo "password=${GH_TOKEN:-$GITHUB_TOKEN}"; }; f';
|
|
5
|
+
|
|
6
|
+
export class WikiPullConflict extends Error {
|
|
7
|
+
constructor(stderr) {
|
|
8
|
+
super("rebase conflict on pull");
|
|
9
|
+
this.name = "WikiPullConflict";
|
|
10
|
+
this.stderr = stderr;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildAuthArgs(args, token) {
|
|
15
|
+
if (token) {
|
|
16
|
+
return [
|
|
17
|
+
"-c",
|
|
18
|
+
"credential.helper=",
|
|
19
|
+
"-c",
|
|
20
|
+
`credential.helper=${CREDENTIAL_HELPER_BODY}`,
|
|
21
|
+
...args,
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
return [...args];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class WikiRepo {
|
|
28
|
+
#wikiDir;
|
|
29
|
+
#parentDir;
|
|
30
|
+
|
|
31
|
+
constructor({ wikiDir, parentDir }) {
|
|
32
|
+
this.#wikiDir = wikiDir;
|
|
33
|
+
this.#parentDir = parentDir;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
isCloned() {
|
|
37
|
+
const r = spawnSync(
|
|
38
|
+
"git",
|
|
39
|
+
["-C", this.#wikiDir, "rev-parse", "--git-dir"],
|
|
40
|
+
{
|
|
41
|
+
stdio: "pipe",
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
return r.status === 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
ensureCloned(url) {
|
|
48
|
+
if (this.isCloned()) return { cloned: true, reason: "already-cloned" };
|
|
49
|
+
const r = this.#authGit(["clone", url, this.#wikiDir]);
|
|
50
|
+
if (r.status !== 0) {
|
|
51
|
+
return {
|
|
52
|
+
cloned: false,
|
|
53
|
+
reason: r.stderr?.toString().trim() || "clone failed",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return { cloned: true, reason: "cloned" };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
inheritIdentity() {
|
|
60
|
+
const name = this.#parentConfig("user.name");
|
|
61
|
+
const email = this.#parentConfig("user.email");
|
|
62
|
+
if (name) this.#git(["config", "user.name", name]);
|
|
63
|
+
if (email) this.#git(["config", "user.email", email]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fetch() {
|
|
67
|
+
this.#authGit(["-C", this.#wikiDir, "fetch", "origin", "master"]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
isClean() {
|
|
71
|
+
const r = this.#git(["status", "--porcelain"]);
|
|
72
|
+
return r.stdout.toString().trim() === "";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
pull() {
|
|
76
|
+
this.fetch();
|
|
77
|
+
const r = this.#git(["rebase", "origin/master"]);
|
|
78
|
+
if (r.status !== 0) {
|
|
79
|
+
this.#git(["rebase", "--abort"]);
|
|
80
|
+
throw new WikiPullConflict(r.stderr?.toString().trim() || "");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
commitAndPush(message) {
|
|
85
|
+
if (this.isClean()) return { pushed: false, reason: "clean" };
|
|
86
|
+
this.#git(["add", "-A"]);
|
|
87
|
+
this.#git(["commit", "-m", message]);
|
|
88
|
+
this.fetch();
|
|
89
|
+
const rebase = this.#git(["rebase", "origin/master"]);
|
|
90
|
+
if (rebase.status !== 0) {
|
|
91
|
+
this.#git(["rebase", "--abort"]);
|
|
92
|
+
this.#git(["merge", "origin/master", "-X", "ours", "--no-edit"]);
|
|
93
|
+
}
|
|
94
|
+
this.#authGit(["-C", this.#wikiDir, "push", "origin", "master"]);
|
|
95
|
+
return { pushed: true, reason: "pushed" };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#parentConfig(key) {
|
|
99
|
+
const r = spawnSync(
|
|
100
|
+
"git",
|
|
101
|
+
["-C", this.#parentDir, "config", "--get", key],
|
|
102
|
+
{
|
|
103
|
+
stdio: "pipe",
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
return r.status === 0 ? r.stdout.toString().trim() : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#git(args) {
|
|
110
|
+
return spawnSync("git", ["-C", this.#wikiDir, ...args], { stdio: "pipe" });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#authGit(args) {
|
|
114
|
+
const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
|
115
|
+
const fullArgs = buildAuthArgs(args, token);
|
|
116
|
+
return spawnSync("git", fullArgs, {
|
|
117
|
+
stdio: "pipe",
|
|
118
|
+
env: token ? process.env : undefined,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|