@ferrule-io/ok-fine 0.0.0-stage → 0.3.6
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/LICENSE +202 -0
- package/README.md +92 -2
- package/dist/auth/discovery.js +56 -0
- package/dist/auth/http-auth.js +50 -0
- package/dist/auth/verifier.js +133 -0
- package/dist/cli-options.js +99 -0
- package/dist/cli.js +115 -0
- package/dist/config.js +196 -0
- package/dist/data-dir-lock.js +60 -0
- package/dist/errors.js +13 -0
- package/dist/http/mcp-route.js +32 -0
- package/dist/http/rest.js +243 -0
- package/dist/main.js +32 -0
- package/dist/mcp/server.js +202 -0
- package/dist/okf/concept.js +99 -0
- package/dist/okf/frontmatter.js +180 -0
- package/dist/okf/index-file.js +74 -0
- package/dist/okf/lint.js +460 -0
- package/dist/okf/log-file.js +74 -0
- package/dist/okf/markdown.js +106 -0
- package/dist/okf/paths.js +99 -0
- package/dist/okf/repository.js +53 -0
- package/dist/okf/semantics.js +121 -0
- package/dist/server.js +234 -0
- package/dist/service/feedback.js +40 -0
- package/dist/service/knowledge-service.js +819 -0
- package/dist/service/principal.js +22 -0
- package/dist/store/archive.js +108 -0
- package/dist/store/backend.js +4 -0
- package/dist/store/bundle.js +250 -0
- package/dist/store/catalog.js +295 -0
- package/dist/store/fs-util.js +65 -0
- package/dist/store/git-backend.js +564 -0
- package/dist/store/git.js +201 -0
- package/dist/store/mutex.js +19 -0
- package/dist/store/path-index.js +54 -0
- package/dist/store/repo.js +471 -0
- package/dist/version.js +2 -0
- package/package.json +56 -4
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { chmod, copyFile, mkdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { pathExists } from "./fs-util.js";
|
|
5
|
+
export class GitError extends Error {
|
|
6
|
+
args;
|
|
7
|
+
code;
|
|
8
|
+
stderr;
|
|
9
|
+
constructor(args, code, stderr) {
|
|
10
|
+
super(`git ${args.join(" ")} failed with exit code ${code}:\n${stderr}`);
|
|
11
|
+
this.name = "GitError";
|
|
12
|
+
this.args = args;
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.stderr = stderr;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function redactRemote(url) {
|
|
18
|
+
return url.replace(/(https?:\/\/)[^/@\s]+@/g, "$1");
|
|
19
|
+
}
|
|
20
|
+
export function isPushRejection(stderr) {
|
|
21
|
+
return stderr.includes("[rejected]") || stderr.includes("non-fast-forward") || stderr.includes("fetch first");
|
|
22
|
+
}
|
|
23
|
+
const MAX_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
24
|
+
// Inherited values would retarget or re-author ok-fine's own repository.
|
|
25
|
+
const STRIPPED_HOST_GIT_ENV = {
|
|
26
|
+
GIT_ALTERNATE_OBJECT_DIRECTORIES: true,
|
|
27
|
+
GIT_CONFIG: true,
|
|
28
|
+
GIT_CONFIG_PARAMETERS: true,
|
|
29
|
+
GIT_CONFIG_COUNT: true,
|
|
30
|
+
GIT_OBJECT_DIRECTORY: true,
|
|
31
|
+
GIT_DIR: true,
|
|
32
|
+
GIT_WORK_TREE: true,
|
|
33
|
+
GIT_IMPLICIT_WORK_TREE: true,
|
|
34
|
+
GIT_GRAFT_FILE: true,
|
|
35
|
+
GIT_INDEX_FILE: true,
|
|
36
|
+
GIT_NO_REPLACE_OBJECTS: true,
|
|
37
|
+
GIT_REPLACE_REF_BASE: true,
|
|
38
|
+
GIT_PREFIX: true,
|
|
39
|
+
GIT_SHALLOW_FILE: true,
|
|
40
|
+
GIT_COMMON_DIR: true, // `git rev-parse --local-env-vars`
|
|
41
|
+
GIT_AUTHOR_NAME: true,
|
|
42
|
+
GIT_AUTHOR_EMAIL: true,
|
|
43
|
+
GIT_AUTHOR_DATE: true,
|
|
44
|
+
GIT_COMMITTER_NAME: true,
|
|
45
|
+
GIT_COMMITTER_EMAIL: true,
|
|
46
|
+
GIT_COMMITTER_DATE: true,
|
|
47
|
+
};
|
|
48
|
+
export class Git {
|
|
49
|
+
repoDir;
|
|
50
|
+
homeDir;
|
|
51
|
+
config;
|
|
52
|
+
env;
|
|
53
|
+
baseArgs;
|
|
54
|
+
constructor(repoDir, homeDir, config) {
|
|
55
|
+
this.repoDir = repoDir;
|
|
56
|
+
this.homeDir = homeDir;
|
|
57
|
+
this.config = config;
|
|
58
|
+
if (config.gitHostEnv) {
|
|
59
|
+
// No HOME override and no GIT_CONFIG_NOSYSTEM: macOS osxkeychain lives in the system config.
|
|
60
|
+
this.env = {
|
|
61
|
+
...Object.fromEntries(Object.entries(config.gitHostEnv).filter(([k]) => !Object.hasOwn(STRIPPED_HOST_GIT_ENV, k))),
|
|
62
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
63
|
+
LC_ALL: "C",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
this.env = {
|
|
68
|
+
PATH: process.env.PATH ?? "",
|
|
69
|
+
HOME: homeDir,
|
|
70
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
71
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
72
|
+
LC_ALL: "C",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (config.gitHttpUsername) {
|
|
76
|
+
this.env.GIT_HTTP_USERNAME = config.gitHttpUsername;
|
|
77
|
+
}
|
|
78
|
+
if (config.gitHttpPassword) {
|
|
79
|
+
this.env.GIT_HTTP_PASSWORD = config.gitHttpPassword;
|
|
80
|
+
}
|
|
81
|
+
this.baseArgs = [
|
|
82
|
+
"-c",
|
|
83
|
+
`safe.directory=${repoDir}`,
|
|
84
|
+
"-c",
|
|
85
|
+
"core.quotepath=off",
|
|
86
|
+
"-c",
|
|
87
|
+
"commit.gpgsign=false",
|
|
88
|
+
"-c",
|
|
89
|
+
"user.name=ok-fine",
|
|
90
|
+
"-c",
|
|
91
|
+
"user.email=ok-fine@localhost",
|
|
92
|
+
// Host git config must not run user hooks, drop files, convert line endings, or change parsed output.
|
|
93
|
+
"-c",
|
|
94
|
+
"core.hooksPath=/dev/null",
|
|
95
|
+
"-c",
|
|
96
|
+
"core.excludesFile=/dev/null",
|
|
97
|
+
"-c",
|
|
98
|
+
"core.attributesFile=/dev/null",
|
|
99
|
+
"-c",
|
|
100
|
+
"core.autocrlf=false",
|
|
101
|
+
"-c",
|
|
102
|
+
"log.showSignature=false",
|
|
103
|
+
];
|
|
104
|
+
if (config.gitHttpUsername && config.gitHttpPassword) {
|
|
105
|
+
this.baseArgs.push("-c", "credential.helper=", "-c", "credential.helper=!f() { echo username=$GIT_HTTP_USERNAME; echo password=$GIT_HTTP_PASSWORD; }; f");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async prepareSsh() {
|
|
109
|
+
if (!this.config.gitSshKeyPath) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const sshDir = join(this.homeDir, ".ssh");
|
|
113
|
+
await mkdir(sshDir, { recursive: true, mode: 0o700 });
|
|
114
|
+
const targetKeyPath = join(sshDir, "id_okf");
|
|
115
|
+
await copyFile(this.config.gitSshKeyPath, targetKeyPath);
|
|
116
|
+
await chmod(targetKeyPath, 0o600);
|
|
117
|
+
let sshCmd = `ssh -i "${targetKeyPath}" -o IdentitiesOnly=yes -o BatchMode=yes`;
|
|
118
|
+
// The chart always sets the path; the Secret key is optional, so strict mode requires the file to exist.
|
|
119
|
+
const knownHosts = this.config.gitSshKnownHostsPath;
|
|
120
|
+
if (knownHosts && (await pathExists(knownHosts))) {
|
|
121
|
+
sshCmd += ` -o UserKnownHostsFile="${knownHosts}" -o StrictHostKeyChecking=yes`;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
sshCmd += ` -o UserKnownHostsFile="${join(sshDir, "known_hosts")}" -o StrictHostKeyChecking=accept-new`;
|
|
125
|
+
}
|
|
126
|
+
this.env.GIT_SSH_COMMAND = sshCmd;
|
|
127
|
+
}
|
|
128
|
+
run(args, opts) {
|
|
129
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
130
|
+
const allArgs = [...this.baseArgs, ...args];
|
|
131
|
+
// spawn, not execFile: execFile does not forward `detached`.
|
|
132
|
+
const child = spawn("git", allArgs, {
|
|
133
|
+
cwd: this.repoDir,
|
|
134
|
+
env: this.env,
|
|
135
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
136
|
+
// Own session, no controlling terminal: ssh/credential prompts fail instead of taking over the MCP client's terminal.
|
|
137
|
+
detached: true,
|
|
138
|
+
});
|
|
139
|
+
const stdoutChunks = [];
|
|
140
|
+
const stderrChunks = [];
|
|
141
|
+
let outputBytes = 0;
|
|
142
|
+
let overflow = false;
|
|
143
|
+
const collect = (chunks) => (chunk) => {
|
|
144
|
+
outputBytes += chunk.length;
|
|
145
|
+
if (outputBytes > MAX_OUTPUT_BYTES) {
|
|
146
|
+
overflow = true;
|
|
147
|
+
child.kill();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
chunks.push(chunk);
|
|
151
|
+
};
|
|
152
|
+
child.stdout.on("data", collect(stdoutChunks));
|
|
153
|
+
child.stderr.on("data", collect(stderrChunks));
|
|
154
|
+
let settled = false;
|
|
155
|
+
const settle = (code, failed, extraStderr = "") => {
|
|
156
|
+
if (settled)
|
|
157
|
+
return;
|
|
158
|
+
settled = true;
|
|
159
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
160
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8") + extraStderr;
|
|
161
|
+
if (failed && !opts?.allowFail) {
|
|
162
|
+
reject(new GitError(args, code, stderr));
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
resolve({ code, stdout, stderr });
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
child.on("error", (err) => settle(1, true, err.message));
|
|
169
|
+
child.on("close", (code) => {
|
|
170
|
+
if (overflow)
|
|
171
|
+
settle(1, true, "\noutput exceeded the 64 MiB limit");
|
|
172
|
+
else
|
|
173
|
+
settle(code ?? 1, code !== 0);
|
|
174
|
+
});
|
|
175
|
+
// EPIPE when git exits without reading stdin surfaces through the exit code.
|
|
176
|
+
child.stdin.on("error", () => undefined);
|
|
177
|
+
child.stdin.end(opts?.stdin);
|
|
178
|
+
return promise;
|
|
179
|
+
}
|
|
180
|
+
spawnStdout(args) {
|
|
181
|
+
const allArgs = [...this.baseArgs, ...args];
|
|
182
|
+
const child = spawn("git", allArgs, {
|
|
183
|
+
cwd: this.repoDir,
|
|
184
|
+
env: this.env,
|
|
185
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
186
|
+
// Own session, no controlling terminal: ssh/credential prompts fail instead of taking over the MCP client's terminal.
|
|
187
|
+
detached: true,
|
|
188
|
+
});
|
|
189
|
+
let stderrBuffer = "";
|
|
190
|
+
child.stderr.on("data", (chunk) => {
|
|
191
|
+
stderrBuffer += chunk.toString();
|
|
192
|
+
});
|
|
193
|
+
child.on("close", (code) => {
|
|
194
|
+
if (code !== 0 && code !== null) {
|
|
195
|
+
child.stdout.destroy(new GitError(args, code, stderrBuffer));
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
return child.stdout;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=git.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export class Mutex {
|
|
2
|
+
tail = Promise.resolve();
|
|
3
|
+
async run(fn) {
|
|
4
|
+
const prev = this.tail;
|
|
5
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
6
|
+
this.tail = promise;
|
|
7
|
+
await prev;
|
|
8
|
+
try {
|
|
9
|
+
return await fn();
|
|
10
|
+
}
|
|
11
|
+
finally {
|
|
12
|
+
resolve();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async idle() {
|
|
16
|
+
await this.tail;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=mutex.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { BundleTree } from "./bundle.js";
|
|
2
|
+
/** In-memory set of file paths per project, with a cached BundleTree snapshot per project. */
|
|
3
|
+
export class PathIndex {
|
|
4
|
+
files = new Map();
|
|
5
|
+
trees = new Map();
|
|
6
|
+
projects() {
|
|
7
|
+
return Array.from(this.files.keys())
|
|
8
|
+
.filter((project) => (this.files.get(project)?.size ?? 0) > 0)
|
|
9
|
+
.sort((a, b) => a.localeCompare(b, "en"));
|
|
10
|
+
}
|
|
11
|
+
tree(project) {
|
|
12
|
+
let tree = this.trees.get(project);
|
|
13
|
+
if (!tree) {
|
|
14
|
+
const files = this.files.get(project);
|
|
15
|
+
tree = files && files.size > 0 ? new BundleTree(files) : BundleTree.EMPTY;
|
|
16
|
+
this.trees.set(project, tree);
|
|
17
|
+
}
|
|
18
|
+
return tree;
|
|
19
|
+
}
|
|
20
|
+
setProject(project, paths) {
|
|
21
|
+
const files = new Set(paths);
|
|
22
|
+
if (files.size === 0) {
|
|
23
|
+
this.files.delete(project);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
this.files.set(project, files);
|
|
27
|
+
}
|
|
28
|
+
this.trees.delete(project);
|
|
29
|
+
}
|
|
30
|
+
add(project, path) {
|
|
31
|
+
let files = this.files.get(project);
|
|
32
|
+
if (!files) {
|
|
33
|
+
files = new Set();
|
|
34
|
+
this.files.set(project, files);
|
|
35
|
+
}
|
|
36
|
+
files.add(path);
|
|
37
|
+
this.trees.delete(project);
|
|
38
|
+
}
|
|
39
|
+
remove(project, path) {
|
|
40
|
+
const files = this.files.get(project);
|
|
41
|
+
if (files) {
|
|
42
|
+
files.delete(path);
|
|
43
|
+
if (files.size === 0) {
|
|
44
|
+
this.files.delete(project);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
this.trees.delete(project);
|
|
48
|
+
}
|
|
49
|
+
clear() {
|
|
50
|
+
this.files.clear();
|
|
51
|
+
this.trees.clear();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=path-index.js.map
|