@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/wiki-repo.js DELETED
@@ -1,167 +0,0 @@
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
- /** Error thrown when a wiki pull encounters a rebase conflict that cannot be resolved automatically. */
7
- export class WikiPullConflict extends Error {
8
- /** Create a WikiPullConflict with the stderr output from the failed rebase. */
9
- constructor(stderr) {
10
- super("rebase conflict on pull");
11
- this.name = "WikiPullConflict";
12
- this.stderr = stderr;
13
- }
14
- }
15
-
16
- /** Prepend credential-helper config arguments to a git command when a token is available. */
17
- export function buildAuthArgs(args, token) {
18
- if (token) {
19
- return [
20
- "-c",
21
- "credential.helper=",
22
- "-c",
23
- `credential.helper=${CREDENTIAL_HELPER_BODY}`,
24
- ...args,
25
- ];
26
- }
27
- return [...args];
28
- }
29
-
30
- /** Git operations wrapper for the GitHub wiki repository used as agent team memory. */
31
- export class WikiRepo {
32
- #wikiDir;
33
- #parentDir;
34
- #resolveToken;
35
-
36
- /**
37
- * Create a WikiRepo targeting the given wiki directory and its parent project directory.
38
- * @param {{ wikiDir: string, parentDir: string, resolveToken: () => string | null }} opts
39
- * `resolveToken` is called lazily before each network operation. Return a
40
- * GitHub token string to authenticate, or `null` to run anonymously. The
41
- * callback owns the entire resolution policy — libwiki does not read
42
- * `process.env` directly. Throws propagate to the caller so credential
43
- * misconfiguration surfaces loudly. Commands typically pass
44
- * `() => config.ghToken()` from `@forwardimpact/libconfig`.
45
- */
46
- constructor({ wikiDir, parentDir, resolveToken }) {
47
- if (typeof wikiDir !== "string" || wikiDir === "") {
48
- throw new TypeError("WikiRepo: wikiDir must be a non-empty string");
49
- }
50
- if (typeof parentDir !== "string" || parentDir === "") {
51
- throw new TypeError("WikiRepo: parentDir must be a non-empty string");
52
- }
53
- if (typeof resolveToken !== "function") {
54
- throw new TypeError("WikiRepo: resolveToken callback is required");
55
- }
56
- this.#wikiDir = wikiDir;
57
- this.#parentDir = parentDir;
58
- this.#resolveToken = resolveToken;
59
- }
60
-
61
- /** Check whether the wiki directory is an initialized git repository. */
62
- isCloned() {
63
- const r = spawnSync(
64
- "git",
65
- ["-C", this.#wikiDir, "rev-parse", "--git-dir"],
66
- {
67
- stdio: "pipe",
68
- },
69
- );
70
- return r.status === 0;
71
- }
72
-
73
- /** Clone the wiki from the given URL if it is not already cloned. */
74
- ensureCloned(url) {
75
- if (this.isCloned()) return { cloned: true, reason: "already-cloned" };
76
- const r = this.#authGit(["clone", url, this.#wikiDir]);
77
- if (r.status !== 0) {
78
- return {
79
- cloned: false,
80
- reason: r.stderr?.toString().trim() || "clone failed",
81
- };
82
- }
83
- return { cloned: true, reason: "cloned" };
84
- }
85
-
86
- /** Copy git user.name and user.email from the parent repository into the wiki repository. */
87
- inheritIdentity() {
88
- const name = this.#parentConfig("user.name");
89
- const email = this.#parentConfig("user.email");
90
- if (name) this.#git(["config", "user.name", name]);
91
- if (email) this.#git(["config", "user.email", email]);
92
- }
93
-
94
- /** Fetch the latest master branch from the wiki remote using token auth if available. */
95
- fetch() {
96
- this.#authGit(["-C", this.#wikiDir, "fetch", "origin", "master"]);
97
- }
98
-
99
- /** Return true if the wiki working tree has no uncommitted changes. */
100
- isClean() {
101
- const r = this.#git(["status", "--porcelain"]);
102
- return r.stdout.toString().trim() === "";
103
- }
104
-
105
- /** Fetch and rebase on origin/master, throwing WikiPullConflict if the rebase fails. */
106
- pull() {
107
- this.fetch();
108
- const r = this.#git(["rebase", "origin/master"]);
109
- if (r.status !== 0) {
110
- this.#git(["rebase", "--abort"]);
111
- throw new WikiPullConflict(r.stderr?.toString().trim() || "");
112
- }
113
- }
114
-
115
- /** Stage and commit any working-tree changes, then fetch, rebase on origin/master (falling back to a merge with -X ours if rebase fails), and push if HEAD is ahead of origin/master. The commit gate and the push gate are independent so a clean tree with local commits still pushes. */
116
- commitAndPush(message) {
117
- const hasWorkingTreeChanges = !this.isClean();
118
- if (hasWorkingTreeChanges) {
119
- this.#git(["add", "-A"]);
120
- this.#git(["commit", "-m", message]);
121
- }
122
- if (!this.#hasCommitsAhead()) {
123
- return { pushed: false, reason: "clean" };
124
- }
125
- this.fetch();
126
- const rebase = this.#git(["rebase", "origin/master"]);
127
- if (rebase.status !== 0) {
128
- this.#git(["rebase", "--abort"]);
129
- this.#git(["merge", "origin/master", "-X", "ours", "--no-edit"]);
130
- }
131
- this.#authGit(["-C", this.#wikiDir, "push", "origin", "master"]);
132
- return { pushed: true, reason: "pushed" };
133
- }
134
-
135
- #hasCommitsAhead() {
136
- const r = this.#git(["rev-list", "--count", "origin/master..HEAD"]);
137
- const count = parseInt(r.stdout?.toString().trim() || "0", 10);
138
- return count > 0;
139
- }
140
-
141
- #parentConfig(key) {
142
- const r = spawnSync(
143
- "git",
144
- ["-C", this.#parentDir, "config", "--get", key],
145
- {
146
- stdio: "pipe",
147
- },
148
- );
149
- return r.status === 0 ? r.stdout.toString().trim() : null;
150
- }
151
-
152
- #git(args) {
153
- return spawnSync("git", ["-C", this.#wikiDir, ...args], { stdio: "pipe" });
154
- }
155
-
156
- #authGit(args) {
157
- const token = this.#resolveToken();
158
- const fullArgs = buildAuthArgs(args, token);
159
- // The credential helper body keeps `${GH_TOKEN:-$GITHUB_TOKEN}` literal so
160
- // git's child shell expands it at auth time — the token never sits in argv.
161
- // Inject the resolved token into the spawn env so the helper's lazy
162
- // expansion finds it even when the resolver pulled from `.env` or
163
- // `gh auth token` rather than the ambient process env.
164
- const env = token ? { ...process.env, GH_TOKEN: token } : undefined;
165
- return spawnSync("git", fullArgs, { stdio: "pipe", env });
166
- }
167
- }