comprehende 0.1.0 → 0.3.0
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 +20 -49
- package/dist/api/error.js +8 -0
- package/dist/api/live.js +249 -0
- package/dist/api/paths.js +96 -0
- package/dist/api/snapshot.js +45 -0
- package/dist/api/types.js +1 -0
- package/dist/cli/args.js +7 -1
- package/dist/cli/commands.js +6 -0
- package/dist/cli/main.js +22 -10
- package/dist/git/blob.js +21 -0
- package/dist/git/diff.js +44 -2
- package/dist/git/exec.js +29 -3
- package/dist/git/lfs.js +39 -0
- package/dist/schema/cli-pin.js +18 -0
- package/dist/schema/image-diff.js +51 -0
- package/dist/schema/image.js +35 -0
- package/dist/schema/parse.js +5 -1
- package/dist/schema/skill-sync.js +90 -0
- package/dist/server/http.js +83 -197
- package/dist/ui/assets/index-BxIxxAYx.css +1 -0
- package/dist/ui/assets/index-D4bY-beP.js +1598 -0
- package/dist/ui/index.html +16 -2
- package/package.json +15 -11
- package/skills/comprehende/SKILL.md +44 -29
- package/skills/comprehende/references/example.md +21 -0
- package/skills/comprehende/references/review.schema.json +6 -1
- package/dist/cli/args.d.ts +0 -20
- package/dist/cli/args.js.map +0 -1
- package/dist/cli/commands.d.ts +0 -12
- package/dist/cli/commands.js.map +0 -1
- package/dist/cli/main.d.ts +0 -2
- package/dist/cli/main.js.map +0 -1
- package/dist/git/blame.d.ts +0 -8
- package/dist/git/blame.js.map +0 -1
- package/dist/git/diff.d.ts +0 -13
- package/dist/git/diff.js.map +0 -1
- package/dist/git/exec.d.ts +0 -10
- package/dist/git/exec.js.map +0 -1
- package/dist/git/log.d.ts +0 -8
- package/dist/git/log.js.map +0 -1
- package/dist/git/repo.d.ts +0 -7
- package/dist/git/repo.js.map +0 -1
- package/dist/git/show.d.ts +0 -2
- package/dist/git/show.js.map +0 -1
- package/dist/package-root.d.ts +0 -2
- package/dist/package-root.js.map +0 -1
- package/dist/review/coverage.d.ts +0 -20
- package/dist/review/coverage.js.map +0 -1
- package/dist/review/effort.d.ts +0 -1
- package/dist/review/effort.js +0 -17
- package/dist/review/effort.js.map +0 -1
- package/dist/review/generate.d.ts +0 -20
- package/dist/review/generate.js +0 -290
- package/dist/review/generate.js.map +0 -1
- package/dist/schema/blame-runs.d.ts +0 -14
- package/dist/schema/blame-runs.js.map +0 -1
- package/dist/schema/hunk-meta.d.ts +0 -26
- package/dist/schema/hunk-meta.js.map +0 -1
- package/dist/schema/hunk-patch.d.ts +0 -2
- package/dist/schema/hunk-patch.js +0 -49
- package/dist/schema/hunk-patch.js.map +0 -1
- package/dist/schema/identity.d.ts +0 -3
- package/dist/schema/identity.js.map +0 -1
- package/dist/schema/parse.d.ts +0 -13
- package/dist/schema/parse.js.map +0 -1
- package/dist/schema/skill-paths.d.ts +0 -7
- package/dist/schema/skill-paths.js.map +0 -1
- package/dist/schema/types.d.ts +0 -73
- package/dist/schema/types.js.map +0 -1
- package/dist/server/http.d.ts +0 -12
- package/dist/server/http.js.map +0 -1
- package/dist/ui/assets/index-BCZwzr3E.js +0 -1585
- package/dist/ui/assets/index-DsGDgUsX.css +0 -1
package/dist/git/diff.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isImagePath, isLfsPointerText } from "../schema/image.js";
|
|
1
2
|
import { git } from "./exec.js";
|
|
2
3
|
import { rangeLabel, resolveCommit } from "./repo.js";
|
|
3
4
|
const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
@@ -23,7 +24,7 @@ export async function readDiff(cwd, baseRef, headRef) {
|
|
|
23
24
|
"--end-of-options",
|
|
24
25
|
`${baseRef}...${headRef}`,
|
|
25
26
|
]);
|
|
26
|
-
return parseUnifiedDiff(stdout);
|
|
27
|
+
return classifyDiffFiles(parseUnifiedDiff(stdout));
|
|
27
28
|
}
|
|
28
29
|
export async function readHunkIndex(cwd, baseRef, headRef) {
|
|
29
30
|
const { source } = await resolveSource(cwd, baseRef, headRef);
|
|
@@ -31,6 +32,12 @@ export async function readHunkIndex(cwd, baseRef, headRef) {
|
|
|
31
32
|
const hunks = [];
|
|
32
33
|
const skipped = [];
|
|
33
34
|
for (const file of files) {
|
|
35
|
+
if (file.image) {
|
|
36
|
+
for (const hunk of file.hunks) {
|
|
37
|
+
hunks.push(toHunkRef(hunk));
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
34
41
|
if (file.binary) {
|
|
35
42
|
skipped.push({ path: file.path, reason: "binary" });
|
|
36
43
|
continue;
|
|
@@ -196,6 +203,7 @@ class FileBuilder {
|
|
|
196
203
|
path,
|
|
197
204
|
status,
|
|
198
205
|
binary: this.binary,
|
|
206
|
+
image: false,
|
|
199
207
|
headerPatch: this.headerPatch,
|
|
200
208
|
patch: this.patch,
|
|
201
209
|
hunks,
|
|
@@ -282,14 +290,48 @@ function stripDiffPath(raw) {
|
|
|
282
290
|
export function flattenHunks(files) {
|
|
283
291
|
const hunks = [];
|
|
284
292
|
for (const file of files) {
|
|
285
|
-
if (file.binary) {
|
|
293
|
+
if (file.binary && !file.image) {
|
|
286
294
|
continue;
|
|
287
295
|
}
|
|
288
296
|
hunks.push(...file.hunks);
|
|
289
297
|
}
|
|
290
298
|
return hunks;
|
|
291
299
|
}
|
|
300
|
+
export function classifyDiffFiles(files) {
|
|
301
|
+
return files.map(classifyDiffFile);
|
|
302
|
+
}
|
|
303
|
+
function classifyDiffFile(file) {
|
|
304
|
+
if (!isImagePath(file.path) && (file.oldPath === undefined || !isImagePath(file.oldPath))) {
|
|
305
|
+
return file;
|
|
306
|
+
}
|
|
307
|
+
if (file.binary || isLfsPointerText(file.patch)) {
|
|
308
|
+
return asImageFile(file);
|
|
309
|
+
}
|
|
310
|
+
return file;
|
|
311
|
+
}
|
|
312
|
+
function asImageFile(file) {
|
|
313
|
+
return { ...file, image: true, hunks: [imageLiveHunk(file.path, file.oldPath)] };
|
|
314
|
+
}
|
|
315
|
+
export function imageLiveHunk(path, oldPath) {
|
|
316
|
+
const hunk = {
|
|
317
|
+
path,
|
|
318
|
+
oldStart: 0,
|
|
319
|
+
oldLines: 0,
|
|
320
|
+
newStart: 0,
|
|
321
|
+
newLines: 0,
|
|
322
|
+
header: "image",
|
|
323
|
+
lines: [],
|
|
324
|
+
patch: "",
|
|
325
|
+
};
|
|
326
|
+
if (oldPath !== undefined) {
|
|
327
|
+
hunk.oldPath = oldPath;
|
|
328
|
+
}
|
|
329
|
+
return hunk;
|
|
330
|
+
}
|
|
292
331
|
export function fileLanguage(path) {
|
|
332
|
+
if (isImagePath(path)) {
|
|
333
|
+
return "image";
|
|
334
|
+
}
|
|
293
335
|
const ext = path.split(".").pop()?.toLowerCase();
|
|
294
336
|
switch (ext) {
|
|
295
337
|
case "ts":
|
package/dist/git/exec.js
CHANGED
|
@@ -27,7 +27,21 @@ export async function git(cwd, args, opts) {
|
|
|
27
27
|
if (opts?.allowFail) {
|
|
28
28
|
return failure.stdout;
|
|
29
29
|
}
|
|
30
|
-
throw
|
|
30
|
+
throw gitFailure(args, failure);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export async function gitBuffer(cwd, args, opts) {
|
|
34
|
+
try {
|
|
35
|
+
const { stdout } = await execFileAsync("git", ["-c", "core.quotepath=false", ...args], {
|
|
36
|
+
cwd,
|
|
37
|
+
encoding: "buffer",
|
|
38
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
39
|
+
...(opts?.input !== undefined ? { input: Buffer.from(opts.input) } : {}),
|
|
40
|
+
});
|
|
41
|
+
return stdout;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
throw gitFailure(args, asExecFailure(error));
|
|
31
45
|
}
|
|
32
46
|
}
|
|
33
47
|
export async function gitOk(cwd, args) {
|
|
@@ -44,10 +58,22 @@ function asExecFailure(error) {
|
|
|
44
58
|
const record = error;
|
|
45
59
|
return {
|
|
46
60
|
message: typeof record.message === "string" ? record.message : "git failed",
|
|
47
|
-
stdout:
|
|
48
|
-
stderr:
|
|
61
|
+
stdout: bufferText(record.stdout),
|
|
62
|
+
stderr: bufferText(record.stderr),
|
|
49
63
|
code: typeof record.code === "number" ? record.code : null,
|
|
50
64
|
};
|
|
51
65
|
}
|
|
52
66
|
return { message: "git failed", stdout: "", stderr: "", code: null };
|
|
53
67
|
}
|
|
68
|
+
function bufferText(value) {
|
|
69
|
+
if (typeof value === "string") {
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
if (value instanceof Buffer) {
|
|
73
|
+
return value.toString("utf8");
|
|
74
|
+
}
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
function gitFailure(args, failure) {
|
|
78
|
+
return new GitError(`git ${args.join(" ")} failed: ${failure.stderr.trim() || failure.message}`, args, failure.stderr, failure.code);
|
|
79
|
+
}
|
package/dist/git/lfs.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { LFS_POINTER_VERSION } from "../schema/image.js";
|
|
5
|
+
import { git } from "./exec.js";
|
|
6
|
+
const POINTER_MAX = 1024;
|
|
7
|
+
export function parseLfsPointer(bytes) {
|
|
8
|
+
if (bytes.length === 0 || bytes.length > POINTER_MAX) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
if (bytes.includes(0)) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const text = new TextDecoder("utf8").decode(bytes);
|
|
15
|
+
if (!text.startsWith(`${LFS_POINTER_VERSION}\n`)) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const oid = /^oid sha256:([a-f0-9]{64})$/m.exec(text)?.[1];
|
|
19
|
+
const sizeRaw = /^size (\d+)$/m.exec(text)?.[1];
|
|
20
|
+
if (oid === undefined || sizeRaw === undefined) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
return { oid, size: Number(sizeRaw) };
|
|
24
|
+
}
|
|
25
|
+
export function lfsObjectPath(gitCommonDir, oid) {
|
|
26
|
+
return join(gitCommonDir, "lfs", "objects", oid.slice(0, 2), oid.slice(2, 4), oid);
|
|
27
|
+
}
|
|
28
|
+
export async function gitCommonDir(cwd) {
|
|
29
|
+
const raw = (await git(cwd, ["rev-parse", "--git-common-dir"])).trim();
|
|
30
|
+
return isAbsolute(raw) ? raw : resolve(cwd, raw);
|
|
31
|
+
}
|
|
32
|
+
export async function readLfsObject(cwd, pointer) {
|
|
33
|
+
const common = await gitCommonDir(cwd);
|
|
34
|
+
const stored = lfsObjectPath(common, pointer.oid);
|
|
35
|
+
if (!existsSync(stored)) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
return readFile(stored);
|
|
39
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function cliPin(version) {
|
|
2
|
+
return `npx comprehende@${version}`;
|
|
3
|
+
}
|
|
4
|
+
const PIN = /npx comprehende@([^\s`]+)/g;
|
|
5
|
+
export function listedCliPins(markdown) {
|
|
6
|
+
return [...markdown.matchAll(PIN)].map((match) => match[1] ?? "");
|
|
7
|
+
}
|
|
8
|
+
export function applyCliPin(markdown, version) {
|
|
9
|
+
return markdown.replace(PIN, cliPin(version));
|
|
10
|
+
}
|
|
11
|
+
export function cliPinErrors(markdown, version) {
|
|
12
|
+
const expected = cliPin(version);
|
|
13
|
+
const pins = listedCliPins(markdown);
|
|
14
|
+
if (pins.length === 0) {
|
|
15
|
+
return [`SKILL.md must pin ${expected}`];
|
|
16
|
+
}
|
|
17
|
+
return [...new Set(pins.filter((pin) => pin !== version))].map((pin) => `SKILL.md pins npx comprehende@${pin}, package.json version is ${version}`);
|
|
18
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const UNCHANGED_KEEP = 0.35;
|
|
2
|
+
const THRESHOLD = 8;
|
|
3
|
+
/** Paint a same-size RGBA pair. Unchanged pixels go gray; changed pixels go magenta. */
|
|
4
|
+
export function diffRgba(oldPixels, newPixels, width, height, threshold = THRESHOLD) {
|
|
5
|
+
const total = width * height;
|
|
6
|
+
const pixels = new Uint8ClampedArray(total * 4);
|
|
7
|
+
let changed = 0;
|
|
8
|
+
for (let i = 0; i < total; i += 1) {
|
|
9
|
+
const o = i * 4;
|
|
10
|
+
const oldA = oldPixels[o + 3] ?? 0;
|
|
11
|
+
const newA = newPixels[o + 3] ?? 0;
|
|
12
|
+
const empty = oldA === 0 && newA === 0;
|
|
13
|
+
const dr = Math.abs((oldPixels[o] ?? 0) - (newPixels[o] ?? 0));
|
|
14
|
+
const dg = Math.abs((oldPixels[o + 1] ?? 0) - (newPixels[o + 1] ?? 0));
|
|
15
|
+
const db = Math.abs((oldPixels[o + 2] ?? 0) - (newPixels[o + 2] ?? 0));
|
|
16
|
+
const da = Math.abs(oldA - newA);
|
|
17
|
+
const delta = Math.max(dr, dg, db, da);
|
|
18
|
+
if (empty || delta <= threshold) {
|
|
19
|
+
const gray = grayOf(newPixels[o] ?? 0, newPixels[o + 1] ?? 0, newPixels[o + 2] ?? 0);
|
|
20
|
+
const faded = Math.round(gray * UNCHANGED_KEEP);
|
|
21
|
+
pixels[o] = faded;
|
|
22
|
+
pixels[o + 1] = faded;
|
|
23
|
+
pixels[o + 2] = faded;
|
|
24
|
+
pixels[o + 3] = 255;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
changed += 1;
|
|
28
|
+
if (oldA === 0 && newA > 0) {
|
|
29
|
+
pixels[o] = 26;
|
|
30
|
+
pixels[o + 1] = 127;
|
|
31
|
+
pixels[o + 2] = 55;
|
|
32
|
+
pixels[o + 3] = 255;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (newA === 0 && oldA > 0) {
|
|
36
|
+
pixels[o] = 207;
|
|
37
|
+
pixels[o + 1] = 34;
|
|
38
|
+
pixels[o + 2] = 46;
|
|
39
|
+
pixels[o + 3] = 255;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
pixels[o] = 200;
|
|
43
|
+
pixels[o + 1] = 40;
|
|
44
|
+
pixels[o + 2] = 160;
|
|
45
|
+
pixels[o + 3] = 255;
|
|
46
|
+
}
|
|
47
|
+
return { pixels, changed, total };
|
|
48
|
+
}
|
|
49
|
+
function grayOf(r, g, b) {
|
|
50
|
+
return Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b);
|
|
51
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "webp", "bmp", "avif", "ico"];
|
|
2
|
+
const IMAGE_EXT = new Set(IMAGE_EXTENSIONS);
|
|
3
|
+
export const LFS_POINTER_VERSION = "version https://git-lfs.github.com/spec/v1";
|
|
4
|
+
const MEDIA = {
|
|
5
|
+
png: "image/png",
|
|
6
|
+
jpg: "image/jpeg",
|
|
7
|
+
jpeg: "image/jpeg",
|
|
8
|
+
gif: "image/gif",
|
|
9
|
+
webp: "image/webp",
|
|
10
|
+
bmp: "image/bmp",
|
|
11
|
+
avif: "image/avif",
|
|
12
|
+
ico: "image/x-icon",
|
|
13
|
+
};
|
|
14
|
+
export function imageExtension(path) {
|
|
15
|
+
const base = path.split("/").pop() ?? path;
|
|
16
|
+
const dot = base.lastIndexOf(".");
|
|
17
|
+
if (dot <= 0) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const ext = base.slice(dot + 1).toLowerCase();
|
|
21
|
+
return IMAGE_EXT.has(ext) ? ext : undefined;
|
|
22
|
+
}
|
|
23
|
+
export function isImagePath(path) {
|
|
24
|
+
return imageExtension(path) !== undefined;
|
|
25
|
+
}
|
|
26
|
+
export function imageMediaType(path) {
|
|
27
|
+
const ext = imageExtension(path);
|
|
28
|
+
return ext === undefined ? "application/octet-stream" : MEDIA[ext];
|
|
29
|
+
}
|
|
30
|
+
export function isLfsPointerText(text) {
|
|
31
|
+
return text.includes(LFS_POINTER_VERSION) && /oid sha256:[a-f0-9]{64}/.test(text);
|
|
32
|
+
}
|
|
33
|
+
export function isImageHunkRef(ref) {
|
|
34
|
+
return ref.oldStart === 0 && ref.oldLines === 0 && ref.newStart === 0 && ref.newLines === 0;
|
|
35
|
+
}
|
package/dist/schema/parse.js
CHANGED
|
@@ -2,7 +2,7 @@ import { isReviewSize, REVIEW_SIZES } from "./types.js";
|
|
|
2
2
|
const DOCUMENT_KEYS = new Set(["version", "source", "size", "walkthrough", "tickets", "groups"]);
|
|
3
3
|
const SOURCE_KEYS = new Set(["baseRef", "headRef", "range"]);
|
|
4
4
|
const TICKET_KEYS = new Set(["id", "url", "title"]);
|
|
5
|
-
const GROUP_KEYS = new Set(["id", "title", "summary", "lookFor", "dependsOn", "suggestedOrder", "hunkRefs"]);
|
|
5
|
+
const GROUP_KEYS = new Set(["id", "title", "summary", "lookFor", "dependsOn", "part", "suggestedOrder", "hunkRefs"]);
|
|
6
6
|
const HUNK_KEYS = new Set(["path", "oldPath", "oldStart", "oldLines", "newStart", "newLines"]);
|
|
7
7
|
export function parseReviewDocument(input) {
|
|
8
8
|
const errors = [];
|
|
@@ -129,6 +129,7 @@ function parseGroups(value, errors) {
|
|
|
129
129
|
const hunkRefs = parseHunkRefs(item.hunkRefs, `groups[${i}].hunkRefs`, errors);
|
|
130
130
|
const lookFor = parseStringList(item.lookFor, `groups[${i}].lookFor`, errors);
|
|
131
131
|
const dependsOn = parseStringList(item.dependsOn, `groups[${i}].dependsOn`, errors);
|
|
132
|
+
const part = item.part === undefined ? undefined : requiredString(item.part, `groups[${i}].part`, errors);
|
|
132
133
|
if (id === undefined || title === undefined || summary === undefined || suggestedOrder === undefined) {
|
|
133
134
|
return;
|
|
134
135
|
}
|
|
@@ -143,6 +144,9 @@ function parseGroups(value, errors) {
|
|
|
143
144
|
if (dependsOn !== undefined) {
|
|
144
145
|
group.dependsOn = dependsOn;
|
|
145
146
|
}
|
|
147
|
+
if (part !== undefined) {
|
|
148
|
+
group.part = part;
|
|
149
|
+
}
|
|
146
150
|
groups.push(group);
|
|
147
151
|
});
|
|
148
152
|
const known = new Set(groups.map((group) => group.id));
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
import { cliPinErrors } from "./cli-pin.js";
|
|
5
|
+
import { skillPaths } from "./skill-paths.js";
|
|
6
|
+
import { findPackageRoot, readPackageVersion } from "../package-root.js";
|
|
7
|
+
export function skillSyncErrors(input) {
|
|
8
|
+
const errors = [];
|
|
9
|
+
if (input.publishedSchema !== input.canonicalSchema) {
|
|
10
|
+
errors.push("skills/comprehende/references/review.schema.json drifted. Run: pnpm sync:skill");
|
|
11
|
+
}
|
|
12
|
+
if (input.installedSchema !== input.canonicalSchema) {
|
|
13
|
+
errors.push(".agents/skills/comprehende/references/review.schema.json drifted. Run: pnpm sync:skill");
|
|
14
|
+
}
|
|
15
|
+
errors.push(...cliPinErrors(input.publishedSkillMd, input.version));
|
|
16
|
+
const publishedKeys = [...input.publishedFiles.keys()].sort();
|
|
17
|
+
const installedKeys = [...input.installedFiles.keys()].sort();
|
|
18
|
+
if (publishedKeys.join("\n") !== installedKeys.join("\n")) {
|
|
19
|
+
errors.push(".agents/skills/comprehende is not a copy of skills/comprehende. Run: pnpm sync:skill");
|
|
20
|
+
return errors;
|
|
21
|
+
}
|
|
22
|
+
for (const rel of publishedKeys) {
|
|
23
|
+
if (input.publishedFiles.get(rel) !== input.installedFiles.get(rel)) {
|
|
24
|
+
errors.push(`${rel} differs between skills/ and .agents/. Run: pnpm sync:skill`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return errors;
|
|
28
|
+
}
|
|
29
|
+
export async function loadWorkingTreeSkillSync(root = findPackageRoot()) {
|
|
30
|
+
const paths = skillPaths(root);
|
|
31
|
+
const publishedFiles = await readTree(paths.publishedSkill);
|
|
32
|
+
const installedFiles = await readTree(paths.installedSkill);
|
|
33
|
+
return {
|
|
34
|
+
version: readPackageVersion(),
|
|
35
|
+
canonicalSchema: await readFile(paths.canonicalSchema, "utf8"),
|
|
36
|
+
publishedSchema: await readFile(paths.publishedSchema, "utf8"),
|
|
37
|
+
installedSchema: await readFile(paths.installedSchema, "utf8"),
|
|
38
|
+
publishedSkillMd: publishedFiles.get("SKILL.md") ?? "",
|
|
39
|
+
publishedFiles,
|
|
40
|
+
installedFiles,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function loadStagedSkillSync(root = findPackageRoot()) {
|
|
44
|
+
const publishedFiles = gitStagedTree(root, "skills/comprehende");
|
|
45
|
+
const installedFiles = gitStagedTree(root, ".agents/skills/comprehende");
|
|
46
|
+
const pkg = JSON.parse(gitShowStaged(root, "package.json"));
|
|
47
|
+
return {
|
|
48
|
+
version: pkg.version,
|
|
49
|
+
canonicalSchema: gitShowStaged(root, "src/schema/review.schema.json"),
|
|
50
|
+
publishedSchema: gitShowStaged(root, "skills/comprehende/references/review.schema.json"),
|
|
51
|
+
installedSchema: gitShowStaged(root, ".agents/skills/comprehende/references/review.schema.json"),
|
|
52
|
+
publishedSkillMd: publishedFiles.get("SKILL.md") ?? "",
|
|
53
|
+
publishedFiles,
|
|
54
|
+
installedFiles,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async function readTree(dir) {
|
|
58
|
+
const out = new Map();
|
|
59
|
+
const walk = async (current) => {
|
|
60
|
+
for (const entry of await readdir(current, { withFileTypes: true })) {
|
|
61
|
+
const full = join(current, entry.name);
|
|
62
|
+
if (entry.isDirectory()) {
|
|
63
|
+
await walk(full);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
out.set(relative(dir, full), await readFile(full, "utf8"));
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
await walk(dir);
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
function gitStagedTree(root, prefix) {
|
|
73
|
+
const listing = execFileSync("git", ["ls-files", "-z", "--", prefix], {
|
|
74
|
+
cwd: root,
|
|
75
|
+
encoding: "utf8",
|
|
76
|
+
});
|
|
77
|
+
const out = new Map();
|
|
78
|
+
for (const full of listing.split("\0").filter(Boolean)) {
|
|
79
|
+
out.set(relative(prefix, full), gitShowStaged(root, full));
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
function gitShowStaged(root, path) {
|
|
84
|
+
try {
|
|
85
|
+
return execFileSync("git", ["show", `:${path}`], { cwd: root, encoding: "utf8" });
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
throw new Error(`staged file missing: ${path}`);
|
|
89
|
+
}
|
|
90
|
+
}
|