getgloss 0.5.0 → 0.6.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 +1 -11
- package/dist/cli/index.js +404 -184
- package/dist/cli/index.js.map +1 -1
- package/dist/server/daemon.js +360 -169
- package/dist/server/daemon.js.map +1 -1
- package/dist/web/assets/index-Bj1LDmIl.js +179 -0
- package/dist/web/assets/{index-rvNgmEHK.css → index-BsRo7I09.css} +1 -1
- package/dist/web/gloss-demo-captions.vtt +16 -0
- package/dist/web/gloss-demo-poster.jpg +0 -0
- package/dist/web/gloss-demo.mp4 +0 -0
- package/dist/web/index.html +2 -2
- package/package.json +24 -24
- package/skill/SKILL.md +31 -5
- package/dist/web/assets/index-AW7l4N7K.js +0 -188
package/dist/cli/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import path from "path";
|
|
|
12
12
|
// package.json
|
|
13
13
|
var package_default = {
|
|
14
14
|
name: "getgloss",
|
|
15
|
-
version: "0.
|
|
15
|
+
version: "0.6.0",
|
|
16
16
|
description: "Local browser-based diff review for coding-agent loops.",
|
|
17
17
|
type: "module",
|
|
18
18
|
packageManager: "pnpm@10.33.2",
|
|
@@ -42,31 +42,31 @@ var package_default = {
|
|
|
42
42
|
node: ">=20"
|
|
43
43
|
},
|
|
44
44
|
dependencies: {
|
|
45
|
-
"@hono/node-server": "
|
|
46
|
-
"@
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
react: "
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
zustand: "^5.0.5"
|
|
45
|
+
"@hono/node-server": "1.19.14",
|
|
46
|
+
"@tailwindcss/vite": "4.3.0",
|
|
47
|
+
commander: "14.0.3",
|
|
48
|
+
execa: "9.6.1",
|
|
49
|
+
"get-port": "7.2.0",
|
|
50
|
+
hono: "4.12.21",
|
|
51
|
+
"lucide-react": "1.16.0",
|
|
52
|
+
open: "10.2.0",
|
|
53
|
+
react: "19.2.6",
|
|
54
|
+
"react-dom": "19.2.6",
|
|
55
|
+
ulid: "3.0.2",
|
|
56
|
+
zustand: "5.0.13"
|
|
58
57
|
},
|
|
59
58
|
devDependencies: {
|
|
60
|
-
"@biomejs/biome": "
|
|
61
|
-
"@types/node": "
|
|
62
|
-
"@types/react": "
|
|
63
|
-
"@types/react-dom": "
|
|
64
|
-
"@vitejs/plugin-react": "
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
59
|
+
"@biomejs/biome": "2.4.15",
|
|
60
|
+
"@types/node": "24.12.4",
|
|
61
|
+
"@types/react": "19.2.15",
|
|
62
|
+
"@types/react-dom": "19.2.3",
|
|
63
|
+
"@vitejs/plugin-react": "4.7.0",
|
|
64
|
+
tailwindcss: "4.3.0",
|
|
65
|
+
tsup: "8.5.1",
|
|
66
|
+
tsx: "4.22.3",
|
|
67
|
+
typescript: "5.9.3",
|
|
68
|
+
vite: "6.4.2",
|
|
69
|
+
vitest: "3.2.4"
|
|
70
70
|
},
|
|
71
71
|
keywords: [
|
|
72
72
|
"diff",
|
|
@@ -139,32 +139,40 @@ async function ensureDir(dir) {
|
|
|
139
139
|
// src/cli/git.ts
|
|
140
140
|
import { execa } from "execa";
|
|
141
141
|
|
|
142
|
-
// src/
|
|
142
|
+
// src/shared/language.ts
|
|
143
143
|
import path2 from "path";
|
|
144
|
-
var
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
144
|
+
var languageByExtension = {
|
|
145
|
+
cjs: "js",
|
|
146
|
+
css: "css",
|
|
147
|
+
go: "go",
|
|
148
|
+
html: "html",
|
|
149
|
+
js: "js",
|
|
150
|
+
json: "json",
|
|
151
|
+
jsx: "jsx",
|
|
152
|
+
md: "markdown",
|
|
153
|
+
mjs: "js",
|
|
154
|
+
py: "python",
|
|
155
|
+
rb: "ruby",
|
|
156
|
+
rs: "rust",
|
|
157
|
+
sh: "bash",
|
|
158
|
+
swift: "swift",
|
|
159
|
+
ts: "ts",
|
|
160
|
+
tsx: "tsx",
|
|
161
|
+
yaml: "yaml",
|
|
162
|
+
yml: "yaml"
|
|
163
|
+
};
|
|
148
164
|
function languageForPath(filePath) {
|
|
149
165
|
const ext = path2.extname(filePath).slice(1).toLowerCase();
|
|
150
166
|
if (!ext) {
|
|
151
167
|
return null;
|
|
152
168
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
py: "python",
|
|
161
|
-
rb: "ruby",
|
|
162
|
-
sh: "bash",
|
|
163
|
-
md: "markdown",
|
|
164
|
-
yml: "yaml",
|
|
165
|
-
yaml: "yaml"
|
|
166
|
-
};
|
|
167
|
-
return map[ext] ?? ext;
|
|
169
|
+
return languageByExtension[ext] ?? ext;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/cli/diff-parser.ts
|
|
173
|
+
var hunkHeaderPattern = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/;
|
|
174
|
+
function stripGitPath(input) {
|
|
175
|
+
return input.replace(/^[ab]\//, "");
|
|
168
176
|
}
|
|
169
177
|
function emptyFile() {
|
|
170
178
|
return {
|
|
@@ -466,10 +474,198 @@ async function assertGitAvailable() {
|
|
|
466
474
|
// src/cli/lifecycle.ts
|
|
467
475
|
import { spawn } from "child_process";
|
|
468
476
|
import { existsSync, openSync } from "fs";
|
|
469
|
-
import {
|
|
477
|
+
import { rm } from "fs/promises";
|
|
470
478
|
import { fileURLToPath } from "url";
|
|
471
479
|
import getPort from "get-port";
|
|
472
480
|
|
|
481
|
+
// src/shared/server-info.ts
|
|
482
|
+
import { readFile } from "fs/promises";
|
|
483
|
+
|
|
484
|
+
// src/shared/json.ts
|
|
485
|
+
import { writeFile } from "fs/promises";
|
|
486
|
+
function serializeJson(value) {
|
|
487
|
+
return `${JSON.stringify(value, null, 2)}
|
|
488
|
+
`;
|
|
489
|
+
}
|
|
490
|
+
async function writeJsonFile(filePath, value) {
|
|
491
|
+
await writeFile(filePath, serializeJson(value));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/shared/validation.ts
|
|
495
|
+
var reviewStatuses = ["pending", "submitted", "cancelled", "resolved"];
|
|
496
|
+
var resolutionStatuses = ["partial", "resolved"];
|
|
497
|
+
var reviewUpdateReasons = ["review-resolved", "comment-resolved", "comment-reopened"];
|
|
498
|
+
var sides = ["L", "R"];
|
|
499
|
+
var diffLineTypes = ["context", "add", "delete"];
|
|
500
|
+
var diffScopeModes = ["working", "branch", "explicit"];
|
|
501
|
+
var diffFallbackReasons = ["working-tree-clean", "missing-branch-base"];
|
|
502
|
+
function parseJson(raw, guard, label) {
|
|
503
|
+
const parsed = JSON.parse(raw);
|
|
504
|
+
return parseJsonValue(parsed, guard, label);
|
|
505
|
+
}
|
|
506
|
+
function parseJsonValue(value, guard, label) {
|
|
507
|
+
if (!guard(value)) {
|
|
508
|
+
throw new Error(`Invalid ${label}`);
|
|
509
|
+
}
|
|
510
|
+
return value;
|
|
511
|
+
}
|
|
512
|
+
function isServerInfo(value) {
|
|
513
|
+
return isRecord(value) && isNumber(value.pid) && isNumber(value.port) && isString(value.version) && isString(value.startedAt) && isString(value.stateDir);
|
|
514
|
+
}
|
|
515
|
+
function isHealthResponse(value) {
|
|
516
|
+
return isRecord(value) && isBoolean(value.ok) && isString(value.version) && isNumber(value.activeReviews);
|
|
517
|
+
}
|
|
518
|
+
function isCreateReviewResponse(value) {
|
|
519
|
+
return isRecord(value) && isReviewMeta(value.meta) && isString(value.url);
|
|
520
|
+
}
|
|
521
|
+
function isListReviewsResponse(value) {
|
|
522
|
+
return isRecord(value) && isArrayOf(value.reviews, isReviewMeta);
|
|
523
|
+
}
|
|
524
|
+
function isOpenResult(value) {
|
|
525
|
+
return isRecord(value) && isString(value.reviewId) && isString(value.url) && isNumber(value.files) && isOptionalNumber(value.comments) && isOptionalString(value.feedbackPath) && isOptionalString(value.markdownPath) && isOptionalString(value.artifactDir);
|
|
526
|
+
}
|
|
527
|
+
function isResolveResult(value) {
|
|
528
|
+
return isRecord(value) && value.ok === true && isString(value.reviewId) && isReviewStatus(value.status) && isResolutionStatus(value.resolutionStatus) && isResolutionCounts(value.comments) && isString(value.path) && isResolutionBundle(value.resolution);
|
|
529
|
+
}
|
|
530
|
+
function isReviewRecord(value) {
|
|
531
|
+
return isRecord(value) && isReviewMeta(value.meta) && isDiffPayload(value.diff) && isOptional(value.feedback, isFeedbackBundle) && isOptional(value.resolution, isResolutionBundle);
|
|
532
|
+
}
|
|
533
|
+
function isStoredReviewMeta(value) {
|
|
534
|
+
return isRecord(value) && isString(value.id) && isString(value.cwd) && isBaseRef(value.base) && isNullableString(value.branch) && isReviewStatus(value.status) && isString(value.createdAt) && isOptionalString(value.submittedAt) && isOptionalString(value.resolvedAt) && isOptionalString(value.artifactDir) && isOptionalString(value.feedbackPath) && isOptionalString(value.markdownPath);
|
|
535
|
+
}
|
|
536
|
+
function isReviewMeta(value) {
|
|
537
|
+
return isStoredReviewMeta(value) && isString(value.artifactDir);
|
|
538
|
+
}
|
|
539
|
+
function isDiffPayload(value) {
|
|
540
|
+
return isRecord(value) && isBaseRef(value.base) && isNullableString(value.branch) && isString(value.cwd) && isDiffScope(value.scope) && isDiffStats(value.stats) && isString(value.rawDiff) && isArrayOf(value.files, isDiffFile) && isString(value.capturedAt);
|
|
541
|
+
}
|
|
542
|
+
function isFeedbackBundle(value) {
|
|
543
|
+
return isRecord(value) && value.version === 1 && isString(value.reviewId) && isString(value.timestamp) && isBaseRef(value.base) && isNullableString(value.branch) && isArrayOf(value.comments, isComment);
|
|
544
|
+
}
|
|
545
|
+
function isResolutionBundle(value) {
|
|
546
|
+
return isRecord(value) && isString(value.reviewId) && isResolutionStatus(value.status) && isNullableString(value.summary) && isNullableString(value.resolvedAt) && isArrayOf(value.comments, isResolvedComment);
|
|
547
|
+
}
|
|
548
|
+
function isReviewEvent(value) {
|
|
549
|
+
if (!isRecord(value) || !isString(value.reviewId) || !isString(value.type)) {
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
switch (value.type) {
|
|
553
|
+
case "review.opened":
|
|
554
|
+
case "review.cancelled":
|
|
555
|
+
return true;
|
|
556
|
+
case "review.submitted":
|
|
557
|
+
return isRecord(value.counts) && isNumber(value.counts.files) && isNumber(value.counts.comments);
|
|
558
|
+
case "review.updated":
|
|
559
|
+
return isReviewUpdateReason(value.reason) && isReviewStatus(value.status) && isResolutionStatus(value.resolutionStatus) && isResolutionCounts(value.counts);
|
|
560
|
+
default:
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
function isDiffScope(value) {
|
|
565
|
+
return isRecord(value) && isOneOf(value.mode, diffScopeModes) && isNullableString(value.requestedBase) && isBaseRef(value.base) && isDiffRef(value.comparison) && (value.fallbackReason === null || isOneOf(value.fallbackReason, diffFallbackReasons));
|
|
566
|
+
}
|
|
567
|
+
function isDiffRef(value) {
|
|
568
|
+
return isRecord(value) && isString(value.ref) && isNullableString(value.sha);
|
|
569
|
+
}
|
|
570
|
+
function isBaseRef(value) {
|
|
571
|
+
return isRecord(value) && isString(value.ref) && isString(value.sha);
|
|
572
|
+
}
|
|
573
|
+
function isDiffStats(value) {
|
|
574
|
+
return isRecord(value) && isNumber(value.files) && isNumber(value.additions) && isNumber(value.deletions);
|
|
575
|
+
}
|
|
576
|
+
function isDiffFile(value) {
|
|
577
|
+
return isRecord(value) && isString(value.path) && isNullableString(value.oldPath) && isNumber(value.additions) && isNumber(value.deletions) && isBoolean(value.isBinary) && isBoolean(value.isDeleted) && isBoolean(value.isNew) && isBoolean(value.isRenamed) && isNullableString(value.language) && isArrayOf(value.hunks, isDiffHunk);
|
|
578
|
+
}
|
|
579
|
+
function isDiffHunk(value) {
|
|
580
|
+
return isRecord(value) && isNumber(value.oldStart) && isNumber(value.oldLines) && isNumber(value.newStart) && isNumber(value.newLines) && isString(value.header) && isArrayOf(value.lines, isDiffLine);
|
|
581
|
+
}
|
|
582
|
+
function isDiffLine(value) {
|
|
583
|
+
return isRecord(value) && isOneOf(value.type, diffLineTypes) && isNullableNumber(value.oldLine) && isNullableNumber(value.newLine) && isString(value.content);
|
|
584
|
+
}
|
|
585
|
+
function isComment(value) {
|
|
586
|
+
return isRecord(value) && isString(value.id) && isString(value.filePath) && isNumber(value.startLine) && isNumber(value.endLine) && isOneOf(value.side, sides) && isString(value.body) && isString(value.originalSnippet) && isString(value.createdAt);
|
|
587
|
+
}
|
|
588
|
+
function isResolvedComment(value) {
|
|
589
|
+
return isRecord(value) && isString(value.commentId) && value.status === "resolved" && isOptionalString(value.summary) && isString(value.resolvedAt);
|
|
590
|
+
}
|
|
591
|
+
function isResolutionCounts(value) {
|
|
592
|
+
return isRecord(value) && isNumber(value.total) && isNumber(value.resolved) && isNumber(value.open);
|
|
593
|
+
}
|
|
594
|
+
function isReviewStatus(value) {
|
|
595
|
+
return isOneOf(value, reviewStatuses);
|
|
596
|
+
}
|
|
597
|
+
function isResolutionStatus(value) {
|
|
598
|
+
return isOneOf(value, resolutionStatuses);
|
|
599
|
+
}
|
|
600
|
+
function isReviewUpdateReason(value) {
|
|
601
|
+
return isOneOf(value, reviewUpdateReasons);
|
|
602
|
+
}
|
|
603
|
+
function isRecord(value) {
|
|
604
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
605
|
+
}
|
|
606
|
+
function isArrayOf(value, guard) {
|
|
607
|
+
return Array.isArray(value) && value.every(guard);
|
|
608
|
+
}
|
|
609
|
+
function isOptional(value, guard) {
|
|
610
|
+
return value === void 0 || guard(value);
|
|
611
|
+
}
|
|
612
|
+
function isString(value) {
|
|
613
|
+
return typeof value === "string";
|
|
614
|
+
}
|
|
615
|
+
function isOptionalString(value) {
|
|
616
|
+
return value === void 0 || isString(value);
|
|
617
|
+
}
|
|
618
|
+
function isNullableString(value) {
|
|
619
|
+
return value === null || isString(value);
|
|
620
|
+
}
|
|
621
|
+
function isNumber(value) {
|
|
622
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
623
|
+
}
|
|
624
|
+
function isOptionalNumber(value) {
|
|
625
|
+
return value === void 0 || isNumber(value);
|
|
626
|
+
}
|
|
627
|
+
function isNullableNumber(value) {
|
|
628
|
+
return value === null || isNumber(value);
|
|
629
|
+
}
|
|
630
|
+
function isBoolean(value) {
|
|
631
|
+
return typeof value === "boolean";
|
|
632
|
+
}
|
|
633
|
+
function isOneOf(value, options) {
|
|
634
|
+
return typeof value === "string" && options.includes(value);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/shared/server-info.ts
|
|
638
|
+
async function readServerInfo() {
|
|
639
|
+
let raw;
|
|
640
|
+
try {
|
|
641
|
+
raw = await readFile(globalServerFile(), "utf8");
|
|
642
|
+
} catch (error) {
|
|
643
|
+
if (isFileNotFound(error)) {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
throw new Error(`Could not read server info at ${globalServerFile()}: ${formatError(error)}`, {
|
|
647
|
+
cause: error
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
try {
|
|
651
|
+
return parseJson(raw, isServerInfo, "server info");
|
|
652
|
+
} catch (error) {
|
|
653
|
+
throw new Error(`Invalid server info at ${globalServerFile()}: ${formatError(error)}`, {
|
|
654
|
+
cause: error
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async function writeServerInfo(info) {
|
|
659
|
+
await ensureDir(globalStateDir());
|
|
660
|
+
await writeJsonFile(globalServerFile(), info);
|
|
661
|
+
}
|
|
662
|
+
function isFileNotFound(error) {
|
|
663
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
664
|
+
}
|
|
665
|
+
function formatError(error) {
|
|
666
|
+
return error instanceof Error ? error.message : String(error);
|
|
667
|
+
}
|
|
668
|
+
|
|
473
669
|
// src/cli/server-client.ts
|
|
474
670
|
var ServerClient = class {
|
|
475
671
|
constructor(baseUrl) {
|
|
@@ -477,31 +673,53 @@ var ServerClient = class {
|
|
|
477
673
|
}
|
|
478
674
|
baseUrl;
|
|
479
675
|
async health() {
|
|
480
|
-
return this.get("/api/health");
|
|
676
|
+
return this.get("/api/health", isHealthResponse, "health response");
|
|
481
677
|
}
|
|
482
678
|
async createReview(diff) {
|
|
483
|
-
return this.post("/api/reviews", diff);
|
|
679
|
+
return this.post("/api/reviews", diff, isCreateReviewResponse, "create review response");
|
|
484
680
|
}
|
|
485
681
|
async getReview(reviewId) {
|
|
486
|
-
return this.get(`/api/reviews/${reviewId}
|
|
682
|
+
return this.get(`/api/reviews/${reviewId}`, isReviewRecord, "review response");
|
|
487
683
|
}
|
|
488
684
|
async listReviews() {
|
|
489
|
-
return this.get("/api/reviews");
|
|
685
|
+
return this.get("/api/reviews", isListReviewsResponse, "review list response");
|
|
490
686
|
}
|
|
491
687
|
async getFeedback(reviewId) {
|
|
492
|
-
return this.get(`/api/reviews/${reviewId}/feedback
|
|
688
|
+
return this.get(`/api/reviews/${reviewId}/feedback`, isFeedbackBundle, "feedback response");
|
|
493
689
|
}
|
|
494
690
|
async markResolved(reviewId, summary) {
|
|
495
|
-
|
|
691
|
+
const request = { summary };
|
|
692
|
+
return this.post(
|
|
693
|
+
`/api/reviews/${reviewId}/resolved`,
|
|
694
|
+
request,
|
|
695
|
+
isResolveResult,
|
|
696
|
+
"resolve response"
|
|
697
|
+
);
|
|
496
698
|
}
|
|
497
699
|
async resolveComment(reviewId, commentId, summary) {
|
|
498
|
-
|
|
700
|
+
const request = { summary };
|
|
701
|
+
return this.post(
|
|
702
|
+
`/api/reviews/${reviewId}/comments/${commentId}/resolved`,
|
|
703
|
+
request,
|
|
704
|
+
isResolveResult,
|
|
705
|
+
"resolve comment response"
|
|
706
|
+
);
|
|
499
707
|
}
|
|
500
708
|
async reopenComment(reviewId, commentId) {
|
|
501
|
-
return this.delete(
|
|
709
|
+
return this.delete(
|
|
710
|
+
`/api/reviews/${reviewId}/comments/${commentId}/resolved`,
|
|
711
|
+
isResolveResult,
|
|
712
|
+
"reopen comment response"
|
|
713
|
+
);
|
|
502
714
|
}
|
|
503
715
|
async submitReview(reviewId, comments) {
|
|
504
|
-
|
|
716
|
+
const request = { comments };
|
|
717
|
+
return this.post(
|
|
718
|
+
`/api/reviews/${reviewId}/submit`,
|
|
719
|
+
request,
|
|
720
|
+
isOpenResult,
|
|
721
|
+
"submit review response"
|
|
722
|
+
);
|
|
505
723
|
}
|
|
506
724
|
async watchReview(reviewId, timeoutSeconds) {
|
|
507
725
|
const deadline = timeoutSeconds && timeoutSeconds > 0 ? Date.now() + timeoutSeconds * 1e3 : null;
|
|
@@ -552,7 +770,7 @@ var ServerClient = class {
|
|
|
552
770
|
if (!dataLine) {
|
|
553
771
|
continue;
|
|
554
772
|
}
|
|
555
|
-
const event =
|
|
773
|
+
const event = parseJson(dataLine.slice(5).trim(), isReviewEvent, "review event");
|
|
556
774
|
if (event.type === "review.submitted" || event.type === "review.cancelled") {
|
|
557
775
|
await reader.cancel().catch(() => void 0);
|
|
558
776
|
return event;
|
|
@@ -560,28 +778,29 @@ var ServerClient = class {
|
|
|
560
778
|
}
|
|
561
779
|
}
|
|
562
780
|
}
|
|
563
|
-
async get(path3) {
|
|
781
|
+
async get(path3, guard, label) {
|
|
564
782
|
const response = await fetch(`${this.baseUrl}${path3}`);
|
|
565
|
-
return parseResponse(response);
|
|
783
|
+
return parseResponse(response, guard, label);
|
|
566
784
|
}
|
|
567
|
-
async post(path3, body) {
|
|
785
|
+
async post(path3, body, guard, label) {
|
|
568
786
|
const response = await fetch(`${this.baseUrl}${path3}`, {
|
|
569
787
|
method: "POST",
|
|
570
788
|
headers: { "content-type": "application/json" },
|
|
571
789
|
body: JSON.stringify(body)
|
|
572
790
|
});
|
|
573
|
-
return parseResponse(response);
|
|
791
|
+
return parseResponse(response, guard, label);
|
|
574
792
|
}
|
|
575
|
-
async delete(path3) {
|
|
793
|
+
async delete(path3, guard, label) {
|
|
576
794
|
const response = await fetch(`${this.baseUrl}${path3}`, { method: "DELETE" });
|
|
577
|
-
return parseResponse(response);
|
|
795
|
+
return parseResponse(response, guard, label);
|
|
578
796
|
}
|
|
579
797
|
};
|
|
580
|
-
async function parseResponse(response) {
|
|
798
|
+
async function parseResponse(response, guard, label) {
|
|
581
799
|
if (!response.ok) {
|
|
582
800
|
throw new Error(`${response.status} ${response.statusText}: ${await response.text()}`);
|
|
583
801
|
}
|
|
584
|
-
|
|
802
|
+
const value = await response.json();
|
|
803
|
+
return parseJsonValue(value, guard, label);
|
|
585
804
|
}
|
|
586
805
|
function isPrematureWatchEnd(error) {
|
|
587
806
|
return error instanceof Error && error.message === "watch stream ended before completion";
|
|
@@ -594,13 +813,6 @@ async function sleep(milliseconds) {
|
|
|
594
813
|
}
|
|
595
814
|
|
|
596
815
|
// src/cli/lifecycle.ts
|
|
597
|
-
async function readServerInfo() {
|
|
598
|
-
try {
|
|
599
|
-
return JSON.parse(await readFile(globalServerFile(), "utf8"));
|
|
600
|
-
} catch {
|
|
601
|
-
return null;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
816
|
function serverUrl(info) {
|
|
605
817
|
return `http://localhost:${info.port}`;
|
|
606
818
|
}
|
|
@@ -652,8 +864,7 @@ async function startServer(options = {}) {
|
|
|
652
864
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
653
865
|
stateDir: globalStateDir()
|
|
654
866
|
};
|
|
655
|
-
await
|
|
656
|
-
`);
|
|
867
|
+
await writeServerInfo(info);
|
|
657
868
|
const deadline = Date.now() + 8e3;
|
|
658
869
|
while (Date.now() < deadline) {
|
|
659
870
|
if (await isServerResponsive(info)) {
|
|
@@ -687,17 +898,37 @@ function isPidAlive(pid) {
|
|
|
687
898
|
}
|
|
688
899
|
|
|
689
900
|
// src/server/store.ts
|
|
690
|
-
import { readdir, readFile as readFile2,
|
|
901
|
+
import { readdir, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
691
902
|
import { ulid } from "ulid";
|
|
692
903
|
|
|
693
|
-
// src/shared/
|
|
694
|
-
function
|
|
695
|
-
|
|
696
|
-
if (comment.startLine === comment.endLine) {
|
|
697
|
-
return `${prefix}${comment.startLine}`;
|
|
698
|
-
}
|
|
699
|
-
return `${prefix}${comment.startLine}-${prefix}${comment.endLine}`;
|
|
904
|
+
// src/shared/comments.ts
|
|
905
|
+
function compareCommentsByLocation(a, b) {
|
|
906
|
+
return a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine || a.endLine - b.endLine || a.side.localeCompare(b.side);
|
|
700
907
|
}
|
|
908
|
+
function countCommentFiles(comments) {
|
|
909
|
+
return new Set(comments.map((comment) => comment.filePath)).size;
|
|
910
|
+
}
|
|
911
|
+
function formatLineRange(range, options = {}) {
|
|
912
|
+
const startLine = Math.min(range.startLine, range.endLine);
|
|
913
|
+
const endLine = Math.max(range.startLine, range.endLine);
|
|
914
|
+
if (startLine === endLine) {
|
|
915
|
+
return `${range.side}${startLine}`;
|
|
916
|
+
}
|
|
917
|
+
const endPrefix = options.repeatSideOnEnd === false ? "" : range.side;
|
|
918
|
+
return `${range.side}${startLine}-${endPrefix}${endLine}`;
|
|
919
|
+
}
|
|
920
|
+
function resolutionCounts(feedback, resolvedComments = []) {
|
|
921
|
+
const comments = feedback?.comments ?? [];
|
|
922
|
+
const resolvedIds = new Set(resolvedComments.map((comment) => comment.commentId));
|
|
923
|
+
const resolved = comments.filter((comment) => resolvedIds.has(comment.id)).length;
|
|
924
|
+
return {
|
|
925
|
+
total: comments.length,
|
|
926
|
+
resolved,
|
|
927
|
+
open: comments.length - resolved
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// src/shared/markdown.ts
|
|
701
932
|
function fenceFor(snippet) {
|
|
702
933
|
let fence = "```";
|
|
703
934
|
while (snippet.includes(fence)) {
|
|
@@ -705,40 +936,13 @@ function fenceFor(snippet) {
|
|
|
705
936
|
}
|
|
706
937
|
return fence;
|
|
707
938
|
}
|
|
708
|
-
function languageForPath2(filePath) {
|
|
709
|
-
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
710
|
-
const map = {
|
|
711
|
-
cjs: "js",
|
|
712
|
-
css: "css",
|
|
713
|
-
go: "go",
|
|
714
|
-
html: "html",
|
|
715
|
-
js: "js",
|
|
716
|
-
json: "json",
|
|
717
|
-
jsx: "jsx",
|
|
718
|
-
md: "markdown",
|
|
719
|
-
mjs: "js",
|
|
720
|
-
py: "python",
|
|
721
|
-
rb: "ruby",
|
|
722
|
-
rs: "rust",
|
|
723
|
-
sh: "bash",
|
|
724
|
-
swift: "swift",
|
|
725
|
-
ts: "ts",
|
|
726
|
-
tsx: "tsx",
|
|
727
|
-
yaml: "yaml",
|
|
728
|
-
yml: "yaml"
|
|
729
|
-
};
|
|
730
|
-
return ext ? map[ext] ?? ext : "";
|
|
731
|
-
}
|
|
732
939
|
function languageForSnippet(filePath, snippet) {
|
|
733
940
|
const lines = snippet.split("\n").filter((line) => line.length > 0);
|
|
734
941
|
const looksLikeUnifiedDiff = lines.length > 0 && lines.some((line) => line.startsWith("+") || line.startsWith("-")) && lines.every((line) => line.startsWith("+") || line.startsWith("-") || line.startsWith(" "));
|
|
735
|
-
return looksLikeUnifiedDiff ? "diff" :
|
|
736
|
-
}
|
|
737
|
-
function byFileThenLine(a, b) {
|
|
738
|
-
return a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine || a.endLine - b.endLine || a.side.localeCompare(b.side);
|
|
942
|
+
return looksLikeUnifiedDiff ? "diff" : languageForPath(filePath) ?? "";
|
|
739
943
|
}
|
|
740
944
|
function serializeFeedbackMarkdown(bundle) {
|
|
741
|
-
const comments = [...bundle.comments].sort(
|
|
945
|
+
const comments = [...bundle.comments].sort(compareCommentsByLocation);
|
|
742
946
|
const files = [...new Set(comments.map((comment) => comment.filePath))];
|
|
743
947
|
const lines = [
|
|
744
948
|
`# Gloss feedback - ${bundle.timestamp}`,
|
|
@@ -764,6 +968,11 @@ function serializeFeedbackMarkdown(bundle) {
|
|
|
764
968
|
`;
|
|
765
969
|
}
|
|
766
970
|
|
|
971
|
+
// src/shared/reviews.ts
|
|
972
|
+
function isResolvableReviewStatus(status) {
|
|
973
|
+
return status === "submitted" || status === "resolved";
|
|
974
|
+
}
|
|
975
|
+
|
|
767
976
|
// src/server/store.ts
|
|
768
977
|
var ReviewStore = class {
|
|
769
978
|
reviews = /* @__PURE__ */ new Map();
|
|
@@ -808,9 +1017,7 @@ var ReviewStore = class {
|
|
|
808
1017
|
timestamp,
|
|
809
1018
|
base: record.diff.base,
|
|
810
1019
|
branch: record.diff.branch,
|
|
811
|
-
comments: [...comments].sort(
|
|
812
|
-
(a, b) => a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine || a.endLine - b.endLine || a.side.localeCompare(b.side)
|
|
813
|
-
)
|
|
1020
|
+
comments: [...comments].sort(compareCommentsByLocation)
|
|
814
1021
|
};
|
|
815
1022
|
record.feedback = feedback;
|
|
816
1023
|
record.meta = { ...record.meta, status: "submitted", submittedAt: timestamp };
|
|
@@ -826,17 +1033,15 @@ var ReviewStore = class {
|
|
|
826
1033
|
};
|
|
827
1034
|
await ensureDir(artifactDir);
|
|
828
1035
|
await Promise.all([
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
writeFile2(feedbackPath, `${JSON.stringify(feedback, null, 2)}
|
|
832
|
-
`),
|
|
1036
|
+
writeJsonFile(globalReviewMetaFile(id), record.meta),
|
|
1037
|
+
writeJsonFile(feedbackPath, feedback),
|
|
833
1038
|
writeFile2(markdownPath, serializeFeedbackMarkdown(feedback))
|
|
834
1039
|
]);
|
|
835
1040
|
this.emit({
|
|
836
1041
|
type: "review.submitted",
|
|
837
1042
|
reviewId: id,
|
|
838
1043
|
counts: {
|
|
839
|
-
files:
|
|
1044
|
+
files: countCommentFiles(feedback.comments),
|
|
840
1045
|
comments: feedback.comments.length
|
|
841
1046
|
}
|
|
842
1047
|
});
|
|
@@ -898,7 +1103,7 @@ var ReviewStore = class {
|
|
|
898
1103
|
],
|
|
899
1104
|
record
|
|
900
1105
|
);
|
|
901
|
-
const counts =
|
|
1106
|
+
const counts = resolutionCounts(record.feedback, comments);
|
|
902
1107
|
const fullyResolved = counts.total === counts.resolved;
|
|
903
1108
|
const resolution = {
|
|
904
1109
|
reviewId: id,
|
|
@@ -921,7 +1126,7 @@ var ReviewStore = class {
|
|
|
921
1126
|
(record.resolution?.comments ?? []).filter((comment) => comment.commentId !== commentId),
|
|
922
1127
|
record
|
|
923
1128
|
);
|
|
924
|
-
const counts =
|
|
1129
|
+
const counts = resolutionCounts(record.feedback, comments);
|
|
925
1130
|
const fullyResolved = counts.total > 0 && counts.total === counts.resolved;
|
|
926
1131
|
const resolvedAt = fullyResolved ? (/* @__PURE__ */ new Date()).toISOString() : null;
|
|
927
1132
|
const resolution = {
|
|
@@ -954,10 +1159,8 @@ var ReviewStore = class {
|
|
|
954
1159
|
const dir = globalReviewDir(record.meta.id);
|
|
955
1160
|
await ensureDir(dir);
|
|
956
1161
|
await Promise.all([
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
writeFile2(globalReviewDiffFile(record.meta.id), `${JSON.stringify(record.diff, null, 2)}
|
|
960
|
-
`)
|
|
1162
|
+
writeJsonFile(globalReviewMetaFile(record.meta.id), record.meta),
|
|
1163
|
+
writeJsonFile(globalReviewDiffFile(record.meta.id), record.diff)
|
|
961
1164
|
]);
|
|
962
1165
|
}
|
|
963
1166
|
async loadKnownReview(id) {
|
|
@@ -971,56 +1174,65 @@ var ReviewStore = class {
|
|
|
971
1174
|
let entries;
|
|
972
1175
|
try {
|
|
973
1176
|
entries = await readdir(globalReviewsDir(), { withFileTypes: true });
|
|
974
|
-
} catch {
|
|
975
|
-
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
if (isFileNotFound2(error)) {
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
throw new Error(
|
|
1182
|
+
`Could not read reviews directory at ${globalReviewsDir()}: ${formatError2(error)}`,
|
|
1183
|
+
{
|
|
1184
|
+
cause: error
|
|
1185
|
+
}
|
|
1186
|
+
);
|
|
976
1187
|
}
|
|
977
1188
|
await Promise.all(
|
|
978
1189
|
entries.filter((entry) => entry.isDirectory()).map((entry) => this.loadReview(entry.name))
|
|
979
1190
|
);
|
|
980
1191
|
}
|
|
981
1192
|
async loadReview(id) {
|
|
1193
|
+
const metaPath = globalReviewMetaFile(id);
|
|
1194
|
+
const diffPath = globalReviewDiffFile(id);
|
|
1195
|
+
let metaRaw;
|
|
1196
|
+
let diffRaw;
|
|
982
1197
|
try {
|
|
983
|
-
|
|
984
|
-
readFile2(
|
|
985
|
-
readFile2(
|
|
1198
|
+
[metaRaw, diffRaw] = await Promise.all([
|
|
1199
|
+
readFile2(metaPath, "utf8"),
|
|
1200
|
+
readFile2(diffPath, "utf8")
|
|
986
1201
|
]);
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
let resolution;
|
|
991
|
-
try {
|
|
992
|
-
feedback = JSON.parse(
|
|
993
|
-
await readFile2(globalReviewFeedbackFile(id), "utf8")
|
|
994
|
-
);
|
|
995
|
-
} catch {
|
|
996
|
-
feedback = void 0;
|
|
997
|
-
}
|
|
998
|
-
try {
|
|
999
|
-
resolution = JSON.parse(
|
|
1000
|
-
await readFile2(globalReviewResolvedFile(id), "utf8")
|
|
1001
|
-
);
|
|
1002
|
-
} catch {
|
|
1003
|
-
resolution = void 0;
|
|
1202
|
+
} catch (error) {
|
|
1203
|
+
if (isFileNotFound2(error)) {
|
|
1204
|
+
return null;
|
|
1004
1205
|
}
|
|
1005
|
-
|
|
1006
|
-
meta: {
|
|
1007
|
-
...meta,
|
|
1008
|
-
artifactDir: meta.artifactDir ?? globalReviewDir(id),
|
|
1009
|
-
feedbackPath: meta.feedbackPath ?? (feedback ? globalReviewFeedbackFile(id) : void 0),
|
|
1010
|
-
markdownPath: meta.markdownPath ?? (feedback ? globalReviewMarkdownFile(id) : void 0)
|
|
1011
|
-
},
|
|
1012
|
-
diff,
|
|
1013
|
-
feedback,
|
|
1014
|
-
resolution
|
|
1015
|
-
};
|
|
1016
|
-
this.reviews.set(id, record);
|
|
1017
|
-
return record;
|
|
1018
|
-
} catch {
|
|
1019
|
-
return null;
|
|
1206
|
+
throw new Error(`Could not load review ${id}: ${formatError2(error)}`, { cause: error });
|
|
1020
1207
|
}
|
|
1208
|
+
const meta = parseJsonFile(metaRaw, isStoredReviewMeta, "review metadata", metaPath);
|
|
1209
|
+
const diff = parseJsonFile(diffRaw, isDiffPayload, "review diff", diffPath);
|
|
1210
|
+
const feedback = await readOptionalJsonFile(
|
|
1211
|
+
globalReviewFeedbackFile(id),
|
|
1212
|
+
isFeedbackBundle,
|
|
1213
|
+
"review feedback"
|
|
1214
|
+
);
|
|
1215
|
+
const resolution = await readOptionalJsonFile(
|
|
1216
|
+
globalReviewResolvedFile(id),
|
|
1217
|
+
isResolutionBundle,
|
|
1218
|
+
"review resolution"
|
|
1219
|
+
);
|
|
1220
|
+
const record = {
|
|
1221
|
+
meta: {
|
|
1222
|
+
...meta,
|
|
1223
|
+
artifactDir: meta.artifactDir ?? globalReviewDir(id),
|
|
1224
|
+
feedbackPath: meta.feedbackPath ?? (feedback ? globalReviewFeedbackFile(id) : void 0),
|
|
1225
|
+
markdownPath: meta.markdownPath ?? (feedback ? globalReviewMarkdownFile(id) : void 0)
|
|
1226
|
+
},
|
|
1227
|
+
diff,
|
|
1228
|
+
feedback,
|
|
1229
|
+
resolution
|
|
1230
|
+
};
|
|
1231
|
+
this.reviews.set(id, record);
|
|
1232
|
+
return record;
|
|
1021
1233
|
}
|
|
1022
1234
|
assertResolvable(record, id) {
|
|
1023
|
-
if (record.meta.status
|
|
1235
|
+
if (!isResolvableReviewStatus(record.meta.status)) {
|
|
1024
1236
|
throw new Error(`Review ${id} is ${record.meta.status} and cannot be resolved`);
|
|
1025
1237
|
}
|
|
1026
1238
|
if (!record.feedback) {
|
|
@@ -1038,17 +1250,15 @@ var ReviewStore = class {
|
|
|
1038
1250
|
const resolvedPath = globalReviewResolvedFile(record.meta.id);
|
|
1039
1251
|
await ensureDir(globalReviewDir(record.meta.id));
|
|
1040
1252
|
await Promise.all([
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
writeFile2(globalReviewMetaFile(record.meta.id), `${JSON.stringify(record.meta, null, 2)}
|
|
1044
|
-
`)
|
|
1253
|
+
writeJsonFile(resolvedPath, resolution),
|
|
1254
|
+
writeJsonFile(globalReviewMetaFile(record.meta.id), record.meta)
|
|
1045
1255
|
]);
|
|
1046
1256
|
const result = {
|
|
1047
1257
|
ok: true,
|
|
1048
1258
|
reviewId: record.meta.id,
|
|
1049
1259
|
status: record.meta.status,
|
|
1050
1260
|
resolutionStatus: resolution.status,
|
|
1051
|
-
comments:
|
|
1261
|
+
comments: resolutionCounts(record.feedback, resolution.comments),
|
|
1052
1262
|
path: resolvedPath,
|
|
1053
1263
|
resolution
|
|
1054
1264
|
};
|
|
@@ -1070,19 +1280,34 @@ var ReviewStore = class {
|
|
|
1070
1280
|
(a, b) => (feedbackIndex.get(a.commentId) ?? Number.MAX_SAFE_INTEGER) - (feedbackIndex.get(b.commentId) ?? Number.MAX_SAFE_INTEGER)
|
|
1071
1281
|
);
|
|
1072
1282
|
}
|
|
1073
|
-
resolutionCounts(record, comments) {
|
|
1074
|
-
const total = record.feedback.comments.length;
|
|
1075
|
-
const resolvedIds = new Set(comments.map((comment) => comment.commentId));
|
|
1076
|
-
const resolved = record.feedback.comments.filter(
|
|
1077
|
-
(comment) => resolvedIds.has(comment.id)
|
|
1078
|
-
).length;
|
|
1079
|
-
return {
|
|
1080
|
-
total,
|
|
1081
|
-
resolved,
|
|
1082
|
-
open: total - resolved
|
|
1083
|
-
};
|
|
1084
|
-
}
|
|
1085
1283
|
};
|
|
1284
|
+
async function readOptionalJsonFile(filePath, guard, label) {
|
|
1285
|
+
let raw;
|
|
1286
|
+
try {
|
|
1287
|
+
raw = await readFile2(filePath, "utf8");
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
if (isFileNotFound2(error)) {
|
|
1290
|
+
return void 0;
|
|
1291
|
+
}
|
|
1292
|
+
throw new Error(`Could not read ${label} at ${filePath}: ${formatError2(error)}`, {
|
|
1293
|
+
cause: error
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
return parseJsonFile(raw, guard, label, filePath);
|
|
1297
|
+
}
|
|
1298
|
+
function parseJsonFile(raw, guard, label, filePath) {
|
|
1299
|
+
try {
|
|
1300
|
+
return parseJson(raw, guard, label);
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
throw new Error(`Invalid ${label} at ${filePath}: ${formatError2(error)}`, { cause: error });
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
function isFileNotFound2(error) {
|
|
1306
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
1307
|
+
}
|
|
1308
|
+
function formatError2(error) {
|
|
1309
|
+
return error instanceof Error ? error.message : String(error);
|
|
1310
|
+
}
|
|
1086
1311
|
var reviewStore = new ReviewStore();
|
|
1087
1312
|
|
|
1088
1313
|
// src/cli/status.ts
|
|
@@ -1179,7 +1404,7 @@ program.command("status").description("Show server and active reviews").action(a
|
|
|
1179
1404
|
responsive && info ? `Gloss server running at ${serverUrl(info)} with ${reviews.length} active review(s)` : "Gloss server is not running"
|
|
1180
1405
|
);
|
|
1181
1406
|
});
|
|
1182
|
-
program.command("stop").description("Stop the managed background server").
|
|
1407
|
+
program.command("stop").description("Stop the managed background server").action(async () => {
|
|
1183
1408
|
const globals = program.opts();
|
|
1184
1409
|
const result = await stopServer();
|
|
1185
1410
|
globals.json ? printJson(result) : printPlain(result.stopped ? "Gloss server stopped" : "Gloss server was not running");
|
|
@@ -1230,11 +1455,6 @@ program.command("doctor").description("Diagnose setup and validate git/state").a
|
|
|
1230
1455
|
ok: info ? await isServerResponsive(info) : false,
|
|
1231
1456
|
detail: info ? serverUrl(info) : "not started"
|
|
1232
1457
|
});
|
|
1233
|
-
checks.push({
|
|
1234
|
-
name: "@pierre/diffs license",
|
|
1235
|
-
ok: true,
|
|
1236
|
-
detail: "apache-2.0 dependency present"
|
|
1237
|
-
});
|
|
1238
1458
|
if (globals.json) {
|
|
1239
1459
|
printJson({ checks });
|
|
1240
1460
|
} else {
|