@plinth-music/cli 0.22.0 → 0.23.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/CHANGELOG.md +38 -0
- package/dist/build-stamp.json +2 -2
- package/dist/cli.js +1962 -201
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7584,7 +7584,7 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
|
|
|
7584
7584
|
// package.json
|
|
7585
7585
|
var package_default = {
|
|
7586
7586
|
name: "@plinth-music/cli",
|
|
7587
|
-
version: "0.
|
|
7587
|
+
version: "0.23.0",
|
|
7588
7588
|
description: "Plinth capstone sync daemon and CLI",
|
|
7589
7589
|
type: "module",
|
|
7590
7590
|
private: false,
|
|
@@ -22148,7 +22148,22 @@ var BASE_ROW = {
|
|
|
22148
22148
|
};
|
|
22149
22149
|
var TombstoneSchema = exports_external.object({
|
|
22150
22150
|
...BASE_ROW,
|
|
22151
|
-
deleted_at: exports_external.string().datetime({ offset: true })
|
|
22151
|
+
deleted_at: exports_external.string().datetime({ offset: true }),
|
|
22152
|
+
permanent_delete: exports_external.literal(true).optional(),
|
|
22153
|
+
purge_generation: exports_external.string().uuid().optional(),
|
|
22154
|
+
retirement_sequence: exports_external.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional()
|
|
22155
|
+
}).superRefine((row, ctx) => {
|
|
22156
|
+
const present = [
|
|
22157
|
+
row.permanent_delete !== undefined,
|
|
22158
|
+
row.purge_generation !== undefined,
|
|
22159
|
+
row.retirement_sequence !== undefined
|
|
22160
|
+
];
|
|
22161
|
+
if (present.some(Boolean) && !present.every(Boolean)) {
|
|
22162
|
+
ctx.addIssue({
|
|
22163
|
+
code: "custom",
|
|
22164
|
+
message: "permanent tombstone fields must be present together"
|
|
22165
|
+
});
|
|
22166
|
+
}
|
|
22152
22167
|
});
|
|
22153
22168
|
function makePageResponseSchema(responseKey, liveSchema) {
|
|
22154
22169
|
return exports_external.object({
|
|
@@ -23893,6 +23908,17 @@ function moveEntityFile(workspaceSlug, entityType, fromSlug, toSlug, beforeMove)
|
|
|
23893
23908
|
throw err;
|
|
23894
23909
|
}
|
|
23895
23910
|
}
|
|
23911
|
+
function deleteMirrorFile(workspaceSlug, entityType, entitySlug) {
|
|
23912
|
+
const target = entityFilePath(workspaceSlug, entityType, entitySlug);
|
|
23913
|
+
try {
|
|
23914
|
+
unlinkSync(target);
|
|
23915
|
+
return true;
|
|
23916
|
+
} catch (err) {
|
|
23917
|
+
if (err?.code === "ENOENT")
|
|
23918
|
+
return false;
|
|
23919
|
+
throw err;
|
|
23920
|
+
}
|
|
23921
|
+
}
|
|
23896
23922
|
var ARTIST_MEMORY_ANCHOR = "memory.md";
|
|
23897
23923
|
function artistMemoryFilePath(workspaceSlug, entitySlug) {
|
|
23898
23924
|
assertSlug("entity slug", entitySlug);
|
|
@@ -29609,6 +29635,13 @@ class ApiError extends Error {
|
|
|
29609
29635
|
this.name = "ApiError";
|
|
29610
29636
|
}
|
|
29611
29637
|
}
|
|
29638
|
+
var PermanentNameRetiredSchema = exports_external.object({
|
|
29639
|
+
code: exports_external.literal("permanent_name_retired"),
|
|
29640
|
+
entity_kind: exports_external.enum(["document", "workspace_file"]),
|
|
29641
|
+
logical_key: exports_external.string().min(1),
|
|
29642
|
+
action: exports_external.literal("refresh_required"),
|
|
29643
|
+
error: exports_external.string().optional()
|
|
29644
|
+
});
|
|
29612
29645
|
function parseRetryAfterMs(headerValue, now = () => Date.now()) {
|
|
29613
29646
|
if (!headerValue)
|
|
29614
29647
|
return;
|
|
@@ -29622,6 +29655,92 @@ function parseRetryAfterMs(headerValue, now = () => Date.now()) {
|
|
|
29622
29655
|
const delta = dateMs - now();
|
|
29623
29656
|
return delta > 0 ? delta : 0;
|
|
29624
29657
|
}
|
|
29658
|
+
var OperationEnvelopeBase = exports_external.object({
|
|
29659
|
+
entity_kind: exports_external.enum(["document", "workspace_file"]),
|
|
29660
|
+
operation_id: exports_external.string().uuid()
|
|
29661
|
+
});
|
|
29662
|
+
var OperationBoundBase = OperationEnvelopeBase.extend({
|
|
29663
|
+
logical_key: exports_external.string().min(1),
|
|
29664
|
+
payload_digest: exports_external.string().regex(/^[0-9a-f]{64}$/)
|
|
29665
|
+
});
|
|
29666
|
+
var OperationResolutionSchema = exports_external.discriminatedUnion("status", [
|
|
29667
|
+
OperationBoundBase.extend({ status: exports_external.literal("pending") }),
|
|
29668
|
+
OperationBoundBase.extend({ status: exports_external.literal("finalized"), live: exports_external.unknown() }),
|
|
29669
|
+
OperationBoundBase.extend({ status: exports_external.literal("terminal_cleaned") }),
|
|
29670
|
+
OperationEnvelopeBase.extend({ status: exports_external.literal("retired_not_found") })
|
|
29671
|
+
]);
|
|
29672
|
+
var CurrentNamespaceSchema = exports_external.object({
|
|
29673
|
+
entity_kind: exports_external.enum(["document", "workspace_file"]),
|
|
29674
|
+
logical_key: exports_external.string().min(1),
|
|
29675
|
+
latest_retirement: exports_external.object({
|
|
29676
|
+
purge_generation: exports_external.string().uuid(),
|
|
29677
|
+
retirement_sequence: exports_external.number().int().positive().max(Number.MAX_SAFE_INTEGER)
|
|
29678
|
+
}).nullable(),
|
|
29679
|
+
live_owner: exports_external.object({ id: exports_external.string().uuid() }).nullable()
|
|
29680
|
+
});
|
|
29681
|
+
async function resolvePermanentDeletionOperation(args) {
|
|
29682
|
+
const { baseUrl, pat, entityKind, operationId, fetchImpl = fetch } = args;
|
|
29683
|
+
const url2 = `${baseUrl.replace(/\/$/, "")}/api/sync/permanent-deletions/operations/resolve`;
|
|
29684
|
+
let res;
|
|
29685
|
+
try {
|
|
29686
|
+
res = await fetchImpl(url2, {
|
|
29687
|
+
method: "POST",
|
|
29688
|
+
headers: {
|
|
29689
|
+
Authorization: `Bearer ${pat}`,
|
|
29690
|
+
Accept: "application/json",
|
|
29691
|
+
"Content-Type": "application/json"
|
|
29692
|
+
},
|
|
29693
|
+
body: JSON.stringify({ entity_kind: entityKind, operation_id: operationId })
|
|
29694
|
+
});
|
|
29695
|
+
} catch (error52) {
|
|
29696
|
+
const detail = error52 instanceof Error ? error52.message : "network error";
|
|
29697
|
+
throw new ApiError(0, `Could not reach ${url2}: ${detail}`);
|
|
29698
|
+
}
|
|
29699
|
+
if (res.status === 401)
|
|
29700
|
+
throw new ApiError(401, "Token was rejected by the server.");
|
|
29701
|
+
if (res.status === 429) {
|
|
29702
|
+
throw new ApiError(429, "Rate limited by the server.", parseRetryAfterMs(res.headers.get("retry-after")));
|
|
29703
|
+
}
|
|
29704
|
+
if (!res.ok) {
|
|
29705
|
+
throw new ApiError(res.status, `resolve permanent deletion operation failed with HTTP ${res.status}`);
|
|
29706
|
+
}
|
|
29707
|
+
const parsed = OperationResolutionSchema.safeParse(await res.json());
|
|
29708
|
+
if (!parsed.success) {
|
|
29709
|
+
throw new ApiError(res.status, `operation resolution response did not match the expected shape: ${parsed.error.message}`);
|
|
29710
|
+
}
|
|
29711
|
+
return parsed.data;
|
|
29712
|
+
}
|
|
29713
|
+
async function getCurrentPermanentNamespace(args) {
|
|
29714
|
+
const { baseUrl, pat, entityKind, logicalKey, fetchImpl = fetch } = args;
|
|
29715
|
+
const params = new URLSearchParams({ entity_kind: entityKind, logical_key: logicalKey });
|
|
29716
|
+
const url2 = `${baseUrl.replace(/\/$/, "")}/api/sync/permanent-deletions/namespaces/current?${params}`;
|
|
29717
|
+
let res;
|
|
29718
|
+
try {
|
|
29719
|
+
res = await fetchImpl(url2, {
|
|
29720
|
+
method: "GET",
|
|
29721
|
+
headers: { Authorization: `Bearer ${pat}`, Accept: "application/json" }
|
|
29722
|
+
});
|
|
29723
|
+
} catch (error52) {
|
|
29724
|
+
const detail = error52 instanceof Error ? error52.message : "network error";
|
|
29725
|
+
throw new ApiError(0, `Could not reach ${url2}: ${detail}`);
|
|
29726
|
+
}
|
|
29727
|
+
if (res.status === 401)
|
|
29728
|
+
throw new ApiError(401, "Token was rejected by the server.");
|
|
29729
|
+
if (res.status === 429) {
|
|
29730
|
+
throw new ApiError(429, "Rate limited by the server.", parseRetryAfterMs(res.headers.get("retry-after")));
|
|
29731
|
+
}
|
|
29732
|
+
if (!res.ok) {
|
|
29733
|
+
throw new ApiError(res.status, `get current permanent namespace failed with HTTP ${res.status}`);
|
|
29734
|
+
}
|
|
29735
|
+
const parsed = CurrentNamespaceSchema.safeParse(await res.json());
|
|
29736
|
+
if (!parsed.success) {
|
|
29737
|
+
throw new ApiError(res.status, `current permanent namespace response did not match the expected shape: ${parsed.error.message}`);
|
|
29738
|
+
}
|
|
29739
|
+
if (parsed.data.entity_kind !== entityKind || parsed.data.logical_key !== logicalKey) {
|
|
29740
|
+
throw new ApiError(res.status, "current permanent namespace response identity mismatch");
|
|
29741
|
+
}
|
|
29742
|
+
return parsed.data;
|
|
29743
|
+
}
|
|
29625
29744
|
async function whoami(baseUrl, pat, fetchImpl = fetch) {
|
|
29626
29745
|
const url2 = `${baseUrl.replace(/\/$/, "")}/api/auth/cli/whoami`;
|
|
29627
29746
|
let res;
|
|
@@ -29797,6 +29916,16 @@ async function patchEntity(args) {
|
|
|
29797
29916
|
}
|
|
29798
29917
|
if (res.status === 409) {
|
|
29799
29918
|
const json2 = await res.json();
|
|
29919
|
+
const retiredParse = PermanentNameRetiredSchema.safeParse(json2);
|
|
29920
|
+
if (retiredParse.success) {
|
|
29921
|
+
return {
|
|
29922
|
+
ok: false,
|
|
29923
|
+
code: 409,
|
|
29924
|
+
reason: "permanent_name_retired",
|
|
29925
|
+
entityKind: retiredParse.data.entity_kind,
|
|
29926
|
+
logicalKey: retiredParse.data.logical_key
|
|
29927
|
+
};
|
|
29928
|
+
}
|
|
29800
29929
|
const contextParse = ContextDocConflictResponseSchema.safeParse(json2);
|
|
29801
29930
|
if (contextParse.success) {
|
|
29802
29931
|
const existing = strategy.singleResponseSchema.safeParse(contextParse.data.existing);
|
|
@@ -29853,17 +29982,33 @@ async function patchEntity(args) {
|
|
|
29853
29982
|
throw new ApiError(res.status, `patch ${strategy.type} failed with HTTP ${res.status}`);
|
|
29854
29983
|
}
|
|
29855
29984
|
async function createEntity(args) {
|
|
29856
|
-
const {
|
|
29985
|
+
const {
|
|
29986
|
+
strategy,
|
|
29987
|
+
baseUrl,
|
|
29988
|
+
pat,
|
|
29989
|
+
payload,
|
|
29990
|
+
retiredGeneration,
|
|
29991
|
+
operationId,
|
|
29992
|
+
fetchImpl = fetch
|
|
29993
|
+
} = args;
|
|
29857
29994
|
const url2 = `${baseUrl.replace(/\/$/, "")}/api/sync/${strategy.apiSegment}`;
|
|
29995
|
+
if (retiredGeneration === undefined !== (operationId === undefined)) {
|
|
29996
|
+
throw new Error("retired generation and create operation id must be supplied together");
|
|
29997
|
+
}
|
|
29998
|
+
const headers = {
|
|
29999
|
+
Authorization: `Bearer ${pat}`,
|
|
30000
|
+
Accept: "application/json",
|
|
30001
|
+
"Content-Type": "application/json"
|
|
30002
|
+
};
|
|
30003
|
+
if (retiredGeneration) {
|
|
30004
|
+
headers["X-Plinth-Retired-Generation"] = retiredGeneration;
|
|
30005
|
+
headers["X-Plinth-Create-Operation-Id"] = operationId;
|
|
30006
|
+
}
|
|
29858
30007
|
let res;
|
|
29859
30008
|
try {
|
|
29860
30009
|
res = await fetchImpl(url2, {
|
|
29861
30010
|
method: "POST",
|
|
29862
|
-
headers
|
|
29863
|
-
Authorization: `Bearer ${pat}`,
|
|
29864
|
-
Accept: "application/json",
|
|
29865
|
-
"Content-Type": "application/json"
|
|
29866
|
-
},
|
|
30011
|
+
headers,
|
|
29867
30012
|
body: JSON.stringify(payload)
|
|
29868
30013
|
});
|
|
29869
30014
|
} catch (err) {
|
|
@@ -29880,6 +30025,16 @@ async function createEntity(args) {
|
|
|
29880
30025
|
}
|
|
29881
30026
|
if (res.status === 409) {
|
|
29882
30027
|
const json2 = await res.json();
|
|
30028
|
+
const retiredParse = PermanentNameRetiredSchema.safeParse(json2);
|
|
30029
|
+
if (retiredParse.success) {
|
|
30030
|
+
return {
|
|
30031
|
+
ok: false,
|
|
30032
|
+
code: 409,
|
|
30033
|
+
reason: "permanent_name_retired",
|
|
30034
|
+
entityKind: retiredParse.data.entity_kind,
|
|
30035
|
+
logicalKey: retiredParse.data.logical_key
|
|
30036
|
+
};
|
|
30037
|
+
}
|
|
29883
30038
|
const contextParse = ContextDocConflictResponseSchema.safeParse(json2);
|
|
29884
30039
|
if (contextParse.success) {
|
|
29885
30040
|
const existing2 = strategy.singleResponseSchema.safeParse(contextParse.data.existing);
|
|
@@ -30023,7 +30178,19 @@ var FileTombstoneSchema = exports_external.object({
|
|
|
30023
30178
|
id: exports_external.string().uuid(),
|
|
30024
30179
|
path: exports_external.string().min(1),
|
|
30025
30180
|
updated_at: exports_external.string().datetime({ offset: true }),
|
|
30026
|
-
deleted_at: exports_external.string().datetime({ offset: true })
|
|
30181
|
+
deleted_at: exports_external.string().datetime({ offset: true }),
|
|
30182
|
+
permanent_delete: exports_external.literal(true).optional(),
|
|
30183
|
+
purge_generation: exports_external.string().uuid().optional(),
|
|
30184
|
+
retirement_sequence: exports_external.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional()
|
|
30185
|
+
}).superRefine((row, ctx) => {
|
|
30186
|
+
const present2 = [
|
|
30187
|
+
row.permanent_delete !== undefined,
|
|
30188
|
+
row.purge_generation !== undefined,
|
|
30189
|
+
row.retirement_sequence !== undefined
|
|
30190
|
+
];
|
|
30191
|
+
if (present2.some(Boolean) && !present2.every(Boolean)) {
|
|
30192
|
+
ctx.addIssue({ code: "custom", message: "permanent tombstone fields must be present together" });
|
|
30193
|
+
}
|
|
30027
30194
|
});
|
|
30028
30195
|
var FileRowSchema = exports_external.union([FileLiveSchema, FileTombstoneSchema]);
|
|
30029
30196
|
var FilesPageSchema = exports_external.object({
|
|
@@ -30162,6 +30329,8 @@ async function postFile(args) {
|
|
|
30162
30329
|
contentHash,
|
|
30163
30330
|
mimeType,
|
|
30164
30331
|
expectedUpdatedAt,
|
|
30332
|
+
retiredGeneration,
|
|
30333
|
+
operationId,
|
|
30165
30334
|
fetchImpl = fetch
|
|
30166
30335
|
} = args;
|
|
30167
30336
|
const url2 = `${baseUrl.replace(/\/$/, "")}/api/sync/files`;
|
|
@@ -30174,6 +30343,13 @@ async function postFile(args) {
|
|
|
30174
30343
|
};
|
|
30175
30344
|
if (expectedUpdatedAt)
|
|
30176
30345
|
headers["X-Plinth-Expected-Updated-At"] = expectedUpdatedAt;
|
|
30346
|
+
if (retiredGeneration === undefined !== (operationId === undefined)) {
|
|
30347
|
+
throw new Error("retired generation and create operation id must be supplied together");
|
|
30348
|
+
}
|
|
30349
|
+
if (retiredGeneration) {
|
|
30350
|
+
headers["X-Plinth-Retired-Generation"] = retiredGeneration;
|
|
30351
|
+
headers["X-Plinth-Create-Operation-Id"] = operationId;
|
|
30352
|
+
}
|
|
30177
30353
|
let res;
|
|
30178
30354
|
try {
|
|
30179
30355
|
res = await fetchImpl(url2, { method: "POST", headers, body: bytes });
|
|
@@ -30200,6 +30376,22 @@ async function postFile(args) {
|
|
|
30200
30376
|
}
|
|
30201
30377
|
if (res.status === 409) {
|
|
30202
30378
|
const json2 = await res.json();
|
|
30379
|
+
const retired = exports_external.object({
|
|
30380
|
+
code: exports_external.literal("permanent_name_retired"),
|
|
30381
|
+
entity_kind: exports_external.literal("workspace_file"),
|
|
30382
|
+
logical_key: exports_external.string().min(1),
|
|
30383
|
+
action: exports_external.literal("refresh_required"),
|
|
30384
|
+
error: exports_external.string().optional()
|
|
30385
|
+
}).safeParse(json2);
|
|
30386
|
+
if (retired.success) {
|
|
30387
|
+
return {
|
|
30388
|
+
ok: false,
|
|
30389
|
+
code: 409,
|
|
30390
|
+
reason: "permanent_name_retired",
|
|
30391
|
+
entityKind: retired.data.entity_kind,
|
|
30392
|
+
logicalKey: retired.data.logical_key
|
|
30393
|
+
};
|
|
30394
|
+
}
|
|
30203
30395
|
const parsed = PreconditionSchema.safeParse(json2);
|
|
30204
30396
|
if (!parsed.success) {
|
|
30205
30397
|
throw new ApiError(409, `post file 409 body did not match precondition_failed shape`);
|
|
@@ -34096,7 +34288,7 @@ var sessionCommand = defineCommand({
|
|
|
34096
34288
|
|
|
34097
34289
|
// src/commands/start.ts
|
|
34098
34290
|
var import_gray_matter14 = __toESM(require_gray_matter(), 1);
|
|
34099
|
-
import { existsSync as
|
|
34291
|
+
import { existsSync as existsSync10, readFileSync as readFileSync30, statSync as statSync18 } from "node:fs";
|
|
34100
34292
|
|
|
34101
34293
|
// src/lib/daemon-log.ts
|
|
34102
34294
|
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync9, renameSync as renameSync7, statSync as statSync4 } from "node:fs";
|
|
@@ -34340,7 +34532,7 @@ class DestructiveGate {
|
|
|
34340
34532
|
}
|
|
34341
34533
|
|
|
34342
34534
|
// src/lib/resolve-pending.ts
|
|
34343
|
-
import { readFileSync as
|
|
34535
|
+
import { readFileSync as readFileSync20 } from "node:fs";
|
|
34344
34536
|
|
|
34345
34537
|
// src/lib/conflicts.ts
|
|
34346
34538
|
import { mkdirSync as mkdirSync10, readFileSync as readFileSync15, readdirSync as readdirSync6, statSync as statSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
@@ -34359,6 +34551,26 @@ function stashConflict(args) {
|
|
|
34359
34551
|
writeFileSync7(target, args.contents, { mode: 384 });
|
|
34360
34552
|
return target;
|
|
34361
34553
|
}
|
|
34554
|
+
function stashEntityConflictOnce(args) {
|
|
34555
|
+
const dir = conflictsRoot(args.workspaceSlug);
|
|
34556
|
+
const head = `${args.entityType}.${args.entitySlug}.`;
|
|
34557
|
+
const tail = new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-\\d{3}Z\\.${args.side}\\.md$`);
|
|
34558
|
+
let names = [];
|
|
34559
|
+
try {
|
|
34560
|
+
names = readdirSync6(dir);
|
|
34561
|
+
} catch {
|
|
34562
|
+
names = [];
|
|
34563
|
+
}
|
|
34564
|
+
const newest = names.filter((name) => name.startsWith(head) && tail.test(name.slice(head.length))).sort().pop();
|
|
34565
|
+
if (newest !== undefined) {
|
|
34566
|
+
const path5 = join12(dir, newest);
|
|
34567
|
+
try {
|
|
34568
|
+
if (readFileSync15(path5, "utf8") === args.contents)
|
|
34569
|
+
return { path: path5, reused: true };
|
|
34570
|
+
} catch {}
|
|
34571
|
+
}
|
|
34572
|
+
return { path: stashConflict(args), reused: false };
|
|
34573
|
+
}
|
|
34362
34574
|
function fileConflictPath(args) {
|
|
34363
34575
|
const { workspaceSlug, relPath, timestamp, side } = args;
|
|
34364
34576
|
const iso = timestamp.toISOString().replace(/[:.]/g, "-");
|
|
@@ -34448,12 +34660,646 @@ function listConflictStashes(workspaceSlug) {
|
|
|
34448
34660
|
}
|
|
34449
34661
|
|
|
34450
34662
|
// src/lib/files/push.ts
|
|
34451
|
-
import { readFileSync as
|
|
34663
|
+
import { readFileSync as readFileSync17, statSync as statSync7 } from "node:fs";
|
|
34664
|
+
|
|
34665
|
+
// src/lib/permanent-deletions.ts
|
|
34666
|
+
import { createHash as createHash3, randomUUID } from "node:crypto";
|
|
34667
|
+
import {
|
|
34668
|
+
chmodSync,
|
|
34669
|
+
closeSync as closeSync4,
|
|
34670
|
+
constants as constants2,
|
|
34671
|
+
existsSync as existsSync4,
|
|
34672
|
+
fsyncSync as fsyncSync2,
|
|
34673
|
+
fstatSync as fstatSync2,
|
|
34674
|
+
lstatSync as lstatSync4,
|
|
34675
|
+
mkdirSync as mkdirSync11,
|
|
34676
|
+
openSync as openSync4,
|
|
34677
|
+
readFileSync as readFileSync16,
|
|
34678
|
+
renameSync as renameSync8,
|
|
34679
|
+
unlinkSync as unlinkSync8,
|
|
34680
|
+
writeSync as writeSync2
|
|
34681
|
+
} from "node:fs";
|
|
34682
|
+
import { dirname as dirname10, join as join13 } from "node:path";
|
|
34683
|
+
var PERMANENT_DELETE_KINDS = ["document", "workspace_file"];
|
|
34684
|
+
var UUID = exports_external.string().uuid();
|
|
34685
|
+
var SHA256 = exports_external.string().regex(/^[0-9a-f]{64}$/);
|
|
34686
|
+
function canonicalLogicalKey(kind, logicalKey) {
|
|
34687
|
+
return kind === "document" ? isValidWorkspaceSlug(logicalKey) : validateFilesRelPath(logicalKey) === logicalKey;
|
|
34688
|
+
}
|
|
34689
|
+
var PendingOperationSchema = exports_external.object({
|
|
34690
|
+
operation_id: UUID,
|
|
34691
|
+
payload_digest: SHA256,
|
|
34692
|
+
state: exports_external.enum(["pending", "transport_unknown", "finalized", "terminal_cleaned"]),
|
|
34693
|
+
row_id: UUID.optional(),
|
|
34694
|
+
updated_at: exports_external.string().datetime({ offset: true })
|
|
34695
|
+
}).superRefine((value, ctx) => {
|
|
34696
|
+
if (value.state === "finalized" && value.row_id === undefined) {
|
|
34697
|
+
ctx.addIssue({ code: "custom", message: "finalized operation requires row_id" });
|
|
34698
|
+
}
|
|
34699
|
+
if (value.state !== "finalized" && value.row_id !== undefined) {
|
|
34700
|
+
ctx.addIssue({ code: "custom", message: "row_id is valid only for finalized operations" });
|
|
34701
|
+
}
|
|
34702
|
+
});
|
|
34703
|
+
var RetirementRecordSchema = exports_external.object({
|
|
34704
|
+
kind: exports_external.enum(PERMANENT_DELETE_KINDS),
|
|
34705
|
+
logical_key: exports_external.string().min(1),
|
|
34706
|
+
row_id: UUID,
|
|
34707
|
+
purge_generation: UUID,
|
|
34708
|
+
retirement_sequence: exports_external.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
34709
|
+
retired_at: exports_external.string().datetime({ offset: true }),
|
|
34710
|
+
disposition: exports_external.enum(["pending", "complete"]),
|
|
34711
|
+
completed_at: exports_external.string().datetime({ offset: true }).optional(),
|
|
34712
|
+
stash_path: exports_external.string().min(1).optional(),
|
|
34713
|
+
stash_hash: SHA256.optional(),
|
|
34714
|
+
operation: PendingOperationSchema.optional()
|
|
34715
|
+
}).superRefine((value, ctx) => {
|
|
34716
|
+
if (!canonicalLogicalKey(value.kind, value.logical_key)) {
|
|
34717
|
+
ctx.addIssue({ code: "custom", message: "retirement logical key is not canonical" });
|
|
34718
|
+
}
|
|
34719
|
+
if (value.disposition === "complete" && value.completed_at === undefined) {
|
|
34720
|
+
ctx.addIssue({ code: "custom", message: "complete retirement requires completed_at" });
|
|
34721
|
+
}
|
|
34722
|
+
if (value.disposition === "pending" && value.completed_at !== undefined) {
|
|
34723
|
+
ctx.addIssue({ code: "custom", message: "pending retirement cannot have completed_at" });
|
|
34724
|
+
}
|
|
34725
|
+
if (value.stash_path === undefined !== (value.stash_hash === undefined)) {
|
|
34726
|
+
ctx.addIssue({ code: "custom", message: "stash_path and stash_hash must be written together" });
|
|
34727
|
+
}
|
|
34728
|
+
});
|
|
34729
|
+
var RefreshMarkerSchema = exports_external.object({
|
|
34730
|
+
kind: exports_external.enum(PERMANENT_DELETE_KINDS),
|
|
34731
|
+
logical_key: exports_external.string().min(1),
|
|
34732
|
+
required_generation: UUID.optional(),
|
|
34733
|
+
started_at: exports_external.string().datetime({ offset: true }),
|
|
34734
|
+
attempts: exports_external.number().int().nonnegative(),
|
|
34735
|
+
retry_after: exports_external.string().datetime({ offset: true }).optional(),
|
|
34736
|
+
state: exports_external.enum(["required", "retention_missing"])
|
|
34737
|
+
}).superRefine((value, ctx) => {
|
|
34738
|
+
if (!canonicalLogicalKey(value.kind, value.logical_key)) {
|
|
34739
|
+
ctx.addIssue({ code: "custom", message: "refresh logical key is not canonical" });
|
|
34740
|
+
}
|
|
34741
|
+
});
|
|
34742
|
+
var JournalBodySchema = exports_external.object({
|
|
34743
|
+
version: exports_external.literal(1),
|
|
34744
|
+
revision: exports_external.number().int().nonnegative(),
|
|
34745
|
+
workspace_slug: exports_external.string().min(1),
|
|
34746
|
+
records: exports_external.array(RetirementRecordSchema),
|
|
34747
|
+
refresh: exports_external.array(RefreshMarkerSchema)
|
|
34748
|
+
});
|
|
34749
|
+
var JournalFileSchema = JournalBodySchema.extend({ checksum: SHA256 });
|
|
34750
|
+
|
|
34751
|
+
class PermanentDeletionJournalError extends Error {
|
|
34752
|
+
stage;
|
|
34753
|
+
constructor(message, stage) {
|
|
34754
|
+
super(message);
|
|
34755
|
+
this.stage = stage;
|
|
34756
|
+
this.name = "PermanentDeletionJournalError";
|
|
34757
|
+
}
|
|
34758
|
+
}
|
|
34759
|
+
function permanentDeletionJournalPath(workspaceSlug) {
|
|
34760
|
+
if (!isValidWorkspaceSlug(workspaceSlug)) {
|
|
34761
|
+
throw new PermanentDeletionJournalError(`refusing unsafe workspace slug: ${JSON.stringify(workspaceSlug)}`, "validate");
|
|
34762
|
+
}
|
|
34763
|
+
return join13(plinthHome(), workspaceSlug, "permanent-deletions.json");
|
|
34764
|
+
}
|
|
34765
|
+
function recordKey2(kind, logicalKey) {
|
|
34766
|
+
return `${kind}\x00${logicalKey}`;
|
|
34767
|
+
}
|
|
34768
|
+
function canonical(value) {
|
|
34769
|
+
if (value === null || typeof value !== "object")
|
|
34770
|
+
return JSON.stringify(value);
|
|
34771
|
+
if (Array.isArray(value))
|
|
34772
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
34773
|
+
const obj = value;
|
|
34774
|
+
return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${canonical(obj[key])}`).join(",")}}`;
|
|
34775
|
+
}
|
|
34776
|
+
function canonicalPayloadDigest(value) {
|
|
34777
|
+
return createHash3("sha256").update(canonical(value), "utf8").digest("hex");
|
|
34778
|
+
}
|
|
34779
|
+
function fileCreatePayloadDigest(contentHash, mimeType) {
|
|
34780
|
+
return canonicalPayloadDigest({ content_hash: contentHash, mime_type: mimeType });
|
|
34781
|
+
}
|
|
34782
|
+
function checksum(body) {
|
|
34783
|
+
return createHash3("sha256").update(canonical(body), "utf8").digest("hex");
|
|
34784
|
+
}
|
|
34785
|
+
function emptyJournal(workspaceSlug) {
|
|
34786
|
+
return { version: 1, revision: 0, workspace_slug: workspaceSlug, records: [], refresh: [] };
|
|
34787
|
+
}
|
|
34788
|
+
function validateUniqueness(journal) {
|
|
34789
|
+
const records = new Set;
|
|
34790
|
+
for (const record2 of journal.records) {
|
|
34791
|
+
const key = recordKey2(record2.kind, record2.logical_key);
|
|
34792
|
+
if (records.has(key)) {
|
|
34793
|
+
throw new PermanentDeletionJournalError(`permanent deletion journal contains duplicate record ${record2.kind}:${record2.logical_key}`, "validate");
|
|
34794
|
+
}
|
|
34795
|
+
records.add(key);
|
|
34796
|
+
}
|
|
34797
|
+
const refresh = new Set;
|
|
34798
|
+
for (const marker of journal.refresh) {
|
|
34799
|
+
const key = recordKey2(marker.kind, marker.logical_key);
|
|
34800
|
+
if (refresh.has(key)) {
|
|
34801
|
+
throw new PermanentDeletionJournalError(`permanent deletion journal contains duplicate refresh marker ${marker.kind}:${marker.logical_key}`, "validate");
|
|
34802
|
+
}
|
|
34803
|
+
refresh.add(key);
|
|
34804
|
+
}
|
|
34805
|
+
}
|
|
34806
|
+
function assertOwnerOnlyFile(path5) {
|
|
34807
|
+
const stat = lstatSync4(path5);
|
|
34808
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
34809
|
+
throw new PermanentDeletionJournalError(`${path5} is not a regular file`, "validate");
|
|
34810
|
+
}
|
|
34811
|
+
if ((stat.mode & 511) !== 384) {
|
|
34812
|
+
throw new PermanentDeletionJournalError(`${path5} must have mode 0600`, "validate");
|
|
34813
|
+
}
|
|
34814
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
34815
|
+
if (uid !== undefined && stat.uid !== uid) {
|
|
34816
|
+
throw new PermanentDeletionJournalError(`${path5} is not owned by the current user`, "validate");
|
|
34817
|
+
}
|
|
34818
|
+
}
|
|
34819
|
+
function assertPrivateDirectory(path5) {
|
|
34820
|
+
const stat = lstatSync4(path5);
|
|
34821
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
34822
|
+
throw new PermanentDeletionJournalError(`${path5} is not a private directory`, "validate");
|
|
34823
|
+
}
|
|
34824
|
+
if ((stat.mode & 511) !== 448) {
|
|
34825
|
+
throw new PermanentDeletionJournalError(`${path5} must have mode 0700`, "validate");
|
|
34826
|
+
}
|
|
34827
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
34828
|
+
if (uid !== undefined && stat.uid !== uid) {
|
|
34829
|
+
throw new PermanentDeletionJournalError(`${path5} is not owned by the current user`, "validate");
|
|
34830
|
+
}
|
|
34831
|
+
}
|
|
34832
|
+
function parseJournal(workspaceSlug, raw) {
|
|
34833
|
+
let json2;
|
|
34834
|
+
try {
|
|
34835
|
+
json2 = JSON.parse(raw);
|
|
34836
|
+
} catch (error52) {
|
|
34837
|
+
const detail = error52 instanceof Error ? error52.message : String(error52);
|
|
34838
|
+
throw new PermanentDeletionJournalError(`permanent deletion journal is not valid JSON: ${detail}`, "validate");
|
|
34839
|
+
}
|
|
34840
|
+
const parsed = JournalFileSchema.safeParse(json2);
|
|
34841
|
+
if (!parsed.success) {
|
|
34842
|
+
throw new PermanentDeletionJournalError(`permanent deletion journal does not match schema v1: ${parsed.error.message}`, "validate");
|
|
34843
|
+
}
|
|
34844
|
+
const { checksum: storedChecksum, ...body } = parsed.data;
|
|
34845
|
+
if (body.workspace_slug !== workspaceSlug) {
|
|
34846
|
+
throw new PermanentDeletionJournalError(`permanent deletion journal belongs to ${body.workspace_slug}, not ${workspaceSlug}`, "validate");
|
|
34847
|
+
}
|
|
34848
|
+
if (checksum(body) !== storedChecksum) {
|
|
34849
|
+
throw new PermanentDeletionJournalError("permanent deletion journal checksum mismatch", "validate");
|
|
34850
|
+
}
|
|
34851
|
+
validateUniqueness(body);
|
|
34852
|
+
return body;
|
|
34853
|
+
}
|
|
34854
|
+
function fsyncDirectory(path5) {
|
|
34855
|
+
const fd = openSync4(path5, constants2.O_RDONLY);
|
|
34856
|
+
try {
|
|
34857
|
+
fsyncSync2(fd);
|
|
34858
|
+
} finally {
|
|
34859
|
+
closeSync4(fd);
|
|
34860
|
+
}
|
|
34861
|
+
}
|
|
34862
|
+
function loadPermanentDeletionJournal(workspaceSlug) {
|
|
34863
|
+
const target = permanentDeletionJournalPath(workspaceSlug);
|
|
34864
|
+
if (!existsSync4(target))
|
|
34865
|
+
return emptyJournal(workspaceSlug);
|
|
34866
|
+
assertPrivateDirectory(dirname10(target));
|
|
34867
|
+
assertOwnerOnlyFile(target);
|
|
34868
|
+
const journal = parseJournal(workspaceSlug, readFileSync16(target, "utf8"));
|
|
34869
|
+
try {
|
|
34870
|
+
fsyncDirectory(dirname10(target));
|
|
34871
|
+
} catch (error52) {
|
|
34872
|
+
const detail = error52 instanceof Error ? error52.message : String(error52);
|
|
34873
|
+
throw new PermanentDeletionJournalError(`permanent deletion journal durability remains indeterminate: ${detail}`, "durability_indeterminate");
|
|
34874
|
+
}
|
|
34875
|
+
return journal;
|
|
34876
|
+
}
|
|
34877
|
+
function validatedCurrentRevision(workspaceSlug, expected) {
|
|
34878
|
+
const current = loadPermanentDeletionJournal(workspaceSlug);
|
|
34879
|
+
if (current.revision !== expected) {
|
|
34880
|
+
throw new PermanentDeletionJournalError(`permanent deletion journal revision changed from ${expected} to ${current.revision}`, "validate");
|
|
34881
|
+
}
|
|
34882
|
+
return current;
|
|
34883
|
+
}
|
|
34884
|
+
function writePermanentDeletionJournal(prior, next) {
|
|
34885
|
+
if (prior.workspace_slug !== next.workspace_slug) {
|
|
34886
|
+
throw new PermanentDeletionJournalError("journal workspace cannot change", "validate");
|
|
34887
|
+
}
|
|
34888
|
+
validatedCurrentRevision(prior.workspace_slug, prior.revision);
|
|
34889
|
+
const body = JournalBodySchema.parse({ ...next, revision: prior.revision + 1 });
|
|
34890
|
+
validateUniqueness(body);
|
|
34891
|
+
const target = permanentDeletionJournalPath(body.workspace_slug);
|
|
34892
|
+
const parent = dirname10(target);
|
|
34893
|
+
mkdirSync11(parent, { recursive: true, mode: 448 });
|
|
34894
|
+
chmodSync(parent, 448);
|
|
34895
|
+
assertPrivateDirectory(parent);
|
|
34896
|
+
if (existsSync4(target))
|
|
34897
|
+
assertOwnerOnlyFile(target);
|
|
34898
|
+
const tmp = join13(parent, `.permanent-deletions.${process.pid}.${randomUUID()}.tmp`);
|
|
34899
|
+
const wire = JSON.stringify({ ...body, checksum: checksum(body) }, null, 2) + `
|
|
34900
|
+
`;
|
|
34901
|
+
let fd;
|
|
34902
|
+
try {
|
|
34903
|
+
fd = openSync4(tmp, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | (constants2.O_NOFOLLOW ?? 0), 384);
|
|
34904
|
+
const stat = fstatSync2(fd);
|
|
34905
|
+
if (!stat.isFile() || (stat.mode & 511) !== 384) {
|
|
34906
|
+
throw new PermanentDeletionJournalError("journal temporary file is not owner-only", "prepare");
|
|
34907
|
+
}
|
|
34908
|
+
const bytes = Buffer.from(wire, "utf8");
|
|
34909
|
+
let offset = 0;
|
|
34910
|
+
while (offset < bytes.length)
|
|
34911
|
+
offset += writeSync2(fd, bytes, offset, bytes.length - offset);
|
|
34912
|
+
fsyncSync2(fd);
|
|
34913
|
+
closeSync4(fd);
|
|
34914
|
+
fd = undefined;
|
|
34915
|
+
} catch (error52) {
|
|
34916
|
+
if (fd !== undefined) {
|
|
34917
|
+
try {
|
|
34918
|
+
closeSync4(fd);
|
|
34919
|
+
} catch {}
|
|
34920
|
+
}
|
|
34921
|
+
try {
|
|
34922
|
+
unlinkSync8(tmp);
|
|
34923
|
+
} catch {}
|
|
34924
|
+
if (error52 instanceof PermanentDeletionJournalError)
|
|
34925
|
+
throw error52;
|
|
34926
|
+
const detail = error52 instanceof Error ? error52.message : String(error52);
|
|
34927
|
+
throw new PermanentDeletionJournalError(`could not prepare journal write: ${detail}`, "write");
|
|
34928
|
+
}
|
|
34929
|
+
try {
|
|
34930
|
+
renameSync8(tmp, target);
|
|
34931
|
+
} catch (error52) {
|
|
34932
|
+
let installed2 = false;
|
|
34933
|
+
try {
|
|
34934
|
+
assertOwnerOnlyFile(target);
|
|
34935
|
+
const candidate = parseJournal(body.workspace_slug, readFileSync16(target, "utf8"));
|
|
34936
|
+
installed2 = candidate.revision === body.revision && checksum(candidate) === checksum(body);
|
|
34937
|
+
} catch {
|
|
34938
|
+
installed2 = false;
|
|
34939
|
+
}
|
|
34940
|
+
try {
|
|
34941
|
+
unlinkSync8(tmp);
|
|
34942
|
+
} catch {}
|
|
34943
|
+
if (!installed2) {
|
|
34944
|
+
const detail = error52 instanceof Error ? error52.message : String(error52);
|
|
34945
|
+
throw new PermanentDeletionJournalError(`could not rename journal: ${detail}`, "rename");
|
|
34946
|
+
}
|
|
34947
|
+
}
|
|
34948
|
+
assertOwnerOnlyFile(target);
|
|
34949
|
+
const installed = parseJournal(body.workspace_slug, readFileSync16(target, "utf8"));
|
|
34950
|
+
if (installed.revision !== body.revision || checksum(installed) !== checksum(body)) {
|
|
34951
|
+
throw new PermanentDeletionJournalError("renamed journal revision did not validate", "durability_indeterminate");
|
|
34952
|
+
}
|
|
34953
|
+
try {
|
|
34954
|
+
fsyncDirectory(parent);
|
|
34955
|
+
} catch (firstError) {
|
|
34956
|
+
assertOwnerOnlyFile(target);
|
|
34957
|
+
const reopened = parseJournal(body.workspace_slug, readFileSync16(target, "utf8"));
|
|
34958
|
+
if (reopened.revision !== body.revision || checksum(reopened) !== checksum(body)) {
|
|
34959
|
+
throw new PermanentDeletionJournalError("journal changed after rename", "durability_indeterminate");
|
|
34960
|
+
}
|
|
34961
|
+
try {
|
|
34962
|
+
fsyncDirectory(parent);
|
|
34963
|
+
} catch (secondError) {
|
|
34964
|
+
const first = firstError instanceof Error ? firstError.message : String(firstError);
|
|
34965
|
+
const second = secondError instanceof Error ? secondError.message : String(secondError);
|
|
34966
|
+
throw new PermanentDeletionJournalError(`journal renamed but parent directory sync failed (${first}; retry: ${second})`, "durability_indeterminate");
|
|
34967
|
+
}
|
|
34968
|
+
}
|
|
34969
|
+
return body;
|
|
34970
|
+
}
|
|
34971
|
+
function updatePermanentDeletionJournal(workspaceSlug, update) {
|
|
34972
|
+
const prior = loadPermanentDeletionJournal(workspaceSlug);
|
|
34973
|
+
const next = update(prior);
|
|
34974
|
+
if (next === prior)
|
|
34975
|
+
return prior;
|
|
34976
|
+
return writePermanentDeletionJournal(prior, next);
|
|
34977
|
+
}
|
|
34978
|
+
function findRetirement(journal, kind, logicalKey) {
|
|
34979
|
+
return journal.records.find((record2) => record2.kind === kind && record2.logical_key === logicalKey);
|
|
34980
|
+
}
|
|
34981
|
+
function findRefreshMarker(journal, kind, logicalKey) {
|
|
34982
|
+
return journal.refresh.find((marker) => marker.kind === kind && marker.logical_key === logicalKey);
|
|
34983
|
+
}
|
|
34984
|
+
function replaceRecord(journal, record2) {
|
|
34985
|
+
return {
|
|
34986
|
+
...journal,
|
|
34987
|
+
records: [
|
|
34988
|
+
...journal.records.filter((item) => recordKey2(item.kind, item.logical_key) !== recordKey2(record2.kind, record2.logical_key)),
|
|
34989
|
+
record2
|
|
34990
|
+
]
|
|
34991
|
+
};
|
|
34992
|
+
}
|
|
34993
|
+
function requireFeedRefresh(args) {
|
|
34994
|
+
const now = args.now ?? (() => new Date);
|
|
34995
|
+
return updatePermanentDeletionJournal(args.workspaceSlug, (journal) => {
|
|
34996
|
+
const existing = findRefreshMarker(journal, args.kind, args.logicalKey);
|
|
34997
|
+
if (existing)
|
|
34998
|
+
return journal;
|
|
34999
|
+
return {
|
|
35000
|
+
...journal,
|
|
35001
|
+
refresh: [...journal.refresh, {
|
|
35002
|
+
kind: args.kind,
|
|
35003
|
+
logical_key: args.logicalKey,
|
|
35004
|
+
...args.requiredGeneration ? { required_generation: args.requiredGeneration } : {},
|
|
35005
|
+
started_at: now().toISOString(),
|
|
35006
|
+
attempts: 0,
|
|
35007
|
+
state: "required"
|
|
35008
|
+
}]
|
|
35009
|
+
};
|
|
35010
|
+
});
|
|
35011
|
+
}
|
|
35012
|
+
function markRetentionMissing(args) {
|
|
35013
|
+
const now = args.now ?? (() => new Date);
|
|
35014
|
+
return updatePermanentDeletionJournal(args.workspaceSlug, (journal) => {
|
|
35015
|
+
const existing = findRefreshMarker(journal, args.kind, args.logicalKey);
|
|
35016
|
+
if (!existing)
|
|
35017
|
+
return journal;
|
|
35018
|
+
const attempts = existing.attempts + 1;
|
|
35019
|
+
const backoffMs = Math.min(3600000, 1000 * 2 ** Math.min(attempts, 12));
|
|
35020
|
+
return {
|
|
35021
|
+
...journal,
|
|
35022
|
+
refresh: journal.refresh.map((marker) => marker === existing ? {
|
|
35023
|
+
...marker,
|
|
35024
|
+
attempts,
|
|
35025
|
+
state: "retention_missing",
|
|
35026
|
+
retry_after: new Date(now().getTime() + backoffMs).toISOString()
|
|
35027
|
+
} : marker)
|
|
35028
|
+
};
|
|
35029
|
+
});
|
|
35030
|
+
}
|
|
35031
|
+
function clearFeedRefresh(args) {
|
|
35032
|
+
return updatePermanentDeletionJournal(args.workspaceSlug, (journal) => {
|
|
35033
|
+
const record2 = findRetirement(journal, args.kind, args.logicalKey);
|
|
35034
|
+
if (!record2 || record2.disposition !== "complete" || record2.purge_generation !== args.generation) {
|
|
35035
|
+
throw new PermanentDeletionJournalError("cannot clear refresh before matching disposition is complete", "validate");
|
|
35036
|
+
}
|
|
35037
|
+
const next = journal.refresh.filter((marker) => recordKey2(marker.kind, marker.logical_key) !== recordKey2(args.kind, args.logicalKey));
|
|
35038
|
+
return next.length === journal.refresh.length ? journal : { ...journal, refresh: next };
|
|
35039
|
+
});
|
|
35040
|
+
}
|
|
35041
|
+
function recordRetirementPending(args) {
|
|
35042
|
+
return updatePermanentDeletionJournal(args.workspaceSlug, (journal) => {
|
|
35043
|
+
const existing = findRetirement(journal, args.kind, args.logicalKey);
|
|
35044
|
+
if (existing && existing.retirement_sequence > args.retirementSequence)
|
|
35045
|
+
return journal;
|
|
35046
|
+
if (existing && existing.retirement_sequence === args.retirementSequence && existing.purge_generation !== args.purgeGeneration) {
|
|
35047
|
+
throw new PermanentDeletionJournalError("one retirement_sequence maps to multiple generations", "validate");
|
|
35048
|
+
}
|
|
35049
|
+
if (existing?.purge_generation === args.purgeGeneration)
|
|
35050
|
+
return journal;
|
|
35051
|
+
return replaceRecord(journal, {
|
|
35052
|
+
kind: args.kind,
|
|
35053
|
+
logical_key: args.logicalKey,
|
|
35054
|
+
row_id: args.rowId,
|
|
35055
|
+
purge_generation: args.purgeGeneration,
|
|
35056
|
+
retirement_sequence: args.retirementSequence,
|
|
35057
|
+
retired_at: args.retiredAt,
|
|
35058
|
+
disposition: "pending"
|
|
35059
|
+
});
|
|
35060
|
+
});
|
|
35061
|
+
}
|
|
35062
|
+
function completeRetirement(args) {
|
|
35063
|
+
if (args.stashPath === undefined !== (args.stashHash === undefined)) {
|
|
35064
|
+
throw new PermanentDeletionJournalError("stash path and hash must be supplied together", "validate");
|
|
35065
|
+
}
|
|
35066
|
+
const now = args.now ?? (() => new Date);
|
|
35067
|
+
return updatePermanentDeletionJournal(args.workspaceSlug, (journal) => {
|
|
35068
|
+
const existing = findRetirement(journal, args.kind, args.logicalKey);
|
|
35069
|
+
if (!existing || existing.purge_generation !== args.generation) {
|
|
35070
|
+
throw new PermanentDeletionJournalError("cannot complete an unknown retirement generation", "validate");
|
|
35071
|
+
}
|
|
35072
|
+
if (existing.disposition === "complete")
|
|
35073
|
+
return journal;
|
|
35074
|
+
return replaceRecord(journal, {
|
|
35075
|
+
...existing,
|
|
35076
|
+
disposition: "complete",
|
|
35077
|
+
completed_at: now().toISOString(),
|
|
35078
|
+
...args.stashPath ? { stash_path: args.stashPath, stash_hash: args.stashHash } : {}
|
|
35079
|
+
});
|
|
35080
|
+
});
|
|
35081
|
+
}
|
|
35082
|
+
function recordRetirementStash(args) {
|
|
35083
|
+
return updatePermanentDeletionJournal(args.workspaceSlug, (journal) => {
|
|
35084
|
+
const existing = findRetirement(journal, args.kind, args.logicalKey);
|
|
35085
|
+
if (!existing || existing.purge_generation !== args.generation) {
|
|
35086
|
+
throw new PermanentDeletionJournalError("cannot attach a stash to an unknown retirement", "validate");
|
|
35087
|
+
}
|
|
35088
|
+
if (existing.stash_path === args.stashPath && existing.stash_hash === args.stashHash) {
|
|
35089
|
+
return journal;
|
|
35090
|
+
}
|
|
35091
|
+
if (existing.stash_path && existing.stash_hash !== args.stashHash) {
|
|
35092
|
+
throw new PermanentDeletionJournalError("retirement stash changed after it was recorded", "validate");
|
|
35093
|
+
}
|
|
35094
|
+
return replaceRecord(journal, {
|
|
35095
|
+
...existing,
|
|
35096
|
+
stash_path: args.stashPath,
|
|
35097
|
+
stash_hash: args.stashHash
|
|
35098
|
+
});
|
|
35099
|
+
});
|
|
35100
|
+
}
|
|
35101
|
+
function prepareGenerationCreate(args) {
|
|
35102
|
+
const now = args.now ?? (() => new Date);
|
|
35103
|
+
let decision = { kind: "ordinary" };
|
|
35104
|
+
updatePermanentDeletionJournal(args.workspaceSlug, (journal) => {
|
|
35105
|
+
if (findRefreshMarker(journal, args.kind, args.logicalKey)) {
|
|
35106
|
+
decision = { kind: "blocked", reason: "refresh_required" };
|
|
35107
|
+
return journal;
|
|
35108
|
+
}
|
|
35109
|
+
const retirement = findRetirement(journal, args.kind, args.logicalKey);
|
|
35110
|
+
if (!retirement) {
|
|
35111
|
+
decision = { kind: "ordinary" };
|
|
35112
|
+
return journal;
|
|
35113
|
+
}
|
|
35114
|
+
if (retirement.disposition !== "complete") {
|
|
35115
|
+
decision = { kind: "blocked", reason: "disposition_pending" };
|
|
35116
|
+
return journal;
|
|
35117
|
+
}
|
|
35118
|
+
const operation = retirement.operation;
|
|
35119
|
+
if (operation) {
|
|
35120
|
+
if (operation.payload_digest === args.payloadDigest && (operation.state === "pending" || operation.state === "transport_unknown")) {
|
|
35121
|
+
decision = {
|
|
35122
|
+
kind: "authorised",
|
|
35123
|
+
retiredGeneration: retirement.purge_generation,
|
|
35124
|
+
operationId: operation.operation_id,
|
|
35125
|
+
payloadDigest: operation.payload_digest
|
|
35126
|
+
};
|
|
35127
|
+
return journal;
|
|
35128
|
+
}
|
|
35129
|
+
if (operation.state !== "terminal_cleaned") {
|
|
35130
|
+
decision = { kind: "blocked", reason: "operation_unresolved" };
|
|
35131
|
+
return journal;
|
|
35132
|
+
}
|
|
35133
|
+
}
|
|
35134
|
+
const nextOperation = {
|
|
35135
|
+
operation_id: randomUUID(),
|
|
35136
|
+
payload_digest: args.payloadDigest,
|
|
35137
|
+
state: "pending",
|
|
35138
|
+
updated_at: now().toISOString()
|
|
35139
|
+
};
|
|
35140
|
+
decision = {
|
|
35141
|
+
kind: "authorised",
|
|
35142
|
+
retiredGeneration: retirement.purge_generation,
|
|
35143
|
+
operationId: nextOperation.operation_id,
|
|
35144
|
+
payloadDigest: nextOperation.payload_digest
|
|
35145
|
+
};
|
|
35146
|
+
return replaceRecord(journal, { ...retirement, operation: nextOperation });
|
|
35147
|
+
});
|
|
35148
|
+
return decision;
|
|
35149
|
+
}
|
|
35150
|
+
function updateCreateOperation(args) {
|
|
35151
|
+
const now = args.now ?? (() => new Date);
|
|
35152
|
+
return updatePermanentDeletionJournal(args.workspaceSlug, (journal) => {
|
|
35153
|
+
const retirement = findRetirement(journal, args.kind, args.logicalKey);
|
|
35154
|
+
if (!retirement?.operation || retirement.operation.operation_id !== args.operationId) {
|
|
35155
|
+
throw new PermanentDeletionJournalError("operation update does not match the journal", "validate");
|
|
35156
|
+
}
|
|
35157
|
+
const from = retirement.operation.state;
|
|
35158
|
+
const allowed = {
|
|
35159
|
+
pending: new Set(["pending", "transport_unknown", "finalized", "terminal_cleaned"]),
|
|
35160
|
+
transport_unknown: new Set(["transport_unknown", "pending", "finalized", "terminal_cleaned"]),
|
|
35161
|
+
finalized: new Set(["finalized", "terminal_cleaned"]),
|
|
35162
|
+
terminal_cleaned: new Set(["terminal_cleaned"])
|
|
35163
|
+
};
|
|
35164
|
+
if (!allowed[from].has(args.state)) {
|
|
35165
|
+
throw new PermanentDeletionJournalError(`invalid create operation transition ${from} -> ${args.state}`, "validate");
|
|
35166
|
+
}
|
|
35167
|
+
if (args.state === "finalized" && args.rowId === undefined) {
|
|
35168
|
+
throw new PermanentDeletionJournalError("finalized operation requires a row id", "validate");
|
|
35169
|
+
}
|
|
35170
|
+
return replaceRecord(journal, {
|
|
35171
|
+
...retirement,
|
|
35172
|
+
operation: {
|
|
35173
|
+
operation_id: retirement.operation.operation_id,
|
|
35174
|
+
payload_digest: retirement.operation.payload_digest,
|
|
35175
|
+
state: args.state,
|
|
35176
|
+
...args.state === "finalized" && args.rowId ? { row_id: args.rowId } : {},
|
|
35177
|
+
updated_at: now().toISOString()
|
|
35178
|
+
}
|
|
35179
|
+
});
|
|
35180
|
+
});
|
|
35181
|
+
}
|
|
35182
|
+
|
|
35183
|
+
// src/lib/permanent-mutex.ts
|
|
35184
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
35185
|
+
|
|
35186
|
+
// src/lib/per-id-mutex.ts
|
|
35187
|
+
class PerIdMutex {
|
|
35188
|
+
chains = new Map;
|
|
35189
|
+
async run(key, fn) {
|
|
35190
|
+
const prior = this.chains.get(key) ?? Promise.resolve();
|
|
35191
|
+
const next = prior.then(fn, fn);
|
|
35192
|
+
this.chains.set(key, next);
|
|
35193
|
+
const cleanup = () => {
|
|
35194
|
+
if (this.chains.get(key) === next) {
|
|
35195
|
+
this.chains.delete(key);
|
|
35196
|
+
}
|
|
35197
|
+
};
|
|
35198
|
+
next.then(cleanup, cleanup);
|
|
35199
|
+
return next;
|
|
35200
|
+
}
|
|
35201
|
+
size() {
|
|
35202
|
+
return this.chains.size;
|
|
35203
|
+
}
|
|
35204
|
+
}
|
|
35205
|
+
|
|
35206
|
+
// src/lib/permanent-mutex.ts
|
|
35207
|
+
var mutex = new PerIdMutex;
|
|
35208
|
+
var heldKeys = new AsyncLocalStorage;
|
|
35209
|
+
function permanentDeletionMutexKey(kind, logicalKey) {
|
|
35210
|
+
return `permanent:${kind}:${logicalKey}`;
|
|
35211
|
+
}
|
|
35212
|
+
function withPermanentDeletionLock(kind, logicalKey, run) {
|
|
35213
|
+
const key = permanentDeletionMutexKey(kind, logicalKey);
|
|
35214
|
+
const held = heldKeys.getStore();
|
|
35215
|
+
if (held?.has(key))
|
|
35216
|
+
return run();
|
|
35217
|
+
return mutex.run(key, () => heldKeys.run(new Set([...held ?? [], key]), run));
|
|
35218
|
+
}
|
|
35219
|
+
|
|
35220
|
+
// src/lib/permanent-create.ts
|
|
35221
|
+
async function prepareOrResolveGenerationCreate(args) {
|
|
35222
|
+
return withPermanentDeletionLock(args.kind, args.logicalKey, () => prepareOrResolveGenerationCreateLocked(args));
|
|
35223
|
+
}
|
|
35224
|
+
async function prepareOrResolveGenerationCreateLocked(args) {
|
|
35225
|
+
let decision = prepareGenerationCreate(args);
|
|
35226
|
+
if (decision.kind !== "blocked" || decision.reason !== "operation_unresolved") {
|
|
35227
|
+
return decision;
|
|
35228
|
+
}
|
|
35229
|
+
const journal = loadPermanentDeletionJournal(args.workspaceSlug);
|
|
35230
|
+
const retirement = findRetirement(journal, args.kind, args.logicalKey);
|
|
35231
|
+
const operation = retirement?.operation;
|
|
35232
|
+
if (!operation)
|
|
35233
|
+
return decision;
|
|
35234
|
+
const entityKind = args.kind;
|
|
35235
|
+
const resolved = await resolvePermanentDeletionOperation({
|
|
35236
|
+
baseUrl: args.baseUrl,
|
|
35237
|
+
pat: args.pat,
|
|
35238
|
+
entityKind,
|
|
35239
|
+
operationId: operation.operation_id,
|
|
35240
|
+
fetchImpl: args.fetchImpl
|
|
35241
|
+
});
|
|
35242
|
+
if (resolved.entity_kind !== entityKind || resolved.operation_id !== operation.operation_id) {
|
|
35243
|
+
throw new Error("operation resolution identity does not match the local journal");
|
|
35244
|
+
}
|
|
35245
|
+
if (resolved.status === "retired_not_found") {
|
|
35246
|
+
updateCreateOperation({
|
|
35247
|
+
workspaceSlug: args.workspaceSlug,
|
|
35248
|
+
kind: args.kind,
|
|
35249
|
+
logicalKey: args.logicalKey,
|
|
35250
|
+
operationId: operation.operation_id,
|
|
35251
|
+
state: "terminal_cleaned",
|
|
35252
|
+
now: args.now
|
|
35253
|
+
});
|
|
35254
|
+
return prepareGenerationCreate(args);
|
|
35255
|
+
}
|
|
35256
|
+
if (resolved.logical_key !== args.logicalKey || resolved.payload_digest !== operation.payload_digest) {
|
|
35257
|
+
throw new Error("operation resolution binding does not match the local journal");
|
|
35258
|
+
}
|
|
35259
|
+
if (resolved.status === "pending") {
|
|
35260
|
+
updateCreateOperation({
|
|
35261
|
+
workspaceSlug: args.workspaceSlug,
|
|
35262
|
+
kind: args.kind,
|
|
35263
|
+
logicalKey: args.logicalKey,
|
|
35264
|
+
operationId: operation.operation_id,
|
|
35265
|
+
state: "pending",
|
|
35266
|
+
now: args.now
|
|
35267
|
+
});
|
|
35268
|
+
return { kind: "blocked", reason: "operation_unresolved" };
|
|
35269
|
+
}
|
|
35270
|
+
if (resolved.status === "terminal_cleaned") {
|
|
35271
|
+
updateCreateOperation({
|
|
35272
|
+
workspaceSlug: args.workspaceSlug,
|
|
35273
|
+
kind: args.kind,
|
|
35274
|
+
logicalKey: args.logicalKey,
|
|
35275
|
+
operationId: operation.operation_id,
|
|
35276
|
+
state: "terminal_cleaned",
|
|
35277
|
+
now: args.now
|
|
35278
|
+
});
|
|
35279
|
+
return prepareGenerationCreate(args);
|
|
35280
|
+
}
|
|
35281
|
+
const id = exports_external.object({ id: exports_external.string().uuid() }).parse(resolved.live).id;
|
|
35282
|
+
updateCreateOperation({
|
|
35283
|
+
workspaceSlug: args.workspaceSlug,
|
|
35284
|
+
kind: args.kind,
|
|
35285
|
+
logicalKey: args.logicalKey,
|
|
35286
|
+
operationId: operation.operation_id,
|
|
35287
|
+
state: "finalized",
|
|
35288
|
+
rowId: id,
|
|
35289
|
+
now: args.now
|
|
35290
|
+
});
|
|
35291
|
+
return {
|
|
35292
|
+
kind: "finalized",
|
|
35293
|
+
live: resolved.live,
|
|
35294
|
+
operationId: operation.operation_id,
|
|
35295
|
+
payloadDigest: operation.payload_digest
|
|
35296
|
+
};
|
|
35297
|
+
}
|
|
34452
35298
|
|
|
34453
35299
|
// src/lib/files/content-hash.ts
|
|
34454
|
-
import { createHash as
|
|
35300
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
34455
35301
|
function sha256Bytes(bytes) {
|
|
34456
|
-
return
|
|
35302
|
+
return createHash4("sha256").update(bytes).digest("hex");
|
|
34457
35303
|
}
|
|
34458
35304
|
|
|
34459
35305
|
// src/lib/files/mime.ts
|
|
@@ -34552,6 +35398,9 @@ async function restoreFileFromCloud(args) {
|
|
|
34552
35398
|
});
|
|
34553
35399
|
}
|
|
34554
35400
|
async function pushFilesUpsert(args) {
|
|
35401
|
+
return withPermanentDeletionLock("workspace_file", args.relPath, () => pushFilesUpsertLocked(args));
|
|
35402
|
+
}
|
|
35403
|
+
async function pushFilesUpsertLocked(args) {
|
|
34555
35404
|
const { workspaceSlug, baseUrl, pat, absolutePath, relPath, state, prepared, deps } = args;
|
|
34556
35405
|
const fetchImpl = deps?.fetchImpl;
|
|
34557
35406
|
const now = deps?.now ?? (() => new Date);
|
|
@@ -34604,7 +35453,7 @@ async function pushFilesUpsert(args) {
|
|
|
34604
35453
|
state: next
|
|
34605
35454
|
};
|
|
34606
35455
|
}
|
|
34607
|
-
bytes =
|
|
35456
|
+
bytes = readFileSync17(absolutePath);
|
|
34608
35457
|
hash2 = sha256Bytes(bytes);
|
|
34609
35458
|
inode = st.ino;
|
|
34610
35459
|
}
|
|
@@ -34619,7 +35468,61 @@ async function pushFilesUpsert(args) {
|
|
|
34619
35468
|
return { outcome: { kind: "noop", relPath }, state };
|
|
34620
35469
|
}
|
|
34621
35470
|
const mimeType = inferMime(relPath);
|
|
35471
|
+
const createProtocol = existing ? { kind: "ordinary" } : await prepareOrResolveGenerationCreate({
|
|
35472
|
+
workspaceSlug,
|
|
35473
|
+
baseUrl,
|
|
35474
|
+
pat,
|
|
35475
|
+
kind: "workspace_file",
|
|
35476
|
+
logicalKey: relPath,
|
|
35477
|
+
payloadDigest: fileCreatePayloadDigest(hash2, mimeType),
|
|
35478
|
+
now,
|
|
35479
|
+
fetchImpl
|
|
35480
|
+
});
|
|
35481
|
+
if (createProtocol.kind === "blocked") {
|
|
35482
|
+
output?.warn(`files/${relPath} is held by permanent-delete reconciliation (${createProtocol.reason}).`);
|
|
35483
|
+
return {
|
|
35484
|
+
outcome: { kind: "permanent_delete_blocked", relPath, reason: createProtocol.reason },
|
|
35485
|
+
state
|
|
35486
|
+
};
|
|
35487
|
+
}
|
|
35488
|
+
if (createProtocol.kind === "finalized") {
|
|
35489
|
+
const parsed = FileLiveSchema.safeParse(createProtocol.live);
|
|
35490
|
+
if (!parsed.success) {
|
|
35491
|
+
throw new ApiError(200, "finalized file create operation live projection did not match the file schema");
|
|
35492
|
+
}
|
|
35493
|
+
if (parsed.data.path !== relPath) {
|
|
35494
|
+
throw new ApiError(200, "finalized file create operation live projection had the wrong path");
|
|
35495
|
+
}
|
|
35496
|
+
const adoptedState = persistFiles(state, {
|
|
35497
|
+
id: parsed.data.id,
|
|
35498
|
+
path: relPath,
|
|
35499
|
+
content_hash: parsed.data.content_hash,
|
|
35500
|
+
size_bytes: parsed.data.size_bytes,
|
|
35501
|
+
mime_type: parsed.data.mime_type,
|
|
35502
|
+
updated_at: parsed.data.updated_at,
|
|
35503
|
+
inode
|
|
35504
|
+
});
|
|
35505
|
+
return pushFilesUpsert({
|
|
35506
|
+
...args,
|
|
35507
|
+
state: adoptedState,
|
|
35508
|
+
prepared: { bytes, hash: hash2, size, inode }
|
|
35509
|
+
});
|
|
35510
|
+
}
|
|
34622
35511
|
const acceptPush = (row, action) => {
|
|
35512
|
+
if (createProtocol.kind === "authorised" && row.path !== relPath) {
|
|
35513
|
+
throw new ApiError(200, "generation-bearing file create response had the wrong path");
|
|
35514
|
+
}
|
|
35515
|
+
if (createProtocol.kind === "authorised") {
|
|
35516
|
+
updateCreateOperation({
|
|
35517
|
+
workspaceSlug,
|
|
35518
|
+
kind: "workspace_file",
|
|
35519
|
+
logicalKey: relPath,
|
|
35520
|
+
operationId: createProtocol.operationId,
|
|
35521
|
+
state: "finalized",
|
|
35522
|
+
rowId: row.id,
|
|
35523
|
+
now
|
|
35524
|
+
});
|
|
35525
|
+
}
|
|
34623
35526
|
output?.log(filePushLine({ path: relPath, id: row.id, action }));
|
|
34624
35527
|
return {
|
|
34625
35528
|
outcome: { kind: action, relPath, id: row.id },
|
|
@@ -34655,22 +35558,57 @@ async function pushFilesUpsert(args) {
|
|
|
34655
35558
|
output?.log(fileFailedLine({ path: relPath, status: 400, reason: "hash_mismatch" }));
|
|
34656
35559
|
return { outcome: { kind: "ignored", relPath, reason: "hash_mismatch" }, state };
|
|
34657
35560
|
};
|
|
34658
|
-
|
|
34659
|
-
|
|
34660
|
-
|
|
34661
|
-
|
|
34662
|
-
|
|
34663
|
-
|
|
34664
|
-
|
|
34665
|
-
|
|
34666
|
-
|
|
34667
|
-
|
|
35561
|
+
let res;
|
|
35562
|
+
try {
|
|
35563
|
+
res = await postFile({
|
|
35564
|
+
baseUrl,
|
|
35565
|
+
pat,
|
|
35566
|
+
relPath,
|
|
35567
|
+
bytes,
|
|
35568
|
+
contentHash: hash2,
|
|
35569
|
+
mimeType,
|
|
35570
|
+
expectedUpdatedAt: existing?.updated_at ?? null,
|
|
35571
|
+
...createProtocol.kind === "authorised" ? {
|
|
35572
|
+
retiredGeneration: createProtocol.retiredGeneration,
|
|
35573
|
+
operationId: createProtocol.operationId
|
|
35574
|
+
} : {},
|
|
35575
|
+
fetchImpl
|
|
35576
|
+
});
|
|
35577
|
+
} catch (error52) {
|
|
35578
|
+
if (createProtocol.kind === "authorised" && (!(error52 instanceof ApiError) || error52.status === 0 || error52.status >= 500 || error52.status >= 200 && error52.status < 300)) {
|
|
35579
|
+
updateCreateOperation({
|
|
35580
|
+
workspaceSlug,
|
|
35581
|
+
kind: "workspace_file",
|
|
35582
|
+
logicalKey: relPath,
|
|
35583
|
+
operationId: createProtocol.operationId,
|
|
35584
|
+
state: "transport_unknown",
|
|
35585
|
+
now
|
|
35586
|
+
});
|
|
35587
|
+
}
|
|
35588
|
+
throw error52;
|
|
35589
|
+
}
|
|
34668
35590
|
if (res.ok)
|
|
34669
35591
|
return acceptPush(res.row, existing ? "updated" : "created");
|
|
34670
35592
|
if (res.code === 413)
|
|
34671
35593
|
return rejectOverCap(res.maxBytes);
|
|
34672
35594
|
if (res.code === 400)
|
|
34673
35595
|
return rejectHashMismatch();
|
|
35596
|
+
if ("reason" in res && res.reason === "permanent_name_retired") {
|
|
35597
|
+
if (res.entityKind !== "workspace_file" || res.logicalKey !== relPath) {
|
|
35598
|
+
throw new ApiError(409, "permanent-name refusal identity did not match the file create");
|
|
35599
|
+
}
|
|
35600
|
+
requireFeedRefresh({
|
|
35601
|
+
workspaceSlug,
|
|
35602
|
+
kind: "workspace_file",
|
|
35603
|
+
logicalKey: res.logicalKey,
|
|
35604
|
+
now
|
|
35605
|
+
});
|
|
35606
|
+
output?.warn(`files/${res.logicalKey} was permanently deleted in Plinth; local bytes are held while the file feed is refreshed.`);
|
|
35607
|
+
return {
|
|
35608
|
+
outcome: { kind: "permanent_delete_blocked", relPath: res.logicalKey, reason: "refresh_required" },
|
|
35609
|
+
state
|
|
35610
|
+
};
|
|
35611
|
+
}
|
|
34674
35612
|
let nextState = state;
|
|
34675
35613
|
let cloudRow;
|
|
34676
35614
|
if (existing) {
|
|
@@ -34692,6 +35630,25 @@ async function pushFilesUpsert(args) {
|
|
|
34692
35630
|
return rejectOverCap(retry.maxBytes);
|
|
34693
35631
|
if (retry.code === 400)
|
|
34694
35632
|
return rejectHashMismatch();
|
|
35633
|
+
if ("reason" in retry && retry.reason === "permanent_name_retired") {
|
|
35634
|
+
if (retry.entityKind !== "workspace_file" || retry.logicalKey !== relPath) {
|
|
35635
|
+
throw new ApiError(409, "permanent-name refusal identity did not match the file retry");
|
|
35636
|
+
}
|
|
35637
|
+
requireFeedRefresh({
|
|
35638
|
+
workspaceSlug,
|
|
35639
|
+
kind: "workspace_file",
|
|
35640
|
+
logicalKey: retry.logicalKey,
|
|
35641
|
+
now
|
|
35642
|
+
});
|
|
35643
|
+
return {
|
|
35644
|
+
outcome: {
|
|
35645
|
+
kind: "permanent_delete_blocked",
|
|
35646
|
+
relPath: retry.logicalKey,
|
|
35647
|
+
reason: "refresh_required"
|
|
35648
|
+
},
|
|
35649
|
+
state
|
|
35650
|
+
};
|
|
35651
|
+
}
|
|
34695
35652
|
cloudRow = undefined;
|
|
34696
35653
|
}
|
|
34697
35654
|
}
|
|
@@ -34757,29 +35714,9 @@ async function pushFilesDelete(args) {
|
|
|
34757
35714
|
return { outcome: { kind: "conflict", relPath, stashPath: "" }, state: nextState };
|
|
34758
35715
|
}
|
|
34759
35716
|
|
|
34760
|
-
// src/lib/per-id-mutex.ts
|
|
34761
|
-
class PerIdMutex {
|
|
34762
|
-
chains = new Map;
|
|
34763
|
-
async run(key, fn) {
|
|
34764
|
-
const prior = this.chains.get(key) ?? Promise.resolve();
|
|
34765
|
-
const next = prior.then(fn, fn);
|
|
34766
|
-
this.chains.set(key, next);
|
|
34767
|
-
const cleanup = () => {
|
|
34768
|
-
if (this.chains.get(key) === next) {
|
|
34769
|
-
this.chains.delete(key);
|
|
34770
|
-
}
|
|
34771
|
-
};
|
|
34772
|
-
next.then(cleanup, cleanup);
|
|
34773
|
-
return next;
|
|
34774
|
-
}
|
|
34775
|
-
size() {
|
|
34776
|
-
return this.chains.size;
|
|
34777
|
-
}
|
|
34778
|
-
}
|
|
34779
|
-
|
|
34780
35717
|
// src/lib/push.ts
|
|
34781
35718
|
var import_gray_matter10 = __toESM(require_gray_matter(), 1);
|
|
34782
|
-
import { existsSync as
|
|
35719
|
+
import { existsSync as existsSync5, readFileSync as readFileSync19, statSync as statSync8 } from "node:fs";
|
|
34783
35720
|
import { basename as basename5, relative as relative5 } from "node:path";
|
|
34784
35721
|
|
|
34785
35722
|
// src/lib/frontmatter-id.ts
|
|
@@ -34829,7 +35766,7 @@ function frontmatterIdWriteIsSound(next, id, expectedBody) {
|
|
|
34829
35766
|
|
|
34830
35767
|
// src/lib/pull-write.ts
|
|
34831
35768
|
var import_gray_matter9 = __toESM(require_gray_matter(), 1);
|
|
34832
|
-
import { readFileSync as
|
|
35769
|
+
import { readFileSync as readFileSync18 } from "node:fs";
|
|
34833
35770
|
function computeMirrorHashes(strategy, content) {
|
|
34834
35771
|
const parsed = import_gray_matter9.default(content);
|
|
34835
35772
|
const fm = parsed.data ?? {};
|
|
@@ -34873,7 +35810,7 @@ function relocateForSlugChange(args) {
|
|
|
34873
35810
|
}
|
|
34874
35811
|
let raw = null;
|
|
34875
35812
|
try {
|
|
34876
|
-
raw =
|
|
35813
|
+
raw = readFileSync18(fromPath, "utf8");
|
|
34877
35814
|
} catch (err) {
|
|
34878
35815
|
const code = err?.code;
|
|
34879
35816
|
if (code === "ENOENT") {
|
|
@@ -34939,12 +35876,21 @@ function relocateForSlugChange(args) {
|
|
|
34939
35876
|
}
|
|
34940
35877
|
function writeMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, suppressor, pathSlug) {
|
|
34941
35878
|
const targetSlug = pathSlug ?? doc2.slug;
|
|
35879
|
+
const content = renderMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, targetSlug);
|
|
35880
|
+
if (suppressor) {
|
|
35881
|
+
const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
|
|
35882
|
+
suppressor.registerWrite(target, computeMirrorHashes(strategy, content));
|
|
35883
|
+
}
|
|
35884
|
+
writeMirrorFile(workspaceSlug, strategy.type, targetSlug, content);
|
|
35885
|
+
return content;
|
|
35886
|
+
}
|
|
35887
|
+
function renderMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, targetSlug) {
|
|
34942
35888
|
let content = strategy.serialise(doc2);
|
|
34943
35889
|
if (strategy.preserveLocalBodyOnPull) {
|
|
34944
35890
|
const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
|
|
34945
35891
|
let existing = null;
|
|
34946
35892
|
try {
|
|
34947
|
-
existing =
|
|
35893
|
+
existing = readFileSync18(target, "utf8");
|
|
34948
35894
|
} catch (err) {
|
|
34949
35895
|
if (err?.code !== "ENOENT")
|
|
34950
35896
|
throw err;
|
|
@@ -34958,11 +35904,6 @@ function writeMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, suppressor,
|
|
|
34958
35904
|
`;
|
|
34959
35905
|
}
|
|
34960
35906
|
}
|
|
34961
|
-
if (suppressor) {
|
|
34962
|
-
const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
|
|
34963
|
-
suppressor.registerWrite(target, computeMirrorHashes(strategy, content));
|
|
34964
|
-
}
|
|
34965
|
-
writeMirrorFile(workspaceSlug, strategy.type, targetSlug, content);
|
|
34966
35907
|
return content;
|
|
34967
35908
|
}
|
|
34968
35909
|
|
|
@@ -35076,7 +36017,7 @@ function stampStateFromMirror(args) {
|
|
|
35076
36017
|
const { strategy, workspaceSlug, doc: doc2, state, priorMemoryHash } = args;
|
|
35077
36018
|
const targetSlug = args.pathSlug ?? doc2.slug;
|
|
35078
36019
|
const mirrorPath = entityFilePath(workspaceSlug, strategy.type, targetSlug);
|
|
35079
|
-
const mirrorRaw =
|
|
36020
|
+
const mirrorRaw = readFileSync19(mirrorPath, "utf8");
|
|
35080
36021
|
const mirrorParsed = import_gray_matter10.default(mirrorRaw);
|
|
35081
36022
|
const mirrorFm = mirrorParsed.data ?? {};
|
|
35082
36023
|
const mirrorLifted = splitUserFrontmatter(strategy, mirrorFm);
|
|
@@ -35196,7 +36137,7 @@ async function pushFile(args) {
|
|
|
35196
36137
|
const identity = args.identity ?? "auto";
|
|
35197
36138
|
const fetchImpl = deps?.fetchImpl;
|
|
35198
36139
|
const now = deps?.now ?? (() => new Date);
|
|
35199
|
-
const
|
|
36140
|
+
const mutex2 = deps?.mutex ?? new PerIdMutex;
|
|
35200
36141
|
const output = deps?.output;
|
|
35201
36142
|
const suppressor = deps?.suppressor;
|
|
35202
36143
|
const echo = deps?.echo;
|
|
@@ -35222,14 +36163,14 @@ async function pushFile(args) {
|
|
|
35222
36163
|
absolutePath,
|
|
35223
36164
|
state,
|
|
35224
36165
|
unlinkContext,
|
|
35225
|
-
mutex,
|
|
36166
|
+
mutex: mutex2,
|
|
35226
36167
|
fetchImpl,
|
|
35227
36168
|
output
|
|
35228
36169
|
});
|
|
35229
36170
|
}
|
|
35230
36171
|
let raw;
|
|
35231
36172
|
try {
|
|
35232
|
-
raw =
|
|
36173
|
+
raw = readFileSync19(absolutePath, "utf8");
|
|
35233
36174
|
} catch (err) {
|
|
35234
36175
|
if (err?.code === "ENOENT") {
|
|
35235
36176
|
return runPushDelete({
|
|
@@ -35242,7 +36183,7 @@ async function pushFile(args) {
|
|
|
35242
36183
|
absolutePath,
|
|
35243
36184
|
state,
|
|
35244
36185
|
unlinkContext: null,
|
|
35245
|
-
mutex,
|
|
36186
|
+
mutex: mutex2,
|
|
35246
36187
|
fetchImpl,
|
|
35247
36188
|
output
|
|
35248
36189
|
});
|
|
@@ -35304,7 +36245,7 @@ async function pushFile(args) {
|
|
|
35304
36245
|
raw,
|
|
35305
36246
|
parsed,
|
|
35306
36247
|
state,
|
|
35307
|
-
mutex,
|
|
36248
|
+
mutex: mutex2,
|
|
35308
36249
|
fetchImpl,
|
|
35309
36250
|
now,
|
|
35310
36251
|
output,
|
|
@@ -35325,7 +36266,7 @@ async function pushFile(args) {
|
|
|
35325
36266
|
parsed,
|
|
35326
36267
|
stateEntry,
|
|
35327
36268
|
state,
|
|
35328
|
-
mutex,
|
|
36269
|
+
mutex: mutex2,
|
|
35329
36270
|
fetchImpl,
|
|
35330
36271
|
now,
|
|
35331
36272
|
output,
|
|
@@ -35579,7 +36520,7 @@ async function runPushUpsert(args) {
|
|
|
35579
36520
|
if (liveEntry !== snapshotEntry) {
|
|
35580
36521
|
let onDisk = null;
|
|
35581
36522
|
try {
|
|
35582
|
-
onDisk =
|
|
36523
|
+
onDisk = readFileSync19(args.absolutePath, "utf8");
|
|
35583
36524
|
} catch {
|
|
35584
36525
|
onDisk = null;
|
|
35585
36526
|
}
|
|
@@ -35788,6 +36729,28 @@ async function runPushUpsert(args) {
|
|
|
35788
36729
|
base: workingState
|
|
35789
36730
|
};
|
|
35790
36731
|
}
|
|
36732
|
+
if (patchResult.reason === "permanent_name_retired") {
|
|
36733
|
+
if (strategy.type !== "documents" || patchResult.entityKind !== "document" || patchResult.logicalKey !== args.stateEntry.slug) {
|
|
36734
|
+
throw new ApiError(409, "permanent-name refusal identity did not match the document update");
|
|
36735
|
+
}
|
|
36736
|
+
requireFeedRefresh({
|
|
36737
|
+
workspaceSlug: args.workspaceSlug,
|
|
36738
|
+
kind: "document",
|
|
36739
|
+
logicalKey: patchResult.logicalKey,
|
|
36740
|
+
now: args.now
|
|
36741
|
+
});
|
|
36742
|
+
args.output?.warn(`${strategy.type}/${patchResult.logicalKey} was permanently deleted in Plinth; local content is held while the document feed is refreshed.`);
|
|
36743
|
+
return {
|
|
36744
|
+
outcome: {
|
|
36745
|
+
kind: "permanent_delete_blocked",
|
|
36746
|
+
entityType: strategy.type,
|
|
36747
|
+
entitySlug: patchResult.logicalKey,
|
|
36748
|
+
reason: "refresh_required"
|
|
36749
|
+
},
|
|
36750
|
+
state: workingState,
|
|
36751
|
+
base: workingState
|
|
36752
|
+
};
|
|
36753
|
+
}
|
|
35791
36754
|
if (patchResult.reason === "slug_conflict") {
|
|
35792
36755
|
throw new Error(`patch ${strategy.type} returned slug_conflict for a body+frontmatter PATCH (no slug sent)`);
|
|
35793
36756
|
}
|
|
@@ -36019,7 +36982,7 @@ function writeCreatedIdBack(args) {
|
|
|
36019
36982
|
if (entityFilePath(args.workspaceSlug, args.strategy.type, args.slug) !== args.absolutePath) {
|
|
36020
36983
|
return;
|
|
36021
36984
|
}
|
|
36022
|
-
const readBack = args.readBack ?? ((p) =>
|
|
36985
|
+
const readBack = args.readBack ?? ((p) => readFileSync19(p, "utf8"));
|
|
36023
36986
|
if (readBack(args.absolutePath) !== args.raw)
|
|
36024
36987
|
return;
|
|
36025
36988
|
args.suppressor?.registerWrite(args.absolutePath, computeMirrorHashes(args.strategy, write.content));
|
|
@@ -36027,6 +36990,12 @@ function writeCreatedIdBack(args) {
|
|
|
36027
36990
|
} catch {}
|
|
36028
36991
|
}
|
|
36029
36992
|
async function runPushCreate(args) {
|
|
36993
|
+
if (args.strategy.type === "documents") {
|
|
36994
|
+
return withPermanentDeletionLock("document", args.slug, () => runPushCreateLocked(args));
|
|
36995
|
+
}
|
|
36996
|
+
return runPushCreateLocked(args);
|
|
36997
|
+
}
|
|
36998
|
+
async function runPushCreateLocked(args) {
|
|
36030
36999
|
const strategy = args.strategy;
|
|
36031
37000
|
const key = `create:${strategy.type}:${args.slug}`;
|
|
36032
37001
|
return args.mutex.run(key, async () => {
|
|
@@ -36064,15 +37033,108 @@ async function runPushCreate(args) {
|
|
|
36064
37033
|
else
|
|
36065
37034
|
payload.body = normalised;
|
|
36066
37035
|
}
|
|
36067
|
-
const
|
|
36068
|
-
|
|
37036
|
+
const createProtocol = strategy.type === "documents" ? await prepareOrResolveGenerationCreate({
|
|
37037
|
+
workspaceSlug: args.workspaceSlug,
|
|
36069
37038
|
baseUrl: args.baseUrl,
|
|
36070
37039
|
pat: args.pat,
|
|
36071
|
-
|
|
37040
|
+
kind: "document",
|
|
37041
|
+
logicalKey: args.slug,
|
|
37042
|
+
payloadDigest: canonicalPayloadDigest(payload),
|
|
37043
|
+
now: args.now,
|
|
36072
37044
|
fetchImpl: args.fetchImpl
|
|
36073
|
-
});
|
|
37045
|
+
}) : { kind: "ordinary" };
|
|
37046
|
+
if (createProtocol.kind === "blocked") {
|
|
37047
|
+
args.output?.warn(`${strategy.type}/${args.slug} is held by permanent-delete reconciliation (${createProtocol.reason}).`);
|
|
37048
|
+
return {
|
|
37049
|
+
outcome: {
|
|
37050
|
+
kind: "permanent_delete_blocked",
|
|
37051
|
+
entityType: strategy.type,
|
|
37052
|
+
entitySlug: args.slug,
|
|
37053
|
+
reason: createProtocol.reason
|
|
37054
|
+
},
|
|
37055
|
+
state: args.state,
|
|
37056
|
+
base: args.state
|
|
37057
|
+
};
|
|
37058
|
+
}
|
|
37059
|
+
if (createProtocol.kind === "finalized") {
|
|
37060
|
+
const parsedLive = strategy.singleResponseSchema.safeParse(createProtocol.live);
|
|
37061
|
+
if (!parsedLive.success) {
|
|
37062
|
+
throw new ApiError(200, "finalized create operation live projection did not match the entity schema");
|
|
37063
|
+
}
|
|
37064
|
+
if (parsedLive.data.slug !== args.slug) {
|
|
37065
|
+
throw new ApiError(200, "finalized create operation live projection had the wrong slug");
|
|
37066
|
+
}
|
|
37067
|
+
const acknowledged = computeMirrorHashes(strategy, strategy.serialise(parsedLive.data));
|
|
37068
|
+
const adoptedEntry = {
|
|
37069
|
+
id: parsedLive.data.id,
|
|
37070
|
+
slug: parsedLive.data.slug,
|
|
37071
|
+
updated_at: parsedLive.data.updated_at,
|
|
37072
|
+
revision: parsedLive.data.revision,
|
|
37073
|
+
body_hash: acknowledged.bodyHash,
|
|
37074
|
+
frontmatter_hash: acknowledged.frontmatterHash,
|
|
37075
|
+
inode: safeInode2(args.absolutePath)
|
|
37076
|
+
};
|
|
37077
|
+
const adoptedState = persistEntry(args.state, strategy.type, adoptedEntry);
|
|
37078
|
+
return runPushUpsert({
|
|
37079
|
+
echo: args.echo,
|
|
37080
|
+
strategy,
|
|
37081
|
+
workspaceSlug: args.workspaceSlug,
|
|
37082
|
+
baseUrl: args.baseUrl,
|
|
37083
|
+
pat: args.pat,
|
|
37084
|
+
absolutePath: args.absolutePath,
|
|
37085
|
+
raw: args.raw,
|
|
37086
|
+
parsed: args.parsed,
|
|
37087
|
+
stateEntry: adoptedEntry,
|
|
37088
|
+
state: adoptedState,
|
|
37089
|
+
mutex: args.mutex,
|
|
37090
|
+
fetchImpl: args.fetchImpl,
|
|
37091
|
+
now: args.now,
|
|
37092
|
+
output: args.output,
|
|
37093
|
+
quarantinedKeys: args.quarantinedKeys
|
|
37094
|
+
});
|
|
37095
|
+
}
|
|
37096
|
+
let createResult;
|
|
37097
|
+
try {
|
|
37098
|
+
createResult = await createEntity({
|
|
37099
|
+
strategy,
|
|
37100
|
+
baseUrl: args.baseUrl,
|
|
37101
|
+
pat: args.pat,
|
|
37102
|
+
payload,
|
|
37103
|
+
...createProtocol.kind === "authorised" ? {
|
|
37104
|
+
retiredGeneration: createProtocol.retiredGeneration,
|
|
37105
|
+
operationId: createProtocol.operationId
|
|
37106
|
+
} : {},
|
|
37107
|
+
fetchImpl: args.fetchImpl
|
|
37108
|
+
});
|
|
37109
|
+
} catch (error52) {
|
|
37110
|
+
if (createProtocol.kind === "authorised" && (!(error52 instanceof ApiError) || error52.status === 0 || error52.status >= 500 || error52.status >= 200 && error52.status < 300)) {
|
|
37111
|
+
updateCreateOperation({
|
|
37112
|
+
workspaceSlug: args.workspaceSlug,
|
|
37113
|
+
kind: "document",
|
|
37114
|
+
logicalKey: args.slug,
|
|
37115
|
+
operationId: createProtocol.operationId,
|
|
37116
|
+
state: "transport_unknown",
|
|
37117
|
+
now: args.now
|
|
37118
|
+
});
|
|
37119
|
+
}
|
|
37120
|
+
throw error52;
|
|
37121
|
+
}
|
|
36074
37122
|
if (createResult.ok) {
|
|
36075
37123
|
const doc2 = createResult.doc;
|
|
37124
|
+
if (createProtocol.kind === "authorised" && doc2.slug !== args.slug) {
|
|
37125
|
+
throw new ApiError(200, "generation-bearing create response had the wrong slug");
|
|
37126
|
+
}
|
|
37127
|
+
if (createProtocol.kind === "authorised") {
|
|
37128
|
+
updateCreateOperation({
|
|
37129
|
+
workspaceSlug: args.workspaceSlug,
|
|
37130
|
+
kind: "document",
|
|
37131
|
+
logicalKey: args.slug,
|
|
37132
|
+
operationId: createProtocol.operationId,
|
|
37133
|
+
state: "finalized",
|
|
37134
|
+
rowId: doc2.id,
|
|
37135
|
+
now: args.now
|
|
37136
|
+
});
|
|
37137
|
+
}
|
|
36076
37138
|
const createFm = args.parsed.data ?? {};
|
|
36077
37139
|
const createLifted = splitUserFrontmatter(strategy, createFm);
|
|
36078
37140
|
const userFm = userFrontmatter(strategy, createLifted.frontmatter);
|
|
@@ -36130,6 +37192,27 @@ async function runPushCreate(args) {
|
|
|
36130
37192
|
base: args.state
|
|
36131
37193
|
};
|
|
36132
37194
|
}
|
|
37195
|
+
if (createResult.reason === "permanent_name_retired") {
|
|
37196
|
+
if (strategy.type !== "documents" || createResult.entityKind !== "document" || createResult.logicalKey !== args.slug) {
|
|
37197
|
+
throw new ApiError(409, "permanent-name refusal identity did not match the document create");
|
|
37198
|
+
}
|
|
37199
|
+
requireFeedRefresh({
|
|
37200
|
+
workspaceSlug: args.workspaceSlug,
|
|
37201
|
+
kind: "document",
|
|
37202
|
+
logicalKey: createResult.logicalKey,
|
|
37203
|
+
now: args.now
|
|
37204
|
+
});
|
|
37205
|
+
return {
|
|
37206
|
+
outcome: {
|
|
37207
|
+
kind: "permanent_delete_blocked",
|
|
37208
|
+
entityType: strategy.type,
|
|
37209
|
+
entitySlug: createResult.logicalKey,
|
|
37210
|
+
reason: "refresh_required"
|
|
37211
|
+
},
|
|
37212
|
+
state: args.state,
|
|
37213
|
+
base: args.state
|
|
37214
|
+
};
|
|
37215
|
+
}
|
|
36133
37216
|
if (createResult.reason === "context_doc_conflict") {
|
|
36134
37217
|
return reconcileContextDocConflict({
|
|
36135
37218
|
echo: args.echo,
|
|
@@ -36218,7 +37301,7 @@ async function runPushDelete(args) {
|
|
|
36218
37301
|
return args.mutex.run(id, async () => {
|
|
36219
37302
|
const snapshot = getEntry(args.state, strategy.type, id) ?? null;
|
|
36220
37303
|
const live = snapshot ? committedEntryIfNewer(args.workspaceSlug, strategy.type, snapshot) ?? snapshot : null;
|
|
36221
|
-
if (
|
|
37304
|
+
if (existsSync5(args.absolutePath)) {
|
|
36222
37305
|
args.output?.log(entityDeleteSuppressedLine({
|
|
36223
37306
|
entityType: strategy.type,
|
|
36224
37307
|
slug,
|
|
@@ -36310,7 +37393,7 @@ async function pushRename(args) {
|
|
|
36310
37393
|
} = args;
|
|
36311
37394
|
const fetchImpl = deps?.fetchImpl;
|
|
36312
37395
|
const now = deps?.now ?? (() => new Date);
|
|
36313
|
-
const
|
|
37396
|
+
const mutex2 = deps?.mutex ?? new PerIdMutex;
|
|
36314
37397
|
const output = deps?.output;
|
|
36315
37398
|
const echo = deps?.echo;
|
|
36316
37399
|
const strategy = requireStrategy(entityType);
|
|
@@ -36321,7 +37404,7 @@ async function pushRename(args) {
|
|
|
36321
37404
|
} catch {
|
|
36322
37405
|
toSlug = basename5(toPath, ".md");
|
|
36323
37406
|
}
|
|
36324
|
-
return
|
|
37407
|
+
return mutex2.run(id, async () => {
|
|
36325
37408
|
const snapshot = getEntry(state, entityType, id);
|
|
36326
37409
|
const live = snapshot ? committedEntryIfNewer(workspaceSlug, entityType, snapshot) ?? snapshot : undefined;
|
|
36327
37410
|
const effectiveExpected = live?.updated_at ?? expectedUpdatedAt;
|
|
@@ -36371,7 +37454,7 @@ async function pushRename(args) {
|
|
|
36371
37454
|
};
|
|
36372
37455
|
if (!updated.body_hash || !updated.frontmatter_hash) {
|
|
36373
37456
|
try {
|
|
36374
|
-
const mirrorRaw =
|
|
37457
|
+
const mirrorRaw = readFileSync19(toPath, "utf8");
|
|
36375
37458
|
const mirrorParsed = import_gray_matter10.default(mirrorRaw);
|
|
36376
37459
|
const renameFm = mirrorParsed.data ?? {};
|
|
36377
37460
|
const renameLifted = splitUserFrontmatter(strategy, renameFm);
|
|
@@ -36408,7 +37491,7 @@ async function pushRename(args) {
|
|
|
36408
37491
|
if (patchResult.reason === "slug_conflict") {
|
|
36409
37492
|
let contents2 = "";
|
|
36410
37493
|
try {
|
|
36411
|
-
contents2 =
|
|
37494
|
+
contents2 = readFileSync19(toPath, "utf8");
|
|
36412
37495
|
} catch {}
|
|
36413
37496
|
const stashPath2 = stashConflict({
|
|
36414
37497
|
workspaceSlug,
|
|
@@ -36456,7 +37539,7 @@ async function pushRename(args) {
|
|
|
36456
37539
|
}
|
|
36457
37540
|
let contents = "";
|
|
36458
37541
|
try {
|
|
36459
|
-
contents =
|
|
37542
|
+
contents = readFileSync19(toPath, "utf8");
|
|
36460
37543
|
} catch {}
|
|
36461
37544
|
const stashPath = stashConflict({
|
|
36462
37545
|
workspaceSlug,
|
|
@@ -36493,14 +37576,14 @@ async function pushRename(args) {
|
|
|
36493
37576
|
|
|
36494
37577
|
// src/lib/resolve-pending.ts
|
|
36495
37578
|
async function drainResolvedBatches(deps) {
|
|
36496
|
-
const
|
|
37579
|
+
const mutex2 = deps.mutex ?? new PerIdMutex;
|
|
36497
37580
|
let applied = 0;
|
|
36498
37581
|
let discarded = 0;
|
|
36499
37582
|
for (const batch of listPendingBatches(deps.workspaceSlug)) {
|
|
36500
37583
|
if (batch.status === "pending")
|
|
36501
37584
|
continue;
|
|
36502
37585
|
if (batch.status === "approved") {
|
|
36503
|
-
await applyBatch(batch, deps,
|
|
37586
|
+
await applyBatch(batch, deps, mutex2);
|
|
36504
37587
|
deps.output?.log(`[DESTRUCTIVE_APPLIED batch=${batch.batch_id} ops=${batch.ops.length}]`);
|
|
36505
37588
|
applied++;
|
|
36506
37589
|
} else {
|
|
@@ -36515,7 +37598,7 @@ async function drainResolvedBatches(deps) {
|
|
|
36515
37598
|
function opLabel(op) {
|
|
36516
37599
|
return op.target === "file" ? `files/${op.path}` : `${op.entityType}/${op.slug}`;
|
|
36517
37600
|
}
|
|
36518
|
-
async function applyBatch(batch, deps,
|
|
37601
|
+
async function applyBatch(batch, deps, mutex2) {
|
|
36519
37602
|
for (const op of batch.ops) {
|
|
36520
37603
|
try {
|
|
36521
37604
|
if (op.target === "file") {
|
|
@@ -36535,7 +37618,7 @@ async function applyBatch(batch, deps, mutex) {
|
|
|
36535
37618
|
deps: {
|
|
36536
37619
|
fetchImpl: deps.fetchImpl,
|
|
36537
37620
|
now: deps.now,
|
|
36538
|
-
mutex,
|
|
37621
|
+
mutex: mutex2,
|
|
36539
37622
|
output: deps.output,
|
|
36540
37623
|
echo: deps.echo
|
|
36541
37624
|
}
|
|
@@ -36589,7 +37672,7 @@ async function discardBatchOps(batch, deps) {
|
|
|
36589
37672
|
const path5 = opPath(deps.workspaceSlug, op);
|
|
36590
37673
|
let local = null;
|
|
36591
37674
|
try {
|
|
36592
|
-
local =
|
|
37675
|
+
local = readFileSync20(path5, "utf8");
|
|
36593
37676
|
} catch {
|
|
36594
37677
|
local = null;
|
|
36595
37678
|
}
|
|
@@ -36628,7 +37711,7 @@ async function discardFileOp(op, deps) {
|
|
|
36628
37711
|
const abs = filesSubtreePath(deps.workspaceSlug, op.path);
|
|
36629
37712
|
let local = null;
|
|
36630
37713
|
try {
|
|
36631
|
-
local =
|
|
37714
|
+
local = readFileSync20(abs);
|
|
36632
37715
|
} catch {
|
|
36633
37716
|
local = null;
|
|
36634
37717
|
}
|
|
@@ -37597,10 +38680,10 @@ class FileDestructiveGate {
|
|
|
37597
38680
|
}
|
|
37598
38681
|
|
|
37599
38682
|
// src/lib/files/reconcile.ts
|
|
37600
|
-
import { readFileSync as
|
|
38683
|
+
import { readFileSync as readFileSync21, statSync as statSync10 } from "node:fs";
|
|
37601
38684
|
|
|
37602
38685
|
// src/lib/unsynced.ts
|
|
37603
|
-
import { join as
|
|
38686
|
+
import { join as join14, relative as relative8 } from "node:path";
|
|
37604
38687
|
var RECORDABLE_REASONS = new Set([
|
|
37605
38688
|
"rate_limit",
|
|
37606
38689
|
"server",
|
|
@@ -37645,7 +38728,7 @@ function recordFileUnsynced(args) {
|
|
|
37645
38728
|
});
|
|
37646
38729
|
}
|
|
37647
38730
|
async function drainUnsyncedOps(args) {
|
|
37648
|
-
const { workspaceSlug, baseUrl, pat, getState, applyState, fetchImpl, mutex, output, echo } = args;
|
|
38731
|
+
const { workspaceSlug, baseUrl, pat, getState, applyState, fetchImpl, mutex: mutex2, output, echo } = args;
|
|
37649
38732
|
const now = args.now ?? (() => new Date);
|
|
37650
38733
|
const ops = listUnsynced(getState());
|
|
37651
38734
|
let attempted = 0;
|
|
@@ -37662,7 +38745,7 @@ async function drainUnsyncedOps(args) {
|
|
|
37662
38745
|
attempted += 1;
|
|
37663
38746
|
try {
|
|
37664
38747
|
if (op.target === "entity") {
|
|
37665
|
-
const absolutePath =
|
|
38748
|
+
const absolutePath = join14(workspaceMirrorPath(workspaceSlug), op.path);
|
|
37666
38749
|
const classified = classifyMirrorPath({ workspaceSlug, absolutePath });
|
|
37667
38750
|
if (!classified) {
|
|
37668
38751
|
clear("entity", op.path);
|
|
@@ -37679,7 +38762,7 @@ async function drainUnsyncedOps(args) {
|
|
|
37679
38762
|
state: base2,
|
|
37680
38763
|
exists: op.op === "upsert",
|
|
37681
38764
|
unlinkContext: null,
|
|
37682
|
-
deps: { fetchImpl, now, mutex, output, echo }
|
|
38765
|
+
deps: { fetchImpl, now, mutex: mutex2, output, echo }
|
|
37683
38766
|
});
|
|
37684
38767
|
applyState(result.base ?? base2, result.state);
|
|
37685
38768
|
if (result.outcome.kind === "ignored" && result.outcome.reason === DELETE_DECLINED_PATH_EXISTS) {
|
|
@@ -37707,7 +38790,7 @@ async function drainUnsyncedOps(args) {
|
|
|
37707
38790
|
deps: { fetchImpl, now, output }
|
|
37708
38791
|
});
|
|
37709
38792
|
};
|
|
37710
|
-
const result =
|
|
38793
|
+
const result = mutex2 ? await mutex2.run(`files:${op.path}`, doFile) : await doFile();
|
|
37711
38794
|
applyState(base2, result.state);
|
|
37712
38795
|
}
|
|
37713
38796
|
clear(op.target, op.path);
|
|
@@ -38475,9 +39558,9 @@ class NodeFsHandler {
|
|
|
38475
39558
|
if (this.fsw.closed) {
|
|
38476
39559
|
return;
|
|
38477
39560
|
}
|
|
38478
|
-
const
|
|
39561
|
+
const dirname12 = sp.dirname(file2);
|
|
38479
39562
|
const basename7 = sp.basename(file2);
|
|
38480
|
-
const parent = this.fsw._getWatchedDir(
|
|
39563
|
+
const parent = this.fsw._getWatchedDir(dirname12);
|
|
38481
39564
|
let prevStats = stats;
|
|
38482
39565
|
if (parent.has(basename7))
|
|
38483
39566
|
return;
|
|
@@ -38504,7 +39587,7 @@ class NodeFsHandler {
|
|
|
38504
39587
|
prevStats = newStats2;
|
|
38505
39588
|
}
|
|
38506
39589
|
} catch (error52) {
|
|
38507
|
-
this.fsw._remove(
|
|
39590
|
+
this.fsw._remove(dirname12, basename7);
|
|
38508
39591
|
}
|
|
38509
39592
|
} else if (parent.has(basename7)) {
|
|
38510
39593
|
const at = newStats.atimeMs;
|
|
@@ -39631,7 +40714,7 @@ function readBlobForPush(args) {
|
|
|
39631
40714
|
}
|
|
39632
40715
|
let bytes;
|
|
39633
40716
|
try {
|
|
39634
|
-
bytes =
|
|
40717
|
+
bytes = readFileSync21(absolutePath);
|
|
39635
40718
|
} catch (err) {
|
|
39636
40719
|
if (err?.code === "ENOENT")
|
|
39637
40720
|
return { kind: "gone" };
|
|
@@ -39941,7 +41024,7 @@ function applyArtistMemoryOnPull(args) {
|
|
|
39941
41024
|
const cloudMemory = live.memory;
|
|
39942
41025
|
const generated = live.memory_generated === true;
|
|
39943
41026
|
const local = readArtistMemoryFile(workspaceSlug, slug);
|
|
39944
|
-
const refuse = (localContent,
|
|
41027
|
+
const refuse = (localContent, canonical2) => {
|
|
39945
41028
|
const refused = refuseGeneratedMemoryEdit({
|
|
39946
41029
|
state,
|
|
39947
41030
|
workspaceSlug,
|
|
@@ -39951,7 +41034,7 @@ function applyArtistMemoryOnPull(args) {
|
|
|
39951
41034
|
conflictEntity: ENTITY_TYPE,
|
|
39952
41035
|
conflictSlug: conflictSlug(slug),
|
|
39953
41036
|
localContent,
|
|
39954
|
-
canonicalBody:
|
|
41037
|
+
canonicalBody: canonical2,
|
|
39955
41038
|
writeCanonical: (body) => writeMemory(workspaceSlug, slug, body, suppressor),
|
|
39956
41039
|
now,
|
|
39957
41040
|
output: out
|
|
@@ -40055,7 +41138,7 @@ async function pushArtistMemory(args) {
|
|
|
40055
41138
|
id: entry2.id,
|
|
40056
41139
|
fetchImpl
|
|
40057
41140
|
});
|
|
40058
|
-
const
|
|
41141
|
+
const canonical2 = row.memory ?? "";
|
|
40059
41142
|
const refused = refuseGeneratedMemoryEdit({
|
|
40060
41143
|
state: base2,
|
|
40061
41144
|
workspaceSlug,
|
|
@@ -40065,7 +41148,7 @@ async function pushArtistMemory(args) {
|
|
|
40065
41148
|
conflictEntity: ENTITY_TYPE,
|
|
40066
41149
|
conflictSlug: conflictSlug(slug),
|
|
40067
41150
|
localContent: local,
|
|
40068
|
-
canonicalBody:
|
|
41151
|
+
canonicalBody: canonical2,
|
|
40069
41152
|
writeCanonical: (body) => writeMemory(workspaceSlug, slug, body, suppressor),
|
|
40070
41153
|
now,
|
|
40071
41154
|
output
|
|
@@ -40074,7 +41157,7 @@ async function pushArtistMemory(args) {
|
|
|
40074
41157
|
...entry2,
|
|
40075
41158
|
updated_at: row.updated_at,
|
|
40076
41159
|
revision: row.revision,
|
|
40077
|
-
memory_hash: stableHash(
|
|
41160
|
+
memory_hash: stableHash(canonical2),
|
|
40078
41161
|
memory_generated: row.memory_generated
|
|
40079
41162
|
};
|
|
40080
41163
|
return {
|
|
@@ -40170,7 +41253,7 @@ function adoptCloudMemory(args) {
|
|
|
40170
41253
|
return persistEntry2(state, updated);
|
|
40171
41254
|
}
|
|
40172
41255
|
async function reconcileArtistMemory(args) {
|
|
40173
|
-
const { workspaceSlug, baseUrl, pat, fetchImpl, output, suppressor, mutex } = args;
|
|
41256
|
+
const { workspaceSlug, baseUrl, pat, fetchImpl, output, suppressor, mutex: mutex2 } = args;
|
|
40174
41257
|
const now = args.now ?? (() => new Date);
|
|
40175
41258
|
let state = args.state;
|
|
40176
41259
|
let transitioned = 0;
|
|
@@ -40212,7 +41295,7 @@ async function reconcileArtistMemory(args) {
|
|
|
40212
41295
|
now,
|
|
40213
41296
|
output,
|
|
40214
41297
|
suppressor,
|
|
40215
|
-
mutex
|
|
41298
|
+
mutex: mutex2
|
|
40216
41299
|
});
|
|
40217
41300
|
state = result.state;
|
|
40218
41301
|
if (result.outcome.kind === "pushed")
|
|
@@ -40692,10 +41775,10 @@ function anyUnknown(state) {
|
|
|
40692
41775
|
}
|
|
40693
41776
|
|
|
40694
41777
|
// src/lib/memory/write.ts
|
|
40695
|
-
import { readFileSync as
|
|
41778
|
+
import { readFileSync as readFileSync22, statSync as statSync11 } from "node:fs";
|
|
40696
41779
|
|
|
40697
41780
|
// src/lib/files/extracted-text.ts
|
|
40698
|
-
import { createHash as
|
|
41781
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
40699
41782
|
var DERIVED_TEXT_VERSION = 2;
|
|
40700
41783
|
function yamlString3(value) {
|
|
40701
41784
|
return JSON.stringify(value);
|
|
@@ -40769,7 +41852,7 @@ ${doc2.text}
|
|
|
40769
41852
|
`;
|
|
40770
41853
|
}
|
|
40771
41854
|
function extractedTextHash(rendered) {
|
|
40772
|
-
return
|
|
41855
|
+
return createHash5("sha256").update(rendered, "utf8").digest("hex");
|
|
40773
41856
|
}
|
|
40774
41857
|
|
|
40775
41858
|
// src/lib/memory/splice.ts
|
|
@@ -40887,7 +41970,7 @@ function writeDocumentAnnotation(args) {
|
|
|
40887
41970
|
}
|
|
40888
41971
|
let current;
|
|
40889
41972
|
try {
|
|
40890
|
-
current =
|
|
41973
|
+
current = readFileSync22(target, "utf8");
|
|
40891
41974
|
} catch {
|
|
40892
41975
|
return { kind: "absent" };
|
|
40893
41976
|
}
|
|
@@ -42619,7 +43702,7 @@ class PullSuppressor {
|
|
|
42619
43702
|
|
|
42620
43703
|
// src/lib/discover-entities.ts
|
|
42621
43704
|
var import_gray_matter12 = __toESM(require_gray_matter(), 1);
|
|
42622
|
-
import { readFileSync as
|
|
43705
|
+
import { readFileSync as readFileSync23, statSync as statSync12 } from "node:fs";
|
|
42623
43706
|
var DISCOVERY_WRITE_CAP_DEFAULT = 10;
|
|
42624
43707
|
function discoveryWriteCap() {
|
|
42625
43708
|
const raw = process.env.PLINTH_DISCOVERY_WRITE_CAP;
|
|
@@ -42718,7 +43801,7 @@ function planDiscoveryWrites(args) {
|
|
|
42718
43801
|
counts.discovered++;
|
|
42719
43802
|
let frontmatter;
|
|
42720
43803
|
try {
|
|
42721
|
-
frontmatter = import_gray_matter12.default(
|
|
43804
|
+
frontmatter = import_gray_matter12.default(readFileSync23(absolutePath, "utf8")).data ?? {};
|
|
42722
43805
|
} catch {
|
|
42723
43806
|
counts.discovered--;
|
|
42724
43807
|
continue;
|
|
@@ -42947,7 +44030,7 @@ function recordDiscoveryFailure(args) {
|
|
|
42947
44030
|
|
|
42948
44031
|
// src/lib/frontmatter-id-backfill.ts
|
|
42949
44032
|
var import_gray_matter13 = __toESM(require_gray_matter(), 1);
|
|
42950
|
-
import { readFileSync as
|
|
44033
|
+
import { readFileSync as readFileSync24, statSync as statSync13 } from "node:fs";
|
|
42951
44034
|
var FRONTMATTER_ID_BACKFILL_VERSION = 1;
|
|
42952
44035
|
function backfillWorthReporting(r) {
|
|
42953
44036
|
return r.written > 0 || r.failed > 0 || r.skipped.foreignId > 0 || r.skipped.unwritable > 0 || r.skipped.slugCollision > 0;
|
|
@@ -42990,7 +44073,7 @@ function safeInode3(path5) {
|
|
|
42990
44073
|
function backfillFrontmatterIds(args) {
|
|
42991
44074
|
const { workspaceSlug, state } = args;
|
|
42992
44075
|
const compose = args.compose ?? withFrontmatterId;
|
|
42993
|
-
const readRaw = args.readFile ?? ((p) =>
|
|
44076
|
+
const readRaw = args.readFile ?? ((p) => readFileSync24(p, "utf8"));
|
|
42994
44077
|
const result = emptyResult();
|
|
42995
44078
|
if (state.frontmatter_id_backfill_version === FRONTMATTER_ID_BACKFILL_VERSION) {
|
|
42996
44079
|
return result;
|
|
@@ -43118,7 +44201,7 @@ function backfillFrontmatterIds(args) {
|
|
|
43118
44201
|
}
|
|
43119
44202
|
|
|
43120
44203
|
// src/lib/reconcile.ts
|
|
43121
|
-
import { readFileSync as
|
|
44204
|
+
import { readFileSync as readFileSync25, statSync as statSync14 } from "node:fs";
|
|
43122
44205
|
async function reconcileDivergedRows(args) {
|
|
43123
44206
|
const { workspaceSlug, baseUrl, pat, fetchImpl, output, isFrozen } = args;
|
|
43124
44207
|
const now = args.now ?? (() => new Date);
|
|
@@ -43230,7 +44313,7 @@ async function reconcileDivergedRows(args) {
|
|
|
43230
44313
|
}
|
|
43231
44314
|
let content;
|
|
43232
44315
|
try {
|
|
43233
|
-
content =
|
|
44316
|
+
content = readFileSync25(absolutePath, "utf8");
|
|
43234
44317
|
} catch (err) {
|
|
43235
44318
|
if (err?.code === "ENOENT") {
|
|
43236
44319
|
state = clearEntityCloudRowDeleted(state, strategy.mirrorRelativePath(entry2.slug));
|
|
@@ -43331,7 +44414,7 @@ async function reconcileDivergedRows(args) {
|
|
|
43331
44414
|
}
|
|
43332
44415
|
|
|
43333
44416
|
// src/lib/workspace-rules/sync.ts
|
|
43334
|
-
import { existsSync as
|
|
44417
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
43335
44418
|
import { relative as relative12 } from "node:path";
|
|
43336
44419
|
|
|
43337
44420
|
// src/lib/workspace-rules/api.ts
|
|
@@ -43480,9 +44563,9 @@ function migrateHouseStyleFile(workspaceSlug) {
|
|
|
43480
44563
|
return false;
|
|
43481
44564
|
throw err;
|
|
43482
44565
|
}
|
|
43483
|
-
if (
|
|
44566
|
+
if (existsSync6(target))
|
|
43484
44567
|
return false;
|
|
43485
|
-
if (!
|
|
44568
|
+
if (!existsSync6(legacy))
|
|
43486
44569
|
return false;
|
|
43487
44570
|
return renameContextFile(workspaceSlug, HOUSE_STYLE_LEGACY_FILENAME, WORKSPACE_RULES_FILENAME);
|
|
43488
44571
|
}
|
|
@@ -44580,17 +45663,18 @@ function createThrottledFetch(baseFetch, opts = {}) {
|
|
|
44580
45663
|
|
|
44581
45664
|
// src/commands/sync.ts
|
|
44582
45665
|
import {
|
|
44583
|
-
existsSync as
|
|
45666
|
+
existsSync as existsSync9,
|
|
45667
|
+
lstatSync as lstatSync6,
|
|
44584
45668
|
readdirSync as readdirSync7,
|
|
44585
|
-
readFileSync as
|
|
45669
|
+
readFileSync as readFileSync29,
|
|
44586
45670
|
statSync as statSync17,
|
|
44587
|
-
unlinkSync as
|
|
45671
|
+
unlinkSync as unlinkSync9
|
|
44588
45672
|
} from "node:fs";
|
|
44589
|
-
import { join as
|
|
45673
|
+
import { join as join18 } from "node:path";
|
|
44590
45674
|
|
|
44591
45675
|
// src/lib/conflicts-view.ts
|
|
44592
|
-
import { existsSync as
|
|
44593
|
-
import { join as
|
|
45676
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
45677
|
+
import { join as join17 } from "node:path";
|
|
44594
45678
|
var NO_GENERATED_MEMORY = {
|
|
44595
45679
|
artistMemoryGenerated: () => false,
|
|
44596
45680
|
workspaceMemoryGenerated: () => false,
|
|
@@ -44636,7 +45720,7 @@ function resolveConflictRows(workspaceSlug, stashes, generatedMemory = NO_GENERA
|
|
|
44636
45720
|
if (validateFilesRelPath(rel) === null) {
|
|
44637
45721
|
return { kind: "unsafe-path", stash };
|
|
44638
45722
|
}
|
|
44639
|
-
const abs =
|
|
45723
|
+
const abs = join17(root, "files", rel);
|
|
44640
45724
|
if (hasSymlinkedComponent(workspaceSlug, abs)) {
|
|
44641
45725
|
return { kind: "unsafe-path", stash };
|
|
44642
45726
|
}
|
|
@@ -44644,7 +45728,7 @@ function resolveConflictRows(workspaceSlug, stashes, generatedMemory = NO_GENERA
|
|
|
44644
45728
|
kind: "restorable",
|
|
44645
45729
|
stash,
|
|
44646
45730
|
mirrorPath: abs,
|
|
44647
|
-
mirrorPathExists:
|
|
45731
|
+
mirrorPathExists: existsSync7(abs)
|
|
44648
45732
|
};
|
|
44649
45733
|
});
|
|
44650
45734
|
}
|
|
@@ -44766,9 +45850,377 @@ function refreshConflictsView(workspaceSlug) {
|
|
|
44766
45850
|
}
|
|
44767
45851
|
|
|
44768
45852
|
// src/lib/files/pull.ts
|
|
44769
|
-
import { existsSync as
|
|
44770
|
-
import { dirname as
|
|
45853
|
+
import { existsSync as existsSync8, lstatSync as lstatSync5, readFileSync as readFileSync27, statSync as statSync15 } from "node:fs";
|
|
45854
|
+
import { dirname as dirname13 } from "node:path";
|
|
45855
|
+
|
|
45856
|
+
// src/lib/permanent-disposition.ts
|
|
45857
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
45858
|
+
import { readFileSync as readFileSync26 } from "node:fs";
|
|
45859
|
+
function sha256(value) {
|
|
45860
|
+
return createHash6("sha256").update(value).digest("hex");
|
|
45861
|
+
}
|
|
45862
|
+
function verifyStash(path5, expectedHash) {
|
|
45863
|
+
const actual = sha256(readFileSync26(path5));
|
|
45864
|
+
if (actual !== expectedHash) {
|
|
45865
|
+
throw new Error(`permanent deletion stash verification failed for ${path5}`);
|
|
45866
|
+
}
|
|
45867
|
+
}
|
|
45868
|
+
function disposePermanentDocument(args) {
|
|
45869
|
+
return withPermanentDeletionLock("document", args.tombstone.logicalKey, async () => disposePermanentDocumentLocked(args));
|
|
45870
|
+
}
|
|
45871
|
+
function disposePermanentDocumentLocked(args) {
|
|
45872
|
+
const now = args.now ?? (() => new Date);
|
|
45873
|
+
const t = args.tombstone;
|
|
45874
|
+
recordRetirementPending({
|
|
45875
|
+
workspaceSlug: args.workspaceSlug,
|
|
45876
|
+
kind: "document",
|
|
45877
|
+
logicalKey: t.logicalKey,
|
|
45878
|
+
rowId: t.id,
|
|
45879
|
+
purgeGeneration: t.purgeGeneration,
|
|
45880
|
+
retirementSequence: t.retirementSequence,
|
|
45881
|
+
retiredAt: t.deletedAt
|
|
45882
|
+
});
|
|
45883
|
+
let journal = loadPermanentDeletionJournal(args.workspaceSlug);
|
|
45884
|
+
let record2 = findRetirement(journal, "document", t.logicalKey);
|
|
45885
|
+
if (record2.purge_generation !== t.purgeGeneration) {
|
|
45886
|
+
return { state: args.state, deleted: false };
|
|
45887
|
+
}
|
|
45888
|
+
if (args.liveOwnerId === t.id) {
|
|
45889
|
+
throw new Error("permanent document namespace reports the retired row as live owner");
|
|
45890
|
+
}
|
|
45891
|
+
if (record2.disposition === "complete") {
|
|
45892
|
+
const stale = args.liveOwnerId === null ? getEntryBySlug(args.state, args.strategy.type, t.logicalKey) : getEntry(args.state, args.strategy.type, t.id);
|
|
45893
|
+
const state2 = stale ? removeEntry(args.state, args.strategy.type, stale.id).state : args.state;
|
|
45894
|
+
return { state: state2, deleted: false, stashPath: record2.stash_path };
|
|
45895
|
+
}
|
|
45896
|
+
const retiredEntry = getEntry(args.state, args.strategy.type, t.id);
|
|
45897
|
+
const stateOwner = getEntryBySlug(args.state, args.strategy.type, t.logicalKey);
|
|
45898
|
+
const protectedOwner = args.liveOwnerId !== null && args.liveOwnerId !== t.id;
|
|
45899
|
+
let state = args.state;
|
|
45900
|
+
let deleted = false;
|
|
45901
|
+
let stashPath = record2.stash_path;
|
|
45902
|
+
let stashHash = record2.stash_hash;
|
|
45903
|
+
if (!protectedOwner) {
|
|
45904
|
+
const target = entityFilePath(args.workspaceSlug, args.strategy.type, retiredEntry?.slug ?? t.logicalKey);
|
|
45905
|
+
let raw;
|
|
45906
|
+
try {
|
|
45907
|
+
raw = readFileSync26(target, "utf8");
|
|
45908
|
+
} catch (error52) {
|
|
45909
|
+
if (error52?.code !== "ENOENT")
|
|
45910
|
+
throw error52;
|
|
45911
|
+
}
|
|
45912
|
+
if (raw !== undefined && !stashPath) {
|
|
45913
|
+
let matchesAcknowledged = false;
|
|
45914
|
+
if (retiredEntry) {
|
|
45915
|
+
try {
|
|
45916
|
+
const hashes = computeMirrorHashes(args.strategy, raw);
|
|
45917
|
+
matchesAcknowledged = hashes.bodyHash === retiredEntry.body_hash && hashes.frontmatterHash === retiredEntry.frontmatter_hash;
|
|
45918
|
+
} catch {
|
|
45919
|
+
matchesAcknowledged = false;
|
|
45920
|
+
}
|
|
45921
|
+
}
|
|
45922
|
+
if (!matchesAcknowledged) {
|
|
45923
|
+
const stash = stashEntityConflictOnce({
|
|
45924
|
+
workspaceSlug: args.workspaceSlug,
|
|
45925
|
+
entityType: args.strategy.type,
|
|
45926
|
+
entitySlug: t.logicalKey,
|
|
45927
|
+
timestamp: now(),
|
|
45928
|
+
contents: raw,
|
|
45929
|
+
side: "local"
|
|
45930
|
+
});
|
|
45931
|
+
stashPath = stash.path;
|
|
45932
|
+
stashHash = sha256(raw);
|
|
45933
|
+
verifyStash(stash.path, stashHash);
|
|
45934
|
+
recordRetirementStash({
|
|
45935
|
+
workspaceSlug: args.workspaceSlug,
|
|
45936
|
+
kind: "document",
|
|
45937
|
+
logicalKey: t.logicalKey,
|
|
45938
|
+
generation: t.purgeGeneration,
|
|
45939
|
+
stashPath,
|
|
45940
|
+
stashHash
|
|
45941
|
+
});
|
|
45942
|
+
args.output?.warn(`${args.strategy.type}/${t.logicalKey}: permanently deleted; recoverable local content was moved to ${stashPath}.`);
|
|
45943
|
+
}
|
|
45944
|
+
}
|
|
45945
|
+
args.suppressor?.registerDelete(target);
|
|
45946
|
+
deleted = deleteMirrorFile(args.workspaceSlug, args.strategy.type, retiredEntry?.slug ?? t.logicalKey);
|
|
45947
|
+
}
|
|
45948
|
+
const staleEntry = args.liveOwnerId === null ? stateOwner : retiredEntry;
|
|
45949
|
+
if (staleEntry)
|
|
45950
|
+
state = removeEntry(state, args.strategy.type, staleEntry.id).state;
|
|
45951
|
+
completeRetirement({
|
|
45952
|
+
workspaceSlug: args.workspaceSlug,
|
|
45953
|
+
kind: "document",
|
|
45954
|
+
logicalKey: t.logicalKey,
|
|
45955
|
+
generation: t.purgeGeneration,
|
|
45956
|
+
...stashPath && stashHash ? { stashPath, stashHash } : {},
|
|
45957
|
+
now
|
|
45958
|
+
});
|
|
45959
|
+
return { state, deleted, ...stashPath ? { stashPath } : {} };
|
|
45960
|
+
}
|
|
45961
|
+
function disposePermanentFile(args) {
|
|
45962
|
+
return withPermanentDeletionLock("workspace_file", args.tombstone.logicalKey, async () => disposePermanentFileLocked(args));
|
|
45963
|
+
}
|
|
45964
|
+
function disposePermanentFileLocked(args) {
|
|
45965
|
+
const now = args.now ?? (() => new Date);
|
|
45966
|
+
const t = args.tombstone;
|
|
45967
|
+
recordRetirementPending({
|
|
45968
|
+
workspaceSlug: args.workspaceSlug,
|
|
45969
|
+
kind: "workspace_file",
|
|
45970
|
+
logicalKey: t.logicalKey,
|
|
45971
|
+
rowId: t.id,
|
|
45972
|
+
purgeGeneration: t.purgeGeneration,
|
|
45973
|
+
retirementSequence: t.retirementSequence,
|
|
45974
|
+
retiredAt: t.deletedAt
|
|
45975
|
+
});
|
|
45976
|
+
const journal = loadPermanentDeletionJournal(args.workspaceSlug);
|
|
45977
|
+
const record2 = findRetirement(journal, "workspace_file", t.logicalKey);
|
|
45978
|
+
if (record2.purge_generation !== t.purgeGeneration) {
|
|
45979
|
+
return { state: args.state, deleted: false, textDeleted: false };
|
|
45980
|
+
}
|
|
45981
|
+
if (args.liveOwnerId === t.id) {
|
|
45982
|
+
throw new Error("permanent file namespace reports the retired row as live owner");
|
|
45983
|
+
}
|
|
45984
|
+
if (record2.disposition === "complete") {
|
|
45985
|
+
const stale = getFileEntry(args.state, t.logicalKey);
|
|
45986
|
+
const state2 = stale && (args.liveOwnerId === null || stale.id === t.id) ? removeFileEntry(args.state, t.logicalKey).state : args.state;
|
|
45987
|
+
return {
|
|
45988
|
+
state: state2,
|
|
45989
|
+
deleted: false,
|
|
45990
|
+
textDeleted: false,
|
|
45991
|
+
...record2.stash_path ? { stashPath: record2.stash_path } : {}
|
|
45992
|
+
};
|
|
45993
|
+
}
|
|
45994
|
+
const existing = getFileEntry(args.state, t.logicalKey);
|
|
45995
|
+
const protectedOwner = args.liveOwnerId !== null && args.liveOwnerId !== t.id;
|
|
45996
|
+
let state = args.state;
|
|
45997
|
+
let deleted = false;
|
|
45998
|
+
let textDeleted = false;
|
|
45999
|
+
let stashPath = record2.stash_path;
|
|
46000
|
+
let stashHash = record2.stash_hash;
|
|
46001
|
+
if (!protectedOwner) {
|
|
46002
|
+
const target = filesSubtreePath(args.workspaceSlug, t.logicalKey);
|
|
46003
|
+
let bytes;
|
|
46004
|
+
try {
|
|
46005
|
+
bytes = readFileSync26(target);
|
|
46006
|
+
} catch (error52) {
|
|
46007
|
+
if (error52?.code !== "ENOENT")
|
|
46008
|
+
throw error52;
|
|
46009
|
+
}
|
|
46010
|
+
if (bytes !== undefined && !stashPath) {
|
|
46011
|
+
const matchesAcknowledged = existing !== undefined && sha256Bytes(bytes) === existing.content_hash;
|
|
46012
|
+
if (!matchesAcknowledged) {
|
|
46013
|
+
const stash = stashFileConflictOnce({
|
|
46014
|
+
workspaceSlug: args.workspaceSlug,
|
|
46015
|
+
relPath: t.logicalKey,
|
|
46016
|
+
timestamp: now(),
|
|
46017
|
+
bytes,
|
|
46018
|
+
side: "local"
|
|
46019
|
+
});
|
|
46020
|
+
stashPath = stash.path;
|
|
46021
|
+
stashHash = sha256(bytes);
|
|
46022
|
+
verifyStash(stash.path, stashHash);
|
|
46023
|
+
recordRetirementStash({
|
|
46024
|
+
workspaceSlug: args.workspaceSlug,
|
|
46025
|
+
kind: "workspace_file",
|
|
46026
|
+
logicalKey: t.logicalKey,
|
|
46027
|
+
generation: t.purgeGeneration,
|
|
46028
|
+
stashPath,
|
|
46029
|
+
stashHash
|
|
46030
|
+
});
|
|
46031
|
+
args.output?.warn(`files/${t.logicalKey}: permanently deleted; recoverable local bytes were moved to ${stashPath}.`);
|
|
46032
|
+
}
|
|
46033
|
+
}
|
|
46034
|
+
args.suppressor?.registerDelete(target);
|
|
46035
|
+
deleted = deleteFilesFile(args.workspaceSlug, t.logicalKey);
|
|
46036
|
+
textDeleted = deleteExtractedText(args.workspaceSlug, t.logicalKey);
|
|
46037
|
+
}
|
|
46038
|
+
if (existing && (args.liveOwnerId === null || existing.id === t.id)) {
|
|
46039
|
+
state = removeFileEntry(state, t.logicalKey).state;
|
|
46040
|
+
}
|
|
46041
|
+
completeRetirement({
|
|
46042
|
+
workspaceSlug: args.workspaceSlug,
|
|
46043
|
+
kind: "workspace_file",
|
|
46044
|
+
logicalKey: t.logicalKey,
|
|
46045
|
+
generation: t.purgeGeneration,
|
|
46046
|
+
...stashPath && stashHash ? { stashPath, stashHash } : {},
|
|
46047
|
+
now
|
|
46048
|
+
});
|
|
46049
|
+
return { state, deleted, textDeleted, ...stashPath ? { stashPath } : {} };
|
|
46050
|
+
}
|
|
46051
|
+
|
|
46052
|
+
// src/lib/files/pull.ts
|
|
46053
|
+
function dueFileRefreshMarkers(workspaceSlug, now) {
|
|
46054
|
+
try {
|
|
46055
|
+
return loadPermanentDeletionJournal(workspaceSlug).refresh.filter((marker) => marker.kind === "workspace_file" && (marker.retry_after === undefined || new Date(marker.retry_after).getTime() <= now.getTime()));
|
|
46056
|
+
} catch {
|
|
46057
|
+
return null;
|
|
46058
|
+
}
|
|
46059
|
+
}
|
|
46060
|
+
async function replayPermanentFiles(args, markers) {
|
|
46061
|
+
const now = args.now ?? (() => new Date);
|
|
46062
|
+
const rows = [];
|
|
46063
|
+
let cursor = null;
|
|
46064
|
+
let resultingCursor = args.cursor;
|
|
46065
|
+
while (true) {
|
|
46066
|
+
const page = await syncFilesPage({
|
|
46067
|
+
baseUrl: args.baseUrl,
|
|
46068
|
+
pat: args.pat,
|
|
46069
|
+
cursor,
|
|
46070
|
+
fetchImpl: args.fetchImpl
|
|
46071
|
+
});
|
|
46072
|
+
rows.push(...page.rows);
|
|
46073
|
+
if (page.next_cursor === null)
|
|
46074
|
+
break;
|
|
46075
|
+
resultingCursor = page.next_cursor;
|
|
46076
|
+
cursor = page.next_cursor;
|
|
46077
|
+
}
|
|
46078
|
+
let state = args.state;
|
|
46079
|
+
let written = 0;
|
|
46080
|
+
let deleted = 0;
|
|
46081
|
+
let textWritten = 0;
|
|
46082
|
+
let textDeleted = 0;
|
|
46083
|
+
let textUnknown = 0;
|
|
46084
|
+
let pointerRows = 0;
|
|
46085
|
+
let symlinkRefused = 0;
|
|
46086
|
+
let stashFailed = 0;
|
|
46087
|
+
let completePass = true;
|
|
46088
|
+
let lastCommittedState = args.state;
|
|
46089
|
+
const completed = [];
|
|
46090
|
+
for (const marker of markers) {
|
|
46091
|
+
const candidates = rows.filter((row) => row.path === marker.logical_key && row.deleted_at !== null && row.permanent_delete === true && typeof row.purge_generation === "string" && typeof row.retirement_sequence === "number");
|
|
46092
|
+
candidates.sort((a, b) => b.retirement_sequence - a.retirement_sequence);
|
|
46093
|
+
const candidate = candidates[0];
|
|
46094
|
+
const ambiguousLatest = candidate !== undefined && candidates.some((row) => row.retirement_sequence === candidate.retirement_sequence && row.purge_generation !== candidate.purge_generation);
|
|
46095
|
+
const stateBefore = state;
|
|
46096
|
+
const outcome = await withPermanentDeletionLock("workspace_file", marker.logical_key, async () => {
|
|
46097
|
+
const namespace = await getCurrentPermanentNamespace({
|
|
46098
|
+
baseUrl: args.baseUrl,
|
|
46099
|
+
pat: args.pat,
|
|
46100
|
+
entityKind: "workspace_file",
|
|
46101
|
+
logicalKey: marker.logical_key,
|
|
46102
|
+
fetchImpl: args.fetchImpl
|
|
46103
|
+
});
|
|
46104
|
+
if (!candidate || ambiguousLatest || namespace.latest_retirement === null || namespace.latest_retirement.purge_generation !== candidate.purge_generation || namespace.latest_retirement.retirement_sequence !== candidate.retirement_sequence) {
|
|
46105
|
+
markRetentionMissing({
|
|
46106
|
+
workspaceSlug: args.workspaceSlug,
|
|
46107
|
+
kind: "workspace_file",
|
|
46108
|
+
logicalKey: marker.logical_key,
|
|
46109
|
+
now
|
|
46110
|
+
});
|
|
46111
|
+
args.output?.warn(`files/${marker.logical_key}: permanent-delete replay could not prove one unambiguous current retained tombstone; creates remain fenced and the replay will retry.`);
|
|
46112
|
+
return { state: stateBefore, deleted: false, textDeleted: false, complete: false };
|
|
46113
|
+
}
|
|
46114
|
+
const liveOwnerId = namespace.live_owner?.id ?? null;
|
|
46115
|
+
const disposition = await disposePermanentFile({
|
|
46116
|
+
workspaceSlug: args.workspaceSlug,
|
|
46117
|
+
tombstone: {
|
|
46118
|
+
id: candidate.id,
|
|
46119
|
+
logicalKey: marker.logical_key,
|
|
46120
|
+
deletedAt: candidate.deleted_at,
|
|
46121
|
+
purgeGeneration: candidate.purge_generation,
|
|
46122
|
+
retirementSequence: candidate.retirement_sequence
|
|
46123
|
+
},
|
|
46124
|
+
liveOwnerId,
|
|
46125
|
+
state: stateBefore,
|
|
46126
|
+
now,
|
|
46127
|
+
output: args.output,
|
|
46128
|
+
suppressor: args.suppressor
|
|
46129
|
+
});
|
|
46130
|
+
const committedState = args.commit && disposition.state !== stateBefore ? args.commit(stateBefore, disposition.state) : disposition.state;
|
|
46131
|
+
const current = await getCurrentPermanentNamespace({
|
|
46132
|
+
baseUrl: args.baseUrl,
|
|
46133
|
+
pat: args.pat,
|
|
46134
|
+
entityKind: "workspace_file",
|
|
46135
|
+
logicalKey: marker.logical_key,
|
|
46136
|
+
fetchImpl: args.fetchImpl
|
|
46137
|
+
});
|
|
46138
|
+
const complete = current.latest_retirement?.purge_generation === candidate.purge_generation && current.latest_retirement?.retirement_sequence === candidate.retirement_sequence && (current.live_owner?.id ?? null) === liveOwnerId;
|
|
46139
|
+
return {
|
|
46140
|
+
state: committedState,
|
|
46141
|
+
deleted: disposition.deleted,
|
|
46142
|
+
textDeleted: disposition.textDeleted,
|
|
46143
|
+
complete
|
|
46144
|
+
};
|
|
46145
|
+
});
|
|
46146
|
+
state = outcome.state;
|
|
46147
|
+
if (args.commit && state !== stateBefore)
|
|
46148
|
+
lastCommittedState = state;
|
|
46149
|
+
if (outcome.deleted)
|
|
46150
|
+
deleted++;
|
|
46151
|
+
if (outcome.textDeleted)
|
|
46152
|
+
textDeleted++;
|
|
46153
|
+
if (outcome.complete && candidate) {
|
|
46154
|
+
completed.push({ logicalKey: marker.logical_key, generation: candidate.purge_generation });
|
|
46155
|
+
} else {
|
|
46156
|
+
completePass = false;
|
|
46157
|
+
}
|
|
46158
|
+
}
|
|
46159
|
+
for (const row of rows) {
|
|
46160
|
+
if (row.deleted_at !== null && row.permanent_delete === true)
|
|
46161
|
+
continue;
|
|
46162
|
+
const outcome = await applyFileRow({
|
|
46163
|
+
workspaceSlug: args.workspaceSlug,
|
|
46164
|
+
baseUrl: args.baseUrl,
|
|
46165
|
+
pat: args.pat,
|
|
46166
|
+
row,
|
|
46167
|
+
state,
|
|
46168
|
+
fetchImpl: args.fetchImpl,
|
|
46169
|
+
now,
|
|
46170
|
+
output: args.output,
|
|
46171
|
+
suppressor: args.suppressor,
|
|
46172
|
+
isFileFrozen: args.isFileFrozen,
|
|
46173
|
+
loggedTombstoneSkips: args.loggedTombstoneSkips,
|
|
46174
|
+
forceTextRewalk: args.forceTextRewalk,
|
|
46175
|
+
loggedStashNotices: args.loggedStashNotices
|
|
46176
|
+
});
|
|
46177
|
+
state = outcome.state;
|
|
46178
|
+
if (outcome.action === "written")
|
|
46179
|
+
written++;
|
|
46180
|
+
else if (outcome.action === "deleted")
|
|
46181
|
+
deleted++;
|
|
46182
|
+
textWritten += outcome.textWritten ?? 0;
|
|
46183
|
+
textDeleted += outcome.textDeleted ?? 0;
|
|
46184
|
+
if (outcome.textUnknown)
|
|
46185
|
+
textUnknown++;
|
|
46186
|
+
if (outcome.pointer)
|
|
46187
|
+
pointerRows++;
|
|
46188
|
+
if (outcome.symlinkRefused) {
|
|
46189
|
+
symlinkRefused++;
|
|
46190
|
+
completePass = false;
|
|
46191
|
+
}
|
|
46192
|
+
if (outcome.stashFailed || outcome.textFailed) {
|
|
46193
|
+
if (outcome.stashFailed)
|
|
46194
|
+
stashFailed++;
|
|
46195
|
+
completePass = false;
|
|
46196
|
+
}
|
|
46197
|
+
}
|
|
46198
|
+
if (args.commit && state !== lastCommittedState)
|
|
46199
|
+
state = args.commit(lastCommittedState, state);
|
|
46200
|
+
return {
|
|
46201
|
+
state,
|
|
46202
|
+
fetched: rows.length,
|
|
46203
|
+
written,
|
|
46204
|
+
deleted,
|
|
46205
|
+
textWritten,
|
|
46206
|
+
textDeleted,
|
|
46207
|
+
textUnknown,
|
|
46208
|
+
pointerRows,
|
|
46209
|
+
symlinkRefused,
|
|
46210
|
+
stashFailed,
|
|
46211
|
+
lastCursor: completePass ? resultingCursor : args.cursor,
|
|
46212
|
+
permanentRefreshes: completed,
|
|
46213
|
+
permanentReplayComplete: completePass
|
|
46214
|
+
};
|
|
46215
|
+
}
|
|
44771
46216
|
async function pullFiles(args) {
|
|
46217
|
+
const dueMarkers = dueFileRefreshMarkers(args.workspaceSlug, (args.now ?? (() => new Date))());
|
|
46218
|
+
if (dueMarkers && dueMarkers.length > 0)
|
|
46219
|
+
return replayPermanentFiles(args, dueMarkers);
|
|
46220
|
+
const permanentJournalBlocked = dueMarkers === null;
|
|
46221
|
+
if (permanentJournalBlocked) {
|
|
46222
|
+
args.output?.warn(`files: permanent deletion journal is invalid or unreadable; ordinary rows may apply, but the files cursor and permanent dispositions are held. Run \`plinth status\` for the journal path.`);
|
|
46223
|
+
}
|
|
44772
46224
|
const { workspaceSlug, baseUrl, pat, fetchImpl } = args;
|
|
44773
46225
|
let state = args.state;
|
|
44774
46226
|
let fetched = 0;
|
|
@@ -44780,15 +46232,32 @@ async function pullFiles(args) {
|
|
|
44780
46232
|
let pointerRows = 0;
|
|
44781
46233
|
let symlinkRefused = 0;
|
|
44782
46234
|
let stashFailed = 0;
|
|
46235
|
+
let permanentEncountered = false;
|
|
44783
46236
|
let cursor = args.cursor;
|
|
44784
46237
|
let lastCursor = args.cursor;
|
|
44785
|
-
let cursorBlocked =
|
|
46238
|
+
let cursorBlocked = permanentJournalBlocked;
|
|
44786
46239
|
while (true) {
|
|
44787
46240
|
const page = await syncFilesPage({ baseUrl, pat, cursor, fetchImpl });
|
|
44788
46241
|
const pageBase = state;
|
|
44789
46242
|
let pageTextFailed = false;
|
|
44790
46243
|
for (const row of page.rows) {
|
|
44791
46244
|
fetched++;
|
|
46245
|
+
if (row.deleted_at !== null && row.permanent_delete === true && row.purge_generation !== undefined) {
|
|
46246
|
+
if (permanentJournalBlocked) {
|
|
46247
|
+
pageTextFailed = true;
|
|
46248
|
+
continue;
|
|
46249
|
+
}
|
|
46250
|
+
requireFeedRefresh({
|
|
46251
|
+
workspaceSlug,
|
|
46252
|
+
kind: "workspace_file",
|
|
46253
|
+
logicalKey: row.path,
|
|
46254
|
+
requiredGeneration: row.purge_generation,
|
|
46255
|
+
now: args.now
|
|
46256
|
+
});
|
|
46257
|
+
permanentEncountered = true;
|
|
46258
|
+
pageTextFailed = true;
|
|
46259
|
+
continue;
|
|
46260
|
+
}
|
|
44792
46261
|
const outcome = await applyFileRow({
|
|
44793
46262
|
workspaceSlug,
|
|
44794
46263
|
baseUrl,
|
|
@@ -44839,6 +46308,15 @@ async function pullFiles(args) {
|
|
|
44839
46308
|
lastCursor = page.next_cursor;
|
|
44840
46309
|
cursor = page.next_cursor;
|
|
44841
46310
|
}
|
|
46311
|
+
if (permanentEncountered) {
|
|
46312
|
+
const markers = dueFileRefreshMarkers(workspaceSlug, (args.now ?? (() => new Date))());
|
|
46313
|
+
if (!markers) {
|
|
46314
|
+
throw new Error("permanent deletion journal became unreadable during file replay");
|
|
46315
|
+
}
|
|
46316
|
+
if (markers.length > 0) {
|
|
46317
|
+
return replayPermanentFiles({ ...args, state }, markers);
|
|
46318
|
+
}
|
|
46319
|
+
}
|
|
44842
46320
|
if (textUnknown > 0) {
|
|
44843
46321
|
args.output?.warn(`${textUnknown} file ${textUnknown === 1 ? "row" : "rows"} arrived without a readable \`has_extracted_text\` — this server predates the extracted-text contract, or the field was dropped from the payload. Derived text under views/extracted/ was left untouched rather than deleted, and is not being updated either.`);
|
|
44844
46322
|
}
|
|
@@ -44977,7 +46455,7 @@ async function applyFileRow(args) {
|
|
|
44977
46455
|
}
|
|
44978
46456
|
let localBytes;
|
|
44979
46457
|
try {
|
|
44980
|
-
localBytes =
|
|
46458
|
+
localBytes = readFileSync27(abs);
|
|
44981
46459
|
} catch {
|
|
44982
46460
|
localBytes = undefined;
|
|
44983
46461
|
}
|
|
@@ -45023,7 +46501,7 @@ async function applyFileRow(args) {
|
|
|
45023
46501
|
output
|
|
45024
46502
|
});
|
|
45025
46503
|
}
|
|
45026
|
-
if (existing && existing.content_hash === row.content_hash &&
|
|
46504
|
+
if (existing && existing.content_hash === row.content_hash && existsSync8(abs)) {
|
|
45027
46505
|
if (!args.forceTextRewalk && textIsCurrent(existing, row, workspaceSlug, relPath)) {
|
|
45028
46506
|
return { state, action: "noop", textUnknown };
|
|
45029
46507
|
}
|
|
@@ -45153,7 +46631,7 @@ async function applyPointerRow(args) {
|
|
|
45153
46631
|
if (textSignal === "absent") {
|
|
45154
46632
|
args.output?.warn(`files/${relPath}: Plinth holds neither bytes nor text for this file — it has no local representation in the mirror.`);
|
|
45155
46633
|
}
|
|
45156
|
-
const bytesInMirror =
|
|
46634
|
+
const bytesInMirror = existsSync8(filesSubtreePath(workspaceSlug, relPath));
|
|
45157
46635
|
if (bytesInMirror) {
|
|
45158
46636
|
args.output?.warn(`files/${relPath}: Plinth holds no bytes for this file, but a local copy is present at that path — left exactly as it is. Plinth will not update it.`);
|
|
45159
46637
|
}
|
|
@@ -45179,11 +46657,11 @@ async function applyPointerRow(args) {
|
|
|
45179
46657
|
}
|
|
45180
46658
|
function stashUnpushedLocalEdit(args) {
|
|
45181
46659
|
const { workspaceSlug, relPath, abs, existing, output } = args;
|
|
45182
|
-
if (hasSymlinkedComponent(workspaceSlug,
|
|
46660
|
+
if (hasSymlinkedComponent(workspaceSlug, dirname13(abs)))
|
|
45183
46661
|
return { reason: "symlink" };
|
|
45184
46662
|
let st;
|
|
45185
46663
|
try {
|
|
45186
|
-
st =
|
|
46664
|
+
st = lstatSync5(abs);
|
|
45187
46665
|
} catch (err) {
|
|
45188
46666
|
if (err?.code === "ENOENT")
|
|
45189
46667
|
return null;
|
|
@@ -45193,7 +46671,7 @@ function stashUnpushedLocalEdit(args) {
|
|
|
45193
46671
|
return { reason: "not_a_file" };
|
|
45194
46672
|
let localBytes;
|
|
45195
46673
|
try {
|
|
45196
|
-
localBytes =
|
|
46674
|
+
localBytes = readFileSync27(abs);
|
|
45197
46675
|
} catch (err) {
|
|
45198
46676
|
return { reason: "unreadable", code: errCode(err) };
|
|
45199
46677
|
}
|
|
@@ -45230,7 +46708,7 @@ function textIsCurrent(existing, row, workspaceSlug, relPath) {
|
|
|
45230
46708
|
if (signal === "unknown")
|
|
45231
46709
|
return true;
|
|
45232
46710
|
if (signal === "absent") {
|
|
45233
|
-
return existing.text_hash === undefined && !
|
|
46711
|
+
return existing.text_hash === undefined && !existsSync8(extractedTextPath(workspaceSlug, relPath));
|
|
45234
46712
|
}
|
|
45235
46713
|
if (existing.text_hash === undefined)
|
|
45236
46714
|
return false;
|
|
@@ -45241,7 +46719,7 @@ function textIsCurrent(existing, row, workspaceSlug, relPath) {
|
|
|
45241
46719
|
}
|
|
45242
46720
|
|
|
45243
46721
|
// src/lib/files/rejection-sync.ts
|
|
45244
|
-
import { readFileSync as
|
|
46722
|
+
import { readFileSync as readFileSync28, statSync as statSync16 } from "node:fs";
|
|
45245
46723
|
|
|
45246
46724
|
// src/lib/files/rejections-api.ts
|
|
45247
46725
|
var RejectionsFeedSchema = exports_external.object({
|
|
@@ -45374,7 +46852,7 @@ function settledDisposition(args) {
|
|
|
45374
46852
|
return null;
|
|
45375
46853
|
let bytes;
|
|
45376
46854
|
try {
|
|
45377
|
-
bytes =
|
|
46855
|
+
bytes = readFileSync28(absolute2);
|
|
45378
46856
|
} catch {
|
|
45379
46857
|
return null;
|
|
45380
46858
|
}
|
|
@@ -45451,7 +46929,7 @@ function classifyApiError(err) {
|
|
|
45451
46929
|
return { kind: "transient", status: err.status };
|
|
45452
46930
|
}
|
|
45453
46931
|
function hasExistingContent(workspacePath) {
|
|
45454
|
-
if (!
|
|
46932
|
+
if (!existsSync9(workspacePath))
|
|
45455
46933
|
return false;
|
|
45456
46934
|
try {
|
|
45457
46935
|
return readdirSync7(workspacePath, { withFileTypes: true }).some((e) => {
|
|
@@ -45460,7 +46938,7 @@ function hasExistingContent(workspacePath) {
|
|
|
45460
46938
|
if (!e.isDirectory())
|
|
45461
46939
|
return true;
|
|
45462
46940
|
try {
|
|
45463
|
-
return readdirSync7(
|
|
46941
|
+
return readdirSync7(join18(workspacePath, e.name)).length > 0;
|
|
45464
46942
|
} catch {
|
|
45465
46943
|
return true;
|
|
45466
46944
|
}
|
|
@@ -45490,7 +46968,7 @@ function unlinkIfProvablyOurs(workspaceSlug, strategy, entry2, suppressor) {
|
|
|
45490
46968
|
}
|
|
45491
46969
|
let raw;
|
|
45492
46970
|
try {
|
|
45493
|
-
raw =
|
|
46971
|
+
raw = readFileSync29(target, "utf8");
|
|
45494
46972
|
} catch (err) {
|
|
45495
46973
|
if (err?.code === "ENOENT") {
|
|
45496
46974
|
return { didUnlink: false };
|
|
@@ -45508,7 +46986,7 @@ function unlinkIfProvablyOurs(workspaceSlug, strategy, entry2, suppressor) {
|
|
|
45508
46986
|
}
|
|
45509
46987
|
try {
|
|
45510
46988
|
suppressor?.registerDelete(target);
|
|
45511
|
-
|
|
46989
|
+
unlinkSync9(target);
|
|
45512
46990
|
return { didUnlink: true };
|
|
45513
46991
|
} catch (err) {
|
|
45514
46992
|
if (err?.code === "ENOENT") {
|
|
@@ -45517,6 +46995,118 @@ function unlinkIfProvablyOurs(workspaceSlug, strategy, entry2, suppressor) {
|
|
|
45517
46995
|
throw err;
|
|
45518
46996
|
}
|
|
45519
46997
|
}
|
|
46998
|
+
function dueDocumentRefreshMarkers(workspaceSlug, now) {
|
|
46999
|
+
try {
|
|
47000
|
+
return loadPermanentDeletionJournal(workspaceSlug).refresh.filter((marker) => marker.kind === "document" && (marker.retry_after === undefined || new Date(marker.retry_after).getTime() <= now.getTime()));
|
|
47001
|
+
} catch {
|
|
47002
|
+
return null;
|
|
47003
|
+
}
|
|
47004
|
+
}
|
|
47005
|
+
async function replayPermanentDocuments(args) {
|
|
47006
|
+
const rows = [];
|
|
47007
|
+
let cursor = null;
|
|
47008
|
+
let resultingCursor = args.savedCursor;
|
|
47009
|
+
while (true) {
|
|
47010
|
+
const page = await syncEntity({
|
|
47011
|
+
strategy: args.strategy,
|
|
47012
|
+
baseUrl: args.baseUrl,
|
|
47013
|
+
pat: args.pat,
|
|
47014
|
+
cursor,
|
|
47015
|
+
fetchImpl: args.fetchImpl
|
|
47016
|
+
});
|
|
47017
|
+
rows.push(...page.rows);
|
|
47018
|
+
if (page.next_cursor === null)
|
|
47019
|
+
break;
|
|
47020
|
+
resultingCursor = page.next_cursor;
|
|
47021
|
+
cursor = page.next_cursor;
|
|
47022
|
+
}
|
|
47023
|
+
let state = args.state;
|
|
47024
|
+
const completed = [];
|
|
47025
|
+
let completePass = true;
|
|
47026
|
+
for (const marker of args.markers) {
|
|
47027
|
+
const candidates = rows.filter((row) => row.deleted_at !== null && row.slug === marker.logical_key && row.permanent_delete === true && typeof row.purge_generation === "string" && typeof row.retirement_sequence === "number");
|
|
47028
|
+
candidates.sort((a, b) => b.retirement_sequence - a.retirement_sequence);
|
|
47029
|
+
const candidate = candidates[0];
|
|
47030
|
+
const ambiguousLatest = candidate !== undefined && candidates.some((row) => row.retirement_sequence === candidate.retirement_sequence && row.purge_generation !== candidate.purge_generation);
|
|
47031
|
+
const stateBefore = state;
|
|
47032
|
+
const outcome = await withPermanentDeletionLock("document", marker.logical_key, async () => {
|
|
47033
|
+
const namespace = await getCurrentPermanentNamespace({
|
|
47034
|
+
baseUrl: args.baseUrl,
|
|
47035
|
+
pat: args.pat,
|
|
47036
|
+
entityKind: "document",
|
|
47037
|
+
logicalKey: marker.logical_key,
|
|
47038
|
+
fetchImpl: args.fetchImpl
|
|
47039
|
+
});
|
|
47040
|
+
if (!candidate || ambiguousLatest || namespace.latest_retirement === null || namespace.latest_retirement.purge_generation !== candidate.purge_generation || namespace.latest_retirement.retirement_sequence !== candidate.retirement_sequence) {
|
|
47041
|
+
markRetentionMissing({
|
|
47042
|
+
workspaceSlug: args.workspaceSlug,
|
|
47043
|
+
kind: "document",
|
|
47044
|
+
logicalKey: marker.logical_key,
|
|
47045
|
+
now: args.now
|
|
47046
|
+
});
|
|
47047
|
+
args.output.warn(`documents/${marker.logical_key}: permanent-delete replay could not prove one unambiguous current retained tombstone; creates remain fenced and the replay will retry.`);
|
|
47048
|
+
return { state: stateBefore, complete: false };
|
|
47049
|
+
}
|
|
47050
|
+
const liveOwnerId = namespace.live_owner?.id ?? null;
|
|
47051
|
+
const disposed = await disposePermanentDocument({
|
|
47052
|
+
workspaceSlug: args.workspaceSlug,
|
|
47053
|
+
strategy: args.strategy,
|
|
47054
|
+
tombstone: {
|
|
47055
|
+
id: candidate.id,
|
|
47056
|
+
logicalKey: marker.logical_key,
|
|
47057
|
+
deletedAt: candidate.deleted_at,
|
|
47058
|
+
purgeGeneration: candidate.purge_generation,
|
|
47059
|
+
retirementSequence: candidate.retirement_sequence
|
|
47060
|
+
},
|
|
47061
|
+
liveOwnerId,
|
|
47062
|
+
state: stateBefore,
|
|
47063
|
+
now: args.now,
|
|
47064
|
+
output: args.output,
|
|
47065
|
+
suppressor: args.suppressor
|
|
47066
|
+
});
|
|
47067
|
+
const committedState = args.commit && disposed.state !== stateBefore ? args.commit(stateBefore, disposed.state) : disposed.state;
|
|
47068
|
+
const current = await getCurrentPermanentNamespace({
|
|
47069
|
+
baseUrl: args.baseUrl,
|
|
47070
|
+
pat: args.pat,
|
|
47071
|
+
entityKind: "document",
|
|
47072
|
+
logicalKey: marker.logical_key,
|
|
47073
|
+
fetchImpl: args.fetchImpl
|
|
47074
|
+
});
|
|
47075
|
+
const complete = current.latest_retirement?.purge_generation === candidate.purge_generation && current.latest_retirement?.retirement_sequence === candidate.retirement_sequence && (current.live_owner?.id ?? null) === liveOwnerId;
|
|
47076
|
+
return { state: committedState, complete };
|
|
47077
|
+
});
|
|
47078
|
+
state = outcome.state;
|
|
47079
|
+
if (outcome.complete && candidate) {
|
|
47080
|
+
completed.push({ logicalKey: marker.logical_key, generation: candidate.purge_generation });
|
|
47081
|
+
} else {
|
|
47082
|
+
completePass = false;
|
|
47083
|
+
}
|
|
47084
|
+
}
|
|
47085
|
+
const counters = {
|
|
47086
|
+
fetched: rows.length,
|
|
47087
|
+
written: 0,
|
|
47088
|
+
deleted: 0,
|
|
47089
|
+
moved: 0,
|
|
47090
|
+
refused: 0
|
|
47091
|
+
};
|
|
47092
|
+
for (const row of rows) {
|
|
47093
|
+
if (row.deleted_at !== null && row.permanent_delete === true)
|
|
47094
|
+
continue;
|
|
47095
|
+
const outcome = applyRow(args.workspaceSlug, args.strategy, row, state, args.output, args.suppressor, args.now, args.isFrozen, args.loggedTombstoneSkips);
|
|
47096
|
+
state = outcome.state;
|
|
47097
|
+
if (outcome.action === "written")
|
|
47098
|
+
counters.written++;
|
|
47099
|
+
else if (outcome.action === "deleted")
|
|
47100
|
+
counters.deleted++;
|
|
47101
|
+
else if (outcome.action === "moved")
|
|
47102
|
+
counters.moved = (counters.moved ?? 0) + 1;
|
|
47103
|
+
else if (outcome.action === "slug_refused") {
|
|
47104
|
+
counters.refused = (counters.refused ?? 0) + 1;
|
|
47105
|
+
completePass = false;
|
|
47106
|
+
}
|
|
47107
|
+
}
|
|
47108
|
+
return { state, counters, cursor: resultingCursor, completed, completePass };
|
|
47109
|
+
}
|
|
45520
47110
|
async function runSync(workspaceSlug, deps = {}) {
|
|
45521
47111
|
const out = new Output({
|
|
45522
47112
|
sink: deps.sink,
|
|
@@ -45640,9 +47230,55 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
45640
47230
|
for (const entityType of allEntityTypes()) {
|
|
45641
47231
|
const strategy = requireStrategy(entityType);
|
|
45642
47232
|
const startCursor = entry2.sync_cursors?.[entityType] ?? null;
|
|
47233
|
+
if (entityType === "documents") {
|
|
47234
|
+
const markers = dueDocumentRefreshMarkers(slug, now());
|
|
47235
|
+
if (markers && markers.length > 0) {
|
|
47236
|
+
const replay = await replayPermanentDocuments({
|
|
47237
|
+
workspaceSlug: slug,
|
|
47238
|
+
baseUrl: entry2.api_base_url,
|
|
47239
|
+
pat,
|
|
47240
|
+
state,
|
|
47241
|
+
savedCursor: startCursor,
|
|
47242
|
+
strategy,
|
|
47243
|
+
markers,
|
|
47244
|
+
fetchImpl,
|
|
47245
|
+
output: out,
|
|
47246
|
+
suppressor: deps.suppressor,
|
|
47247
|
+
now,
|
|
47248
|
+
isFrozen: deps.isFrozen,
|
|
47249
|
+
loggedTombstoneSkips: deps.loggedTombstoneSkips,
|
|
47250
|
+
commit: (before, after) => {
|
|
47251
|
+
const next = commitState(before, after);
|
|
47252
|
+
committed = next;
|
|
47253
|
+
return next;
|
|
47254
|
+
}
|
|
47255
|
+
});
|
|
47256
|
+
state = replay.state;
|
|
47257
|
+
commitPending();
|
|
47258
|
+
counters[entityType] = replay.counters;
|
|
47259
|
+
if (replay.completePass) {
|
|
47260
|
+
updateSyncCursor(slug, entityType, replay.cursor);
|
|
47261
|
+
for (const item of replay.completed) {
|
|
47262
|
+
clearFeedRefresh({
|
|
47263
|
+
workspaceSlug: slug,
|
|
47264
|
+
kind: "document",
|
|
47265
|
+
logicalKey: item.logicalKey,
|
|
47266
|
+
generation: item.generation
|
|
47267
|
+
});
|
|
47268
|
+
}
|
|
47269
|
+
}
|
|
47270
|
+
continue;
|
|
47271
|
+
}
|
|
47272
|
+
}
|
|
45643
47273
|
let cursor = startCursor;
|
|
45644
47274
|
let lastAdvancedCursor = startCursor;
|
|
45645
47275
|
let cursorBlocked = false;
|
|
47276
|
+
let permanentEncountered = false;
|
|
47277
|
+
const permanentJournalBlocked = entityType === "documents" && dueDocumentRefreshMarkers(slug, now()) === null;
|
|
47278
|
+
if (permanentJournalBlocked) {
|
|
47279
|
+
cursorBlocked = true;
|
|
47280
|
+
out.warn(`documents: permanent deletion journal is invalid or unreadable; ordinary rows may apply, but the document cursor and permanent dispositions are held. Run \`plinth status\` for the journal path.`);
|
|
47281
|
+
}
|
|
45646
47282
|
const ctr = {
|
|
45647
47283
|
fetched: 0,
|
|
45648
47284
|
written: 0,
|
|
@@ -45660,6 +47296,22 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
45660
47296
|
});
|
|
45661
47297
|
for (const row of page.rows) {
|
|
45662
47298
|
ctr.fetched++;
|
|
47299
|
+
if (entityType === "documents" && row.deleted_at !== null && row.permanent_delete === true && row.purge_generation !== undefined) {
|
|
47300
|
+
if (permanentJournalBlocked) {
|
|
47301
|
+
cursorBlocked = true;
|
|
47302
|
+
continue;
|
|
47303
|
+
}
|
|
47304
|
+
requireFeedRefresh({
|
|
47305
|
+
workspaceSlug: slug,
|
|
47306
|
+
kind: "document",
|
|
47307
|
+
logicalKey: row.slug,
|
|
47308
|
+
requiredGeneration: row.purge_generation,
|
|
47309
|
+
now
|
|
47310
|
+
});
|
|
47311
|
+
permanentEncountered = true;
|
|
47312
|
+
cursorBlocked = true;
|
|
47313
|
+
continue;
|
|
47314
|
+
}
|
|
45663
47315
|
let outcome;
|
|
45664
47316
|
try {
|
|
45665
47317
|
outcome = applyRow(slug, strategy, row, state, out, deps.suppressor, now, deps.isFrozen, deps.loggedTombstoneSkips);
|
|
@@ -45698,6 +47350,46 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
45698
47350
|
cursor = page.next_cursor;
|
|
45699
47351
|
}
|
|
45700
47352
|
counters[entityType] = ctr;
|
|
47353
|
+
if (permanentEncountered && entityType === "documents") {
|
|
47354
|
+
const markers = dueDocumentRefreshMarkers(slug, now());
|
|
47355
|
+
if (!markers || markers.length === 0)
|
|
47356
|
+
continue;
|
|
47357
|
+
const replay = await replayPermanentDocuments({
|
|
47358
|
+
workspaceSlug: slug,
|
|
47359
|
+
baseUrl: entry2.api_base_url,
|
|
47360
|
+
pat,
|
|
47361
|
+
state,
|
|
47362
|
+
savedCursor: startCursor,
|
|
47363
|
+
strategy,
|
|
47364
|
+
markers,
|
|
47365
|
+
fetchImpl,
|
|
47366
|
+
output: out,
|
|
47367
|
+
suppressor: deps.suppressor,
|
|
47368
|
+
now,
|
|
47369
|
+
isFrozen: deps.isFrozen,
|
|
47370
|
+
loggedTombstoneSkips: deps.loggedTombstoneSkips,
|
|
47371
|
+
commit: (before, after) => {
|
|
47372
|
+
const next = commitState(before, after);
|
|
47373
|
+
committed = next;
|
|
47374
|
+
return next;
|
|
47375
|
+
}
|
|
47376
|
+
});
|
|
47377
|
+
state = replay.state;
|
|
47378
|
+
commitPending();
|
|
47379
|
+
counters[entityType] = replay.counters;
|
|
47380
|
+
if (replay.completePass) {
|
|
47381
|
+
updateSyncCursor(slug, entityType, replay.cursor);
|
|
47382
|
+
for (const item of replay.completed) {
|
|
47383
|
+
clearFeedRefresh({
|
|
47384
|
+
workspaceSlug: slug,
|
|
47385
|
+
kind: "document",
|
|
47386
|
+
logicalKey: item.logicalKey,
|
|
47387
|
+
generation: item.generation
|
|
47388
|
+
});
|
|
47389
|
+
}
|
|
47390
|
+
}
|
|
47391
|
+
continue;
|
|
47392
|
+
}
|
|
45701
47393
|
if (lastAdvancedCursor !== startCursor) {
|
|
45702
47394
|
updateSyncCursor(slug, entityType, lastAdvancedCursor);
|
|
45703
47395
|
}
|
|
@@ -45738,6 +47430,16 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
45738
47430
|
if (filesResult.lastCursor !== filesStartCursor) {
|
|
45739
47431
|
updateSyncCursor(slug, "files", filesResult.lastCursor);
|
|
45740
47432
|
}
|
|
47433
|
+
if (filesResult.permanentReplayComplete) {
|
|
47434
|
+
for (const item of filesResult.permanentRefreshes ?? []) {
|
|
47435
|
+
clearFeedRefresh({
|
|
47436
|
+
workspaceSlug: slug,
|
|
47437
|
+
kind: "workspace_file",
|
|
47438
|
+
logicalKey: item.logicalKey,
|
|
47439
|
+
generation: item.generation
|
|
47440
|
+
});
|
|
47441
|
+
}
|
|
47442
|
+
}
|
|
45741
47443
|
const rulesResult = await pullWorkspaceRules({
|
|
45742
47444
|
workspaceSlug: slug,
|
|
45743
47445
|
baseUrl: entry2.api_base_url,
|
|
@@ -45969,7 +47671,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
45969
47671
|
if (!entry2) {
|
|
45970
47672
|
let present2 = false;
|
|
45971
47673
|
try {
|
|
45972
|
-
present2 =
|
|
47674
|
+
present2 = existsSync9(entityFilePath(workspaceSlug, strategy.type, row.slug));
|
|
45973
47675
|
} catch (err) {
|
|
45974
47676
|
if (!(err instanceof MirrorPathError))
|
|
45975
47677
|
throw err;
|
|
@@ -46048,7 +47750,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
46048
47750
|
const writeSlug = relocation.writeSlug;
|
|
46049
47751
|
if (existingEntry && existingEntry.updated_at === live.updated_at && relocation.kind === "not_needed") {
|
|
46050
47752
|
const path5 = entityFilePath(workspaceSlug, strategy.type, live.slug);
|
|
46051
|
-
if (
|
|
47753
|
+
if (existsSync9(path5)) {
|
|
46052
47754
|
if (strategy.type === "artists") {
|
|
46053
47755
|
const artistLive = live;
|
|
46054
47756
|
state = applyArtistMemoryOnPull({
|
|
@@ -46075,7 +47777,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
46075
47777
|
if (!suppressor && existingEntry && existingEntry.updated_at !== live.updated_at && relocation.kind !== "moved") {
|
|
46076
47778
|
let localRaw = null;
|
|
46077
47779
|
try {
|
|
46078
|
-
localRaw =
|
|
47780
|
+
localRaw = readFileSync29(entityFilePath(workspaceSlug, strategy.type, writeSlug), "utf8");
|
|
46079
47781
|
} catch {
|
|
46080
47782
|
localRaw = null;
|
|
46081
47783
|
}
|
|
@@ -46095,6 +47797,38 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
46095
47797
|
}
|
|
46096
47798
|
}
|
|
46097
47799
|
}
|
|
47800
|
+
if (!suppressor && !existingEntry) {
|
|
47801
|
+
const target = entityFilePath(workspaceSlug, strategy.type, writeSlug);
|
|
47802
|
+
let localRaw = null;
|
|
47803
|
+
try {
|
|
47804
|
+
localRaw = lstatSync6(target).isFile() ? readFileSync29(target, "utf8") : null;
|
|
47805
|
+
} catch {
|
|
47806
|
+
localRaw = null;
|
|
47807
|
+
}
|
|
47808
|
+
if (localRaw !== null) {
|
|
47809
|
+
const hashStrategy = strategy;
|
|
47810
|
+
const incomingHashes = computeMirrorHashes(hashStrategy, renderMirrorWithBodyPreserve(workspaceSlug, strategy, live, writeSlug));
|
|
47811
|
+
let diverged = true;
|
|
47812
|
+
try {
|
|
47813
|
+
const localHashes = computeMirrorHashes(hashStrategy, localRaw);
|
|
47814
|
+
diverged = localHashes.bodyHash !== incomingHashes.bodyHash || localHashes.frontmatterHash !== incomingHashes.frontmatterHash;
|
|
47815
|
+
} catch {
|
|
47816
|
+
diverged = true;
|
|
47817
|
+
}
|
|
47818
|
+
if (diverged) {
|
|
47819
|
+
const stash = stashEntityConflictOnce({
|
|
47820
|
+
workspaceSlug,
|
|
47821
|
+
entityType: strategy.type,
|
|
47822
|
+
entitySlug: live.slug,
|
|
47823
|
+
timestamp: now(),
|
|
47824
|
+
contents: localRaw,
|
|
47825
|
+
side: "local"
|
|
47826
|
+
});
|
|
47827
|
+
const where = stash.reused ? `already stashed at` : `stashed it to`;
|
|
47828
|
+
out.warn(`Conflict on ${strategy.type}/${live.slug} → the local file there was not tracked for ` + `the incoming row and differed from it; ${where} ${stash.path} (cloud-wins on boot).`);
|
|
47829
|
+
}
|
|
47830
|
+
}
|
|
47831
|
+
}
|
|
46098
47832
|
const content = writeMirrorWithBodyPreserve(workspaceSlug, strategy, live, suppressor, writeSlug);
|
|
46099
47833
|
const hashes = computeMirrorHashes(strategy, content);
|
|
46100
47834
|
let inode;
|
|
@@ -46233,7 +47967,7 @@ function backfillStateInodes(workspaceSlug, state, statFn = (p) => statSync18(p)
|
|
|
46233
47967
|
return { state, changed: false };
|
|
46234
47968
|
return { state: { ...state, entries: nextEntries }, changed: true };
|
|
46235
47969
|
}
|
|
46236
|
-
function backfillBodyLengths(workspaceSlug, state, readFn = (p) =>
|
|
47970
|
+
function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync30(p, "utf8")) {
|
|
46237
47971
|
let changed = false;
|
|
46238
47972
|
const nextEntries = {};
|
|
46239
47973
|
for (const [entityType, bucket] of Object.entries(state.entries)) {
|
|
@@ -46264,7 +47998,7 @@ function makeWatchHandler(deps) {
|
|
|
46264
47998
|
baseUrl,
|
|
46265
47999
|
pat,
|
|
46266
48000
|
spinner,
|
|
46267
|
-
mutex,
|
|
48001
|
+
mutex: mutex2,
|
|
46268
48002
|
getState,
|
|
46269
48003
|
applyState,
|
|
46270
48004
|
fetchImpl,
|
|
@@ -46308,7 +48042,7 @@ function makeWatchHandler(deps) {
|
|
|
46308
48042
|
output?.log(fileDeleteSuppressedLine({ path: relPath, reason: "pull_echo" }));
|
|
46309
48043
|
return;
|
|
46310
48044
|
}
|
|
46311
|
-
if (
|
|
48045
|
+
if (existsSync10(absolutePath)) {
|
|
46312
48046
|
output?.log(fileDeleteSuppressedLine({ path: relPath, reason: "path_exists" }));
|
|
46313
48047
|
return;
|
|
46314
48048
|
}
|
|
@@ -46320,7 +48054,7 @@ function makeWatchHandler(deps) {
|
|
|
46320
48054
|
return;
|
|
46321
48055
|
}
|
|
46322
48056
|
}
|
|
46323
|
-
await
|
|
48057
|
+
await mutex2.run(`files:${relPath}`, async () => {
|
|
46324
48058
|
const base2 = getState();
|
|
46325
48059
|
const result = await pushFilesDelete({
|
|
46326
48060
|
workspaceSlug,
|
|
@@ -46374,7 +48108,7 @@ function makeWatchHandler(deps) {
|
|
|
46374
48108
|
spinner.update(`Watching ${workspaceSlug} · ${watchingSummary(getState())}`);
|
|
46375
48109
|
return;
|
|
46376
48110
|
}
|
|
46377
|
-
const bytes =
|
|
48111
|
+
const bytes = readFileSync30(absolutePath);
|
|
46378
48112
|
const hash2 = sha256Bytes(bytes);
|
|
46379
48113
|
if (suppressor?.shouldSuppressUpsert(absolutePath, {
|
|
46380
48114
|
bodyHash: hash2,
|
|
@@ -46391,7 +48125,7 @@ function makeWatchHandler(deps) {
|
|
|
46391
48125
|
return;
|
|
46392
48126
|
}
|
|
46393
48127
|
}
|
|
46394
|
-
await
|
|
48128
|
+
await mutex2.run(`files:${relPath}`, async () => {
|
|
46395
48129
|
const base2 = getState();
|
|
46396
48130
|
const result = await pushFilesUpsert({
|
|
46397
48131
|
workspaceSlug,
|
|
@@ -46438,7 +48172,7 @@ function makeWatchHandler(deps) {
|
|
|
46438
48172
|
if (suppressor) {
|
|
46439
48173
|
let raw = null;
|
|
46440
48174
|
try {
|
|
46441
|
-
raw =
|
|
48175
|
+
raw = readFileSync30(absolutePath, "utf8");
|
|
46442
48176
|
} catch {
|
|
46443
48177
|
raw = null;
|
|
46444
48178
|
}
|
|
@@ -46450,7 +48184,7 @@ function makeWatchHandler(deps) {
|
|
|
46450
48184
|
}
|
|
46451
48185
|
}
|
|
46452
48186
|
try {
|
|
46453
|
-
await
|
|
48187
|
+
await mutex2.run(WORKSPACE_RULES_MUTEX_KEY, async () => {
|
|
46454
48188
|
const base2 = getState();
|
|
46455
48189
|
const result = await pushWorkspaceRules({
|
|
46456
48190
|
workspaceSlug,
|
|
@@ -46477,7 +48211,7 @@ function makeWatchHandler(deps) {
|
|
|
46477
48211
|
if (suppressor) {
|
|
46478
48212
|
let raw = null;
|
|
46479
48213
|
try {
|
|
46480
|
-
raw =
|
|
48214
|
+
raw = readFileSync30(absolutePath, "utf8");
|
|
46481
48215
|
} catch {
|
|
46482
48216
|
raw = null;
|
|
46483
48217
|
}
|
|
@@ -46489,7 +48223,7 @@ function makeWatchHandler(deps) {
|
|
|
46489
48223
|
}
|
|
46490
48224
|
}
|
|
46491
48225
|
try {
|
|
46492
|
-
await
|
|
48226
|
+
await mutex2.run(WORKSPACE_MEMORY_MUTEX_KEY, async () => {
|
|
46493
48227
|
const base2 = getState();
|
|
46494
48228
|
const result = await pushWorkspaceMemory({
|
|
46495
48229
|
workspaceSlug,
|
|
@@ -46516,7 +48250,7 @@ function makeWatchHandler(deps) {
|
|
|
46516
48250
|
if (suppressor) {
|
|
46517
48251
|
let raw = null;
|
|
46518
48252
|
try {
|
|
46519
|
-
raw =
|
|
48253
|
+
raw = readFileSync30(absolutePath, "utf8");
|
|
46520
48254
|
} catch {
|
|
46521
48255
|
raw = null;
|
|
46522
48256
|
}
|
|
@@ -46528,7 +48262,7 @@ function makeWatchHandler(deps) {
|
|
|
46528
48262
|
}
|
|
46529
48263
|
}
|
|
46530
48264
|
try {
|
|
46531
|
-
await
|
|
48265
|
+
await mutex2.run(USER_MEMORY_MUTEX_KEY, async () => {
|
|
46532
48266
|
const base2 = getState();
|
|
46533
48267
|
const result = await pushUserMemory({
|
|
46534
48268
|
workspaceSlug,
|
|
@@ -46555,7 +48289,7 @@ function makeWatchHandler(deps) {
|
|
|
46555
48289
|
if (suppressor) {
|
|
46556
48290
|
let raw = null;
|
|
46557
48291
|
try {
|
|
46558
|
-
raw =
|
|
48292
|
+
raw = readFileSync30(absolutePath, "utf8");
|
|
46559
48293
|
} catch {
|
|
46560
48294
|
raw = null;
|
|
46561
48295
|
}
|
|
@@ -46567,7 +48301,7 @@ function makeWatchHandler(deps) {
|
|
|
46567
48301
|
}
|
|
46568
48302
|
}
|
|
46569
48303
|
try {
|
|
46570
|
-
await
|
|
48304
|
+
await mutex2.run(VOICE_MUTEX_KEY, async () => {
|
|
46571
48305
|
const base2 = getState();
|
|
46572
48306
|
const result = await pushVoice({
|
|
46573
48307
|
workspaceSlug,
|
|
@@ -46594,7 +48328,7 @@ function makeWatchHandler(deps) {
|
|
|
46594
48328
|
if (suppressor) {
|
|
46595
48329
|
let raw = null;
|
|
46596
48330
|
try {
|
|
46597
|
-
raw =
|
|
48331
|
+
raw = readFileSync30(absolutePath, "utf8");
|
|
46598
48332
|
} catch {
|
|
46599
48333
|
raw = null;
|
|
46600
48334
|
}
|
|
@@ -46617,7 +48351,7 @@ function makeWatchHandler(deps) {
|
|
|
46617
48351
|
now,
|
|
46618
48352
|
output,
|
|
46619
48353
|
suppressor,
|
|
46620
|
-
mutex
|
|
48354
|
+
mutex: mutex2
|
|
46621
48355
|
});
|
|
46622
48356
|
applyState(base2, result.state);
|
|
46623
48357
|
reportArtistMemoryOutcome(spinner, workspaceSlug, result.state, result.outcome);
|
|
@@ -46640,7 +48374,7 @@ function makeWatchHandler(deps) {
|
|
|
46640
48374
|
const strategy = getStrategy(classifiedFrom.entityType);
|
|
46641
48375
|
let toRaw = null;
|
|
46642
48376
|
try {
|
|
46643
|
-
toRaw =
|
|
48377
|
+
toRaw = readFileSync30(event.toPath, "utf8");
|
|
46644
48378
|
} catch {
|
|
46645
48379
|
toRaw = null;
|
|
46646
48380
|
}
|
|
@@ -46667,7 +48401,7 @@ function makeWatchHandler(deps) {
|
|
|
46667
48401
|
deps: {
|
|
46668
48402
|
fetchImpl,
|
|
46669
48403
|
now,
|
|
46670
|
-
mutex,
|
|
48404
|
+
mutex: mutex2,
|
|
46671
48405
|
output,
|
|
46672
48406
|
echo: suppressor ? { watcher: "live", suppressor } : undefined
|
|
46673
48407
|
}
|
|
@@ -46709,7 +48443,7 @@ function makeWatchHandler(deps) {
|
|
|
46709
48443
|
if (event.kind === "upsert") {
|
|
46710
48444
|
try {
|
|
46711
48445
|
ownEcho = suppressor?.shouldSuppressUpsert(absolutePath, {
|
|
46712
|
-
bodyHash: stableHash(
|
|
48446
|
+
bodyHash: stableHash(readFileSync30(absolutePath, "utf8")),
|
|
46713
48447
|
frontmatterHash: ""
|
|
46714
48448
|
}) === true;
|
|
46715
48449
|
} catch {
|
|
@@ -46764,7 +48498,7 @@ function makeWatchHandler(deps) {
|
|
|
46764
48498
|
if (strategy) {
|
|
46765
48499
|
let raw = null;
|
|
46766
48500
|
try {
|
|
46767
|
-
raw =
|
|
48501
|
+
raw = readFileSync30(absolutePath, "utf8");
|
|
46768
48502
|
} catch {
|
|
46769
48503
|
raw = null;
|
|
46770
48504
|
}
|
|
@@ -46774,7 +48508,7 @@ function makeWatchHandler(deps) {
|
|
|
46774
48508
|
}
|
|
46775
48509
|
}
|
|
46776
48510
|
}
|
|
46777
|
-
if (event.kind === "unlink" &&
|
|
48511
|
+
if (event.kind === "unlink" && existsSync10(absolutePath)) {
|
|
46778
48512
|
output?.log(entityDeleteSuppressedLine({
|
|
46779
48513
|
entityType: classified.entityType,
|
|
46780
48514
|
slug: classified.entitySlug,
|
|
@@ -46813,7 +48547,7 @@ function makeWatchHandler(deps) {
|
|
|
46813
48547
|
deps: {
|
|
46814
48548
|
fetchImpl,
|
|
46815
48549
|
now,
|
|
46816
|
-
mutex,
|
|
48550
|
+
mutex: mutex2,
|
|
46817
48551
|
output,
|
|
46818
48552
|
suppressor,
|
|
46819
48553
|
echo: suppressor ? { watcher: "live", suppressor } : undefined
|
|
@@ -47235,7 +48969,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47235
48969
|
}
|
|
47236
48970
|
const spinner = (deps.spinnerFactory ?? (() => createSpinner({ noColor: deps.noColor })))();
|
|
47237
48971
|
spinner.start(`Watching ${slug} · ${watchingSummary(state)}`);
|
|
47238
|
-
const
|
|
48972
|
+
const mutex2 = new PerIdMutex;
|
|
47239
48973
|
const bootDrain = await drainResolvedBatches({
|
|
47240
48974
|
echo: { watcher: "none" },
|
|
47241
48975
|
workspaceSlug: slug,
|
|
@@ -47247,7 +48981,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47247
48981
|
},
|
|
47248
48982
|
fetchImpl,
|
|
47249
48983
|
now: deps.now,
|
|
47250
|
-
mutex,
|
|
48984
|
+
mutex: mutex2,
|
|
47251
48985
|
output: out
|
|
47252
48986
|
});
|
|
47253
48987
|
if (bootDrain.applied > 0 || bootDrain.discarded > 0) {
|
|
@@ -47265,7 +48999,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47265
48999
|
},
|
|
47266
49000
|
fetchImpl,
|
|
47267
49001
|
now: deps.now,
|
|
47268
|
-
mutex,
|
|
49002
|
+
mutex: mutex2,
|
|
47269
49003
|
output: out
|
|
47270
49004
|
});
|
|
47271
49005
|
if (bootUnsynced.attempted > 0) {
|
|
@@ -47317,7 +49051,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47317
49051
|
applyState: (base2, next) => {
|
|
47318
49052
|
state = rebaseState(state, base2, next);
|
|
47319
49053
|
},
|
|
47320
|
-
mutex,
|
|
49054
|
+
mutex: mutex2,
|
|
47321
49055
|
fetchImpl,
|
|
47322
49056
|
now: deps.now,
|
|
47323
49057
|
output: out,
|
|
@@ -47381,7 +49115,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47381
49115
|
applyState: (base2, next) => {
|
|
47382
49116
|
state = rebaseState(state, base2, next);
|
|
47383
49117
|
},
|
|
47384
|
-
mutex,
|
|
49118
|
+
mutex: mutex2,
|
|
47385
49119
|
fetchImpl,
|
|
47386
49120
|
now: deps.now,
|
|
47387
49121
|
output: out,
|
|
@@ -47433,7 +49167,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47433
49167
|
baseUrl: entry2.api_base_url,
|
|
47434
49168
|
pat,
|
|
47435
49169
|
spinner,
|
|
47436
|
-
mutex,
|
|
49170
|
+
mutex: mutex2,
|
|
47437
49171
|
getState: () => state,
|
|
47438
49172
|
applyState: (base2, next) => {
|
|
47439
49173
|
state = rebaseState(state, base2, next);
|
|
@@ -47497,7 +49231,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47497
49231
|
workspaceSlug: slug,
|
|
47498
49232
|
baseUrl: entry2.api_base_url,
|
|
47499
49233
|
pat,
|
|
47500
|
-
mutex,
|
|
49234
|
+
mutex: mutex2,
|
|
47501
49235
|
commitState,
|
|
47502
49236
|
getState: () => state,
|
|
47503
49237
|
setState: setDaemonState,
|
|
@@ -47525,7 +49259,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47525
49259
|
},
|
|
47526
49260
|
fetchImpl,
|
|
47527
49261
|
now: deps.now,
|
|
47528
|
-
mutex,
|
|
49262
|
+
mutex: mutex2,
|
|
47529
49263
|
output: out
|
|
47530
49264
|
});
|
|
47531
49265
|
if (drained.applied > 0 || drained.discarded > 0) {
|
|
@@ -47543,7 +49277,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47543
49277
|
},
|
|
47544
49278
|
fetchImpl,
|
|
47545
49279
|
now: deps.now,
|
|
47546
|
-
mutex,
|
|
49280
|
+
mutex: mutex2,
|
|
47547
49281
|
output: out
|
|
47548
49282
|
});
|
|
47549
49283
|
if (redriven.resolved > 0) {
|
|
@@ -47555,7 +49289,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47555
49289
|
spinner.update(`Watching ${slug} · ${watchingSummary(state)}`);
|
|
47556
49290
|
}
|
|
47557
49291
|
let rulesBase = state;
|
|
47558
|
-
const rulesRecon = await
|
|
49292
|
+
const rulesRecon = await mutex2.run(WORKSPACE_RULES_MUTEX_KEY, () => {
|
|
47559
49293
|
rulesBase = state;
|
|
47560
49294
|
return reconcileWorkspaceRules({
|
|
47561
49295
|
workspaceSlug: slug,
|
|
@@ -47571,7 +49305,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47571
49305
|
state = rebaseState(state, rulesBase, rulesRecon.state);
|
|
47572
49306
|
reportSingletonReconcile(spinner, slug, state, WORKSPACE_RULES_FILENAME, rulesRecon.outcome);
|
|
47573
49307
|
let memoryBase = state;
|
|
47574
|
-
const memoryRecon = await
|
|
49308
|
+
const memoryRecon = await mutex2.run(WORKSPACE_MEMORY_MUTEX_KEY, () => {
|
|
47575
49309
|
memoryBase = state;
|
|
47576
49310
|
return reconcileWorkspaceMemory({
|
|
47577
49311
|
workspaceSlug: slug,
|
|
@@ -47587,7 +49321,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47587
49321
|
state = rebaseState(state, memoryBase, memoryRecon.state);
|
|
47588
49322
|
reportSingletonReconcile(spinner, slug, state, WORKSPACE_MEMORY_FILENAME, memoryRecon.outcome);
|
|
47589
49323
|
let userMemoryBase = state;
|
|
47590
|
-
const userMemoryRecon = await
|
|
49324
|
+
const userMemoryRecon = await mutex2.run(USER_MEMORY_MUTEX_KEY, () => {
|
|
47591
49325
|
userMemoryBase = state;
|
|
47592
49326
|
return reconcileUserMemory({
|
|
47593
49327
|
workspaceSlug: slug,
|
|
@@ -47604,7 +49338,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47604
49338
|
reportSingletonReconcile(spinner, slug, state, USER_MEMORY_FILENAME, userMemoryRecon.outcome);
|
|
47605
49339
|
try {
|
|
47606
49340
|
let voiceBase = state;
|
|
47607
|
-
const voiceRecon = await
|
|
49341
|
+
const voiceRecon = await mutex2.run(VOICE_MUTEX_KEY, () => {
|
|
47608
49342
|
voiceBase = state;
|
|
47609
49343
|
return reconcileVoice({
|
|
47610
49344
|
workspaceSlug: slug,
|
|
@@ -47635,7 +49369,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47635
49369
|
now: deps.now,
|
|
47636
49370
|
output: out,
|
|
47637
49371
|
suppressor,
|
|
47638
|
-
mutex
|
|
49372
|
+
mutex: mutex2
|
|
47639
49373
|
});
|
|
47640
49374
|
state = rebaseState(state, memBase, memRecon.state);
|
|
47641
49375
|
if (memRecon.pushed > 0) {
|
|
@@ -48291,12 +50025,12 @@ function assessContextWalls(workspaceSlug, deps = {}) {
|
|
|
48291
50025
|
|
|
48292
50026
|
// src/lib/build-stamp.ts
|
|
48293
50027
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
48294
|
-
import { existsSync as
|
|
48295
|
-
import { dirname as
|
|
50028
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as readFileSync31, writeFileSync as writeFileSync8 } from "node:fs";
|
|
50029
|
+
import { dirname as dirname14, join as join19 } from "node:path";
|
|
48296
50030
|
import { fileURLToPath } from "node:url";
|
|
48297
50031
|
var BUILD_STAMP_FILENAME = "build-stamp.json";
|
|
48298
50032
|
function stampPath(distDir) {
|
|
48299
|
-
return
|
|
50033
|
+
return join19(distDir, BUILD_STAMP_FILENAME);
|
|
48300
50034
|
}
|
|
48301
50035
|
function readHead(repoDir) {
|
|
48302
50036
|
try {
|
|
@@ -48315,7 +50049,7 @@ function readHead(repoDir) {
|
|
|
48315
50049
|
}
|
|
48316
50050
|
function readBuildStamp(distDir) {
|
|
48317
50051
|
try {
|
|
48318
|
-
const parsed = JSON.parse(
|
|
50052
|
+
const parsed = JSON.parse(readFileSync31(stampPath(distDir), "utf8"));
|
|
48319
50053
|
if (parsed && typeof parsed.sha === "string" && parsed.sha.length > 0 && typeof parsed.dirty === "boolean" && typeof parsed.built_at === "string") {
|
|
48320
50054
|
return { sha: parsed.sha, dirty: parsed.dirty, built_at: parsed.built_at };
|
|
48321
50055
|
}
|
|
@@ -48327,9 +50061,9 @@ function readBuildStamp(distDir) {
|
|
|
48327
50061
|
function findRepoRoot(fromDir) {
|
|
48328
50062
|
let dir = fromDir;
|
|
48329
50063
|
for (;; ) {
|
|
48330
|
-
if (
|
|
50064
|
+
if (existsSync11(join19(dir, ".git")))
|
|
48331
50065
|
return dir;
|
|
48332
|
-
const parent =
|
|
50066
|
+
const parent = dirname14(dir);
|
|
48333
50067
|
if (parent === dir)
|
|
48334
50068
|
return null;
|
|
48335
50069
|
dir = parent;
|
|
@@ -48353,7 +50087,7 @@ function checkBuildStaleness(deps = {}) {
|
|
|
48353
50087
|
const metaUrl = deps.metaUrl ?? import.meta.url;
|
|
48354
50088
|
let distDir;
|
|
48355
50089
|
try {
|
|
48356
|
-
distDir =
|
|
50090
|
+
distDir = dirname14(fileURLToPath(metaUrl));
|
|
48357
50091
|
} catch {
|
|
48358
50092
|
return { kind: "unknown" };
|
|
48359
50093
|
}
|
|
@@ -48376,12 +50110,12 @@ function checkBuildStaleness(deps = {}) {
|
|
|
48376
50110
|
}
|
|
48377
50111
|
|
|
48378
50112
|
// src/lib/memory-links.ts
|
|
48379
|
-
import { readFileSync as
|
|
50113
|
+
import { readFileSync as readFileSync32 } from "node:fs";
|
|
48380
50114
|
import { relative as relative15, sep as sep8 } from "node:path";
|
|
48381
50115
|
var MEMORY_FILE_RE = /^(workspace-memory\.md|user-memory\.md|artists\/[^/]+\/memory\.md|memory\/[^/]+\.md|memory\/private\/[^/]+\.md)$/i;
|
|
48382
50116
|
function scanMemoryLinks(deps) {
|
|
48383
50117
|
const listFiles = deps.listFiles ?? ((r) => walkSubtreeFiles(r, isVcsInternal));
|
|
48384
|
-
const readFile = deps.readFile ?? ((p) =>
|
|
50118
|
+
const readFile = deps.readFile ?? ((p) => readFileSync32(p, "utf8"));
|
|
48385
50119
|
let root;
|
|
48386
50120
|
let absPaths;
|
|
48387
50121
|
try {
|
|
@@ -48425,7 +50159,7 @@ function scanMemoryLinks(deps) {
|
|
|
48425
50159
|
init_output();
|
|
48426
50160
|
|
|
48427
50161
|
// src/lib/pending-writes.ts
|
|
48428
|
-
import { readFileSync as
|
|
50162
|
+
import { readFileSync as readFileSync33, statSync as statSync19 } from "node:fs";
|
|
48429
50163
|
import { relative as relative16 } from "node:path";
|
|
48430
50164
|
var import_gray_matter15 = __toESM(require_gray_matter(), 1);
|
|
48431
50165
|
var EMPTY3 = { writes: [], count: 0 };
|
|
@@ -48473,7 +50207,7 @@ function scanPendingWrites(args) {
|
|
|
48473
50207
|
continue;
|
|
48474
50208
|
let hash2;
|
|
48475
50209
|
try {
|
|
48476
|
-
hash2 = sha256Bytes(
|
|
50210
|
+
hash2 = sha256Bytes(readFileSync33(absolutePath));
|
|
48477
50211
|
} catch {
|
|
48478
50212
|
continue;
|
|
48479
50213
|
}
|
|
@@ -48504,7 +50238,7 @@ function scanPendingWrites(args) {
|
|
|
48504
50238
|
continue;
|
|
48505
50239
|
let content;
|
|
48506
50240
|
try {
|
|
48507
|
-
content =
|
|
50241
|
+
content = readFileSync33(absolutePath, "utf8");
|
|
48508
50242
|
} catch {
|
|
48509
50243
|
continue;
|
|
48510
50244
|
}
|
|
@@ -48834,6 +50568,33 @@ async function runStatus(args, deps = {}) {
|
|
|
48834
50568
|
}
|
|
48835
50569
|
}
|
|
48836
50570
|
out.info("");
|
|
50571
|
+
let deletionJournal = null;
|
|
50572
|
+
let deletionJournalError = null;
|
|
50573
|
+
try {
|
|
50574
|
+
deletionJournal = loadPermanentDeletionJournal(slug);
|
|
50575
|
+
} catch (error52) {
|
|
50576
|
+
deletionJournalError = error52 instanceof Error ? error52.message : String(error52);
|
|
50577
|
+
}
|
|
50578
|
+
if (deletionJournalError) {
|
|
50579
|
+
out.info(row("Deletion", "BLOCKED — permanent deletion journal needs recovery."));
|
|
50580
|
+
out.info(cont(`${permanentDeletionJournalPath(slug)}: ${deletionJournalError}`));
|
|
50581
|
+
} else {
|
|
50582
|
+
const pending2 = deletionJournal.records.filter((record2) => record2.disposition === "pending");
|
|
50583
|
+
const refresh = deletionJournal.refresh;
|
|
50584
|
+
const operations = deletionJournal.records.filter((record2) => record2.operation?.state === "pending" || record2.operation?.state === "transport_unknown");
|
|
50585
|
+
if (pending2.length === 0 && refresh.length === 0 && operations.length === 0) {
|
|
50586
|
+
out.info(row("Deletion", "No permanent deletion reconciliation pending."));
|
|
50587
|
+
} else {
|
|
50588
|
+
out.info(row("Deletion", `${pending2.length} disposition${pending2.length === 1 ? "" : "s"} pending; ` + `${refresh.length} feed refresh${refresh.length === 1 ? "" : "es"} required; ` + `${operations.length} create operation${operations.length === 1 ? "" : "s"} unresolved.`));
|
|
50589
|
+
for (const marker of refresh.slice(0, 5)) {
|
|
50590
|
+
out.info(cont(`${marker.kind}:${marker.logical_key} — ${marker.state}` + (marker.retry_after ? `, retry after ${marker.retry_after}` : "")));
|
|
50591
|
+
}
|
|
50592
|
+
for (const record2 of operations.slice(0, 5)) {
|
|
50593
|
+
out.info(cont(`${record2.kind}:${record2.logical_key} — create ${record2.operation.state}`));
|
|
50594
|
+
}
|
|
50595
|
+
}
|
|
50596
|
+
}
|
|
50597
|
+
out.info("");
|
|
48837
50598
|
const held = listPendingBatches(slug).filter((b) => b.status === "pending");
|
|
48838
50599
|
if (held.length === 0) {
|
|
48839
50600
|
out.info(row("Pending", "No destructive batches held."));
|
|
@@ -49388,7 +51149,7 @@ var versionCommand = defineCommand({
|
|
|
49388
51149
|
});
|
|
49389
51150
|
|
|
49390
51151
|
// src/commands/voice-gate.ts
|
|
49391
|
-
import { readFileSync as
|
|
51152
|
+
import { readFileSync as readFileSync36 } from "node:fs";
|
|
49392
51153
|
|
|
49393
51154
|
// src/lib/voice-gate/api.ts
|
|
49394
51155
|
var VoiceGateSurfaces = [
|
|
@@ -49490,15 +51251,15 @@ async function callVoiceGate(req) {
|
|
|
49490
51251
|
}
|
|
49491
51252
|
|
|
49492
51253
|
// src/lib/voice-gate/attempts.ts
|
|
49493
|
-
import { createHash as
|
|
51254
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
49494
51255
|
import {
|
|
49495
|
-
mkdirSync as
|
|
49496
|
-
readFileSync as
|
|
49497
|
-
renameSync as
|
|
49498
|
-
unlinkSync as
|
|
51256
|
+
mkdirSync as mkdirSync13,
|
|
51257
|
+
readFileSync as readFileSync34,
|
|
51258
|
+
renameSync as renameSync9,
|
|
51259
|
+
unlinkSync as unlinkSync10,
|
|
49499
51260
|
writeFileSync as writeFileSync9
|
|
49500
51261
|
} from "node:fs";
|
|
49501
|
-
import { join as
|
|
51262
|
+
import { join as join20 } from "node:path";
|
|
49502
51263
|
var MAX_REGENS = 2;
|
|
49503
51264
|
var ENTRY_TTL_MS = 24 * 60 * 60 * 1000;
|
|
49504
51265
|
var AttemptEntrySchema = exports_external.object({
|
|
@@ -49511,20 +51272,20 @@ var AttemptsFileSchema = exports_external.object({
|
|
|
49511
51272
|
entries: exports_external.record(exports_external.string(), AttemptEntrySchema)
|
|
49512
51273
|
});
|
|
49513
51274
|
function attemptsDir(workspaceSlug) {
|
|
49514
|
-
return
|
|
51275
|
+
return join20(plinthHome(), "_voice_gate", workspaceSlug);
|
|
49515
51276
|
}
|
|
49516
51277
|
function attemptsPath(workspaceSlug) {
|
|
49517
|
-
return
|
|
51278
|
+
return join20(attemptsDir(workspaceSlug), "attempts.json");
|
|
49518
51279
|
}
|
|
49519
51280
|
function attemptKey(identity) {
|
|
49520
|
-
return
|
|
51281
|
+
return createHash7("sha256").update(identity, "utf8").digest("hex").slice(0, 32);
|
|
49521
51282
|
}
|
|
49522
51283
|
function emptyFile() {
|
|
49523
51284
|
return { version: 1, entries: {} };
|
|
49524
51285
|
}
|
|
49525
51286
|
function load2(workspaceSlug) {
|
|
49526
51287
|
try {
|
|
49527
|
-
const raw =
|
|
51288
|
+
const raw = readFileSync34(attemptsPath(workspaceSlug), "utf8");
|
|
49528
51289
|
const parsed = AttemptsFileSchema.safeParse(JSON.parse(raw));
|
|
49529
51290
|
return parsed.success ? parsed.data : emptyFile();
|
|
49530
51291
|
} catch {
|
|
@@ -49544,14 +51305,14 @@ function save(workspaceSlug, file2) {
|
|
|
49544
51305
|
const dir = attemptsDir(workspaceSlug);
|
|
49545
51306
|
const target = attemptsPath(workspaceSlug);
|
|
49546
51307
|
const tmp = `${target}.${process.pid}.${tmpCounter2++}.tmp`;
|
|
49547
|
-
|
|
51308
|
+
mkdirSync13(dir, { recursive: true, mode: 448 });
|
|
49548
51309
|
writeFileSync9(tmp, `${JSON.stringify(file2, null, 2)}
|
|
49549
51310
|
`, { mode: 384 });
|
|
49550
51311
|
try {
|
|
49551
|
-
|
|
51312
|
+
renameSync9(tmp, target);
|
|
49552
51313
|
} catch (err) {
|
|
49553
51314
|
try {
|
|
49554
|
-
|
|
51315
|
+
unlinkSync10(tmp);
|
|
49555
51316
|
} catch {}
|
|
49556
51317
|
throw err;
|
|
49557
51318
|
}
|
|
@@ -49687,7 +51448,7 @@ function denyUngated(detail) {
|
|
|
49687
51448
|
}
|
|
49688
51449
|
|
|
49689
51450
|
// src/lib/voice-gate/payload.ts
|
|
49690
|
-
import { readFileSync as
|
|
51451
|
+
import { readFileSync as readFileSync35, statSync as statSync21 } from "node:fs";
|
|
49691
51452
|
import { homedir as homedir2 } from "node:os";
|
|
49692
51453
|
import { isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
|
|
49693
51454
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
@@ -49848,7 +51609,7 @@ function defaultReadBodyFile(path5) {
|
|
|
49848
51609
|
if (stat4.size > MAX_BODY_BYTES) {
|
|
49849
51610
|
throw new Error(`draft body is ${stat4.size} bytes, over the ${MAX_BODY_BYTES}-byte gate limit`);
|
|
49850
51611
|
}
|
|
49851
|
-
return
|
|
51612
|
+
return readFileSync35(path5, "utf8");
|
|
49852
51613
|
}
|
|
49853
51614
|
function parseRecipients(...raw) {
|
|
49854
51615
|
const all = raw.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0);
|
|
@@ -50208,7 +51969,7 @@ async function runVoiceGateDirect(args, deps = {}) {
|
|
|
50208
51969
|
let text;
|
|
50209
51970
|
if (args.file) {
|
|
50210
51971
|
try {
|
|
50211
|
-
text =
|
|
51972
|
+
text = readFileSync36(args.file, "utf8");
|
|
50212
51973
|
} catch (err) {
|
|
50213
51974
|
const msg = err instanceof Error ? err.message : "unknown error";
|
|
50214
51975
|
out.error(`Could not read ${args.file}: ${msg}`);
|