@forwardimpact/libwiki 0.1.3 → 0.1.5
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/package.json +2 -1
- package/src/commands/init.js +12 -3
- package/src/commands/sync.js +15 -10
- package/src/wiki-repo.js +45 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forwardimpact/libwiki",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Wiki lifecycle primitives — stable memory for agent teams so coordination persists across sessions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wiki",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@forwardimpact/libcli": "^0.1.0",
|
|
49
|
+
"@forwardimpact/libconfig": "^0.1.77",
|
|
49
50
|
"@forwardimpact/libutil": "^0.1.0",
|
|
50
51
|
"@forwardimpact/libxmr": "^1.1.0"
|
|
51
52
|
},
|
package/src/commands/init.js
CHANGED
|
@@ -3,10 +3,14 @@ import { spawnSync } from "node:child_process";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import fsAsync from "node:fs/promises";
|
|
5
5
|
import { Finder } from "@forwardimpact/libutil";
|
|
6
|
+
import { createScriptConfig } from "@forwardimpact/libconfig";
|
|
6
7
|
import { WikiRepo } from "../wiki-repo.js";
|
|
7
8
|
import { listSkills } from "../skill-roster.js";
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
/** Resolve the wiki clone URL. Honors the FIT_WIKI_URL env var as an explicit override (for sandboxed environments where `origin` is rewritten to a local proxy that does not serve wiki repos); otherwise derives the URL by appending `.wiki.git` to the parent repo's `origin` remote. */
|
|
11
|
+
export function deriveWikiUrl(parentDir) {
|
|
12
|
+
if (process.env.FIT_WIKI_URL) return process.env.FIT_WIKI_URL;
|
|
13
|
+
|
|
10
14
|
const r = spawnSync("git", ["-C", parentDir, "remote", "get-url", "origin"], {
|
|
11
15
|
encoding: "utf-8",
|
|
12
16
|
stdio: "pipe",
|
|
@@ -18,7 +22,7 @@ function deriveWikiUrl(parentDir) {
|
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
/** Clone the wiki if not already present (URL derived from origin remote), copy git identity from the parent repo, and create metric directories for each kata skill. */
|
|
21
|
-
export function runInitCommand(values, _args, cli) {
|
|
25
|
+
export async function runInitCommand(values, _args, cli) {
|
|
22
26
|
const logger = { debug() {} };
|
|
23
27
|
const finder = new Finder(fsAsync, logger, process);
|
|
24
28
|
const projectRoot = finder.findProjectRoot(process.cwd());
|
|
@@ -37,7 +41,12 @@ export function runInitCommand(values, _args, cli) {
|
|
|
37
41
|
return;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
const
|
|
44
|
+
const config = await createScriptConfig("wiki");
|
|
45
|
+
const repo = new WikiRepo({
|
|
46
|
+
wikiDir,
|
|
47
|
+
parentDir: projectRoot,
|
|
48
|
+
resolveToken: () => config.ghToken(),
|
|
49
|
+
});
|
|
41
50
|
|
|
42
51
|
const cloneResult = repo.ensureCloned(wikiUrl);
|
|
43
52
|
if (!cloneResult.cloned) {
|
package/src/commands/sync.js
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fsAsync from "node:fs/promises";
|
|
3
3
|
import { Finder } from "@forwardimpact/libutil";
|
|
4
|
+
import { createScriptConfig } from "@forwardimpact/libconfig";
|
|
4
5
|
import { WikiRepo, WikiPullConflict } from "../wiki-repo.js";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
export function runPushCommand(values, _args, cli) {
|
|
7
|
+
async function buildRepo(values) {
|
|
8
8
|
const logger = { debug() {} };
|
|
9
9
|
const finder = new Finder(fsAsync, logger, process);
|
|
10
10
|
const projectRoot = finder.findProjectRoot(process.cwd());
|
|
11
11
|
const wikiDir = path.resolve(projectRoot, values["wiki-root"] ?? "wiki");
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const config = await createScriptConfig("wiki");
|
|
14
|
+
return new WikiRepo({
|
|
15
|
+
wikiDir,
|
|
16
|
+
parentDir: projectRoot,
|
|
17
|
+
resolveToken: () => config.ghToken(),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Commit all wiki changes and push them to the remote wiki repository. */
|
|
22
|
+
export async function runPushCommand(values, _args, cli) {
|
|
23
|
+
const repo = await buildRepo(values);
|
|
14
24
|
repo.inheritIdentity();
|
|
15
25
|
|
|
16
26
|
const result = repo.commitAndPush("wiki: update from session");
|
|
@@ -22,13 +32,8 @@ export function runPushCommand(values, _args, cli) {
|
|
|
22
32
|
}
|
|
23
33
|
|
|
24
34
|
/** 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. */
|
|
25
|
-
export function runPullCommand(values, _args, cli) {
|
|
26
|
-
const
|
|
27
|
-
const finder = new Finder(fsAsync, logger, process);
|
|
28
|
-
const projectRoot = finder.findProjectRoot(process.cwd());
|
|
29
|
-
const wikiDir = path.resolve(projectRoot, values["wiki-root"] ?? "wiki");
|
|
30
|
-
|
|
31
|
-
const repo = new WikiRepo({ wikiDir, parentDir: projectRoot });
|
|
35
|
+
export async function runPullCommand(values, _args, cli) {
|
|
36
|
+
const repo = await buildRepo(values);
|
|
32
37
|
repo.inheritIdentity();
|
|
33
38
|
|
|
34
39
|
try {
|
package/src/wiki-repo.js
CHANGED
|
@@ -31,11 +31,31 @@ export function buildAuthArgs(args, token) {
|
|
|
31
31
|
export class WikiRepo {
|
|
32
32
|
#wikiDir;
|
|
33
33
|
#parentDir;
|
|
34
|
+
#resolveToken;
|
|
34
35
|
|
|
35
|
-
/**
|
|
36
|
-
|
|
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
|
+
}
|
|
37
56
|
this.#wikiDir = wikiDir;
|
|
38
57
|
this.#parentDir = parentDir;
|
|
58
|
+
this.#resolveToken = resolveToken;
|
|
39
59
|
}
|
|
40
60
|
|
|
41
61
|
/** Check whether the wiki directory is an initialized git repository. */
|
|
@@ -92,11 +112,16 @@ export class WikiRepo {
|
|
|
92
112
|
}
|
|
93
113
|
}
|
|
94
114
|
|
|
95
|
-
/** Stage
|
|
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. */
|
|
96
116
|
commitAndPush(message) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
+
}
|
|
100
125
|
this.fetch();
|
|
101
126
|
const rebase = this.#git(["rebase", "origin/master"]);
|
|
102
127
|
if (rebase.status !== 0) {
|
|
@@ -107,6 +132,12 @@ export class WikiRepo {
|
|
|
107
132
|
return { pushed: true, reason: "pushed" };
|
|
108
133
|
}
|
|
109
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
|
+
|
|
110
141
|
#parentConfig(key) {
|
|
111
142
|
const r = spawnSync(
|
|
112
143
|
"git",
|
|
@@ -123,11 +154,14 @@ export class WikiRepo {
|
|
|
123
154
|
}
|
|
124
155
|
|
|
125
156
|
#authGit(args) {
|
|
126
|
-
const token =
|
|
157
|
+
const token = this.#resolveToken();
|
|
127
158
|
const fullArgs = buildAuthArgs(args, token);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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 });
|
|
132
166
|
}
|
|
133
167
|
}
|