@gmickel/gno 1.30.7 → 1.32.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 +6 -5
- package/assets/skill/SKILL.md +25 -0
- package/assets/skill/mcp-reference.md +6 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.30.7.zip → gno-browser-clipper-v1.32.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.32.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +19 -0
- package/spec/db/schema.sql +55 -0
- package/spec/mcp.md +176 -11
- package/spec/output-schemas/file-refactor-apply-result.schema.json +305 -0
- package/spec/output-schemas/file-refactor-preview.schema.json +393 -0
- package/spec/output-schemas/section-target-create-result.schema.json +20 -0
- package/spec/output-schemas/section-target-resolve-result.schema.json +194 -0
- package/spec/output-schemas/section-target.schema.json +118 -0
- package/spec/output-schemas/section.schema.json +113 -0
- package/src/core/document-capabilities.ts +13 -0
- package/src/core/file-ops.ts +129 -1
- package/src/core/file-refactor-adapter.ts +329 -0
- package/src/core/file-refactor-apply-edits.ts +61 -0
- package/src/core/file-refactor-apply-fs.ts +512 -0
- package/src/core/file-refactor-apply-safety.ts +340 -0
- package/src/core/file-refactor-apply-validate.ts +401 -0
- package/src/core/file-refactor-contract.ts +486 -0
- package/src/core/file-refactor-destination.ts +123 -0
- package/src/core/file-refactor-from-snapshot.ts +148 -0
- package/src/core/file-refactor-journal-port.ts +150 -0
- package/src/core/file-refactor-journal.ts +347 -0
- package/src/core/file-refactor-paths.ts +60 -0
- package/src/core/file-refactor-plan-classify.ts +208 -0
- package/src/core/file-refactor-plan-validate.ts +169 -0
- package/src/core/file-refactor-planner-types.ts +62 -0
- package/src/core/file-refactor-planner.ts +423 -0
- package/src/core/file-refactor-resolve.ts +280 -0
- package/src/core/file-refactor-service.ts +468 -0
- package/src/core/file-refactors.ts +84 -56
- package/src/core/link-destination-parse.ts +275 -0
- package/src/core/link-inventory-markdown.ts +454 -0
- package/src/core/link-inventory-opaque.ts +244 -0
- package/src/core/link-inventory-types.ts +47 -0
- package/src/core/link-inventory.ts +182 -0
- package/src/core/link-relevance.ts +150 -0
- package/src/core/section-parse.ts +187 -0
- package/src/core/section-target-link.ts +154 -0
- package/src/core/section-target-resolve.ts +351 -0
- package/src/core/section-target-transport.ts +519 -0
- package/src/core/section-target.ts +263 -0
- package/src/core/sections.ts +60 -115
- package/src/mcp/AGENTS.md +1 -0
- package/src/mcp/CLAUDE.md +1 -0
- package/src/mcp/http-egress.ts +1 -0
- package/src/mcp/tools/index.ts +37 -17
- package/src/mcp/tools/sections.ts +512 -0
- package/src/mcp/tools/workspace-write.ts +215 -97
- package/src/sdk/client.ts +238 -116
- package/src/sdk/index.ts +12 -0
- package/src/sdk/types.ts +61 -3
- package/src/serve/file-refactor-http.ts +239 -0
- package/src/serve/public/components/RefactorImpactPreview.tsx +227 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/lib/section-links.ts +189 -0
- package/src/serve/public/pages/DocView.tsx +395 -77
- package/src/serve/routes/api.ts +191 -104
- package/src/serve/routes/section-targets.ts +221 -0
- package/src/serve/server.ts +34 -0
- package/src/store/migrations/026-file-refactor-recovery-journal.ts +72 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/sqlite/adapter.ts +452 -0
- package/src/store/sqlite/file-refactor-journal-store.ts +275 -0
- package/src/store/types.ts +84 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.30.7.zip.sha256 +0 -1
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live filesystem validation before reference-safe refactor mutation.
|
|
3
|
+
*
|
|
4
|
+
* @module src/core/file-refactor-apply-validate
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// node:fs/promises realpath/lstat — no Bun equivalent for symlink-safe identity
|
|
8
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
9
|
+
// node:path — no Bun path utils
|
|
10
|
+
import { dirname, isAbsolute, normalize } from "node:path";
|
|
11
|
+
|
|
12
|
+
import type { PreparedRefactorFile } from "./file-refactor-apply-fs";
|
|
13
|
+
import type {
|
|
14
|
+
FileRefactorApplyResult,
|
|
15
|
+
FileRefactorPreviewPlan,
|
|
16
|
+
FileRefactorReasonCode,
|
|
17
|
+
} from "./file-refactor-contract";
|
|
18
|
+
|
|
19
|
+
import { parseUri } from "../app/constants";
|
|
20
|
+
import {
|
|
21
|
+
applyDestinationEditsDescending,
|
|
22
|
+
FileRefactorEditError,
|
|
23
|
+
} from "./file-refactor-apply-edits";
|
|
24
|
+
import { resolveCollectionAbsPath } from "./file-refactor-apply-fs";
|
|
25
|
+
import {
|
|
26
|
+
FILE_REFACTOR_SCHEMA_VERSION,
|
|
27
|
+
fingerprintUtf8Content,
|
|
28
|
+
} from "./file-refactor-contract";
|
|
29
|
+
import {
|
|
30
|
+
pathDirname,
|
|
31
|
+
validateFileRefactorPlanInputs,
|
|
32
|
+
} from "./file-refactor-plan-validate";
|
|
33
|
+
import { isCanonicalPathContained, validateRelPath } from "./validation";
|
|
34
|
+
|
|
35
|
+
function baseResult(
|
|
36
|
+
plan: FileRefactorPreviewPlan
|
|
37
|
+
): Pick<
|
|
38
|
+
FileRefactorApplyResult,
|
|
39
|
+
"schemaVersion" | "planDigest" | "operation" | "source" | "target"
|
|
40
|
+
> {
|
|
41
|
+
return {
|
|
42
|
+
schemaVersion: FILE_REFACTOR_SCHEMA_VERSION,
|
|
43
|
+
planDigest: plan.planDigest,
|
|
44
|
+
operation: plan.operation,
|
|
45
|
+
source: plan.source,
|
|
46
|
+
target: plan.target,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function unchangedResult(
|
|
51
|
+
plan: FileRefactorPreviewPlan,
|
|
52
|
+
status: "conflict" | "stale_plan" | "unsupported",
|
|
53
|
+
reasonCode: FileRefactorReasonCode,
|
|
54
|
+
recoveryJournalId?: string
|
|
55
|
+
): FileRefactorApplyResult {
|
|
56
|
+
return {
|
|
57
|
+
...baseResult(plan),
|
|
58
|
+
status,
|
|
59
|
+
reasonCode,
|
|
60
|
+
filesystem: { state: "unchanged", recoveryJournalId },
|
|
61
|
+
indexConvergence: { state: "not_attempted" },
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function realpathOrNull(path: string): Promise<string | null> {
|
|
66
|
+
try {
|
|
67
|
+
return await realpath(path);
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function nearestExistingParentRealpath(
|
|
74
|
+
absPath: string
|
|
75
|
+
): Promise<string | null> {
|
|
76
|
+
let candidate = dirname(absPath);
|
|
77
|
+
while (true) {
|
|
78
|
+
const resolved = await realpathOrNull(candidate);
|
|
79
|
+
if (resolved) return resolved;
|
|
80
|
+
const parent = dirname(candidate);
|
|
81
|
+
if (parent === candidate) return null;
|
|
82
|
+
candidate = parent;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function assertExistingPathContained(
|
|
87
|
+
rootReal: string,
|
|
88
|
+
absPath: string
|
|
89
|
+
): Promise<boolean> {
|
|
90
|
+
const real = await realpathOrNull(absPath);
|
|
91
|
+
if (!real) return false;
|
|
92
|
+
return isCanonicalPathContained(rootReal, real);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function assertTargetPathContained(
|
|
96
|
+
rootReal: string,
|
|
97
|
+
targetAbs: string
|
|
98
|
+
): Promise<boolean> {
|
|
99
|
+
// Reject final path if it already exists as a symlink escape.
|
|
100
|
+
try {
|
|
101
|
+
const info = await lstat(targetAbs);
|
|
102
|
+
if (info.isSymbolicLink()) return false;
|
|
103
|
+
const real = await realpath(targetAbs);
|
|
104
|
+
return isCanonicalPathContained(rootReal, real);
|
|
105
|
+
} catch {
|
|
106
|
+
// Target must not exist for apply; validate nearest existing parent.
|
|
107
|
+
}
|
|
108
|
+
const parentReal = await nearestExistingParentRealpath(targetAbs);
|
|
109
|
+
if (!parentReal) return false;
|
|
110
|
+
return isCanonicalPathContained(rootReal, parentReal);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Structural plan integrity independent of live filesystem bytes.
|
|
115
|
+
* Malformed but self-digested canApply=true plans fail closed here.
|
|
116
|
+
*/
|
|
117
|
+
export function validateRefactorPlanStructure(
|
|
118
|
+
plan: FileRefactorPreviewPlan
|
|
119
|
+
): FileRefactorApplyResult | null {
|
|
120
|
+
const inputFailure = validateFileRefactorPlanInputs({
|
|
121
|
+
operation: plan.operation,
|
|
122
|
+
source: plan.source,
|
|
123
|
+
target: plan.target,
|
|
124
|
+
});
|
|
125
|
+
if (inputFailure) {
|
|
126
|
+
return unchangedResult(plan, "unsupported", inputFailure.reasonCode);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (plan.source.collection !== plan.target.collection) {
|
|
130
|
+
return unchangedResult(plan, "unsupported", "cross_collection_unsupported");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (plan.source.uri === plan.target.uri) {
|
|
134
|
+
return unchangedResult(plan, "unsupported", "unsafe_target");
|
|
135
|
+
}
|
|
136
|
+
if (plan.source.relPath === plan.target.relPath) {
|
|
137
|
+
return unchangedResult(plan, "unsupported", "unsafe_target");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (plan.operation === "rename") {
|
|
141
|
+
if (pathDirname(plan.source.relPath) !== pathDirname(plan.target.relPath)) {
|
|
142
|
+
return unchangedResult(plan, "unsupported", "unsafe_target");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const sourceParsed = parseUri(plan.source.uri);
|
|
147
|
+
const targetParsed = parseUri(plan.target.uri);
|
|
148
|
+
if (
|
|
149
|
+
!sourceParsed ||
|
|
150
|
+
!targetParsed ||
|
|
151
|
+
sourceParsed.collection !== plan.source.collection ||
|
|
152
|
+
sourceParsed.path !== plan.source.relPath ||
|
|
153
|
+
targetParsed.collection !== plan.target.collection ||
|
|
154
|
+
targetParsed.path !== plan.target.relPath
|
|
155
|
+
) {
|
|
156
|
+
return unchangedResult(plan, "unsupported", "unsafe_target");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const seenUris = new Set<string>();
|
|
160
|
+
const seenRelPaths = new Set<string>();
|
|
161
|
+
for (const doc of plan.affectedDocuments) {
|
|
162
|
+
if (seenUris.has(doc.uri) || seenRelPaths.has(doc.relPath)) {
|
|
163
|
+
return unchangedResult(plan, "unsupported", "unsafe_target");
|
|
164
|
+
}
|
|
165
|
+
seenUris.add(doc.uri);
|
|
166
|
+
seenRelPaths.add(doc.relPath);
|
|
167
|
+
|
|
168
|
+
const parsed = parseUri(doc.uri);
|
|
169
|
+
if (
|
|
170
|
+
!parsed ||
|
|
171
|
+
parsed.path !== doc.relPath ||
|
|
172
|
+
parsed.collection !== plan.source.collection
|
|
173
|
+
) {
|
|
174
|
+
return unchangedResult(plan, "unsupported", "unsafe_target");
|
|
175
|
+
}
|
|
176
|
+
if (doc.uri !== plan.source.uri && doc.relPath === plan.source.relPath) {
|
|
177
|
+
return unchangedResult(plan, "unsupported", "unsafe_target");
|
|
178
|
+
}
|
|
179
|
+
if (doc.uri === plan.source.uri && doc.relPath !== plan.source.relPath) {
|
|
180
|
+
return unchangedResult(plan, "unsupported", "unsafe_target");
|
|
181
|
+
}
|
|
182
|
+
if (doc.relPath === plan.target.relPath || doc.uri === plan.target.uri) {
|
|
183
|
+
return unchangedResult(plan, "unsupported", "unsafe_target");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const fingerprintUris = plan.preconditions.affectedContentFingerprints.map(
|
|
188
|
+
(entry) => entry.uri
|
|
189
|
+
);
|
|
190
|
+
const fingerprintSet = new Set(fingerprintUris);
|
|
191
|
+
if (fingerprintSet.size !== fingerprintUris.length) {
|
|
192
|
+
return unchangedResult(plan, "unsupported", "unsafe_target");
|
|
193
|
+
}
|
|
194
|
+
if (fingerprintSet.size !== plan.affectedDocuments.length) {
|
|
195
|
+
return unchangedResult(plan, "unsupported", "unsafe_target");
|
|
196
|
+
}
|
|
197
|
+
for (const doc of plan.affectedDocuments) {
|
|
198
|
+
if (!fingerprintSet.has(doc.uri)) {
|
|
199
|
+
return unchangedResult(plan, "unsupported", "unsafe_target");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
type PreparedCore = Omit<
|
|
207
|
+
PreparedRefactorFile,
|
|
208
|
+
"stagePath" | "backupPath" | "stageRelPath" | "backupRelPath"
|
|
209
|
+
>;
|
|
210
|
+
|
|
211
|
+
export async function validateLiveRefactorPlan(
|
|
212
|
+
plan: FileRefactorPreviewPlan,
|
|
213
|
+
collectionRoot: string
|
|
214
|
+
): Promise<
|
|
215
|
+
| { ok: true; files: PreparedCore[] }
|
|
216
|
+
| { ok: false; result: FileRefactorApplyResult }
|
|
217
|
+
> {
|
|
218
|
+
const structural = validateRefactorPlanStructure(plan);
|
|
219
|
+
if (structural) return { ok: false, result: structural };
|
|
220
|
+
|
|
221
|
+
let sourceRel: string;
|
|
222
|
+
let targetRel: string;
|
|
223
|
+
try {
|
|
224
|
+
sourceRel = validateRelPath(plan.source.relPath);
|
|
225
|
+
targetRel = validateRelPath(plan.target.relPath);
|
|
226
|
+
} catch {
|
|
227
|
+
return {
|
|
228
|
+
ok: false,
|
|
229
|
+
result: unchangedResult(plan, "unsupported", "unsafe_target"),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const rootReal = await realpathOrNull(normalize(collectionRoot));
|
|
234
|
+
if (!rootReal || !isAbsolute(rootReal)) {
|
|
235
|
+
return {
|
|
236
|
+
ok: false,
|
|
237
|
+
result: unchangedResult(plan, "unsupported", "unsafe_target"),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const sourceAbs = resolveCollectionAbsPath(collectionRoot, sourceRel);
|
|
242
|
+
const targetAbs = resolveCollectionAbsPath(collectionRoot, targetRel);
|
|
243
|
+
|
|
244
|
+
if (!(await assertExistingPathContained(rootReal, sourceAbs))) {
|
|
245
|
+
return {
|
|
246
|
+
ok: false,
|
|
247
|
+
result: unchangedResult(plan, "unsupported", "unsafe_target"),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
if (!(await assertTargetPathContained(rootReal, targetAbs))) {
|
|
251
|
+
return {
|
|
252
|
+
ok: false,
|
|
253
|
+
result: unchangedResult(plan, "unsupported", "unsafe_target"),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const sourceContent = await Bun.file(sourceAbs)
|
|
258
|
+
.text()
|
|
259
|
+
.catch(() => null);
|
|
260
|
+
if (sourceContent === null) {
|
|
261
|
+
return {
|
|
262
|
+
ok: false,
|
|
263
|
+
result: unchangedResult(plan, "stale_plan", "stale_plan"),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
const sourceFp = await fingerprintUtf8Content(sourceContent);
|
|
267
|
+
if (sourceFp !== plan.preconditions.sourceContentFingerprint) {
|
|
268
|
+
return {
|
|
269
|
+
ok: false,
|
|
270
|
+
result: unchangedResult(plan, "stale_plan", "stale_plan"),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const targetExists = await Bun.file(targetAbs).exists();
|
|
275
|
+
const expectedTargetFp = await fingerprintUtf8Content(
|
|
276
|
+
`${plan.target.collection}:${plan.target.relPath}:${targetExists ? "occupied" : "free"}`
|
|
277
|
+
);
|
|
278
|
+
if (expectedTargetFp !== plan.preconditions.targetPathFingerprint) {
|
|
279
|
+
return {
|
|
280
|
+
ok: false,
|
|
281
|
+
result: unchangedResult(
|
|
282
|
+
plan,
|
|
283
|
+
targetExists ? "conflict" : "stale_plan",
|
|
284
|
+
targetExists ? "occupied_target" : "stale_plan"
|
|
285
|
+
),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (targetExists) {
|
|
289
|
+
return {
|
|
290
|
+
ok: false,
|
|
291
|
+
result: unchangedResult(plan, "conflict", "occupied_target"),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const contentByUri = new Map<string, string>();
|
|
296
|
+
contentByUri.set(plan.source.uri, sourceContent);
|
|
297
|
+
const files: PreparedCore[] = [];
|
|
298
|
+
const preconditionByUri = new Map(
|
|
299
|
+
plan.preconditions.affectedContentFingerprints.map((entry) => [
|
|
300
|
+
entry.uri,
|
|
301
|
+
entry.fingerprint,
|
|
302
|
+
])
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
for (const doc of plan.affectedDocuments) {
|
|
306
|
+
let rel: string;
|
|
307
|
+
try {
|
|
308
|
+
rel = validateRelPath(doc.relPath);
|
|
309
|
+
} catch {
|
|
310
|
+
return {
|
|
311
|
+
ok: false,
|
|
312
|
+
result: unchangedResult(plan, "unsupported", "unsafe_target"),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const abs = resolveCollectionAbsPath(collectionRoot, rel);
|
|
317
|
+
if (!(await assertExistingPathContained(rootReal, abs))) {
|
|
318
|
+
return {
|
|
319
|
+
ok: false,
|
|
320
|
+
result: unchangedResult(plan, "unsupported", "unsafe_target"),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const isSource = doc.uri === plan.source.uri || rel === sourceRel;
|
|
325
|
+
let content = contentByUri.get(doc.uri);
|
|
326
|
+
if (content === undefined) {
|
|
327
|
+
const live = await Bun.file(abs)
|
|
328
|
+
.text()
|
|
329
|
+
.catch(() => null);
|
|
330
|
+
if (live === null) {
|
|
331
|
+
return {
|
|
332
|
+
ok: false,
|
|
333
|
+
result: unchangedResult(plan, "stale_plan", "stale_plan"),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
content = live;
|
|
337
|
+
contentByUri.set(doc.uri, content);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const liveFp = await fingerprintUtf8Content(content);
|
|
341
|
+
if (liveFp !== doc.contentFingerprint) {
|
|
342
|
+
return {
|
|
343
|
+
ok: false,
|
|
344
|
+
result: unchangedResult(plan, "stale_plan", "stale_plan"),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
const precondition = preconditionByUri.get(doc.uri);
|
|
348
|
+
if (precondition !== liveFp) {
|
|
349
|
+
return {
|
|
350
|
+
ok: false,
|
|
351
|
+
result: unchangedResult(plan, "stale_plan", "stale_plan"),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let finalContent: string;
|
|
356
|
+
try {
|
|
357
|
+
finalContent = applyDestinationEditsDescending(content, doc.edits);
|
|
358
|
+
} catch (cause) {
|
|
359
|
+
if (cause instanceof FileRefactorEditError) {
|
|
360
|
+
return {
|
|
361
|
+
ok: false,
|
|
362
|
+
result: unchangedResult(plan, "stale_plan", "stale_plan"),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
throw cause;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (isSource) {
|
|
369
|
+
contentByUri.set(plan.source.uri, finalContent);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
files.push({
|
|
374
|
+
role: "affected",
|
|
375
|
+
sourceAbsPath: abs,
|
|
376
|
+
targetAbsPath: abs,
|
|
377
|
+
relPath: rel,
|
|
378
|
+
finalContent,
|
|
379
|
+
originalFingerprint: liveFp,
|
|
380
|
+
expectedFingerprint: await fingerprintUtf8Content(finalContent),
|
|
381
|
+
isMove: false,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const finalSourceContent = contentByUri.get(plan.source.uri) ?? sourceContent;
|
|
386
|
+
files.push({
|
|
387
|
+
role: "source",
|
|
388
|
+
sourceAbsPath: sourceAbs,
|
|
389
|
+
targetAbsPath: targetAbs,
|
|
390
|
+
relPath: targetRel,
|
|
391
|
+
sourceRelPath: sourceRel,
|
|
392
|
+
finalContent: finalSourceContent,
|
|
393
|
+
originalFingerprint: sourceFp,
|
|
394
|
+
expectedFingerprint: await fingerprintUtf8Content(finalSourceContent),
|
|
395
|
+
isMove: true,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
return { ok: true, files };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export { baseResult };
|