@ferrule-io/ok-fine 0.3.6 → 0.3.8

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.
Files changed (68) hide show
  1. package/README.md +5 -4
  2. package/dist/auth/discovery.js +11 -0
  3. package/dist/auth/verifier.js +44 -1
  4. package/dist/cli-options.js +1 -1
  5. package/dist/cli.js +12 -14
  6. package/dist/config.js +130 -6
  7. package/dist/data-dir-lock.js +58 -19
  8. package/dist/http/rest.js +1 -1
  9. package/dist/http/ui.js +118 -0
  10. package/dist/local-host.js +157 -0
  11. package/dist/mcp/server.js +5 -4
  12. package/dist/okf/lint.js +20 -0
  13. package/dist/server.js +138 -29
  14. package/dist/stdio-proxy.js +349 -0
  15. package/dist/store/fs-util.js +59 -4
  16. package/dist/store/git-backend.js +208 -59
  17. package/dist/store/git.js +92 -19
  18. package/dist/ui/assets/bash-BiGZg9Om.js +1 -0
  19. package/dist/ui/assets/css-BGIxQoYs.js +1 -0
  20. package/dist/ui/assets/css-DUjzNfgv.js +1 -0
  21. package/dist/ui/assets/diff-woXpYk--.js +1 -0
  22. package/dist/ui/assets/dockerfile-IyjqRm3v.js +1 -0
  23. package/dist/ui/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  24. package/dist/ui/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  25. package/dist/ui/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  26. package/dist/ui/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  27. package/dist/ui/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  28. package/dist/ui/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  29. package/dist/ui/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  30. package/dist/ui/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  31. package/dist/ui/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  32. package/dist/ui/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  33. package/dist/ui/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  34. package/dist/ui/assets/github-dark-default-DXG-b-1a.js +1 -0
  35. package/dist/ui/assets/github-light-default-BXViO-2h.js +1 -0
  36. package/dist/ui/assets/go-rLFTqkRN.js +1 -0
  37. package/dist/ui/assets/highlight-CW19vWvw.js +153 -0
  38. package/dist/ui/assets/html-BMn1ogTm.js +1 -0
  39. package/dist/ui/assets/html-BlpXnbzK.js +1 -0
  40. package/dist/ui/assets/index-0b2NNRC5.js +127 -0
  41. package/dist/ui/assets/index-CFuTAZQM.css +1 -0
  42. package/dist/ui/assets/java-BtIQNnN0.js +1 -0
  43. package/dist/ui/assets/java-DxlWrXwe.js +1 -0
  44. package/dist/ui/assets/javascript-BzbQS41l.js +1 -0
  45. package/dist/ui/assets/javascript-Yelw-5ZN.js +1 -0
  46. package/dist/ui/assets/json-qhed-kSA.js +1 -0
  47. package/dist/ui/assets/jsx-D3KKoC5L.js +1 -0
  48. package/dist/ui/assets/jsx-hEbJ82HF.js +1 -0
  49. package/dist/ui/assets/markdown-BYOwaDjH.js +1 -0
  50. package/dist/ui/assets/python-gzcpVVnB.js +1 -0
  51. package/dist/ui/assets/ruby-Di5fYh6S.js +1 -0
  52. package/dist/ui/assets/rust-DpiIf5JO.js +1 -0
  53. package/dist/ui/assets/shellscript-CqHdAlCp.js +1 -0
  54. package/dist/ui/assets/sql-BOsskAmu.js +1 -0
  55. package/dist/ui/assets/sql-CNu3Or2W.js +1 -0
  56. package/dist/ui/assets/toml-CcmNWLt0.js +1 -0
  57. package/dist/ui/assets/tsx-5f3ulCja.js +1 -0
  58. package/dist/ui/assets/tsx-BOvsFOI6.js +1 -0
  59. package/dist/ui/assets/typescript-Utq2Cl8c.js +1 -0
  60. package/dist/ui/assets/typescript-j_1H8WHN.js +1 -0
  61. package/dist/ui/assets/yaml-CxbubiML.js +1 -0
  62. package/dist/ui/assets/yaml-DijMHRDA.js +1 -0
  63. package/dist/ui/favicon.svg +11 -0
  64. package/dist/ui/index.html +16 -0
  65. package/dist/ui/theme-init.js +10 -0
  66. package/dist/version.js +1 -1
  67. package/package.json +25 -4
  68. package/dist/store/repo.js +0 -471
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
2
+ <title>ok-fine</title>
3
+ <defs>
4
+ <linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
5
+ <stop offset="0" stop-color="#8b5cf6"/>
6
+ <stop offset="1" stop-color="#22d3ee"/>
7
+ </linearGradient>
8
+ </defs>
9
+ <rect width="24" height="24" rx="7" fill="url(#g)"/>
10
+ <path d="M7 12.5l3.2 3.2L17 8.8" fill="none" stroke="#fff" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round"/>
11
+ </svg>
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="color-scheme" content="dark light" />
7
+ <link rel="icon" type="image/svg+xml" href="/ui/favicon.svg" />
8
+ <title>ok-fine</title>
9
+ <script src="/ui/theme-init.js"></script>
10
+ <script type="module" crossorigin src="/ui/assets/index-0b2NNRC5.js"></script>
11
+ <link rel="stylesheet" crossorigin href="/ui/assets/index-CFuTAZQM.css">
12
+ </head>
13
+ <body>
14
+ <div id="root"></div>
15
+ </body>
16
+ </html>
@@ -0,0 +1,10 @@
1
+ (() => {
2
+ let pref = "system";
3
+ try {
4
+ const stored = localStorage.getItem("okf.theme");
5
+ if (stored === "light" || stored === "dark" || stored === "system") pref = stored;
6
+ } catch {}
7
+ const dark = pref === "dark" || (pref === "system" && matchMedia("(prefers-color-scheme: dark)").matches);
8
+ document.documentElement.classList.toggle("dark", dark);
9
+ document.documentElement.style.colorScheme = dark ? "dark" : "light";
10
+ })();
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
- export const VERSION = "0.3.6";
1
+ export const VERSION = "0.3.8";
2
2
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ferrule-io/ok-fine",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "OKF v0.2 knowledge repository for AI agents (MCP + REST)",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -29,13 +29,14 @@
29
29
  },
30
30
  "packageManager": "pnpm@11.22.0",
31
31
  "scripts": {
32
- "build": "tsc -p tsconfig.build.json",
33
- "typecheck": "tsc -p tsconfig.json --noEmit",
32
+ "build": "tsc -p tsconfig.build.json && vite build --config web/vite.config.ts",
33
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p web/tsconfig.json",
34
34
  "lint": "biome check --error-on-warnings .",
35
35
  "format": "biome check --write .",
36
36
  "test": "vitest run",
37
37
  "start": "node dist/main.js",
38
- "dev:issuer": "node dist/dev/issuer-cli.js"
38
+ "dev:issuer": "node dist/dev/issuer-cli.js",
39
+ "dev:web": "vite --config web/vite.config.ts"
39
40
  },
40
41
  "dependencies": {
41
42
  "@modelcontextprotocol/server": "^2.3.1",
@@ -50,9 +51,29 @@
50
51
  },
51
52
  "devDependencies": {
52
53
  "@biomejs/biome": "2.5.15",
54
+ "@fontsource-variable/geist": "^5.3.0",
55
+ "@fontsource-variable/geist-mono": "^5.3.0",
53
56
  "@modelcontextprotocol/client": "^2.3.1",
57
+ "@tailwindcss/typography": "^0.5.20",
58
+ "@tailwindcss/vite": "^4.3.3",
59
+ "@tanstack/react-query": "^5.104.1",
54
60
  "@types/node": "^24.19.1",
61
+ "@types/react": "^19.3.0",
62
+ "@types/react-dom": "^19.3.0",
63
+ "@vitejs/plugin-react": "^6.1.1",
64
+ "clsx": "^2.1.1",
65
+ "cmdk": "^1.1.1",
66
+ "dompurify": "^3.4.16",
67
+ "lucide-react": "^1.52.0",
68
+ "motion": "^14.0.0",
69
+ "oauth4webapi": "^3.8.8",
70
+ "react": "^19.3.0",
71
+ "react-dom": "^19.3.0",
72
+ "react-router": "^8.4.0",
73
+ "shiki": "^4.5.0",
74
+ "tailwindcss": "^4.3.3",
55
75
  "typescript": "~5.9.3",
76
+ "vite": "^8.3.2",
56
77
  "vitest": "^4.1.11"
57
78
  }
58
79
  }
@@ -1,471 +0,0 @@
1
- import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { OkfError } from "../errors.js";
4
- import { nowIso } from "../okf/frontmatter.js";
5
- import { PROJECT_RE } from "../okf/paths.js";
6
- import { Git, isPushRejection, redactRemote } from "./git.js";
7
- import { Mutex } from "./mutex.js";
8
- import { pathExists } from "./fs-util.js";
9
- function extractChangedProjects(diffOutput) {
10
- const lines = diffOutput.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
11
- const projects = new Set();
12
- for (const line of lines) {
13
- const firstSegment = line.split("/")[0];
14
- if (firstSegment && PROJECT_RE.test(firstSegment)) {
15
- projects.add(firstSegment);
16
- }
17
- }
18
- return Array.from(projects);
19
- }
20
- export class Repo {
21
- config;
22
- log;
23
- repoDir;
24
- homeDir;
25
- tmpDir;
26
- git;
27
- mutex;
28
- treeChangedHandler = null;
29
- lastSyncAt = null;
30
- lastError = null;
31
- constructor(config, log, repoDir, homeDir, tmpDir) {
32
- this.config = config;
33
- this.log = log;
34
- this.repoDir = repoDir;
35
- this.homeDir = homeDir;
36
- this.tmpDir = tmpDir;
37
- this.git = new Git(repoDir, homeDir, config);
38
- this.mutex = new Mutex();
39
- }
40
- get hasRemote() {
41
- return Boolean(this.config.gitRemoteUrl && this.config.gitRemoteUrl.trim().length > 0);
42
- }
43
- setTreeChangedHandler(fn) {
44
- this.treeChangedHandler = fn;
45
- }
46
- async idle() {
47
- await this.mutex.idle();
48
- }
49
- static async open(config, log) {
50
- const repoDir = join(config.dataDir, "repo");
51
- const homeDir = join(config.dataDir, "home");
52
- const tmpDir = join(config.dataDir, "tmp");
53
- await mkdir(repoDir, { recursive: true });
54
- await mkdir(homeDir, { recursive: true });
55
- await mkdir(tmpDir, { recursive: true });
56
- // Empty tmp directory
57
- try {
58
- const tmpEntries = await readdir(tmpDir);
59
- for (const entry of tmpEntries) {
60
- await rm(join(tmpDir, entry), { recursive: true, force: true });
61
- }
62
- }
63
- catch {
64
- // ignore
65
- }
66
- const repo = new Repo(config, log, repoDir, homeDir, tmpDir);
67
- await repo.git.prepareSsh();
68
- // Check if git initialized
69
- const gitDirExists = await pathExists(join(repoDir, ".git"));
70
- if (!gitDirExists) {
71
- await repo.git.run(["init", "-b", config.gitBranch]);
72
- }
73
- const headRev = await repo.git.run(["rev-parse", "--verify", "HEAD"], { allowFail: true });
74
- let headHasCommits = headRev.code === 0;
75
- if (repo.hasRemote) {
76
- const remotes = await repo.git.run(["remote"], { allowFail: true });
77
- const remoteList = remotes.stdout.split(/\r?\n/).map((r) => r.trim());
78
- if (remoteList.includes("origin")) {
79
- await repo.git.run(["remote", "set-url", "origin", config.gitRemoteUrl]);
80
- }
81
- else {
82
- await repo.git.run(["remote", "add", "origin", config.gitRemoteUrl]);
83
- }
84
- const fetchRes = await repo.git.run(["fetch", "origin", config.gitBranch], { allowFail: true });
85
- if (fetchRes.code !== 0) {
86
- const isRefNotFound = fetchRes.stderr.includes("couldn't find remote ref");
87
- if (!isRefNotFound) {
88
- if (!headHasCommits) {
89
- throw new Error(`Failed to fetch from remote on empty local repository:\n${fetchRes.stderr}`);
90
- }
91
- else {
92
- log.warn({ stderr: fetchRes.stderr }, "Fetch failed on startup; continuing with local commits");
93
- }
94
- }
95
- }
96
- const remoteBranchRev = await repo.git.run(["rev-parse", "--verify", `origin/${config.gitBranch}`], { allowFail: true });
97
- const remoteBranchExists = remoteBranchRev.code === 0;
98
- if (!headHasCommits) {
99
- if (remoteBranchExists) {
100
- await repo.git.run(["checkout", "-B", config.gitBranch, `origin/${config.gitBranch}`]);
101
- headHasCommits = true;
102
- }
103
- else {
104
- // Initialize empty bundle root files
105
- await repo.ensureAttributesAndIgnore();
106
- await repo.git.run([
107
- "commit",
108
- "--author=process:ok-fine <ok-fine@localhost>",
109
- "-m",
110
- "okf: initialize knowledge repository",
111
- ]);
112
- await repo.git.run(["push", "-u", "origin", `${config.gitBranch}:${config.gitBranch}`], {
113
- allowFail: true,
114
- });
115
- headHasCommits = true;
116
- }
117
- }
118
- else {
119
- await repo.sync();
120
- }
121
- }
122
- else {
123
- if (!headHasCommits) {
124
- await repo.ensureAttributesAndIgnore();
125
- await repo.git.run([
126
- "commit",
127
- "--author=process:ok-fine <ok-fine@localhost>",
128
- "-m",
129
- "okf: initialize knowledge repository",
130
- ]);
131
- headHasCommits = true;
132
- }
133
- }
134
- // Ensure .gitattributes and .gitignore exist with required lines
135
- const attrChanged = await repo.ensureAttributesAndIgnore();
136
- if (attrChanged) {
137
- await repo.git.run(["add", ".gitattributes", ".gitignore"]);
138
- const diffCheck = await repo.git.run(["diff", "--cached", "--quiet"], { allowFail: true });
139
- if (diffCheck.code !== 0) {
140
- await repo.git.run([
141
- "commit",
142
- "--author=process:ok-fine <ok-fine@localhost>",
143
- "-m",
144
- "okf: configure merge and ignore attributes",
145
- ]);
146
- if (repo.hasRemote) {
147
- await repo.git.run(["push", "origin", `HEAD:refs/heads/${config.gitBranch}`], {
148
- allowFail: true,
149
- });
150
- }
151
- }
152
- }
153
- return repo;
154
- }
155
- async ensureAttributesAndIgnore() {
156
- let changed = false;
157
- const gitattributesPath = join(this.repoDir, ".gitattributes");
158
- let attrContent = "";
159
- try {
160
- attrContent = await readFile(gitattributesPath, "utf8");
161
- }
162
- catch {
163
- // not exists
164
- }
165
- const requiredAttrLines = [
166
- "**/index.md merge=union",
167
- "**/log.md merge=union",
168
- ];
169
- let newAttrContent = attrContent;
170
- for (const line of requiredAttrLines) {
171
- if (!newAttrContent.includes(line)) {
172
- if (newAttrContent.length > 0 && !newAttrContent.endsWith("\n")) {
173
- newAttrContent += "\n";
174
- }
175
- newAttrContent += `${line}\n`;
176
- }
177
- }
178
- if (newAttrContent !== attrContent) {
179
- await writeFile(gitattributesPath, newAttrContent, "utf8");
180
- await this.git.run(["add", ".gitattributes"]);
181
- changed = true;
182
- }
183
- const gitignorePath = join(this.repoDir, ".gitignore");
184
- let ignoreContent = "";
185
- try {
186
- ignoreContent = await readFile(gitignorePath, "utf8");
187
- }
188
- catch {
189
- // not exists
190
- }
191
- const requiredIgnoreLine = ".*.tmp";
192
- let newIgnoreContent = ignoreContent;
193
- if (!newIgnoreContent.includes(requiredIgnoreLine)) {
194
- if (newIgnoreContent.length > 0 && !newIgnoreContent.endsWith("\n")) {
195
- newIgnoreContent += "\n";
196
- }
197
- newIgnoreContent += `${requiredIgnoreLine}\n`;
198
- }
199
- if (newIgnoreContent !== ignoreContent) {
200
- await writeFile(gitignorePath, newIgnoreContent, "utf8");
201
- await this.git.run(["add", ".gitignore"]);
202
- changed = true;
203
- }
204
- return changed;
205
- }
206
- async commitInternal(subject, author, principal, projects) {
207
- const addArgs = projects.length > 0 ? ["add", "-A", "--", ...projects] : ["add", "-A"];
208
- await this.git.run(addArgs);
209
- const diffCheck = await this.git.run(["diff", "--cached", "--quiet"], { allowFail: true });
210
- if (diffCheck.code === 0) {
211
- return null;
212
- }
213
- await this.git.run([
214
- "commit",
215
- `--author=${author} <ok-fine@localhost>`,
216
- "-m",
217
- subject,
218
- "-m",
219
- `Okf-Principal: sub=${principal.subject} client=${principal.clientId}`,
220
- ]);
221
- const rev = await this.git.run(["rev-parse", "HEAD"]);
222
- return rev.stdout.trim();
223
- }
224
- /**
225
- * Aborts a failed rebase, keeps the original local commits on `ok-fine/conflict-<stamp>` (always as a
226
- * local branch, plus on the remote when the push succeeds), then resets to the remote branch.
227
- * Returns a human-readable description of where the commits were preserved.
228
- */
229
- async preserveConflict() {
230
- await this.git.run(["rebase", "--abort"], { allowFail: true });
231
- const stamp = new Date()
232
- .toISOString()
233
- .replace(/[-:]/g, "")
234
- .replace(/\.\d{3}/, "")
235
- .replace(/Z$/, "Z");
236
- const conflictBranch = `ok-fine/conflict-${stamp}`;
237
- // The local branch keeps the commits reachable on the PVC even if the remote push fails.
238
- await this.git.run(["branch", "-f", conflictBranch, "HEAD"]);
239
- const pushRes = await this.git.run(["push", "origin", `refs/heads/${conflictBranch}:refs/heads/${conflictBranch}`], { allowFail: true });
240
- await this.git.run(["reset", "--hard", `origin/${this.config.gitBranch}`]);
241
- let message = `local commits preserved on ${conflictBranch}`;
242
- if (pushRes.code !== 0) {
243
- const reason = pushRes.stderr.trim().split("\n")[0] ?? "";
244
- message += ` (local branch only; pushing it failed: ${reason})`;
245
- }
246
- this.log.error({ stderr: pushRes.code !== 0 ? pushRes.stderr : undefined }, `rebase conflict; ${message}`);
247
- return message;
248
- }
249
- async transaction(spec, work) {
250
- return this.mutex.run(async () => {
251
- const warnings = [];
252
- // Step 1: Remote sync check before work
253
- if (this.hasRemote) {
254
- const fetchRes = await this.git.run(["fetch", "origin", this.config.gitBranch], { allowFail: true });
255
- if (fetchRes.code !== 0) {
256
- const firstLine = fetchRes.stderr.split(/\r?\n/)[0] ?? "unknown error";
257
- warnings.push(`remote unreachable: ${firstLine}`);
258
- }
259
- else {
260
- const behindRes = await this.git.run(["rev-list", "--count", `HEAD..origin/${this.config.gitBranch}`], { allowFail: true });
261
- const behind = Number.parseInt(behindRes.stdout.trim(), 10) || 0;
262
- if (behind > 0) {
263
- const before = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
264
- const rebaseRes = await this.git.run(["rebase", `origin/${this.config.gitBranch}`], { allowFail: true });
265
- if (rebaseRes.code !== 0) {
266
- warnings.push(await this.preserveConflict());
267
- }
268
- const after = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
269
- if (before !== after) {
270
- const diffRes = await this.git.run(["diff", "--name-only", before, after]);
271
- const changed = extractChangedProjects(diffRes.stdout);
272
- if (this.treeChangedHandler && changed.length > 0) {
273
- await this.treeChangedHandler(changed);
274
- }
275
- }
276
- }
277
- }
278
- }
279
- // Step 2 & 3: Run work and commit
280
- try {
281
- const workRes = await work();
282
- let commitSha = null;
283
- if (workRes.commit) {
284
- const addArgs = spec.projects.length > 0
285
- ? ["add", "-A", "--", ...spec.projects]
286
- : ["add", "-A"];
287
- await this.git.run(addArgs);
288
- const diffCheck = await this.git.run(["diff", "--cached", "--quiet"], { allowFail: true });
289
- if (diffCheck.code !== 0) {
290
- const c = workRes.commit;
291
- const commitArgs = [
292
- "commit",
293
- `--author=${c.author} <ok-fine@localhost>`,
294
- "-m",
295
- c.subject,
296
- ];
297
- if (c.body && c.body.trim().length > 0) {
298
- commitArgs.push("-m", c.body);
299
- }
300
- commitArgs.push("-m", `Okf-Principal: sub=${c.principal.subject} client=${c.principal.clientId}`);
301
- await this.git.run(commitArgs);
302
- commitSha = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
303
- }
304
- }
305
- // Step 4: Push commit if made
306
- let pushed = null;
307
- if (commitSha !== null && this.hasRemote) {
308
- let pushSuccess = false;
309
- for (let attempt = 0; attempt < 3; attempt++) {
310
- const pushRes = await this.git.run(["push", "origin", `HEAD:refs/heads/${this.config.gitBranch}`], { allowFail: true });
311
- if (pushRes.code === 0) {
312
- pushSuccess = true;
313
- pushed = true;
314
- break;
315
- }
316
- if (isPushRejection(pushRes.stderr)) {
317
- const pre = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
318
- await this.git.run(["fetch", "origin", this.config.gitBranch], { allowFail: true });
319
- const rebaseRes = await this.git.run(["rebase", `origin/${this.config.gitBranch}`], { allowFail: true });
320
- if (rebaseRes.code === 0) {
321
- const post = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
322
- const diffRes = await this.git.run(["diff", "--name-only", pre, post]);
323
- const changed = extractChangedProjects(diffRes.stdout);
324
- if (this.treeChangedHandler && changed.length > 0) {
325
- await this.treeChangedHandler(changed);
326
- }
327
- continue; // retry push
328
- }
329
- else {
330
- await this.git.run(["rebase", "--abort"], { allowFail: true });
331
- await this.git.run(["reset", "--hard", `origin/${this.config.gitBranch}`]);
332
- if (this.treeChangedHandler && spec.projects.length > 0) {
333
- await this.treeChangedHandler(spec.projects);
334
- }
335
- throw new OkfError("upstream_conflict", 409, "change conflicts with a concurrent upstream edit; re-read and retry");
336
- }
337
- }
338
- else {
339
- pushed = false;
340
- const firstLine = pushRes.stderr.split(/\r?\n/)[0] ?? "unknown error";
341
- warnings.push(`push failed; will retry on next sync: ${firstLine}`);
342
- break;
343
- }
344
- }
345
- if (!pushSuccess && pushed === null) {
346
- pushed = false;
347
- }
348
- }
349
- return {
350
- value: workRes.value,
351
- commit: commitSha,
352
- pushed,
353
- warnings,
354
- };
355
- }
356
- catch (err) {
357
- await this.git.run(["reset", "--hard", "HEAD"], { allowFail: true });
358
- const cleanArgs = spec.projects.length > 0 ? ["clean", "-fd", "--", ...spec.projects] : ["clean", "-fd"];
359
- await this.git.run(cleanArgs, { allowFail: true });
360
- if (this.treeChangedHandler && spec.projects.length > 0) {
361
- await this.treeChangedHandler(spec.projects);
362
- }
363
- throw err;
364
- }
365
- });
366
- }
367
- async sync() {
368
- return this.mutex.run(async () => {
369
- if (!this.hasRemote) {
370
- return this.syncStatus();
371
- }
372
- const before = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
373
- const fetchRes = await this.git.run(["fetch", "origin", this.config.gitBranch], { allowFail: true });
374
- if (fetchRes.code !== 0) {
375
- this.lastError = fetchRes.stderr.split(/\r?\n/)[0] ?? "fetch failed";
376
- return this.syncStatus();
377
- }
378
- const behindRes = await this.git.run(["rev-list", "--count", `HEAD..origin/${this.config.gitBranch}`], { allowFail: true });
379
- const behind = Number.parseInt(behindRes.stdout.trim(), 10) || 0;
380
- let runError = null;
381
- if (behind > 0) {
382
- const rebaseRes = await this.git.run(["rebase", `origin/${this.config.gitBranch}`], { allowFail: true });
383
- if (rebaseRes.code !== 0) {
384
- runError = `rebase conflict; ${await this.preserveConflict()}`;
385
- }
386
- const after = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
387
- if (before !== after) {
388
- const diffRes = await this.git.run(["diff", "--name-only", before, after]);
389
- const changed = extractChangedProjects(diffRes.stdout);
390
- if (this.treeChangedHandler && changed.length > 0) {
391
- await this.treeChangedHandler(changed);
392
- }
393
- }
394
- }
395
- const aheadRes = await this.git.run(["rev-list", "--count", `origin/${this.config.gitBranch}..HEAD`], { allowFail: true });
396
- const ahead = Number.parseInt(aheadRes.stdout.trim(), 10) || 0;
397
- if (ahead > 0) {
398
- const pushRes = await this.git.run(["push", "origin", `HEAD:refs/heads/${this.config.gitBranch}`], { allowFail: true });
399
- if (pushRes.code !== 0) {
400
- const pushError = `push failed: ${pushRes.stderr.split(/\r?\n/)[0] ?? "unknown error"}`;
401
- runError = runError ? `${runError}; ${pushError}` : pushError;
402
- }
403
- }
404
- // lastError describes the most recent sync run only.
405
- this.lastError = runError;
406
- this.lastSyncAt = nowIso();
407
- return this.syncStatus();
408
- });
409
- }
410
- async syncStatus() {
411
- const remote = this.config.gitRemoteUrl ? redactRemote(this.config.gitRemoteUrl) : null;
412
- let ahead = 0;
413
- let behind = 0;
414
- if (this.hasRemote) {
415
- const aheadRes = await this.git.run(["rev-list", "--count", `origin/${this.config.gitBranch}..HEAD`], { allowFail: true });
416
- if (aheadRes.code === 0) {
417
- ahead = Number.parseInt(aheadRes.stdout.trim(), 10) || 0;
418
- }
419
- const behindRes = await this.git.run(["rev-list", "--count", `HEAD..origin/${this.config.gitBranch}`], { allowFail: true });
420
- if (behindRes.code === 0) {
421
- behind = Number.parseInt(behindRes.stdout.trim(), 10) || 0;
422
- }
423
- }
424
- return {
425
- remote,
426
- branch: this.config.gitBranch,
427
- lastSyncAt: this.lastSyncAt,
428
- lastError: this.lastError,
429
- ahead,
430
- behind,
431
- };
432
- }
433
- async history(path, limit = 20) {
434
- const clampedLimit = Math.max(1, Math.min(100, limit));
435
- const args = [
436
- "log",
437
- "-n",
438
- String(clampedLimit),
439
- "--format=%H%x1f%aI%x1f%an%x1f%s%x1f%(trailers:key=Okf-Principal,valueonly)%x1e",
440
- ];
441
- if (path) {
442
- args.push("--", path);
443
- }
444
- const res = await this.git.run(args, { allowFail: true });
445
- if (res.code !== 0) {
446
- return [];
447
- }
448
- const entries = res.stdout.split("\x1e").filter((e) => e.trim().length > 0);
449
- const commits = [];
450
- for (const entry of entries) {
451
- const parts = entry.trim().split("\x1f");
452
- const sha = parts[0] ?? "";
453
- const at = parts[1] ?? "";
454
- const actor = parts[2] ?? "";
455
- const subject = parts[3] ?? "";
456
- const principalRaw = parts[4]?.trim();
457
- commits.push({
458
- sha,
459
- at,
460
- actor,
461
- subject,
462
- principal: principalRaw && principalRaw.length > 0 ? principalRaw : null,
463
- });
464
- }
465
- return commits;
466
- }
467
- archiveStream(project) {
468
- return this.git.spawnStdout(["archive", "--format=tar.gz", "HEAD", "--", project]);
469
- }
470
- }
471
- //# sourceMappingURL=repo.js.map