@forwardimpact/libwiki 0.1.2 → 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/README.md +6 -2
- package/bin/fit-wiki.js +8 -2
- package/package.json +13 -12
- package/src/agent-roster.js +1 -0
- package/src/block-renderer.js +3 -0
- package/src/commands/init.js +13 -3
- package/src/commands/memo.js +47 -11
- package/src/commands/refresh.js +1 -0
- package/src/commands/sync.js +16 -9
- package/src/marker-migrator.js +1 -0
- package/src/marker-scanner.js +1 -0
- package/src/memo-writer.js +1 -0
- package/src/skill-roster.js +1 -0
- package/src/wiki-repo.js +55 -9
package/README.md
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# libwiki
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
<!-- BEGIN:description — Do not edit. Generated from package.json. -->
|
|
4
|
+
|
|
5
|
+
Wiki lifecycle primitives — stable memory for agent teams so coordination
|
|
6
|
+
persists across sessions.
|
|
7
|
+
|
|
8
|
+
<!-- END:description -->
|
|
5
9
|
|
|
6
10
|
## Getting Started
|
|
7
11
|
|
package/bin/fit-wiki.js
CHANGED
|
@@ -101,8 +101,14 @@ const definition = {
|
|
|
101
101
|
],
|
|
102
102
|
documentation: [
|
|
103
103
|
{
|
|
104
|
-
title: "
|
|
105
|
-
url: "https://www.forwardimpact.team/docs/libraries/
|
|
104
|
+
title: "Operate a Predictable Agent Team",
|
|
105
|
+
url: "https://www.forwardimpact.team/docs/libraries/predictable-team/index.md",
|
|
106
|
+
description:
|
|
107
|
+
"End-to-end guide to wiki memory, XmR charts, and team coordination.",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
title: "Send a Memo or Update a Storyboard",
|
|
111
|
+
url: "https://www.forwardimpact.team/docs/libraries/predictable-team/wiki-operations/index.md",
|
|
106
112
|
description:
|
|
107
113
|
"Send cross-team memos, refresh storyboard charts, and sync the wiki.",
|
|
108
114
|
},
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forwardimpact/libwiki",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Wiki lifecycle primitives
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "Wiki lifecycle primitives — stable memory for agent teams so coordination persists across sessions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wiki",
|
|
7
7
|
"memo",
|
|
@@ -17,16 +17,16 @@
|
|
|
17
17
|
},
|
|
18
18
|
"license": "Apache-2.0",
|
|
19
19
|
"author": "D. Olsson <hi@senzilla.io>",
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
"jobs": [
|
|
21
|
+
{
|
|
22
|
+
"user": "Empowered Engineers",
|
|
23
|
+
"goal": "Operate a Predictable Agent Team",
|
|
24
|
+
"trigger": "An agent finishes a session and its findings vanish because there is no shared memory to write them to.",
|
|
25
|
+
"bigHire": "give agent teams stable memory that persists across sessions.",
|
|
26
|
+
"littleHire": "send a memo or update a storyboard without managing the wiki infrastructure.",
|
|
27
|
+
"competesWith": "git commit messages as memory; ephemeral conversation context; starting every session from scratch"
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
30
|
"type": "module",
|
|
31
31
|
"main": "./src/index.js",
|
|
32
32
|
"exports": {
|
|
@@ -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/agent-roster.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readdirSync, statSync } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { BROADCAST_TARGET } from "./constants.js";
|
|
4
4
|
|
|
5
|
+
/** List all agent markdown files in the agents directory, returning agent names and summary paths. */
|
|
5
6
|
export function listAgents(
|
|
6
7
|
{ agentsDir, wikiRoot },
|
|
7
8
|
fs = { readdirSync, statSync },
|
package/src/block-renderer.js
CHANGED
|
@@ -2,13 +2,16 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { analyze, renderChart, MIN_POINTS } from "@forwardimpact/libxmr";
|
|
4
4
|
|
|
5
|
+
/** Error thrown when an XmR block cannot be rendered due to missing CSV or metric. */
|
|
5
6
|
export class BlockRenderError extends Error {
|
|
7
|
+
/** Create a BlockRenderError with the given reason string. */
|
|
6
8
|
constructor(reason) {
|
|
7
9
|
super(reason);
|
|
8
10
|
this.name = "BlockRenderError";
|
|
9
11
|
}
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
/** Render an XmR chart block for a metric by reading its CSV and producing markdown lines. */
|
|
12
15
|
export function renderBlock({
|
|
13
16
|
metric,
|
|
14
17
|
csvPath,
|
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",
|
|
@@ -17,7 +21,8 @@ function deriveWikiUrl(parentDir) {
|
|
|
17
21
|
return base + ".wiki.git";
|
|
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. */
|
|
25
|
+
export async function runInitCommand(values, _args, cli) {
|
|
21
26
|
const logger = { debug() {} };
|
|
22
27
|
const finder = new Finder(fsAsync, logger, process);
|
|
23
28
|
const projectRoot = finder.findProjectRoot(process.cwd());
|
|
@@ -36,7 +41,12 @@ export function runInitCommand(values, _args, cli) {
|
|
|
36
41
|
return;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
const
|
|
44
|
+
const config = await createScriptConfig("wiki");
|
|
45
|
+
const repo = new WikiRepo({
|
|
46
|
+
wikiDir,
|
|
47
|
+
parentDir: projectRoot,
|
|
48
|
+
resolveToken: () => config.ghToken(),
|
|
49
|
+
});
|
|
40
50
|
|
|
41
51
|
const cloneResult = repo.ensureCloned(wikiUrl);
|
|
42
52
|
if (!cloneResult.cloned) {
|
package/src/commands/memo.js
CHANGED
|
@@ -15,6 +15,38 @@ function writeAndCheck(summaryPath, sender, message, today) {
|
|
|
15
15
|
process.stdout.write(`wrote ${result.path}\n`);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function resolveTargetPath(wikiRoot, target) {
|
|
19
|
+
const summaryPath = path.join(wikiRoot, target + ".md");
|
|
20
|
+
const resolvedRoot = path.resolve(wikiRoot);
|
|
21
|
+
const resolvedTarget = path.resolve(summaryPath);
|
|
22
|
+
const relative = path.relative(resolvedRoot, resolvedTarget);
|
|
23
|
+
const escapesRoot =
|
|
24
|
+
relative === "" || relative.startsWith("..") || path.isAbsolute(relative);
|
|
25
|
+
return { summaryPath, escapesRoot };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeSingleTarget({ wikiRoot, target, sender, message, today, cli }) {
|
|
29
|
+
const { summaryPath, escapesRoot } = resolveTargetPath(wikiRoot, target);
|
|
30
|
+
if (escapesRoot) {
|
|
31
|
+
cli.usageError(`target escapes wiki root: ${target}`);
|
|
32
|
+
process.exit(2);
|
|
33
|
+
}
|
|
34
|
+
if (!existsSync(summaryPath)) {
|
|
35
|
+
cli.usageError(`target summary not found: ${summaryPath}`);
|
|
36
|
+
process.exit(2);
|
|
37
|
+
}
|
|
38
|
+
writeAndCheck(summaryPath, sender, message, today);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function writeBroadcast({ agentsDir, wikiRoot, sender, message, today }) {
|
|
42
|
+
const agents = listAgents({ agentsDir, wikiRoot });
|
|
43
|
+
for (const { agent, summaryPath } of agents) {
|
|
44
|
+
if (agent === sender) continue;
|
|
45
|
+
writeAndCheck(summaryPath, sender, message, today);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 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. */
|
|
18
50
|
export function runMemoCommand(values, _args, cli) {
|
|
19
51
|
const sender = values.from || process.env.LIBEVAL_AGENT_PROFILE;
|
|
20
52
|
|
|
@@ -44,17 +76,21 @@ export function runMemoCommand(values, _args, cli) {
|
|
|
44
76
|
const today = new Date().toISOString().slice(0, 10);
|
|
45
77
|
|
|
46
78
|
if (values.to === BROADCAST_TARGET) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
79
|
+
writeBroadcast({
|
|
80
|
+
agentsDir,
|
|
81
|
+
wikiRoot,
|
|
82
|
+
sender,
|
|
83
|
+
message: values.message,
|
|
84
|
+
today,
|
|
85
|
+
});
|
|
52
86
|
} else {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
87
|
+
writeSingleTarget({
|
|
88
|
+
wikiRoot,
|
|
89
|
+
target: values.to,
|
|
90
|
+
sender,
|
|
91
|
+
message: values.message,
|
|
92
|
+
today,
|
|
93
|
+
cli,
|
|
94
|
+
});
|
|
59
95
|
}
|
|
60
96
|
}
|
package/src/commands/refresh.js
CHANGED
|
@@ -12,6 +12,7 @@ function currentStoryboardPath() {
|
|
|
12
12
|
return `wiki/storyboard-${yyyy}-M${mm}.md`;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/** Re-render all XmR chart blocks in a storyboard file by scanning markers and splicing updated content. */
|
|
15
16
|
export function runRefreshCommand(values, args, cli) {
|
|
16
17
|
const logger = { debug() {} };
|
|
17
18
|
const finder = new Finder(fsAsync, logger, process);
|
package/src/commands/sync.js
CHANGED
|
@@ -1,15 +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
|
+
async function buildRepo(values) {
|
|
7
8
|
const logger = { debug() {} };
|
|
8
9
|
const finder = new Finder(fsAsync, logger, process);
|
|
9
10
|
const projectRoot = finder.findProjectRoot(process.cwd());
|
|
10
11
|
const wikiDir = path.resolve(projectRoot, values["wiki-root"] ?? "wiki");
|
|
11
12
|
|
|
12
|
-
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);
|
|
13
24
|
repo.inheritIdentity();
|
|
14
25
|
|
|
15
26
|
const result = repo.commitAndPush("wiki: update from session");
|
|
@@ -20,13 +31,9 @@ export function runPushCommand(values, _args, cli) {
|
|
|
20
31
|
}
|
|
21
32
|
}
|
|
22
33
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
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 });
|
|
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. */
|
|
35
|
+
export async function runPullCommand(values, _args, cli) {
|
|
36
|
+
const repo = await buildRepo(values);
|
|
30
37
|
repo.inheritIdentity();
|
|
31
38
|
|
|
32
39
|
try {
|
package/src/marker-migrator.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { MEMO_INBOX_MARKER, INBOX_HEADING } from "./constants.js";
|
|
3
3
|
import { listAgents } from "./agent-roster.js";
|
|
4
4
|
|
|
5
|
+
/** Insert memo:inbox markers into agent summary files that have an inbox heading but no marker yet. */
|
|
5
6
|
export function insertMarkers(
|
|
6
7
|
{ agentsDir, wikiRoot },
|
|
7
8
|
fs = { readFileSync, writeFileSync },
|
package/src/marker-scanner.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const OPEN_RE = /^<!--\s*xmr:([^:\s]+):([^\s]+)\s*-->\s*$/;
|
|
2
2
|
const CLOSE_RE = /^<!--\s*\/xmr\s*-->\s*$/;
|
|
3
3
|
|
|
4
|
+
/** Scan text for paired xmr open/close HTML comment markers and return their line positions and metadata. */
|
|
4
5
|
export function scanMarkers(text) {
|
|
5
6
|
const lines = text.split("\n");
|
|
6
7
|
const pairs = [];
|
package/src/memo-writer.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { MEMO_INBOX_MARKER } from "./constants.js";
|
|
3
3
|
|
|
4
|
+
/** Append a timestamped memo bullet below the inbox marker in an agent's summary file. */
|
|
4
5
|
export function writeMemo(
|
|
5
6
|
{ summaryPath, sender, message, today },
|
|
6
7
|
fs = { readFileSync, writeFileSync },
|
package/src/skill-roster.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdirSync, statSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
+
/** List all kata-prefixed skill directory names under the skills directory, sorted alphabetically. */
|
|
4
5
|
export function listSkills({ skillsDir }, fs = { readdirSync, statSync }) {
|
|
5
6
|
const entries = fs.readdirSync(skillsDir);
|
|
6
7
|
const skills = [];
|
package/src/wiki-repo.js
CHANGED
|
@@ -3,7 +3,9 @@ import { spawnSync } from "node:child_process";
|
|
|
3
3
|
const CREDENTIAL_HELPER_BODY =
|
|
4
4
|
'!f() { echo username=x-access-token; echo "password=${GH_TOKEN:-$GITHUB_TOKEN}"; }; f';
|
|
5
5
|
|
|
6
|
+
/** Error thrown when a wiki pull encounters a rebase conflict that cannot be resolved automatically. */
|
|
6
7
|
export class WikiPullConflict extends Error {
|
|
8
|
+
/** Create a WikiPullConflict with the stderr output from the failed rebase. */
|
|
7
9
|
constructor(stderr) {
|
|
8
10
|
super("rebase conflict on pull");
|
|
9
11
|
this.name = "WikiPullConflict";
|
|
@@ -11,6 +13,7 @@ export class WikiPullConflict extends Error {
|
|
|
11
13
|
}
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
/** Prepend credential-helper config arguments to a git command when a token is available. */
|
|
14
17
|
export function buildAuthArgs(args, token) {
|
|
15
18
|
if (token) {
|
|
16
19
|
return [
|
|
@@ -24,15 +27,38 @@ export function buildAuthArgs(args, token) {
|
|
|
24
27
|
return [...args];
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
/** Git operations wrapper for the GitHub wiki repository used as agent team memory. */
|
|
27
31
|
export class WikiRepo {
|
|
28
32
|
#wikiDir;
|
|
29
33
|
#parentDir;
|
|
34
|
+
#resolveToken;
|
|
30
35
|
|
|
31
|
-
|
|
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
|
+
}
|
|
32
56
|
this.#wikiDir = wikiDir;
|
|
33
57
|
this.#parentDir = parentDir;
|
|
58
|
+
this.#resolveToken = resolveToken;
|
|
34
59
|
}
|
|
35
60
|
|
|
61
|
+
/** Check whether the wiki directory is an initialized git repository. */
|
|
36
62
|
isCloned() {
|
|
37
63
|
const r = spawnSync(
|
|
38
64
|
"git",
|
|
@@ -44,6 +70,7 @@ export class WikiRepo {
|
|
|
44
70
|
return r.status === 0;
|
|
45
71
|
}
|
|
46
72
|
|
|
73
|
+
/** Clone the wiki from the given URL if it is not already cloned. */
|
|
47
74
|
ensureCloned(url) {
|
|
48
75
|
if (this.isCloned()) return { cloned: true, reason: "already-cloned" };
|
|
49
76
|
const r = this.#authGit(["clone", url, this.#wikiDir]);
|
|
@@ -56,6 +83,7 @@ export class WikiRepo {
|
|
|
56
83
|
return { cloned: true, reason: "cloned" };
|
|
57
84
|
}
|
|
58
85
|
|
|
86
|
+
/** Copy git user.name and user.email from the parent repository into the wiki repository. */
|
|
59
87
|
inheritIdentity() {
|
|
60
88
|
const name = this.#parentConfig("user.name");
|
|
61
89
|
const email = this.#parentConfig("user.email");
|
|
@@ -63,15 +91,18 @@ export class WikiRepo {
|
|
|
63
91
|
if (email) this.#git(["config", "user.email", email]);
|
|
64
92
|
}
|
|
65
93
|
|
|
94
|
+
/** Fetch the latest master branch from the wiki remote using token auth if available. */
|
|
66
95
|
fetch() {
|
|
67
96
|
this.#authGit(["-C", this.#wikiDir, "fetch", "origin", "master"]);
|
|
68
97
|
}
|
|
69
98
|
|
|
99
|
+
/** Return true if the wiki working tree has no uncommitted changes. */
|
|
70
100
|
isClean() {
|
|
71
101
|
const r = this.#git(["status", "--porcelain"]);
|
|
72
102
|
return r.stdout.toString().trim() === "";
|
|
73
103
|
}
|
|
74
104
|
|
|
105
|
+
/** Fetch and rebase on origin/master, throwing WikiPullConflict if the rebase fails. */
|
|
75
106
|
pull() {
|
|
76
107
|
this.fetch();
|
|
77
108
|
const r = this.#git(["rebase", "origin/master"]);
|
|
@@ -81,10 +112,16 @@ export class WikiRepo {
|
|
|
81
112
|
}
|
|
82
113
|
}
|
|
83
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. */
|
|
84
116
|
commitAndPush(message) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
+
}
|
|
88
125
|
this.fetch();
|
|
89
126
|
const rebase = this.#git(["rebase", "origin/master"]);
|
|
90
127
|
if (rebase.status !== 0) {
|
|
@@ -95,6 +132,12 @@ export class WikiRepo {
|
|
|
95
132
|
return { pushed: true, reason: "pushed" };
|
|
96
133
|
}
|
|
97
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
|
+
|
|
98
141
|
#parentConfig(key) {
|
|
99
142
|
const r = spawnSync(
|
|
100
143
|
"git",
|
|
@@ -111,11 +154,14 @@ export class WikiRepo {
|
|
|
111
154
|
}
|
|
112
155
|
|
|
113
156
|
#authGit(args) {
|
|
114
|
-
const token =
|
|
157
|
+
const token = this.#resolveToken();
|
|
115
158
|
const fullArgs = buildAuthArgs(args, token);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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 });
|
|
120
166
|
}
|
|
121
167
|
}
|