@fabiofiorita/porcelain 0.60.0 → 0.60.1
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
CHANGED
package/main/daemon/server.js
CHANGED
|
@@ -3261,6 +3261,8 @@ var evidenceAssetBodySchema = import_zod22.z.object({
|
|
|
3261
3261
|
}).strict();
|
|
3262
3262
|
var reviewCommentSchema = import_zod22.z.object({
|
|
3263
3263
|
id: import_zod22.z.string(),
|
|
3264
|
+
/** Absent only for comments written before authorship was recorded; clients treat those as user. */
|
|
3265
|
+
author: import_zod22.z.enum(["user", "agent"]).optional(),
|
|
3264
3266
|
path: import_zod22.z.string().min(1),
|
|
3265
3267
|
startLine: import_zod22.z.number().int().positive().optional(),
|
|
3266
3268
|
endLine: import_zod22.z.number().int().positive().optional(),
|
|
@@ -7496,11 +7498,11 @@ function createCommitGeneration() {
|
|
|
7496
7498
|
});
|
|
7497
7499
|
}
|
|
7498
7500
|
function createGitDiffReadingSources(options) {
|
|
7499
|
-
const layersFor = async (repoPath) => options === void 0 ? [] : options.review.layersForRepo(repoPath);
|
|
7501
|
+
const layersFor = async (repoPath, commitHash) => options === void 0 ? [] : options.review.layersForRepo(repoPath, commitHash);
|
|
7500
7502
|
return Object.freeze({
|
|
7501
7503
|
loadWorkingFlow: async (repoPath) => loadWorkingFlow(repoPath, await layersFor(repoPath)),
|
|
7502
7504
|
loadRangeFlow: async (repoPath, base) => loadRangeFlow(repoPath, await layersFor(repoPath), base),
|
|
7503
|
-
loadCommitFlow: async (repoPath, hash) => loadCommitFlow(repoPath, hash, await layersFor(repoPath)),
|
|
7505
|
+
loadCommitFlow: async (repoPath, hash) => loadCommitFlow(repoPath, hash, await layersFor(repoPath, hash)),
|
|
7504
7506
|
workingHunks: (repoPath, path, context) => gitDiffFile(repoPath, path, context).then((result) => result.hunks),
|
|
7505
7507
|
rangeHunks: (repoPath, base, path, context) => gitRangeDiffFile(repoPath, base, path, context).then((result) => result.hunks),
|
|
7506
7508
|
diffFile: gitDiffFile,
|
|
@@ -10677,9 +10679,10 @@ function structuredSource(document, assetsDir, extraFiles = []) {
|
|
|
10677
10679
|
...extraFiles.length === 0 ? {} : { extraFiles }
|
|
10678
10680
|
};
|
|
10679
10681
|
}
|
|
10680
|
-
function reviewBundleSource(data, assetsDir) {
|
|
10682
|
+
function reviewBundleSource(data, assetsDir, commitHash) {
|
|
10681
10683
|
const metadata = {
|
|
10682
10684
|
name: data.title,
|
|
10685
|
+
...commitHash === void 0 ? {} : { commitHash },
|
|
10683
10686
|
layers: data.layers,
|
|
10684
10687
|
files: data.files,
|
|
10685
10688
|
sections: []
|
|
@@ -10830,7 +10833,7 @@ function createMcpToolHandlers(deps) {
|
|
|
10830
10833
|
const resolved = await resolveWorkspace(ref, inventory.value);
|
|
10831
10834
|
return resolved.ok ? { ok: true, value: resolved.value } : { ok: false, result: fail(resolved.message) };
|
|
10832
10835
|
}
|
|
10833
|
-
async function canvasSource(args) {
|
|
10836
|
+
async function canvasSource(args, reviewCommitHash) {
|
|
10834
10837
|
if (args.document !== void 0) {
|
|
10835
10838
|
if (args.templateData !== void 0 || args.template !== void 0 || args.files !== void 0 || args.kind !== void 0 || args.entry !== void 0 || args.title !== void 0) {
|
|
10836
10839
|
return {
|
|
@@ -10884,7 +10887,11 @@ function createMcpToolHandlers(deps) {
|
|
|
10884
10887
|
};
|
|
10885
10888
|
}
|
|
10886
10889
|
const assetsDir = stringField(args, "sourceDir");
|
|
10887
|
-
const source = template === "review" ? reviewBundleSource(
|
|
10890
|
+
const source = template === "review" ? reviewBundleSource(
|
|
10891
|
+
reviewCanvasTemplateDataSchema.parse(parsed.data),
|
|
10892
|
+
assetsDir,
|
|
10893
|
+
reviewCommitHash
|
|
10894
|
+
) : planBundleSource(planCanvasTemplateDataSchema.parse(parsed.data), assetsDir);
|
|
10888
10895
|
return {
|
|
10889
10896
|
ok: true,
|
|
10890
10897
|
title: parsed.data.title,
|
|
@@ -10984,7 +10991,17 @@ function createMcpToolHandlers(deps) {
|
|
|
10984
10991
|
}
|
|
10985
10992
|
if (op !== "create" && op !== "update")
|
|
10986
10993
|
return fail("op must be list, get, create, update, delete, or promote.");
|
|
10987
|
-
const
|
|
10994
|
+
const reviewCommitHash = await (async () => {
|
|
10995
|
+
if (args.template !== "review" || place.worktreePath === null) return void 0;
|
|
10996
|
+
try {
|
|
10997
|
+
const status = await operations.git.statusGit(place.worktreePath);
|
|
10998
|
+
if (!status.ok || status.value.length > 0) return void 0;
|
|
10999
|
+
return (await operations.git.logGit({ repoPath: place.worktreePath, limit: 1 }))[0]?.hash;
|
|
11000
|
+
} catch {
|
|
11001
|
+
return void 0;
|
|
11002
|
+
}
|
|
11003
|
+
})();
|
|
11004
|
+
const source = await canvasSource(args, reviewCommitHash);
|
|
10988
11005
|
if (!source.ok) return source.result;
|
|
10989
11006
|
const canvasId = stringField(args, "id");
|
|
10990
11007
|
if (op === "create" && canvasId !== void 0)
|
|
@@ -11022,10 +11039,17 @@ function createMcpToolHandlers(deps) {
|
|
|
11022
11039
|
...replacing ? { replace: true } : {}
|
|
11023
11040
|
});
|
|
11024
11041
|
return promoted.ok ? ok(
|
|
11025
|
-
`Canvas ${written.value.id} written to the tracked overlay. Files written; nothing staged or committed.`
|
|
11042
|
+
source.template === "review" ? `Review Canvas ${written.value.id} written to the tracked overlay${reviewCommitHash === void 0 ? " for live Changes; update it after committing to bind History" : ` and bound to commit ${reviewCommitHash} for History`}. Files written; nothing staged or committed.` : `Canvas ${written.value.id} written to the tracked overlay. Files written; nothing staged or committed.`
|
|
11026
11043
|
) : fail(`Could not update the tracked Canvas: ${describeError(promoted.error)}`);
|
|
11027
11044
|
}
|
|
11028
|
-
return workspaceResult(
|
|
11045
|
+
return workspaceResult(
|
|
11046
|
+
place,
|
|
11047
|
+
source.template === "review" ? {
|
|
11048
|
+
...written.value,
|
|
11049
|
+
historyCommit: reviewCommitHash ?? null,
|
|
11050
|
+
history: reviewCommitHash === void 0 ? "live-only; update this Canvas after committing to bind History" : "bound"
|
|
11051
|
+
} : written.value
|
|
11052
|
+
);
|
|
11029
11053
|
},
|
|
11030
11054
|
async porcelain_comment(args, place) {
|
|
11031
11055
|
if (place?.worktreePath === null || place === null)
|
|
@@ -11054,6 +11078,7 @@ function createMcpToolHandlers(deps) {
|
|
|
11054
11078
|
return fail("comment.path and comment.body are required to create a comment.");
|
|
11055
11079
|
const created = await operations.review.addReviewComment({
|
|
11056
11080
|
projectPath,
|
|
11081
|
+
author: "agent",
|
|
11057
11082
|
path,
|
|
11058
11083
|
body,
|
|
11059
11084
|
...typeof comment.startLine === "number" ? { startLine: comment.startLine } : {},
|
|
@@ -11404,7 +11429,7 @@ var MCP_TOOLS = Object.freeze([
|
|
|
11404
11429
|
{
|
|
11405
11430
|
name: "porcelain_canvas",
|
|
11406
11431
|
title: "Manage a Canvas",
|
|
11407
|
-
description: "List, read, create, update, delete, or promote an agent-authored Canvas. Prefer document for a custom validated structured Canvas; sourceDir may provide bundle assets. Built-in review and plan templates compile into that same renderer. Review fixes Why/How and owns its file layers; Plan chooses bounded tabs. Each Review create is scoped to the addressed Worktree, and update requires its id. Promotion writes files but never stages or commits.",
|
|
11432
|
+
description: "List, read, create, update, delete, or promote an agent-authored Canvas. Prefer document for a custom validated structured Canvas; sourceDir may provide bundle assets. Built-in review and plan templates compile into that same renderer. Review fixes Why/How and owns its file layers; Plan chooses bounded tabs. Each Review create is scoped to the addressed Worktree, and update requires its id. A clean Review write binds current HEAD for cross-Worktree History; a dirty write reports live-only and must be updated after commit. Promotion writes files but never stages or commits.",
|
|
11408
11433
|
inputSchema: {
|
|
11409
11434
|
type: "object",
|
|
11410
11435
|
properties: {
|
|
@@ -12688,7 +12713,7 @@ function parseComment(value, index) {
|
|
|
12688
12713
|
throw new CommentsFileParseError("invalid-comment", `comments[${index}] is not an object`);
|
|
12689
12714
|
}
|
|
12690
12715
|
for (const key of Object.keys(value)) {
|
|
12691
|
-
if (key !== "id" && key !== "path" && key !== "startLine" && key !== "endLine" && key !== "anchorText" && key !== "body" && key !== "resolved" && key !== "createdAt" && key !== "agentReply") {
|
|
12716
|
+
if (key !== "id" && key !== "author" && key !== "path" && key !== "startLine" && key !== "endLine" && key !== "anchorText" && key !== "body" && key !== "resolved" && key !== "createdAt" && key !== "agentReply") {
|
|
12692
12717
|
throw new CommentsFileParseError(
|
|
12693
12718
|
"invalid-comment",
|
|
12694
12719
|
`comments[${index}] has unknown field ${key}`
|
|
@@ -12698,6 +12723,9 @@ function parseComment(value, index) {
|
|
|
12698
12723
|
if (typeof value.id !== "string" || value.id.length === 0) {
|
|
12699
12724
|
throw new CommentsFileParseError("invalid-comment", `comments[${index}].id is invalid`);
|
|
12700
12725
|
}
|
|
12726
|
+
if (value.author !== void 0 && value.author !== "user" && value.author !== "agent") {
|
|
12727
|
+
throw new CommentsFileParseError("invalid-comment", `comments[${index}].author is invalid`);
|
|
12728
|
+
}
|
|
12701
12729
|
if (typeof value.path !== "string" || value.path.length === 0) {
|
|
12702
12730
|
throw new CommentsFileParseError("invalid-comment", `comments[${index}].path is invalid`);
|
|
12703
12731
|
}
|
|
@@ -12732,6 +12760,7 @@ function parseComment(value, index) {
|
|
|
12732
12760
|
resolved: value.resolved,
|
|
12733
12761
|
createdAt: value.createdAt
|
|
12734
12762
|
};
|
|
12763
|
+
if (value.author !== void 0) comment.author = value.author;
|
|
12735
12764
|
if (value.startLine !== void 0) comment.startLine = value.startLine;
|
|
12736
12765
|
if (value.endLine !== void 0) comment.endLine = value.endLine;
|
|
12737
12766
|
if (value.anchorText !== void 0) comment.anchorText = value.anchorText;
|
|
@@ -12800,6 +12829,7 @@ function cloneComment(comment) {
|
|
|
12800
12829
|
resolved: comment.resolved,
|
|
12801
12830
|
createdAt: comment.createdAt
|
|
12802
12831
|
};
|
|
12832
|
+
if (comment.author !== void 0) next.author = comment.author;
|
|
12803
12833
|
if (comment.startLine !== void 0) next.startLine = comment.startLine;
|
|
12804
12834
|
if (comment.endLine !== void 0) next.endLine = comment.endLine;
|
|
12805
12835
|
if (comment.anchorText !== void 0) next.anchorText = comment.anchorText;
|
|
@@ -12817,6 +12847,7 @@ function planAddReviewComment(file, input) {
|
|
|
12817
12847
|
}
|
|
12818
12848
|
const comment = {
|
|
12819
12849
|
id: input.id,
|
|
12850
|
+
author: input.author ?? "user",
|
|
12820
12851
|
path: input.path,
|
|
12821
12852
|
body: input.body,
|
|
12822
12853
|
resolved: false,
|
|
@@ -12909,6 +12940,7 @@ function createAddReviewComment(deps) {
|
|
|
12909
12940
|
const result = await deps.store.transact(input.projectPath, (current) => {
|
|
12910
12941
|
const planned = planAddReviewComment(current, {
|
|
12911
12942
|
id,
|
|
12943
|
+
author: input.author,
|
|
12912
12944
|
path: input.path,
|
|
12913
12945
|
startLine: input.startLine,
|
|
12914
12946
|
endLine: input.endLine,
|
|
@@ -14844,7 +14876,7 @@ function daemonIdentity(host = process.env.PORCELAIN_DAEMON_HOST?.trim() || (0,
|
|
|
14844
14876
|
|
|
14845
14877
|
// ../daemon/src/net/daemon-version.ts
|
|
14846
14878
|
function daemonVersion() {
|
|
14847
|
-
return "0.60.
|
|
14879
|
+
return "0.60.1";
|
|
14848
14880
|
}
|
|
14849
14881
|
|
|
14850
14882
|
// ../daemon/src/stores/review-store.ts
|
|
@@ -14878,6 +14910,8 @@ var reviewSectionSchema2 = import_zod49.z.object({
|
|
|
14878
14910
|
var reviewSetSchema = import_zod49.z.object({
|
|
14879
14911
|
name: import_zod49.z.string().default("Active review"),
|
|
14880
14912
|
thesis: import_zod49.z.string().max(4096).optional(),
|
|
14913
|
+
/** The clean checkout HEAD this Review was last authored against. */
|
|
14914
|
+
commitHash: import_zod49.z.string().regex(/^[0-9a-f]{40,64}$/).optional(),
|
|
14881
14915
|
layers: import_zod49.z.array(profileLayerSchema).default([]),
|
|
14882
14916
|
files: import_zod49.z.array(reviewSetFileSchema).default([]),
|
|
14883
14917
|
sections: import_zod49.z.array(reviewSectionSchema2).max(30).default([])
|
|
@@ -14950,7 +14984,21 @@ function projectIdentity(repoPath) {
|
|
|
14950
14984
|
return null;
|
|
14951
14985
|
}
|
|
14952
14986
|
}
|
|
14953
|
-
async function
|
|
14987
|
+
async function readReviewCandidate(projectId, record) {
|
|
14988
|
+
try {
|
|
14989
|
+
const raw = JSON.parse(
|
|
14990
|
+
await (0, import_promises33.readFile)(
|
|
14991
|
+
(0, import_node_path48.join)(canvasBundleDir(porcelainHome(), projectId, record.id), "review.json"),
|
|
14992
|
+
"utf8"
|
|
14993
|
+
)
|
|
14994
|
+
);
|
|
14995
|
+
const parsed = reviewSetSchema.safeParse(raw);
|
|
14996
|
+
return parsed.success ? parsed.data : null;
|
|
14997
|
+
} catch {
|
|
14998
|
+
return null;
|
|
14999
|
+
}
|
|
15000
|
+
}
|
|
15001
|
+
async function readCanvasReviewSet(repoPath, commitHash) {
|
|
14954
15002
|
const identity2 = projectIdentity(repoPath);
|
|
14955
15003
|
if (identity2 === null) return null;
|
|
14956
15004
|
try {
|
|
@@ -14960,29 +15008,31 @@ async function readCanvasReviewSet(repoPath) {
|
|
|
14960
15008
|
const candidates = (index.value?.canvases ?? []).filter(
|
|
14961
15009
|
(entry) => typeof entry === "object" && entry !== null && typeof entry.id === "string"
|
|
14962
15010
|
);
|
|
14963
|
-
const reviews = candidates.filter((entry) => entry.template === "review");
|
|
15011
|
+
const reviews = candidates.filter((entry) => entry.template === "review").sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""));
|
|
15012
|
+
if (commitHash !== void 0) {
|
|
15013
|
+
for (const record2 of reviews) {
|
|
15014
|
+
const review = await readReviewCandidate(identity2.projectId, record2);
|
|
15015
|
+
if (review?.commitHash !== void 0 && (review.commitHash === commitHash || review.commitHash.startsWith(commitHash))) {
|
|
15016
|
+
return review;
|
|
15017
|
+
}
|
|
15018
|
+
}
|
|
15019
|
+
return null;
|
|
15020
|
+
}
|
|
14964
15021
|
const exact = reviews.filter((entry) => entry.worktreeId === identity2.worktreeId);
|
|
14965
|
-
const record = (exact.length === 0 ? reviews.filter((entry) => entry.worktreeId == null) : exact)
|
|
15022
|
+
const record = (exact.length === 0 ? reviews.filter((entry) => entry.worktreeId == null) : exact)[0];
|
|
14966
15023
|
if (record === void 0) return null;
|
|
14967
|
-
|
|
14968
|
-
await (0, import_promises33.readFile)(
|
|
14969
|
-
(0, import_node_path48.join)(canvasBundleDir(porcelainHome(), identity2.projectId, record.id), "review.json"),
|
|
14970
|
-
"utf8"
|
|
14971
|
-
)
|
|
14972
|
-
);
|
|
14973
|
-
const parsed = reviewSetSchema.safeParse(raw);
|
|
14974
|
-
return parsed.success ? parsed.data : null;
|
|
15024
|
+
return readReviewCandidate(identity2.projectId, record);
|
|
14975
15025
|
} catch {
|
|
14976
15026
|
return null;
|
|
14977
15027
|
}
|
|
14978
15028
|
}
|
|
14979
|
-
async function readReviewSet(repoPath) {
|
|
14980
|
-
const canvas = await readCanvasReviewSet(repoPath);
|
|
15029
|
+
async function readReviewSet(repoPath, commitHash) {
|
|
15030
|
+
const canvas = await readCanvasReviewSet(repoPath, commitHash);
|
|
14981
15031
|
if (canvas !== null) return sanitizeReview(repoPath, lenientReviewSetSchema.parse(canvas));
|
|
14982
15032
|
return null;
|
|
14983
15033
|
}
|
|
14984
|
-
async function reviewLayersForRepo(repoPath) {
|
|
14985
|
-
return (await readReviewSet(repoPath))?.layers ?? [];
|
|
15034
|
+
async function reviewLayersForRepo(repoPath, commitHash) {
|
|
15035
|
+
return (await readReviewSet(repoPath, commitHash))?.layers ?? [];
|
|
14986
15036
|
}
|
|
14987
15037
|
|
|
14988
15038
|
// ../daemon/src/daemon-composition/daemon-operations.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabiofiorita/porcelain",
|
|
3
|
-
"version": "0.60.
|
|
3
|
+
"version": "0.60.1",
|
|
4
4
|
"description": "Headless Porcelain daemon — plain Node backend for remote machines (npx @fabiofiorita/porcelain@latest serve)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Fabio Fiorita <fabiolfp@gmail.com>",
|