@nema-project/nema 0.0.0-stage → 0.1.4
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 +21 -0
- package/dist/chunks/chunk-HEIQINX3.js +825 -0
- package/dist/chunks/chunk-WT3XUFWU.js +86 -0
- package/dist/claude/adapter.d.ts +19 -0
- package/dist/claude/adapter.js +258 -0
- package/dist/claude/events.d.ts +29 -0
- package/dist/claude/events.js +75 -0
- package/dist/claude/launch.d.ts +22 -0
- package/dist/claude/launch.js +80 -0
- package/dist/claude/token.d.ts +9 -0
- package/dist/claude/token.js +87 -0
- package/dist/codex/adapter.d.ts +17 -0
- package/dist/codex/adapter.js +151 -0
- package/dist/codex/events.d.ts +16 -0
- package/dist/codex/events.js +47 -0
- package/dist/codex/isolation.d.ts +33 -0
- package/dist/codex/isolation.js +115 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.js +75 -0
- package/dist/configCommands.d.ts +13 -0
- package/dist/configCommands.js +69 -0
- package/dist/daemon/cleanup.d.ts +5 -0
- package/dist/daemon/cleanup.js +49 -0
- package/dist/daemon/connection.d.ts +22 -0
- package/dist/daemon/connection.js +166 -0
- package/dist/daemon/rpc.d.ts +14 -0
- package/dist/daemon/rpc.js +20 -0
- package/dist/daemon/runner.d.ts +34 -0
- package/dist/daemon/runner.js +94 -0
- package/dist/daemon/setup.d.ts +39 -0
- package/dist/daemon/setup.js +95 -0
- package/dist/direct/adapter.d.ts +30 -0
- package/dist/direct/adapter.js +190 -0
- package/dist/direct/authCommands.d.ts +54 -0
- package/dist/direct/authCommands.js +268 -0
- package/dist/direct/authStore.d.ts +120 -0
- package/dist/direct/authStore.js +351 -0
- package/dist/direct/byok.d.ts +27 -0
- package/dist/direct/byok.js +67 -0
- package/dist/direct/codexLogin.d.ts +34 -0
- package/dist/direct/codexLogin.js +93 -0
- package/dist/direct/jwt.d.ts +4 -0
- package/dist/direct/jwt.js +22 -0
- package/dist/direct/lock.d.ts +16 -0
- package/dist/direct/lock.js +134 -0
- package/dist/direct/loop.d.ts +33 -0
- package/dist/direct/loop.js +110 -0
- package/dist/direct/siwc.d.ts +63 -0
- package/dist/direct/siwc.js +403 -0
- package/dist/direct/tools.d.ts +17 -0
- package/dist/direct/tools.js +104 -0
- package/dist/direct/transport.d.ts +177 -0
- package/dist/direct/transport.js +437 -0
- package/dist/doctor.d.ts +28 -0
- package/dist/doctor.js +178 -0
- package/dist/extensions/handlers.d.ts +2 -0
- package/dist/extensions/handlers.js +10 -0
- package/dist/extensions/store.d.ts +11 -0
- package/dist/extensions/store.js +132 -0
- package/dist/github/client.d.ts +47 -0
- package/dist/github/client.js +383 -0
- package/dist/github/gh.d.ts +56 -0
- package/dist/github/gh.js +385 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +144 -0
- package/dist/localSettings.d.ts +8 -0
- package/dist/localSettings.js +26 -0
- package/dist/mcp/server.d.ts +5 -0
- package/dist/mcp/server.js +83 -0
- package/dist/ownedProcess.d.ts +39 -0
- package/dist/ownedProcess.js +229 -0
- package/dist/platform.d.ts +10 -0
- package/dist/platform.js +60 -0
- package/dist/report/commands.d.ts +41 -0
- package/dist/report/commands.js +159 -0
- package/dist/report/desktop.d.ts +3 -0
- package/dist/report/desktop.js +123 -0
- package/dist/report/git.d.ts +5 -0
- package/dist/report/git.js +42 -0
- package/dist/report/install.d.ts +22 -0
- package/dist/report/install.js +183 -0
- package/dist/report/send.d.ts +35 -0
- package/dist/report/send.js +208 -0
- package/dist/report/token.d.ts +5 -0
- package/dist/report/token.js +86 -0
- package/dist/repos/git.d.ts +48 -0
- package/dist/repos/git.js +370 -0
- package/dist/repos/graphify.d.ts +32 -0
- package/dist/repos/graphify.js +292 -0
- package/dist/repos/handlers.d.ts +20 -0
- package/dist/repos/handlers.js +255 -0
- package/dist/repos/indexState.d.ts +10 -0
- package/dist/repos/indexState.js +45 -0
- package/dist/repos/pickDirectory.d.ts +11 -0
- package/dist/repos/pickDirectory.js +140 -0
- package/dist/repos/reportCheck.d.ts +13 -0
- package/dist/repos/reportCheck.js +268 -0
- package/dist/repos/watcher.d.ts +64 -0
- package/dist/repos/watcher.js +202 -0
- package/dist/room/client.d.ts +26 -0
- package/dist/room/client.js +76 -0
- package/dist/secrets/file.d.ts +12 -0
- package/dist/secrets/file.js +89 -0
- package/dist/secrets/github.d.ts +10 -0
- package/dist/secrets/github.js +29 -0
- package/dist/secrets/permissions.d.ts +7 -0
- package/dist/secrets/permissions.js +86 -0
- package/dist/systemGit.d.ts +14 -0
- package/dist/systemGit.js +75 -0
- package/dist/wp/commands.d.ts +15 -0
- package/dist/wp/commands.js +103 -0
- package/package.json +26 -4
- package/src/report/SKILL.md +21 -0
- package/README.md +0 -3
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { RpcRouter } from "../daemon/rpc.js";
|
|
2
|
+
import { type GithubReader } from "../github/gh.js";
|
|
3
|
+
import { type GraphifyOpts } from "./graphify.js";
|
|
4
|
+
import type { OwnedHelperScope } from "@nema/shared/ownedHelper";
|
|
5
|
+
export declare function snapshotDir(nemaHome: string, workspaceId: string, repoId: string): string;
|
|
6
|
+
/** Removes the snapshot. A symlink is unlinked and its target is left untouched. */
|
|
7
|
+
export declare function removeSnapshot(dir: string): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* `graphify` is for tests: production passes the packaged binary, not a PATH lookup.
|
|
10
|
+
* `github` is the daemon's GitHub provider; the authenticated `gh` CLI is the production default.
|
|
11
|
+
* `prepareRepo` refreshes that repo before a code call. The daemon passes the watcher.
|
|
12
|
+
*/
|
|
13
|
+
export declare function registerRepoHandlers(router: RpcRouter, opts: {
|
|
14
|
+
nemaHome: string;
|
|
15
|
+
graphify?: GraphifyOpts;
|
|
16
|
+
github?: GithubReader;
|
|
17
|
+
ownedHelpers?: OwnedHelperScope;
|
|
18
|
+
pickDirectory?: () => Promise<string | null>;
|
|
19
|
+
prepareRepo?: (repoId: string, taskId?: string) => Promise<void>;
|
|
20
|
+
}): void;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GH_COMMAND_TOOLS
|
|
3
|
+
} from "../chunks/chunk-HEIQINX3.js";
|
|
4
|
+
|
|
5
|
+
// repos/handlers.js
|
|
6
|
+
import { lstat, rm, unlink } from "node:fs/promises";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { GITHUB_TOOLS } from "../github/client.js";
|
|
9
|
+
import { GithubGh } from "../github/gh.js";
|
|
10
|
+
import { changedFiles, fetchMain, inspectRepo, mainRef, missingAtRef, readFileAtRef, revParse } from "./git.js";
|
|
11
|
+
import { graphifyInstallStatus, queryIndex } from "./graphify.js";
|
|
12
|
+
import { readIndexedSha, syncIndex, withIndexLock } from "./indexState.js";
|
|
13
|
+
import { pickDirectory as openDirectoryDialog } from "./pickDirectory.js";
|
|
14
|
+
import { checkReport, uncoveredCommits } from "./reportCheck.js";
|
|
15
|
+
var WS_RE = /^ws_[A-Za-z0-9_-]{1,32}$/;
|
|
16
|
+
var REPO_RE = /^repo_[A-Za-z0-9_-]{1,32}$/;
|
|
17
|
+
function repoCall(params) {
|
|
18
|
+
if (!params || typeof params !== "object")
|
|
19
|
+
throw new Error("invalid repo params");
|
|
20
|
+
const row = params;
|
|
21
|
+
const workspaceId = row.workspaceId;
|
|
22
|
+
const repoId = row.repoId;
|
|
23
|
+
const repoPath = row.path;
|
|
24
|
+
const mainBranch = row.mainBranch;
|
|
25
|
+
if (typeof workspaceId !== "string" || typeof repoId !== "string" || typeof repoPath !== "string" || typeof mainBranch !== "string") {
|
|
26
|
+
throw new Error("invalid repo params");
|
|
27
|
+
}
|
|
28
|
+
if (!workspaceId || !repoId || !repoPath || !mainBranch)
|
|
29
|
+
throw new Error("invalid repo params");
|
|
30
|
+
return { workspaceId, repoId, path: repoPath, mainBranch };
|
|
31
|
+
}
|
|
32
|
+
function snapshotDir(nemaHome, workspaceId, repoId) {
|
|
33
|
+
if (!WS_RE.test(workspaceId))
|
|
34
|
+
throw new Error("invalid workspace id");
|
|
35
|
+
if (!REPO_RE.test(repoId))
|
|
36
|
+
throw new Error("invalid repo id");
|
|
37
|
+
const root = path.join(nemaHome, "ws");
|
|
38
|
+
const dir = path.join(root, workspaceId, "graphify", repoId);
|
|
39
|
+
const rel = path.relative(root, dir);
|
|
40
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel))
|
|
41
|
+
throw new Error("invalid workspace id");
|
|
42
|
+
return dir;
|
|
43
|
+
}
|
|
44
|
+
async function removeSnapshot(dir) {
|
|
45
|
+
let st;
|
|
46
|
+
try {
|
|
47
|
+
st = await lstat(dir);
|
|
48
|
+
} catch {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (st.isSymbolicLink()) {
|
|
52
|
+
await unlink(dir);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
await rm(dir, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
var CODE_FUNCTIONS = /* @__PURE__ */ new Set(["code__search", "code__explain", "code__path", "code__read", "code__changes"]);
|
|
58
|
+
function invalidArg(name, field) {
|
|
59
|
+
return new Error(`${name} \u53C2\u6570\u65E0\u6548\uFF1A${field}`);
|
|
60
|
+
}
|
|
61
|
+
function firstField(name) {
|
|
62
|
+
switch (name) {
|
|
63
|
+
case "code__search":
|
|
64
|
+
return "query";
|
|
65
|
+
case "code__explain":
|
|
66
|
+
return "symbol";
|
|
67
|
+
case "code__read":
|
|
68
|
+
return "path";
|
|
69
|
+
default:
|
|
70
|
+
return "from";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function argRecord(name, args) {
|
|
74
|
+
if (!args || typeof args !== "object" || Array.isArray(args))
|
|
75
|
+
throw invalidArg(name, firstField(name));
|
|
76
|
+
return args;
|
|
77
|
+
}
|
|
78
|
+
function needString(name, args, field) {
|
|
79
|
+
const value = args[field];
|
|
80
|
+
if (typeof value !== "string")
|
|
81
|
+
throw invalidArg(name, field);
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
function optString(name, args, field) {
|
|
85
|
+
if (args[field] === void 0)
|
|
86
|
+
return void 0;
|
|
87
|
+
return needString(name, args, field);
|
|
88
|
+
}
|
|
89
|
+
function optLine(name, args, field) {
|
|
90
|
+
if (args[field] === void 0)
|
|
91
|
+
return void 0;
|
|
92
|
+
const value = args[field];
|
|
93
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1)
|
|
94
|
+
throw invalidArg(name, field);
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
function pluginParams(params) {
|
|
98
|
+
if (!params || typeof params !== "object")
|
|
99
|
+
throw new Error("\u672A\u77E5\u51FD\u6570");
|
|
100
|
+
const row = params;
|
|
101
|
+
const name = row.name;
|
|
102
|
+
if (typeof name !== "string" || name === "")
|
|
103
|
+
throw new Error("\u672A\u77E5\u51FD\u6570");
|
|
104
|
+
if (name.startsWith("github__")) {
|
|
105
|
+
const tool = name.slice("github__".length);
|
|
106
|
+
if (!GITHUB_TOOLS.includes(tool) && !GH_COMMAND_TOOLS.includes(tool))
|
|
107
|
+
throw new Error(`\u672A\u77E5\u51FD\u6570 ${name}`);
|
|
108
|
+
const args2 = row.args;
|
|
109
|
+
if (!args2 || typeof args2 !== "object" || Array.isArray(args2))
|
|
110
|
+
throw new Error(`${name} \u53C2\u6570\u65E0\u6548\uFF1Aargs`);
|
|
111
|
+
return { name, tool, args: args2 };
|
|
112
|
+
}
|
|
113
|
+
if (!CODE_FUNCTIONS.has(name))
|
|
114
|
+
throw new Error(`\u672A\u77E5\u51FD\u6570 ${name}`);
|
|
115
|
+
const args = argRecord(name, row.args);
|
|
116
|
+
switch (name) {
|
|
117
|
+
case "code__search": {
|
|
118
|
+
const query = needString(name, args, "query");
|
|
119
|
+
return { name, repo: repoCall(row.repo), query };
|
|
120
|
+
}
|
|
121
|
+
case "code__explain": {
|
|
122
|
+
const symbol = needString(name, args, "symbol");
|
|
123
|
+
return { name, repo: repoCall(row.repo), symbol };
|
|
124
|
+
}
|
|
125
|
+
case "code__path": {
|
|
126
|
+
const from = needString(name, args, "from");
|
|
127
|
+
const to = needString(name, args, "to");
|
|
128
|
+
return { name, repo: repoCall(row.repo), from, to };
|
|
129
|
+
}
|
|
130
|
+
case "code__read": {
|
|
131
|
+
const relPath = needString(name, args, "path");
|
|
132
|
+
const startLine = optLine(name, args, "startLine");
|
|
133
|
+
const endLine = optLine(name, args, "endLine");
|
|
134
|
+
return { name, repo: repoCall(row.repo), path: relPath, startLine, endLine };
|
|
135
|
+
}
|
|
136
|
+
case "code__changes": {
|
|
137
|
+
const from = needString(name, args, "from");
|
|
138
|
+
const to = optString(name, args, "to");
|
|
139
|
+
return { name, repo: repoCall(row.repo), from, to };
|
|
140
|
+
}
|
|
141
|
+
default:
|
|
142
|
+
throw new Error(`\u672A\u77E5\u51FD\u6570 ${name}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function stringList(params, field, max) {
|
|
146
|
+
const value = params[field];
|
|
147
|
+
if (!Array.isArray(value) || value.length > max || !value.every((v) => typeof v === "string"))
|
|
148
|
+
throw new Error("invalid repo params");
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
async function indexLocked(root, repo, graphify) {
|
|
152
|
+
const helpers = graphify?.ownedHelpers;
|
|
153
|
+
await fetchMain(repo.path, repo.mainBranch, helpers);
|
|
154
|
+
const sha = await revParse(repo.path, await mainRef(repo.path, repo.mainBranch, helpers), helpers);
|
|
155
|
+
await syncIndex(root, repo.path, sha, graphify);
|
|
156
|
+
return { sha };
|
|
157
|
+
}
|
|
158
|
+
function indexRepo(nemaHome, repo, graphify) {
|
|
159
|
+
const root = snapshotDir(nemaHome, repo.workspaceId, repo.repoId);
|
|
160
|
+
return withIndexLock(root, () => indexLocked(root, repo, graphify));
|
|
161
|
+
}
|
|
162
|
+
async function ensureIndex(nemaHome, repo, graphify) {
|
|
163
|
+
const root = snapshotDir(nemaHome, repo.workspaceId, repo.repoId);
|
|
164
|
+
await withIndexLock(root, async () => {
|
|
165
|
+
if (await readIndexedSha(root) === null)
|
|
166
|
+
await indexLocked(root, repo, graphify);
|
|
167
|
+
});
|
|
168
|
+
return root;
|
|
169
|
+
}
|
|
170
|
+
async function runPluginCall(nemaHome, call, github, graphify) {
|
|
171
|
+
const helpers = graphify?.ownedHelpers;
|
|
172
|
+
if ("tool" in call)
|
|
173
|
+
return github.call(call.tool, call.args);
|
|
174
|
+
switch (call.name) {
|
|
175
|
+
case "code__search":
|
|
176
|
+
return queryIndex(await ensureIndex(nemaHome, call.repo, graphify), "query", [call.query], graphify);
|
|
177
|
+
case "code__explain":
|
|
178
|
+
return queryIndex(await ensureIndex(nemaHome, call.repo, graphify), "explain", [call.symbol], graphify);
|
|
179
|
+
case "code__path":
|
|
180
|
+
return queryIndex(await ensureIndex(nemaHome, call.repo, graphify), "path", [call.from, call.to], graphify);
|
|
181
|
+
case "code__read": {
|
|
182
|
+
const ref = await mainRef(call.repo.path, call.repo.mainBranch, helpers);
|
|
183
|
+
const range = call.startLine !== void 0 || call.endLine !== void 0 ? { startLine: call.startLine, endLine: call.endLine } : void 0;
|
|
184
|
+
return readFileAtRef(call.repo.path, ref, call.path, range, helpers);
|
|
185
|
+
}
|
|
186
|
+
case "code__changes": {
|
|
187
|
+
const to = call.to ?? await mainRef(call.repo.path, call.repo.mainBranch, helpers);
|
|
188
|
+
return changedFiles(call.repo.path, call.from, to, call.repo.mainBranch, helpers);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function registerRepoHandlers(router, opts) {
|
|
193
|
+
const github = opts.github ?? new GithubGh({ ownedHelpers: opts.ownedHelpers });
|
|
194
|
+
router.register("dialog.pickDirectory", async () => ({ path: await (opts.pickDirectory ?? openDirectoryDialog)() }));
|
|
195
|
+
router.register("repo.inspect", async (params) => {
|
|
196
|
+
if (!params || typeof params !== "object" || typeof params.path !== "string") {
|
|
197
|
+
throw new Error("invalid repo params");
|
|
198
|
+
}
|
|
199
|
+
const repoPath = params.path;
|
|
200
|
+
if (!repoPath)
|
|
201
|
+
throw new Error("invalid repo params");
|
|
202
|
+
return inspectRepo(repoPath, opts.nemaHome, opts.ownedHelpers);
|
|
203
|
+
});
|
|
204
|
+
router.register("repo.index", async (params) => indexRepo(opts.nemaHome, repoCall(params), opts.graphify));
|
|
205
|
+
router.register("repo.changes", async (params) => {
|
|
206
|
+
const repo = repoCall(params);
|
|
207
|
+
const { from, to } = params;
|
|
208
|
+
if (typeof from !== "string" || typeof to !== "string" || !from || !to)
|
|
209
|
+
throw new Error("invalid repo params");
|
|
210
|
+
return changedFiles(repo.path, from, to, repo.mainBranch, opts.ownedHelpers);
|
|
211
|
+
});
|
|
212
|
+
router.register("report.check", async (params) => {
|
|
213
|
+
const repo = repoCall(params);
|
|
214
|
+
const commits = stringList(params, "commits", 50);
|
|
215
|
+
const files = stringList(params, "files", 500);
|
|
216
|
+
await fetchMain(repo.path, repo.mainBranch, opts.ownedHelpers);
|
|
217
|
+
return checkReport(repo.path, await mainRef(repo.path, repo.mainBranch, opts.ownedHelpers), commits, files, opts.ownedHelpers);
|
|
218
|
+
});
|
|
219
|
+
router.register("repo.uncovered", async (params) => {
|
|
220
|
+
const repo = repoCall(params);
|
|
221
|
+
const { from, to } = params;
|
|
222
|
+
if (typeof from !== "string" || typeof to !== "string" || !from || !to)
|
|
223
|
+
throw new Error("invalid repo params");
|
|
224
|
+
const covered = stringList(params, "covered", 1e4);
|
|
225
|
+
return uncoveredCommits(repo.path, await mainRef(repo.path, repo.mainBranch, opts.ownedHelpers), from, to, covered, opts.ownedHelpers);
|
|
226
|
+
});
|
|
227
|
+
router.register("repo.exists", async (params) => {
|
|
228
|
+
const repo = repoCall(params);
|
|
229
|
+
const paths = stringList(params, "paths", 500);
|
|
230
|
+
return { missing: await missingAtRef(repo.path, await mainRef(repo.path, repo.mainBranch, opts.ownedHelpers), paths, opts.ownedHelpers) };
|
|
231
|
+
});
|
|
232
|
+
router.register("repo.forget", async (params) => {
|
|
233
|
+
const repo = repoCall(params);
|
|
234
|
+
const root = snapshotDir(opts.nemaHome, repo.workspaceId, repo.repoId);
|
|
235
|
+
await withIndexLock(root, () => removeSnapshot(root));
|
|
236
|
+
return { ok: true };
|
|
237
|
+
});
|
|
238
|
+
router.register("plugin.call", async (params) => {
|
|
239
|
+
const call = pluginParams(params);
|
|
240
|
+
if (opts.prepareRepo && "repo" in call) {
|
|
241
|
+
const taskId = params && typeof params === "object" && typeof params.taskId === "string" ? params.taskId : "";
|
|
242
|
+
await opts.prepareRepo(call.repo.repoId, taskId === "" ? void 0 : taskId);
|
|
243
|
+
}
|
|
244
|
+
return runPluginCall(opts.nemaHome, call, github, opts.graphify);
|
|
245
|
+
});
|
|
246
|
+
router.register("plugins.status", async () => {
|
|
247
|
+
const [code, gh] = await Promise.all([graphifyInstallStatus(opts.graphify), github.status()]);
|
|
248
|
+
return { code, github: { installed: gh.installed, loggedIn: gh.loggedIn, detail: gh.detail } };
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
export {
|
|
252
|
+
registerRepoHandlers,
|
|
253
|
+
removeSnapshot,
|
|
254
|
+
snapshotDir
|
|
255
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type GraphifyOpts } from "./graphify.js";
|
|
2
|
+
/** Runs `fn` after every earlier job for the same snapshot root, so builds never overlap. */
|
|
3
|
+
export declare function withIndexLock<T>(root: string, fn: () => Promise<T>): Promise<T>;
|
|
4
|
+
export declare function readIndexedSha(root: string): Promise<string | null>;
|
|
5
|
+
/**
|
|
6
|
+
* Brings the index to `sha`. Call it inside `withIndexLock`. An index at another sha is updated
|
|
7
|
+
* incrementally; no index is built in full. The marker is removed first, so any failure leaves the
|
|
8
|
+
* snapshot marked as not indexed and the next call rebuilds it in full.
|
|
9
|
+
*/
|
|
10
|
+
export declare function syncIndex(root: string, repoPath: string, sha: string, graphify?: GraphifyOpts): Promise<void>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// repos/indexState.js
|
|
2
|
+
import { readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { diffNameStatus, exportSnapshot } from "./git.js";
|
|
5
|
+
import { buildIndex, updateIndex } from "./graphify.js";
|
|
6
|
+
var MARKER = "indexed-sha";
|
|
7
|
+
var locks = /* @__PURE__ */ new Map();
|
|
8
|
+
function withIndexLock(root, fn) {
|
|
9
|
+
const prev = locks.get(root) ?? Promise.resolve();
|
|
10
|
+
const run = prev.catch(() => {
|
|
11
|
+
}).then(fn);
|
|
12
|
+
locks.set(root, run);
|
|
13
|
+
void run.catch(() => {
|
|
14
|
+
}).finally(() => {
|
|
15
|
+
if (locks.get(root) === run)
|
|
16
|
+
locks.delete(root);
|
|
17
|
+
});
|
|
18
|
+
return run;
|
|
19
|
+
}
|
|
20
|
+
async function readIndexedSha(root) {
|
|
21
|
+
try {
|
|
22
|
+
const sha = (await readFile(path.join(root, MARKER), "utf8")).trim();
|
|
23
|
+
return sha === "" ? null : sha;
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function syncIndex(root, repoPath, sha, graphify) {
|
|
29
|
+
const indexed = await readIndexedSha(root);
|
|
30
|
+
if (indexed === sha)
|
|
31
|
+
return;
|
|
32
|
+
await rm(path.join(root, MARKER), { force: true });
|
|
33
|
+
if (indexed !== null) {
|
|
34
|
+
await updateIndex(root, await diffNameStatus(repoPath, indexed, sha, graphify?.ownedHelpers), repoPath, sha, graphify);
|
|
35
|
+
} else {
|
|
36
|
+
await exportSnapshot(repoPath, sha, path.join(root, "src"), graphify?.ownedHelpers);
|
|
37
|
+
await buildIndex(root, graphify);
|
|
38
|
+
}
|
|
39
|
+
await writeFile(path.join(root, MARKER), sha);
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
readIndexedSha,
|
|
43
|
+
syncIndex,
|
|
44
|
+
withIndexLock
|
|
45
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type CommandRunner = (cmd: string, args: string[], timeoutMs: number) => Promise<{
|
|
2
|
+
stdout: string;
|
|
3
|
+
}>;
|
|
4
|
+
export declare function pickerCommand(platform: NodeJS.Platform): {
|
|
5
|
+
cmd: string;
|
|
6
|
+
args: string[];
|
|
7
|
+
};
|
|
8
|
+
/** A cancelled dialog is an empty result. Anything that is not one absolute path is refused. */
|
|
9
|
+
export declare function normalizePickedPath(stdout: string, platform: NodeJS.Platform): string | null;
|
|
10
|
+
/** Opens the OS folder dialog on this machine. A second call shares the dialog already open. */
|
|
11
|
+
export declare function pickDirectory(platform?: NodeJS.Platform, run?: CommandRunner): Promise<string | null>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// repos/pickDirectory.js
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
var PICK_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
5
|
+
var WIN_FOLDER_SCRIPT = `
|
|
6
|
+
$ErrorActionPreference = 'Stop'
|
|
7
|
+
Add-Type -TypeDefinition @'
|
|
8
|
+
using System;
|
|
9
|
+
using System.Runtime.InteropServices;
|
|
10
|
+
public class NemaFolderDialog {
|
|
11
|
+
[ComImport, Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")]
|
|
12
|
+
class FileOpenDialogRCW {}
|
|
13
|
+
[ComImport, Guid("d57c7288-d4ad-4768-be02-9d969532d960"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
14
|
+
interface IFileOpenDialog {
|
|
15
|
+
[PreserveSig] int Show(IntPtr parent);
|
|
16
|
+
void SetFileTypes(uint cFileTypes, IntPtr rgFilterSpec);
|
|
17
|
+
void SetFileTypeIndex(uint iFileType);
|
|
18
|
+
void GetFileTypeIndex(out uint piFileType);
|
|
19
|
+
void Advise(IntPtr pfde, out uint pdwCookie);
|
|
20
|
+
void Unadvise(uint dwCookie);
|
|
21
|
+
void SetOptions(uint fos);
|
|
22
|
+
void GetOptions(out uint fos);
|
|
23
|
+
void SetDefaultFolder(IShellItem psi);
|
|
24
|
+
void SetFolder(IShellItem psi);
|
|
25
|
+
void GetFolder(out IShellItem ppsi);
|
|
26
|
+
void GetCurrentSelection(out IShellItem ppsi);
|
|
27
|
+
void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName);
|
|
28
|
+
void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName);
|
|
29
|
+
void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle);
|
|
30
|
+
void SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string pszText);
|
|
31
|
+
void SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string pszLabel);
|
|
32
|
+
void GetResult(out IShellItem ppsi);
|
|
33
|
+
void AddPlace(IShellItem psi, uint fdap);
|
|
34
|
+
void SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);
|
|
35
|
+
void Close(int hr);
|
|
36
|
+
void SetClientGuid(ref Guid guid);
|
|
37
|
+
void ClearClientData();
|
|
38
|
+
void SetFilter(IntPtr pFilter);
|
|
39
|
+
void GetResults(out IntPtr ppenum);
|
|
40
|
+
void GetSelectedItems(out IntPtr ppsai);
|
|
41
|
+
}
|
|
42
|
+
[ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
43
|
+
interface IShellItem {
|
|
44
|
+
void BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppv);
|
|
45
|
+
void GetParent(out IShellItem ppsi);
|
|
46
|
+
void GetDisplayName(uint sigdnName, out IntPtr ppszName);
|
|
47
|
+
void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
|
|
48
|
+
void Compare(IShellItem psi, uint hint, out int piOrder);
|
|
49
|
+
}
|
|
50
|
+
public static string Pick() {
|
|
51
|
+
var dialog = (IFileOpenDialog)new FileOpenDialogRCW();
|
|
52
|
+
dialog.SetOptions(0x20 | 0x40 | 0x800 | 0x8);
|
|
53
|
+
int hr = dialog.Show(IntPtr.Zero);
|
|
54
|
+
if (hr == unchecked((int)0x800704C7)) return "";
|
|
55
|
+
if (hr != 0) Marshal.ThrowExceptionForHR(hr);
|
|
56
|
+
IShellItem item;
|
|
57
|
+
dialog.GetResult(out item);
|
|
58
|
+
IntPtr psz;
|
|
59
|
+
item.GetDisplayName(0x80058000, out psz);
|
|
60
|
+
string path = Marshal.PtrToStringUni(psz);
|
|
61
|
+
Marshal.FreeCoTaskMem(psz);
|
|
62
|
+
return path ?? "";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
'@
|
|
66
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
67
|
+
[Console]::Out.Write([NemaFolderDialog]::Pick())
|
|
68
|
+
`;
|
|
69
|
+
function pickerCommand(platform) {
|
|
70
|
+
if (platform === "win32") {
|
|
71
|
+
const encoded = Buffer.from(WIN_FOLDER_SCRIPT, "utf16le").toString("base64");
|
|
72
|
+
return { cmd: "powershell.exe", args: ["-NoProfile", "-STA", "-EncodedCommand", encoded] };
|
|
73
|
+
}
|
|
74
|
+
if (platform === "darwin")
|
|
75
|
+
return { cmd: "osascript", args: ["-e", "POSIX path of (choose folder)"] };
|
|
76
|
+
return { cmd: "zenity", args: ["--file-selection", "--directory"] };
|
|
77
|
+
}
|
|
78
|
+
function pathApi(platform) {
|
|
79
|
+
return platform === "win32" ? path.win32 : path.posix;
|
|
80
|
+
}
|
|
81
|
+
function normalizePickedPath(stdout, platform) {
|
|
82
|
+
const picked = stdout.replace(/^\uFEFF/, "").replace(/\r?\n$/, "").trim();
|
|
83
|
+
if (!picked || picked.includes("\0") || /[\r\n]/.test(picked) || picked.length > 4096)
|
|
84
|
+
return null;
|
|
85
|
+
const api = pathApi(platform);
|
|
86
|
+
if (!api.isAbsolute(picked))
|
|
87
|
+
return null;
|
|
88
|
+
const sep = api.sep;
|
|
89
|
+
if (picked.length > 3 && (picked.endsWith(sep) || platform === "win32" && picked.endsWith("/")))
|
|
90
|
+
return picked.slice(0, -1);
|
|
91
|
+
return picked;
|
|
92
|
+
}
|
|
93
|
+
function isCancel(error) {
|
|
94
|
+
if (!error || typeof error !== "object")
|
|
95
|
+
return false;
|
|
96
|
+
const code = error.code;
|
|
97
|
+
if (code !== 1 && code !== "1")
|
|
98
|
+
return false;
|
|
99
|
+
const stderr = String(error.stderr ?? "");
|
|
100
|
+
return stderr.trim() === "" || /User canceled|-128/.test(stderr);
|
|
101
|
+
}
|
|
102
|
+
function defaultRun(cmd, args, timeoutMs) {
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
execFile(cmd, args, { windowsHide: true, timeout: timeoutMs, maxBuffer: 64 * 1024, encoding: "utf8" }, (error, stdout, stderr) => {
|
|
105
|
+
if (error) {
|
|
106
|
+
error.stderr = stderr;
|
|
107
|
+
reject(error);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
resolve({ stdout });
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
var pending = null;
|
|
115
|
+
function pickDirectory(platform = process.platform, run = defaultRun) {
|
|
116
|
+
if (pending && run === defaultRun)
|
|
117
|
+
return pending;
|
|
118
|
+
const work = (async () => {
|
|
119
|
+
const { cmd, args } = pickerCommand(platform);
|
|
120
|
+
try {
|
|
121
|
+
const { stdout } = await run(cmd, args, PICK_TIMEOUT_MS);
|
|
122
|
+
return normalizePickedPath(stdout, platform);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (isCancel(error))
|
|
125
|
+
return null;
|
|
126
|
+
throw new Error("\u65E0\u6CD5\u6253\u5F00\u6587\u4EF6\u5939\u9009\u62E9\u5668");
|
|
127
|
+
}
|
|
128
|
+
})();
|
|
129
|
+
if (run !== defaultRun)
|
|
130
|
+
return work;
|
|
131
|
+
pending = work.finally(() => {
|
|
132
|
+
pending = null;
|
|
133
|
+
});
|
|
134
|
+
return pending;
|
|
135
|
+
}
|
|
136
|
+
export {
|
|
137
|
+
normalizePickedPath,
|
|
138
|
+
pickDirectory,
|
|
139
|
+
pickerCommand
|
|
140
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReportCheck } from "@nema/shared";
|
|
2
|
+
import type { OwnedHelperScope } from "@nema/shared/ownedHelper";
|
|
3
|
+
/**
|
|
4
|
+
* Checks reported `commits` and claimed `files` against `mainRefName` (`origin/<main>`, or the local main
|
|
5
|
+
* when there is no origin). The caller fetches first.
|
|
6
|
+
*/
|
|
7
|
+
export declare function checkReport(repoPath: string, mainRefName: string, commits: string[], files: string[], helpers?: OwnedHelperScope): Promise<ReportCheck>;
|
|
8
|
+
/**
|
|
9
|
+
* Non-merge commits in `from..to` on main that no report covers: neither one of `covered`, nor patch-id-equal
|
|
10
|
+
* to one of them, nor a squash of a parent-linked run of covered commits that never reached main themselves.
|
|
11
|
+
* `from` and `to` must each be `mainRefName` or a sha that main contains. Returned oldest first.
|
|
12
|
+
*/
|
|
13
|
+
export declare function uncoveredCommits(repoPath: string, mainRefName: string, from: string, to: string, covered: string[], helpers?: OwnedHelperScope): Promise<string[]>;
|