@plinth-music/cli 0.23.1 → 0.23.3
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 +91 -0
- package/dist/build-stamp.json +2 -2
- package/dist/cli.js +1633 -663
- package/package.json +4 -1
package/dist/cli.js
CHANGED
|
@@ -4671,7 +4671,7 @@ var require_omap = __commonJS((exports, module) => {
|
|
|
4671
4671
|
function resolveYamlOmap(data) {
|
|
4672
4672
|
if (data === null)
|
|
4673
4673
|
return true;
|
|
4674
|
-
var objectKeys =
|
|
4674
|
+
var objectKeys = {}, index, length, pair, pairKey, pairHasKey, object = data;
|
|
4675
4675
|
for (index = 0, length = object.length;index < length; index += 1) {
|
|
4676
4676
|
pair = object[index];
|
|
4677
4677
|
pairHasKey = false;
|
|
@@ -4687,10 +4687,9 @@ var require_omap = __commonJS((exports, module) => {
|
|
|
4687
4687
|
}
|
|
4688
4688
|
if (!pairHasKey)
|
|
4689
4689
|
return false;
|
|
4690
|
-
if (
|
|
4691
|
-
objectKeys.push(pairKey);
|
|
4692
|
-
else
|
|
4690
|
+
if (_hasOwnProperty.call(objectKeys, pairKey))
|
|
4693
4691
|
return false;
|
|
4692
|
+
Object.defineProperty(objectKeys, pairKey, { value: true });
|
|
4694
4693
|
}
|
|
4695
4694
|
return true;
|
|
4696
4695
|
}
|
|
@@ -5033,6 +5032,7 @@ var require_loader = __commonJS((exports, module) => {
|
|
|
5033
5032
|
this.legacy = options2["legacy"] || false;
|
|
5034
5033
|
this.json = options2["json"] || false;
|
|
5035
5034
|
this.listener = options2["listener"] || null;
|
|
5035
|
+
this.maxTotalMergeKeys = typeof options2["maxTotalMergeKeys"] === "number" ? options2["maxTotalMergeKeys"] : 1e4;
|
|
5036
5036
|
this.implicitTypes = this.schema.compiledImplicit;
|
|
5037
5037
|
this.typeMap = this.schema.compiledTypeMap;
|
|
5038
5038
|
this.length = input.length;
|
|
@@ -5040,6 +5040,7 @@ var require_loader = __commonJS((exports, module) => {
|
|
|
5040
5040
|
this.line = 0;
|
|
5041
5041
|
this.lineStart = 0;
|
|
5042
5042
|
this.lineIndent = 0;
|
|
5043
|
+
this.totalMergeKeys = 0;
|
|
5043
5044
|
this.documents = [];
|
|
5044
5045
|
}
|
|
5045
5046
|
function generateError(state, message) {
|
|
@@ -5113,14 +5114,22 @@ var require_loader = __commonJS((exports, module) => {
|
|
|
5113
5114
|
state.result += _result;
|
|
5114
5115
|
}
|
|
5115
5116
|
}
|
|
5117
|
+
function chargeMergeWork(state) {
|
|
5118
|
+
state.totalMergeKeys += 1;
|
|
5119
|
+
if (state.maxTotalMergeKeys !== -1 && state.totalMergeKeys > state.maxTotalMergeKeys) {
|
|
5120
|
+
throwError(state, "merge keys exceeded maxTotalMergeKeys (" + state.maxTotalMergeKeys + ")");
|
|
5121
|
+
}
|
|
5122
|
+
}
|
|
5116
5123
|
function mergeMappings(state, destination, source, overridableKeys) {
|
|
5117
5124
|
var sourceKeys, key, index, quantity;
|
|
5118
5125
|
if (!common.isObject(source)) {
|
|
5119
5126
|
throwError(state, "cannot merge mappings; the provided source object is unacceptable");
|
|
5120
5127
|
}
|
|
5128
|
+
chargeMergeWork(state);
|
|
5121
5129
|
sourceKeys = Object.keys(source);
|
|
5122
5130
|
for (index = 0, quantity = sourceKeys.length;index < quantity; index += 1) {
|
|
5123
5131
|
key = sourceKeys[index];
|
|
5132
|
+
chargeMergeWork(state);
|
|
5124
5133
|
if (!_hasOwnProperty.call(destination, key)) {
|
|
5125
5134
|
setProperty(destination, key, source[key]);
|
|
5126
5135
|
overridableKeys[key] = true;
|
|
@@ -5149,6 +5158,9 @@ var require_loader = __commonJS((exports, module) => {
|
|
|
5149
5158
|
}
|
|
5150
5159
|
if (keyTag === "tag:yaml.org,2002:merge") {
|
|
5151
5160
|
if (Array.isArray(valueNode)) {
|
|
5161
|
+
if (valueNode.length > 100) {
|
|
5162
|
+
throwError(state, "abnormal merge sequence size");
|
|
5163
|
+
}
|
|
5152
5164
|
for (index = 0, quantity = valueNode.length;index < quantity; index += 1) {
|
|
5153
5165
|
mergeMappings(state, _result, valueNode[index], overridableKeys);
|
|
5154
5166
|
}
|
|
@@ -7584,7 +7596,7 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
|
|
|
7584
7596
|
// package.json
|
|
7585
7597
|
var package_default = {
|
|
7586
7598
|
name: "@plinth-music/cli",
|
|
7587
|
-
version: "0.23.
|
|
7599
|
+
version: "0.23.3",
|
|
7588
7600
|
description: "Plinth capstone sync daemon and CLI",
|
|
7589
7601
|
type: "module",
|
|
7590
7602
|
private: false,
|
|
@@ -7628,6 +7640,9 @@ var package_default = {
|
|
|
7628
7640
|
"gray-matter": "^4.0.3",
|
|
7629
7641
|
ora: "^9.4.0",
|
|
7630
7642
|
zod: "^4.4.3"
|
|
7643
|
+
},
|
|
7644
|
+
overrides: {
|
|
7645
|
+
"js-yaml": "3.15.2"
|
|
7631
7646
|
}
|
|
7632
7647
|
};
|
|
7633
7648
|
|
|
@@ -7843,8 +7858,48 @@ function emptyCorpusCounts() {
|
|
|
7843
7858
|
return { nonMarkdown: 0, unreadable: 0 };
|
|
7844
7859
|
}
|
|
7845
7860
|
|
|
7861
|
+
// src/lib/matter.ts
|
|
7862
|
+
var import_gray_matter = __toESM(require_gray_matter(), 1);
|
|
7863
|
+
var FRONTMATTER_IS_CODE = "it is marked as code, which Plinth never runs";
|
|
7864
|
+
var PATCHED = Symbol.for("plinth.matter.engines-before-patch");
|
|
7865
|
+
var marked = (table) => table[PATCHED];
|
|
7866
|
+
var refuse = {
|
|
7867
|
+
parse() {
|
|
7868
|
+
throw new Error(FRONTMATTER_IS_CODE);
|
|
7869
|
+
},
|
|
7870
|
+
stringify() {
|
|
7871
|
+
throw new Error(FRONTMATTER_IS_CODE);
|
|
7872
|
+
}
|
|
7873
|
+
};
|
|
7874
|
+
var engines = import_gray_matter.default.engines;
|
|
7875
|
+
if (marked(engines) === undefined) {
|
|
7876
|
+
const before = Object.keys(engines).sort();
|
|
7877
|
+
const safeLoad = engines.yaml.parse;
|
|
7878
|
+
Object.assign(engines, {
|
|
7879
|
+
javascript: refuse,
|
|
7880
|
+
coffee: refuse,
|
|
7881
|
+
yaml: { parse: (input) => safeLoad(input), stringify: engines.yaml.stringify }
|
|
7882
|
+
});
|
|
7883
|
+
Object.defineProperty(engines, PATCHED, { value: before });
|
|
7884
|
+
}
|
|
7885
|
+
var ENGINES_BEFORE_PATCH = marked(engines);
|
|
7886
|
+
function matter(input, read) {
|
|
7887
|
+
return read === undefined ? import_gray_matter.default(input) : import_gray_matter.default(input, { cache: false });
|
|
7888
|
+
}
|
|
7889
|
+
function stringifyFrontmatter(body, data) {
|
|
7890
|
+
const out = import_gray_matter.default.stringify({ content: body }, data);
|
|
7891
|
+
const bodyOnly = body.endsWith(`
|
|
7892
|
+
`) ? body : `${body}
|
|
7893
|
+
`;
|
|
7894
|
+
if (out === bodyOnly && import_gray_matter.default.test(body.replace(/^/, ""))) {
|
|
7895
|
+
return `---
|
|
7896
|
+
---
|
|
7897
|
+
${bodyOnly}`;
|
|
7898
|
+
}
|
|
7899
|
+
return out;
|
|
7900
|
+
}
|
|
7901
|
+
|
|
7846
7902
|
// src/lib/backlinks/scan.ts
|
|
7847
|
-
var import_gray_matter4 = __toESM(require_gray_matter(), 1);
|
|
7848
7903
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
7849
7904
|
import { relative, sep as sep2 } from "node:path";
|
|
7850
7905
|
|
|
@@ -7860,9 +7915,6 @@ import {
|
|
|
7860
7915
|
} from "node:fs";
|
|
7861
7916
|
import { dirname, join as join2, resolve, sep } from "node:path";
|
|
7862
7917
|
|
|
7863
|
-
// src/lib/entities/_helpers.ts
|
|
7864
|
-
var import_gray_matter = __toESM(require_gray_matter(), 1);
|
|
7865
|
-
|
|
7866
7918
|
// node_modules/zod/v4/classic/external.js
|
|
7867
7919
|
var exports_external = {};
|
|
7868
7920
|
__export(exports_external, {
|
|
@@ -22189,7 +22241,7 @@ function buildFrontmatter(doc2, projectionKeys) {
|
|
|
22189
22241
|
return { ...doc2.frontmatter, ...projection };
|
|
22190
22242
|
}
|
|
22191
22243
|
function serialiseRow(doc2, projectionKeys, bodyAccessor) {
|
|
22192
|
-
const out =
|
|
22244
|
+
const out = stringifyFrontmatter(bodyAccessor(doc2), buildFrontmatter(doc2, projectionKeys));
|
|
22193
22245
|
return out.endsWith(`
|
|
22194
22246
|
`) ? out : out + `
|
|
22195
22247
|
`;
|
|
@@ -22829,7 +22881,6 @@ var documentsStrategy = {
|
|
|
22829
22881
|
};
|
|
22830
22882
|
|
|
22831
22883
|
// src/lib/entities/meetings.ts
|
|
22832
|
-
var import_gray_matter2 = __toESM(require_gray_matter(), 1);
|
|
22833
22884
|
var MeetingLiveSchema = exports_external.object({
|
|
22834
22885
|
...BASE_ROW,
|
|
22835
22886
|
deleted_at: exports_external.null(),
|
|
@@ -22984,7 +23035,7 @@ function serialise(doc2) {
|
|
|
22984
23035
|
decisions: doc2.decisions,
|
|
22985
23036
|
follow_up_summary: doc2.follow_up_summary
|
|
22986
23037
|
});
|
|
22987
|
-
const out =
|
|
23038
|
+
const out = stringifyFrontmatter(body, fm);
|
|
22988
23039
|
return out.endsWith(`
|
|
22989
23040
|
`) ? out : out + `
|
|
22990
23041
|
`;
|
|
@@ -24475,7 +24526,6 @@ function readContextFile(workspaceSlug, relativePath) {
|
|
|
24475
24526
|
}
|
|
24476
24527
|
|
|
24477
24528
|
// src/lib/backlinks/parse.ts
|
|
24478
|
-
var import_gray_matter3 = __toESM(require_gray_matter(), 1);
|
|
24479
24529
|
var RENDERED_BLOCKS = [
|
|
24480
24530
|
{ name: "tasks", begin: TASKS_BLOCK_BEGIN, end: TASKS_BLOCK_END },
|
|
24481
24531
|
{ name: "comments", begin: COMMENTS_BLOCK_BEGIN, end: COMMENTS_BLOCK_END },
|
|
@@ -24557,7 +24607,7 @@ function frontmatterLinks(body) {
|
|
|
24557
24607
|
return null;
|
|
24558
24608
|
let data;
|
|
24559
24609
|
try {
|
|
24560
|
-
data =
|
|
24610
|
+
data = matter(body).data;
|
|
24561
24611
|
} catch {
|
|
24562
24612
|
return null;
|
|
24563
24613
|
}
|
|
@@ -24825,7 +24875,7 @@ function buildArtistIndex(relPaths, root, readFile) {
|
|
|
24825
24875
|
const slug = m[1];
|
|
24826
24876
|
let name = null;
|
|
24827
24877
|
try {
|
|
24828
|
-
const raw =
|
|
24878
|
+
const raw = matter(readFile(`${root}${sep2}${rel}`)).data;
|
|
24829
24879
|
if (typeof raw.name === "string" && raw.name.trim().length > 0) {
|
|
24830
24880
|
name = raw.name.trim();
|
|
24831
24881
|
}
|
|
@@ -25634,7 +25684,8 @@ var TripReasonSchema = exports_external.object({
|
|
|
25634
25684
|
entityType: exports_external.string().optional(),
|
|
25635
25685
|
typeCount: exports_external.number().int().nonnegative().optional(),
|
|
25636
25686
|
typePopulation: exports_external.number().int().nonnegative().optional(),
|
|
25637
|
-
pct: exports_external.number().optional()
|
|
25687
|
+
pct: exports_external.number().optional(),
|
|
25688
|
+
loneRemoved: exports_external.number().int().nonnegative().optional()
|
|
25638
25689
|
});
|
|
25639
25690
|
var PendingBatchFileSchema = exports_external.object({
|
|
25640
25691
|
version: exports_external.literal(1),
|
|
@@ -25743,6 +25794,9 @@ function describeTrip(reason) {
|
|
|
25743
25794
|
const pct = reason.pct !== undefined ? ` (${Math.round(reason.pct * 100)}%)` : "";
|
|
25744
25795
|
return `${reason.typeCount}/${reason.typePopulation} ${reason.entityType}${pct} — proportion threshold`;
|
|
25745
25796
|
}
|
|
25797
|
+
if (reason.loneRemoved !== undefined) {
|
|
25798
|
+
return reason.count === 1 ? "1 large overwrite — size threshold" : `${reason.count} destructive ops, including a large overwrite — size threshold`;
|
|
25799
|
+
}
|
|
25746
25800
|
return `${reason.count} destructive ops — count threshold`;
|
|
25747
25801
|
}
|
|
25748
25802
|
function formatBytes(n) {
|
|
@@ -26365,6 +26419,23 @@ var RejectedWriteSchema = exports_external.object({
|
|
|
26365
26419
|
max_bytes: exports_external.number().int().nonnegative().optional(),
|
|
26366
26420
|
reported_at: exports_external.string().datetime({ offset: true }).optional()
|
|
26367
26421
|
});
|
|
26422
|
+
var FilePullHoldSchema = exports_external.object({
|
|
26423
|
+
target: exports_external.string().min(1),
|
|
26424
|
+
path: exports_external.string().min(1),
|
|
26425
|
+
kind: exports_external.string().min(1),
|
|
26426
|
+
row_id: exports_external.string().min(1),
|
|
26427
|
+
row_updated_at: exports_external.string().min(1),
|
|
26428
|
+
row_hash: exports_external.string().min(1),
|
|
26429
|
+
reason: exports_external.string().optional(),
|
|
26430
|
+
code: exports_external.string().optional(),
|
|
26431
|
+
local: exports_external.object({
|
|
26432
|
+
size: exports_external.number().int().nonnegative(),
|
|
26433
|
+
mtime_ms: exports_external.number(),
|
|
26434
|
+
ino: exports_external.number().int().nonnegative()
|
|
26435
|
+
}).nullable().optional(),
|
|
26436
|
+
attempts: exports_external.number().int().positive(),
|
|
26437
|
+
next_retry_at: exports_external.string().optional()
|
|
26438
|
+
});
|
|
26368
26439
|
var TeamFileRejectionSchema = exports_external.object({
|
|
26369
26440
|
path: exports_external.string().min(1),
|
|
26370
26441
|
reason: exports_external.string().min(1),
|
|
@@ -26392,6 +26463,7 @@ var WorkspaceStateV2Schema = exports_external.object({
|
|
|
26392
26463
|
files: FileEntriesSchema.optional(),
|
|
26393
26464
|
unsynced: exports_external.array(UnsyncedOpSchema).optional(),
|
|
26394
26465
|
rejected: exports_external.array(RejectedWriteSchema).optional(),
|
|
26466
|
+
file_pull_holds: exports_external.array(FilePullHoldSchema).optional(),
|
|
26395
26467
|
workspace_rules: WorkspaceRulesStateSchema.optional(),
|
|
26396
26468
|
workspace_memory: WorkspaceMemoryStateSchema.optional(),
|
|
26397
26469
|
user_memory: UserMemoryStateSchema.optional(),
|
|
@@ -26487,25 +26559,50 @@ function isLaterVersion(a, b) {
|
|
|
26487
26559
|
return ta > tb;
|
|
26488
26560
|
}
|
|
26489
26561
|
function committedEntryIfNewer(workspaceSlug, entityType, entry) {
|
|
26562
|
+
const committed = readCommittedEntry(workspaceSlug, entityType, entry.id);
|
|
26563
|
+
return isLaterVersion(committed, entry) ? committed : undefined;
|
|
26564
|
+
}
|
|
26565
|
+
function readCommittedEntry(workspaceSlug, entityType, id) {
|
|
26566
|
+
const parsed = readCommittedJson(workspaceSlug);
|
|
26567
|
+
const entries = parsed?.entries;
|
|
26568
|
+
const committed = entries?.[entityType]?.[id];
|
|
26569
|
+
if (committed === undefined)
|
|
26570
|
+
return;
|
|
26571
|
+
const valid = StateEntrySchema.safeParse(committed);
|
|
26572
|
+
return valid.success ? valid.data : undefined;
|
|
26573
|
+
}
|
|
26574
|
+
function readCommittedFileEntry(workspaceSlug, relPath) {
|
|
26575
|
+
const read = readCommittedFileAck(workspaceSlug, relPath);
|
|
26576
|
+
return read.kind === "entry" ? read.entry : undefined;
|
|
26577
|
+
}
|
|
26578
|
+
function readCommittedFileAck(workspaceSlug, relPath) {
|
|
26579
|
+
const json2 = readCommittedJson(workspaceSlug);
|
|
26580
|
+
if (!json2)
|
|
26581
|
+
return { kind: "unknown" };
|
|
26582
|
+
const files = json2.files;
|
|
26583
|
+
if (files === undefined)
|
|
26584
|
+
return { kind: "absent" };
|
|
26585
|
+
if (files === null || typeof files !== "object")
|
|
26586
|
+
return { kind: "unknown" };
|
|
26587
|
+
const committed = files[relPath];
|
|
26588
|
+
if (committed === undefined)
|
|
26589
|
+
return { kind: "absent" };
|
|
26590
|
+
const valid = FileStateEntrySchema.safeParse(committed);
|
|
26591
|
+
return valid.success ? { kind: "entry", entry: valid.data } : { kind: "unknown" };
|
|
26592
|
+
}
|
|
26593
|
+
function readCommittedJson(workspaceSlug) {
|
|
26490
26594
|
let raw;
|
|
26491
26595
|
try {
|
|
26492
26596
|
raw = readFileSync7(stateFilePath(workspaceSlug), "utf8");
|
|
26493
26597
|
} catch {
|
|
26494
26598
|
return;
|
|
26495
26599
|
}
|
|
26496
|
-
let committed;
|
|
26497
26600
|
try {
|
|
26498
26601
|
const parsed = JSON.parse(raw);
|
|
26499
|
-
|
|
26602
|
+
return parsed !== null && typeof parsed === "object" ? parsed : undefined;
|
|
26500
26603
|
} catch {
|
|
26501
26604
|
return;
|
|
26502
26605
|
}
|
|
26503
|
-
if (committed === undefined)
|
|
26504
|
-
return;
|
|
26505
|
-
const valid = StateEntrySchema.safeParse(committed);
|
|
26506
|
-
if (!valid.success)
|
|
26507
|
-
return;
|
|
26508
|
-
return isLaterVersion(valid.data, entry) ? valid.data : undefined;
|
|
26509
26606
|
}
|
|
26510
26607
|
function mergeKey(fresh, before, after) {
|
|
26511
26608
|
if (before === after)
|
|
@@ -26612,6 +26709,7 @@ function mergeWorkspaceState(fresh, before, after, wantCorrected = true) {
|
|
|
26612
26709
|
files: mergedFiles,
|
|
26613
26710
|
unsynced: mergedList(fresh.unsynced, before.unsynced, after.unsynced),
|
|
26614
26711
|
rejected: mergedList(fresh.rejected, before.rejected, after.rejected),
|
|
26712
|
+
file_pull_holds: mergedList(fresh.file_pull_holds, before.file_pull_holds, after.file_pull_holds),
|
|
26615
26713
|
workspace_rules: rules.value,
|
|
26616
26714
|
workspace_memory: memory.value,
|
|
26617
26715
|
user_memory: userMemory.value,
|
|
@@ -26749,6 +26847,27 @@ function removeFileEntry(state, path) {
|
|
|
26749
26847
|
delete nextFiles[path];
|
|
26750
26848
|
return { state: { ...state, files: nextFiles }, changed: true };
|
|
26751
26849
|
}
|
|
26850
|
+
function listFilePullHolds(state) {
|
|
26851
|
+
return (state.file_pull_holds ?? []).filter((h) => h.target === "file");
|
|
26852
|
+
}
|
|
26853
|
+
function getFilePullHold(state, path) {
|
|
26854
|
+
return state.file_pull_holds?.find((h) => h.target === "file" && h.path === path);
|
|
26855
|
+
}
|
|
26856
|
+
function upsertFilePullHold(state, hold) {
|
|
26857
|
+
const list = state.file_pull_holds ?? [];
|
|
26858
|
+
const at = list.findIndex((h) => h.target === hold.target && h.path === hold.path);
|
|
26859
|
+
if (at !== -1 && stableHash(list[at]) === stableHash(hold))
|
|
26860
|
+
return state;
|
|
26861
|
+
const next = at === -1 ? [...list, hold] : list.map((h, i) => i === at ? hold : h);
|
|
26862
|
+
return { ...state, file_pull_holds: next };
|
|
26863
|
+
}
|
|
26864
|
+
function removeFilePullHold(state, path, kind) {
|
|
26865
|
+
const list = state.file_pull_holds;
|
|
26866
|
+
const hit = (h) => h.target === "file" && h.path === path && (kind === undefined || h.kind === kind);
|
|
26867
|
+
if (!list?.some(hit))
|
|
26868
|
+
return state;
|
|
26869
|
+
return { ...state, file_pull_holds: list.filter((h) => !hit(h)) };
|
|
26870
|
+
}
|
|
26752
26871
|
function fileEntryCount(state) {
|
|
26753
26872
|
return Object.keys(state.files ?? {}).length;
|
|
26754
26873
|
}
|
|
@@ -27630,7 +27749,6 @@ function decideRegen(input) {
|
|
|
27630
27749
|
}
|
|
27631
27750
|
|
|
27632
27751
|
// src/lib/context/roster.ts
|
|
27633
|
-
var import_gray_matter5 = __toESM(require_gray_matter(), 1);
|
|
27634
27752
|
import { readdirSync as readdirSync3, readFileSync as readFileSync8 } from "node:fs";
|
|
27635
27753
|
import { join as join7 } from "node:path";
|
|
27636
27754
|
function readRoster(workspaceSlug) {
|
|
@@ -27655,7 +27773,7 @@ function readRoster(workspaceSlug) {
|
|
|
27655
27773
|
continue;
|
|
27656
27774
|
}
|
|
27657
27775
|
try {
|
|
27658
|
-
const fm =
|
|
27776
|
+
const fm = matter(raw).data;
|
|
27659
27777
|
const name = typeof fm.name === "string" && fm.name.length > 0 ? fm.name : slug;
|
|
27660
27778
|
const status = typeof fm.status === "string" ? fm.status : null;
|
|
27661
27779
|
roster.push({ slug, name, status });
|
|
@@ -29045,7 +29163,6 @@ var SKILL_LOCATIONS = [
|
|
|
29045
29163
|
var SECURITY_NOTE = "Scoped to a single Plinth workspace: do not read or write across " + "workspace folders; each has its own token and isolation boundary.";
|
|
29046
29164
|
|
|
29047
29165
|
// src/lib/context/artist-data.ts
|
|
29048
|
-
var import_gray_matter6 = __toESM(require_gray_matter(), 1);
|
|
29049
29166
|
import { readFileSync as readFileSync10 } from "node:fs";
|
|
29050
29167
|
function artistUuidBySlug(state) {
|
|
29051
29168
|
const map2 = new Map;
|
|
@@ -29172,7 +29289,7 @@ function readEntityFrontmatter(workspaceSlug, entityType, slug) {
|
|
|
29172
29289
|
return null;
|
|
29173
29290
|
}
|
|
29174
29291
|
try {
|
|
29175
|
-
return
|
|
29292
|
+
return matter(raw).data;
|
|
29176
29293
|
} catch {
|
|
29177
29294
|
return null;
|
|
29178
29295
|
}
|
|
@@ -30545,9 +30662,15 @@ function fileDeleteSuppressedLine(f) {
|
|
|
30545
30662
|
function entityDeleteSuppressedLine(f) {
|
|
30546
30663
|
return `[ENTITY_DELETE_SUPPRESSED entityType=${f.entityType} slug=${f.slug} reason=${f.reason}]`;
|
|
30547
30664
|
}
|
|
30665
|
+
function entityPushHeldLine(f) {
|
|
30666
|
+
return `[PUSH_HELD entityType=${f.entityType} slug=${f.slug} reason=${f.reason}]`;
|
|
30667
|
+
}
|
|
30548
30668
|
function entityRestoreDeferredLine(f) {
|
|
30549
30669
|
return `[ENTITY_RESTORE_DEFERRED entityType=${f.entityType} slug=${f.slug} wait_s=${f.waitSeconds}]`;
|
|
30550
30670
|
}
|
|
30671
|
+
function entityPullDeferredLine(f) {
|
|
30672
|
+
return `[PULL_DEFERRED entityType=${f.entityType} slug=${f.slug} id=${f.id} reason=${f.reason}]`;
|
|
30673
|
+
}
|
|
30551
30674
|
function fileRestoreDeferredLine(f) {
|
|
30552
30675
|
return `[FILE_RESTORE_DEFERRED path=${f.path} wait_s=${f.waitSeconds}]`;
|
|
30553
30676
|
}
|
|
@@ -30566,6 +30689,16 @@ function fileStashFailedLine(f) {
|
|
|
30566
30689
|
const codePart = f.code ? ` code=${f.code}` : "";
|
|
30567
30690
|
return `[FILE_STASH_FAILED path=${f.path} reason=${f.reason}${codePart}]`;
|
|
30568
30691
|
}
|
|
30692
|
+
function fileRestoreSkippedLine(f) {
|
|
30693
|
+
return `[FILE_RESTORE_SKIPPED path=${f.path} row_id=${f.rowId} reason=${f.reason}]`;
|
|
30694
|
+
}
|
|
30695
|
+
function filePullSkippedLine(f) {
|
|
30696
|
+
return `[FILE_PULL_SKIPPED path=${f.path} row_id=${f.rowId} reason=${f.reason}]`;
|
|
30697
|
+
}
|
|
30698
|
+
function filePullHeldLine(f) {
|
|
30699
|
+
const retryPart = f.retryInSeconds !== undefined ? ` retry_s=${f.retryInSeconds}` : "";
|
|
30700
|
+
return `[FILE_PULL_HELD path=${f.path} kind=${f.kind}${retryPart}]`;
|
|
30701
|
+
}
|
|
30569
30702
|
function fileRejectionSyncFailedLine(f) {
|
|
30570
30703
|
const parts = [`[FILE_REJECTION_SYNC_FAILED verb=${f.verb}`];
|
|
30571
30704
|
if (f.path !== undefined)
|
|
@@ -30739,6 +30872,34 @@ function recordEntityRejected(args) {
|
|
|
30739
30872
|
rejected_at: args.now().toISOString()
|
|
30740
30873
|
}));
|
|
30741
30874
|
}
|
|
30875
|
+
var FRONTMATTER_UNREADABLE_REASON = "frontmatter_unreadable";
|
|
30876
|
+
function frontmatterUnreadableSentence(why) {
|
|
30877
|
+
const reason = why.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
30878
|
+
return `not synced, because its frontmatter could not be read (${reason}). ` + `Fix the frontmatter and it syncs.`;
|
|
30879
|
+
}
|
|
30880
|
+
function recordEntityFrontmatterUnreadable(args) {
|
|
30881
|
+
let path;
|
|
30882
|
+
try {
|
|
30883
|
+
path = relative4(workspaceMirrorPath(args.workspaceSlug), args.absolutePath);
|
|
30884
|
+
} catch {
|
|
30885
|
+
path = args.absolutePath;
|
|
30886
|
+
}
|
|
30887
|
+
const message = frontmatterUnreadableSentence(args.why);
|
|
30888
|
+
const withoutStale = removeUnsynced(args.state, "entity", path);
|
|
30889
|
+
const existing = listRejected(args.state).find((o) => o.target === "entity" && o.path === path);
|
|
30890
|
+
if (!withoutStale.changed && existing?.reason === FRONTMATTER_UNREADABLE_REASON && existing.message === message) {
|
|
30891
|
+
return args.state;
|
|
30892
|
+
}
|
|
30893
|
+
return commitState(args.state, upsertRejected(withoutStale.state, {
|
|
30894
|
+
target: "entity",
|
|
30895
|
+
op: "upsert",
|
|
30896
|
+
path,
|
|
30897
|
+
status: 0,
|
|
30898
|
+
reason: FRONTMATTER_UNREADABLE_REASON,
|
|
30899
|
+
message,
|
|
30900
|
+
rejected_at: args.now().toISOString()
|
|
30901
|
+
}).state);
|
|
30902
|
+
}
|
|
30742
30903
|
function recordFileRejected(args) {
|
|
30743
30904
|
const cat = categorizeError(args.err);
|
|
30744
30905
|
const withoutStale = removeUnsynced(args.state, "file", args.relPath).state;
|
|
@@ -34319,8 +34480,7 @@ var sessionCommand = defineCommand({
|
|
|
34319
34480
|
});
|
|
34320
34481
|
|
|
34321
34482
|
// src/commands/start.ts
|
|
34322
|
-
|
|
34323
|
-
import { existsSync as existsSync10, readFileSync as readFileSync30, statSync as statSync18 } from "node:fs";
|
|
34483
|
+
import { existsSync as existsSync10, readFileSync as readFileSync33, statSync as statSync18 } from "node:fs";
|
|
34324
34484
|
|
|
34325
34485
|
// src/lib/daemon-log.ts
|
|
34326
34486
|
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync9, renameSync as renameSync7, statSync as statSync4 } from "node:fs";
|
|
@@ -34386,188 +34546,27 @@ class DaemonLog {
|
|
|
34386
34546
|
}
|
|
34387
34547
|
|
|
34388
34548
|
// src/lib/destructive-gate.ts
|
|
34389
|
-
|
|
34390
|
-
import { readFileSync as readFileSync14, statSync as statSync5 } from "node:fs";
|
|
34549
|
+
import { readFileSync as readFileSync16, statSync as statSync6 } from "node:fs";
|
|
34391
34550
|
import { basename as basename4 } from "node:path";
|
|
34392
|
-
var GUTTING_RATIO_DEFAULT = 0.5;
|
|
34393
|
-
var GUTTING_MIN_PRIOR_BYTES = 200;
|
|
34394
|
-
function envFloat(name, def) {
|
|
34395
|
-
const v = process.env[name];
|
|
34396
|
-
if (v === undefined || v === "")
|
|
34397
|
-
return def;
|
|
34398
|
-
const n = Number.parseFloat(v);
|
|
34399
|
-
return Number.isFinite(n) && n > 0 ? n : def;
|
|
34400
|
-
}
|
|
34401
|
-
function envInt(name, def) {
|
|
34402
|
-
const v = process.env[name];
|
|
34403
|
-
if (v === undefined || v === "")
|
|
34404
|
-
return def;
|
|
34405
|
-
const n = Number.parseInt(v, 10);
|
|
34406
|
-
return Number.isFinite(n) && n > 0 ? n : def;
|
|
34407
|
-
}
|
|
34408
|
-
function guttingRatio() {
|
|
34409
|
-
return envFloat("PLINTH_GATE_GUTTING_RATIO", GUTTING_RATIO_DEFAULT);
|
|
34410
|
-
}
|
|
34411
|
-
function isGutting(priorLen, newLen) {
|
|
34412
|
-
if (priorLen === undefined)
|
|
34413
|
-
return false;
|
|
34414
|
-
if (priorLen < GUTTING_MIN_PRIOR_BYTES)
|
|
34415
|
-
return false;
|
|
34416
|
-
return newLen <= guttingRatio() * priorLen;
|
|
34417
|
-
}
|
|
34418
|
-
function classifyDestructive(event, entityType, state, deps = {}) {
|
|
34419
|
-
if (event.kind === "renamed")
|
|
34420
|
-
return null;
|
|
34421
|
-
if (event.kind === "unlink") {
|
|
34422
|
-
const entry3 = event.stateEntry;
|
|
34423
|
-
if (!entry3)
|
|
34424
|
-
return null;
|
|
34425
|
-
return {
|
|
34426
|
-
kind: "delete",
|
|
34427
|
-
entityType,
|
|
34428
|
-
slug: entry3.slug,
|
|
34429
|
-
id: entry3.id,
|
|
34430
|
-
updatedAt: entry3.updated_at,
|
|
34431
|
-
absolutePath: event.absolutePath
|
|
34432
|
-
};
|
|
34433
|
-
}
|
|
34434
|
-
const readFile = deps.readFile ?? ((p) => readFileSync14(p, "utf8"));
|
|
34435
|
-
const statInode = deps.statInode ?? ((p) => {
|
|
34436
|
-
try {
|
|
34437
|
-
return statSync5(p).ino;
|
|
34438
|
-
} catch {
|
|
34439
|
-
return;
|
|
34440
|
-
}
|
|
34441
|
-
});
|
|
34442
|
-
let raw;
|
|
34443
|
-
try {
|
|
34444
|
-
raw = readFile(event.absolutePath);
|
|
34445
|
-
} catch {
|
|
34446
|
-
return null;
|
|
34447
|
-
}
|
|
34448
|
-
let newLen;
|
|
34449
|
-
try {
|
|
34450
|
-
const body = import_gray_matter7.default(raw).content;
|
|
34451
|
-
newLen = (getStrategy(entityType)?.stripLocalBlocks?.(body) ?? body).length;
|
|
34452
|
-
} catch {
|
|
34453
|
-
return null;
|
|
34454
|
-
}
|
|
34455
|
-
const liveInode = statInode(event.absolutePath);
|
|
34456
|
-
const slug = basename4(event.absolutePath, ".md");
|
|
34457
|
-
const entry2 = (liveInode !== undefined ? getEntryByInode(state, entityType, liveInode) : undefined) ?? getEntryBySlug(state, entityType, slug);
|
|
34458
|
-
if (!entry2)
|
|
34459
|
-
return null;
|
|
34460
|
-
if (!isGutting(entry2.body_len, newLen))
|
|
34461
|
-
return null;
|
|
34462
|
-
return {
|
|
34463
|
-
kind: "overwrite",
|
|
34464
|
-
entityType,
|
|
34465
|
-
slug: entry2.slug,
|
|
34466
|
-
id: entry2.id,
|
|
34467
|
-
updatedAt: entry2.updated_at,
|
|
34468
|
-
absolutePath: event.absolutePath,
|
|
34469
|
-
priorLen: entry2.body_len ?? 0,
|
|
34470
|
-
newLen
|
|
34471
|
-
};
|
|
34472
|
-
}
|
|
34473
|
-
function defaultThresholds() {
|
|
34474
|
-
return {
|
|
34475
|
-
absCount: envInt("PLINTH_GATE_ABS_COUNT", 3),
|
|
34476
|
-
propMinOps: envInt("PLINTH_GATE_PROP_MIN_OPS", 2),
|
|
34477
|
-
propPct: envFloat("PLINTH_GATE_PROP_PCT", 0.5)
|
|
34478
|
-
};
|
|
34479
|
-
}
|
|
34480
|
-
function evaluateBatch(ops, typePopulations, thresholds = defaultThresholds()) {
|
|
34481
|
-
const count = ops.length;
|
|
34482
|
-
if (count === 0)
|
|
34483
|
-
return { tripped: false };
|
|
34484
|
-
if (count >= thresholds.absCount) {
|
|
34485
|
-
return { tripped: true, reason: { kind: "count", count } };
|
|
34486
|
-
}
|
|
34487
|
-
const byType = new Map;
|
|
34488
|
-
for (const op of ops) {
|
|
34489
|
-
byType.set(op.entityType, (byType.get(op.entityType) ?? 0) + 1);
|
|
34490
|
-
}
|
|
34491
|
-
for (const [entityType, typeCount] of byType) {
|
|
34492
|
-
const typePopulation = typePopulations[entityType] ?? typeCount;
|
|
34493
|
-
if (typeCount >= thresholds.propMinOps && typePopulation > 0 && typeCount >= thresholds.propPct * typePopulation) {
|
|
34494
|
-
return {
|
|
34495
|
-
tripped: true,
|
|
34496
|
-
reason: {
|
|
34497
|
-
kind: "proportion",
|
|
34498
|
-
count,
|
|
34499
|
-
entityType,
|
|
34500
|
-
typeCount,
|
|
34501
|
-
typePopulation,
|
|
34502
|
-
pct: typeCount / typePopulation
|
|
34503
|
-
}
|
|
34504
|
-
};
|
|
34505
|
-
}
|
|
34506
|
-
}
|
|
34507
|
-
return { tripped: false };
|
|
34508
|
-
}
|
|
34509
|
-
function dedupeOps(ops) {
|
|
34510
|
-
const byKey = new Map;
|
|
34511
|
-
for (const op of ops)
|
|
34512
|
-
byKey.set(`${op.entityType}:${op.id}`, op);
|
|
34513
|
-
return Array.from(byKey.values());
|
|
34514
|
-
}
|
|
34515
|
-
var DEFAULT_SETTLE_MS = 600;
|
|
34516
|
-
var DEFAULT_MAX_BATCH = 500;
|
|
34517
|
-
|
|
34518
|
-
class DestructiveGate {
|
|
34519
|
-
pending = [];
|
|
34520
|
-
timer = null;
|
|
34521
|
-
settleMs;
|
|
34522
|
-
maxBatch;
|
|
34523
|
-
onSettle;
|
|
34524
|
-
onError;
|
|
34525
|
-
constructor(opts) {
|
|
34526
|
-
this.settleMs = opts.settleMs ?? DEFAULT_SETTLE_MS;
|
|
34527
|
-
this.maxBatch = opts.maxBatch ?? DEFAULT_MAX_BATCH;
|
|
34528
|
-
this.onSettle = opts.onSettle;
|
|
34529
|
-
this.onError = opts.onError;
|
|
34530
|
-
}
|
|
34531
|
-
consider(op) {
|
|
34532
|
-
this.pending.push(op);
|
|
34533
|
-
if (this.pending.length >= this.maxBatch) {
|
|
34534
|
-
this.flush();
|
|
34535
|
-
return;
|
|
34536
|
-
}
|
|
34537
|
-
if (this.timer)
|
|
34538
|
-
clearTimeout(this.timer);
|
|
34539
|
-
this.timer = setTimeout(() => {
|
|
34540
|
-
this.flush();
|
|
34541
|
-
}, this.settleMs);
|
|
34542
|
-
}
|
|
34543
|
-
async drain() {
|
|
34544
|
-
await this.flush();
|
|
34545
|
-
}
|
|
34546
|
-
size() {
|
|
34547
|
-
return this.pending.length;
|
|
34548
|
-
}
|
|
34549
|
-
async flush() {
|
|
34550
|
-
if (this.timer) {
|
|
34551
|
-
clearTimeout(this.timer);
|
|
34552
|
-
this.timer = null;
|
|
34553
|
-
}
|
|
34554
|
-
if (this.pending.length === 0)
|
|
34555
|
-
return;
|
|
34556
|
-
const batch = this.pending;
|
|
34557
|
-
this.pending = [];
|
|
34558
|
-
try {
|
|
34559
|
-
await this.onSettle(batch);
|
|
34560
|
-
} catch (err) {
|
|
34561
|
-
this.onError?.(err);
|
|
34562
|
-
}
|
|
34563
|
-
}
|
|
34564
|
-
}
|
|
34565
34551
|
|
|
34566
|
-
// src/lib/
|
|
34567
|
-
import { readFileSync as
|
|
34552
|
+
// src/lib/pull-write.ts
|
|
34553
|
+
import { readFileSync as readFileSync15 } from "node:fs";
|
|
34568
34554
|
|
|
34569
34555
|
// src/lib/conflicts.ts
|
|
34570
|
-
import {
|
|
34556
|
+
import { createHash as createHash3, randomBytes } from "node:crypto";
|
|
34557
|
+
import {
|
|
34558
|
+
closeSync as closeSync4,
|
|
34559
|
+
constants as fsConstants2,
|
|
34560
|
+
fstatSync as fstatSync2,
|
|
34561
|
+
mkdirSync as mkdirSync10,
|
|
34562
|
+
openSync as openSync4,
|
|
34563
|
+
readFileSync as readFileSync14,
|
|
34564
|
+
readdirSync as readdirSync6,
|
|
34565
|
+
renameSync as renameSync8,
|
|
34566
|
+
statSync as statSync5,
|
|
34567
|
+
unlinkSync as unlinkSync8,
|
|
34568
|
+
writeFileSync as writeFileSync7
|
|
34569
|
+
} from "node:fs";
|
|
34571
34570
|
import { dirname as dirname9, join as join12 } from "node:path";
|
|
34572
34571
|
function conflictsRoot(workspaceSlug) {
|
|
34573
34572
|
return join12(plinthHome(), "_conflicts", workspaceSlug);
|
|
@@ -34597,7 +34596,7 @@ function stashEntityConflictOnce(args) {
|
|
|
34597
34596
|
if (newest !== undefined) {
|
|
34598
34597
|
const path5 = join12(dir, newest);
|
|
34599
34598
|
try {
|
|
34600
|
-
if (
|
|
34599
|
+
if (readFileSync14(path5, "utf8") === args.contents)
|
|
34601
34600
|
return { path: path5, reused: true };
|
|
34602
34601
|
} catch {}
|
|
34603
34602
|
}
|
|
@@ -34609,13 +34608,95 @@ function fileConflictPath(args) {
|
|
|
34609
34608
|
return join12(conflictsRoot(workspaceSlug), `${fileConflictStem(relPath)}.${iso}.${side}`);
|
|
34610
34609
|
}
|
|
34611
34610
|
function fileConflictStem(relPath) {
|
|
34612
|
-
|
|
34611
|
+
const flat = `files.${relPath.replace(/[/\\]/g, "__")}`;
|
|
34612
|
+
if (Buffer.byteLength(flat) + STAMP_AND_SIDE_BYTES <= NAME_MAX_BYTES && !LONG_STEM_RE.test(flat)) {
|
|
34613
|
+
return flat;
|
|
34614
|
+
}
|
|
34615
|
+
return `${truncateUtf8(flat, LONG_PREFIX_BYTES)}~${pathHash(relPath)}`;
|
|
34616
|
+
}
|
|
34617
|
+
var NAME_MAX_BYTES = 255;
|
|
34618
|
+
var STAMP_AND_SIDE_BYTES = 1 + 24 + 1 + 5;
|
|
34619
|
+
var PATH_HASH_HEX = 16;
|
|
34620
|
+
var LONG_PREFIX_BYTES = NAME_MAX_BYTES - STAMP_AND_SIDE_BYTES - 1 - PATH_HASH_HEX;
|
|
34621
|
+
var LONG_STEM_RE = /^(files\..*)~([0-9a-f]{16})$/;
|
|
34622
|
+
function pathHash(relPath) {
|
|
34623
|
+
return createHash3("sha256").update(relPath, "utf8").digest("hex").slice(0, PATH_HASH_HEX);
|
|
34624
|
+
}
|
|
34625
|
+
function truncateUtf8(s, maxBytes) {
|
|
34626
|
+
let bytes = 0;
|
|
34627
|
+
let out = "";
|
|
34628
|
+
for (const ch of s) {
|
|
34629
|
+
const n = Buffer.byteLength(ch);
|
|
34630
|
+
if (bytes + n > maxBytes)
|
|
34631
|
+
break;
|
|
34632
|
+
bytes += n;
|
|
34633
|
+
out += ch;
|
|
34634
|
+
}
|
|
34635
|
+
return out;
|
|
34636
|
+
}
|
|
34637
|
+
function pathRecordFile(workspaceSlug, hash2) {
|
|
34638
|
+
return join12(conflictsRoot(workspaceSlug), `.${hash2}.path`);
|
|
34639
|
+
}
|
|
34640
|
+
function ensurePathRecord(workspaceSlug, relPath) {
|
|
34641
|
+
const target = pathRecordFile(workspaceSlug, pathHash(relPath));
|
|
34642
|
+
if (readPathRecord(target) === relPath)
|
|
34643
|
+
return;
|
|
34644
|
+
const tmp = `${target}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
34645
|
+
writeFileSync7(tmp, relPath, { mode: 384 });
|
|
34646
|
+
try {
|
|
34647
|
+
renameSync8(tmp, target);
|
|
34648
|
+
} catch (err) {
|
|
34649
|
+
try {
|
|
34650
|
+
unlinkSync8(tmp);
|
|
34651
|
+
} catch {}
|
|
34652
|
+
throw err;
|
|
34653
|
+
}
|
|
34654
|
+
}
|
|
34655
|
+
function readStashFolderFile(target) {
|
|
34656
|
+
let fd;
|
|
34657
|
+
try {
|
|
34658
|
+
fd = openSync4(target, fsConstants2.O_RDONLY | fsConstants2.O_NONBLOCK | fsConstants2.O_NOFOLLOW);
|
|
34659
|
+
} catch {
|
|
34660
|
+
return null;
|
|
34661
|
+
}
|
|
34662
|
+
try {
|
|
34663
|
+
return fstatSync2(fd).isFile() ? readFileSync14(fd) : null;
|
|
34664
|
+
} catch {
|
|
34665
|
+
return null;
|
|
34666
|
+
} finally {
|
|
34667
|
+
closeSync4(fd);
|
|
34668
|
+
}
|
|
34669
|
+
}
|
|
34670
|
+
function readPathRecord(target) {
|
|
34671
|
+
return readStashFolderFile(target)?.toString("utf8") ?? null;
|
|
34672
|
+
}
|
|
34673
|
+
function prepareFileStash(workspaceSlug, relPath) {
|
|
34674
|
+
mkdirSync10(conflictsRoot(workspaceSlug), { recursive: true, mode: 448 });
|
|
34675
|
+
if (LONG_STEM_RE.test(fileConflictStem(relPath)))
|
|
34676
|
+
ensurePathRecord(workspaceSlug, relPath);
|
|
34677
|
+
}
|
|
34678
|
+
function resolveLongStem(workspaceSlug, head) {
|
|
34679
|
+
const m = LONG_STEM_RE.exec(head);
|
|
34680
|
+
if (!m)
|
|
34681
|
+
return null;
|
|
34682
|
+
const recorded = readPathRecord(pathRecordFile(workspaceSlug, m[2]));
|
|
34683
|
+
return recorded !== null && fileConflictStem(recorded) === head ? recorded : null;
|
|
34613
34684
|
}
|
|
34614
34685
|
function stashFileConflict(args) {
|
|
34615
|
-
|
|
34616
|
-
|
|
34617
|
-
|
|
34618
|
-
|
|
34686
|
+
prepareFileStash(args.workspaceSlug, args.relPath);
|
|
34687
|
+
let stamp = args.timestamp;
|
|
34688
|
+
for (let attempt = 0;attempt < 1000; attempt++) {
|
|
34689
|
+
const target = fileConflictPath({ ...args, timestamp: stamp });
|
|
34690
|
+
try {
|
|
34691
|
+
writeFileSync7(target, args.bytes, { mode: 384, flag: "wx" });
|
|
34692
|
+
return target;
|
|
34693
|
+
} catch (err) {
|
|
34694
|
+
if (err?.code !== "EEXIST")
|
|
34695
|
+
throw err;
|
|
34696
|
+
stamp = new Date(stamp.getTime() + 1);
|
|
34697
|
+
}
|
|
34698
|
+
}
|
|
34699
|
+
throw new Error(`no free recovery-copy name for files/${args.relPath} near ${args.timestamp.toISOString()}`);
|
|
34619
34700
|
}
|
|
34620
34701
|
function stashFileConflictOnce(args) {
|
|
34621
34702
|
const dir = conflictsRoot(args.workspaceSlug);
|
|
@@ -34630,10 +34711,13 @@ function stashFileConflictOnce(args) {
|
|
|
34630
34711
|
const newest = names.filter((n) => n.startsWith(head) && tail.test(n.slice(head.length))).sort().pop();
|
|
34631
34712
|
if (newest !== undefined) {
|
|
34632
34713
|
const path5 = join12(dir, newest);
|
|
34633
|
-
|
|
34634
|
-
|
|
34635
|
-
|
|
34636
|
-
|
|
34714
|
+
const existing = readStashFolderFile(path5);
|
|
34715
|
+
if (existing !== null && Buffer.compare(args.bytes, existing) === 0) {
|
|
34716
|
+
try {
|
|
34717
|
+
prepareFileStash(args.workspaceSlug, args.relPath);
|
|
34718
|
+
} catch {}
|
|
34719
|
+
return { path: path5, reused: true };
|
|
34720
|
+
}
|
|
34637
34721
|
}
|
|
34638
34722
|
return { path: stashFileConflict(args), reused: false };
|
|
34639
34723
|
}
|
|
@@ -34643,18 +34727,28 @@ function parseStamp(stamp) {
|
|
|
34643
34727
|
const d = new Date(iso);
|
|
34644
34728
|
return Number.isNaN(d.getTime()) ? null : d;
|
|
34645
34729
|
}
|
|
34646
|
-
function labelFor(file2) {
|
|
34730
|
+
function labelFor(file2, workspaceSlug) {
|
|
34647
34731
|
const m = STAMP_RE2.exec(file2);
|
|
34648
34732
|
if (!m)
|
|
34649
|
-
return file2;
|
|
34733
|
+
return { label: file2 };
|
|
34650
34734
|
const head = file2.slice(0, m.index);
|
|
34735
|
+
const exact = LONG_STEM_RE.test(head) ? resolveLongStem(workspaceSlug, head) : null;
|
|
34736
|
+
if (exact !== null)
|
|
34737
|
+
return { label: `files/${exact}`, pathRecorded: true };
|
|
34738
|
+
if (LONG_STEM_RE.test(head)) {
|
|
34739
|
+
const cut = LONG_STEM_RE.exec(head)[1];
|
|
34740
|
+
return {
|
|
34741
|
+
label: `files/${cut.slice("files.".length).replace(/__/g, "/")}…`,
|
|
34742
|
+
pathUnrecorded: true
|
|
34743
|
+
};
|
|
34744
|
+
}
|
|
34651
34745
|
if (head.startsWith("files.")) {
|
|
34652
|
-
return `files/${head.slice("files.".length).replace(/__/g, "/")}
|
|
34746
|
+
return { label: `files/${head.slice("files.".length).replace(/__/g, "/")}` };
|
|
34653
34747
|
}
|
|
34654
34748
|
const dot = head.indexOf(".");
|
|
34655
34749
|
if (dot === -1)
|
|
34656
|
-
return head;
|
|
34657
|
-
return `${head.slice(0, dot)}/${head.slice(dot + 1)}
|
|
34750
|
+
return { label: head };
|
|
34751
|
+
return { label: `${head.slice(0, dot)}/${head.slice(dot + 1)}` };
|
|
34658
34752
|
}
|
|
34659
34753
|
function listConflictStashes(workspaceSlug) {
|
|
34660
34754
|
const dir = conflictsRoot(workspaceSlug);
|
|
@@ -34669,7 +34763,7 @@ function listConflictStashes(workspaceSlug) {
|
|
|
34669
34763
|
const absPath = join12(dir, file2);
|
|
34670
34764
|
let st;
|
|
34671
34765
|
try {
|
|
34672
|
-
st =
|
|
34766
|
+
st = statSync5(absPath);
|
|
34673
34767
|
} catch {
|
|
34674
34768
|
continue;
|
|
34675
34769
|
}
|
|
@@ -34679,10 +34773,13 @@ function listConflictStashes(workspaceSlug) {
|
|
|
34679
34773
|
continue;
|
|
34680
34774
|
const m = STAMP_RE2.exec(file2);
|
|
34681
34775
|
const side = m?.[2];
|
|
34776
|
+
const labelled = labelFor(file2, workspaceSlug);
|
|
34682
34777
|
out.push({
|
|
34683
34778
|
file: file2,
|
|
34684
34779
|
absPath,
|
|
34685
|
-
label:
|
|
34780
|
+
label: labelled.label,
|
|
34781
|
+
...labelled.pathUnrecorded ? { pathUnrecorded: true } : {},
|
|
34782
|
+
...labelled.pathRecorded ? { pathRecorded: true } : {},
|
|
34686
34783
|
stashedAt: m ? parseStamp(m[1]) : null,
|
|
34687
34784
|
side: side === "local" || side === "cloud" ? side : null,
|
|
34688
34785
|
mtimeMs: st.mtimeMs
|
|
@@ -34691,24 +34788,386 @@ function listConflictStashes(workspaceSlug) {
|
|
|
34691
34788
|
return out.sort((a, b) => (b.stashedAt?.getTime() ?? b.mtimeMs) - (a.stashedAt?.getTime() ?? a.mtimeMs));
|
|
34692
34789
|
}
|
|
34693
34790
|
|
|
34791
|
+
// src/lib/pull-write.ts
|
|
34792
|
+
var UNCACHED = { cache: false };
|
|
34793
|
+
function parseUncached(raw) {
|
|
34794
|
+
return matter(raw, UNCACHED);
|
|
34795
|
+
}
|
|
34796
|
+
var NOT_A_MAPPING = "it is not a list of `key: value` lines";
|
|
34797
|
+
var SELF_REFERENCE = "an alias in it refers to itself";
|
|
34798
|
+
function readFrontmatter(raw) {
|
|
34799
|
+
let parsed;
|
|
34800
|
+
try {
|
|
34801
|
+
parsed = parseUncached(raw);
|
|
34802
|
+
} catch (err) {
|
|
34803
|
+
return { ok: false, why: yamlReason(err) };
|
|
34804
|
+
}
|
|
34805
|
+
if (Object.prototype.toString.call(parsed.data) !== "[object Object]") {
|
|
34806
|
+
return { ok: false, why: NOT_A_MAPPING };
|
|
34807
|
+
}
|
|
34808
|
+
try {
|
|
34809
|
+
JSON.stringify(parsed.data);
|
|
34810
|
+
} catch {
|
|
34811
|
+
return { ok: false, why: SELF_REFERENCE };
|
|
34812
|
+
}
|
|
34813
|
+
return { ok: true, parsed };
|
|
34814
|
+
}
|
|
34815
|
+
function yamlReason(err) {
|
|
34816
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
34817
|
+
return (message.split(`
|
|
34818
|
+
`)[0] ?? "").replace(/:\s*$/, "").trim();
|
|
34819
|
+
}
|
|
34820
|
+
function frontmatterParseError(raw) {
|
|
34821
|
+
const read = readFrontmatter(raw);
|
|
34822
|
+
return read.ok ? null : read.why;
|
|
34823
|
+
}
|
|
34824
|
+
function computeMirrorHashes(strategy, content) {
|
|
34825
|
+
const parsed = matter(content);
|
|
34826
|
+
const fm = parsed.data ?? {};
|
|
34827
|
+
const lifted = splitUserFrontmatter(strategy, fm);
|
|
34828
|
+
const userFm = userFrontmatter(strategy, lifted.frontmatter);
|
|
34829
|
+
const hashBody = hashableBody(strategy, parsed.content);
|
|
34830
|
+
return {
|
|
34831
|
+
bodyHash: stableHash(hashBody),
|
|
34832
|
+
frontmatterHash: stableHash({ ...userFm, ...lifted.topLevel }),
|
|
34833
|
+
bodyLen: hashBody.length,
|
|
34834
|
+
frontmatterId: fm["id"]
|
|
34835
|
+
};
|
|
34836
|
+
}
|
|
34837
|
+
function hashableBody(strategy, body) {
|
|
34838
|
+
return strategy.stripLocalBlocks?.(body) ?? body;
|
|
34839
|
+
}
|
|
34840
|
+
function relocateForSlugChange(args) {
|
|
34841
|
+
const { workspaceSlug, strategy, fromSlug, toSlug, acked, echo, now } = args;
|
|
34842
|
+
if (fromSlug === toSlug)
|
|
34843
|
+
return { kind: "not_needed", writeSlug: toSlug };
|
|
34844
|
+
if (!echo) {
|
|
34845
|
+
return { kind: "skipped", writeSlug: toSlug, fromSlug, reason: "unguarded" };
|
|
34846
|
+
}
|
|
34847
|
+
if (strategy.mirrorRelativePath(fromSlug).split("/").length !== 2) {
|
|
34848
|
+
return { kind: "skipped", writeSlug: toSlug, fromSlug, reason: "foldered" };
|
|
34849
|
+
}
|
|
34850
|
+
let fromPath;
|
|
34851
|
+
try {
|
|
34852
|
+
fromPath = entityFilePath(workspaceSlug, strategy.type, fromSlug);
|
|
34853
|
+
entityFilePath(workspaceSlug, strategy.type, toSlug);
|
|
34854
|
+
} catch (err) {
|
|
34855
|
+
if (err instanceof MirrorPathError) {
|
|
34856
|
+
return {
|
|
34857
|
+
kind: "skipped",
|
|
34858
|
+
writeSlug: toSlug,
|
|
34859
|
+
fromSlug,
|
|
34860
|
+
reason: "unresolvable"
|
|
34861
|
+
};
|
|
34862
|
+
}
|
|
34863
|
+
throw err;
|
|
34864
|
+
}
|
|
34865
|
+
let raw = null;
|
|
34866
|
+
try {
|
|
34867
|
+
raw = readFileSync15(fromPath, "utf8");
|
|
34868
|
+
} catch (err) {
|
|
34869
|
+
const code = err?.code;
|
|
34870
|
+
if (code === "ENOENT") {
|
|
34871
|
+
return { kind: "absent", writeSlug: toSlug, fromSlug };
|
|
34872
|
+
}
|
|
34873
|
+
return {
|
|
34874
|
+
kind: "refused",
|
|
34875
|
+
writeSlug: fromSlug,
|
|
34876
|
+
fromSlug,
|
|
34877
|
+
reason: "source_unreadable"
|
|
34878
|
+
};
|
|
34879
|
+
}
|
|
34880
|
+
let diverged = true;
|
|
34881
|
+
try {
|
|
34882
|
+
const hashes = computeMirrorHashes(strategy, raw);
|
|
34883
|
+
diverged = hashes.bodyHash !== acked.bodyHash || hashes.frontmatterHash !== acked.frontmatterHash;
|
|
34884
|
+
} catch {
|
|
34885
|
+
diverged = true;
|
|
34886
|
+
}
|
|
34887
|
+
const stashPath = diverged ? stashConflict({
|
|
34888
|
+
workspaceSlug,
|
|
34889
|
+
entityType: strategy.type,
|
|
34890
|
+
entitySlug: fromSlug,
|
|
34891
|
+
timestamp: now(),
|
|
34892
|
+
contents: raw,
|
|
34893
|
+
side: "local"
|
|
34894
|
+
}) : undefined;
|
|
34895
|
+
try {
|
|
34896
|
+
let registered = false;
|
|
34897
|
+
const result = moveEntityFile(workspaceSlug, strategy.type, fromSlug, toSlug, () => {
|
|
34898
|
+
if (echo.watcher === "live") {
|
|
34899
|
+
echo.suppressor.registerDelete(fromPath);
|
|
34900
|
+
registered = true;
|
|
34901
|
+
}
|
|
34902
|
+
});
|
|
34903
|
+
if (result === "moved") {
|
|
34904
|
+
return { kind: "moved", writeSlug: toSlug, fromSlug, stashPath };
|
|
34905
|
+
}
|
|
34906
|
+
if (registered && echo.watcher === "live") {
|
|
34907
|
+
echo.suppressor.cancelDelete(fromPath);
|
|
34908
|
+
}
|
|
34909
|
+
return { kind: "absent", writeSlug: toSlug, fromSlug };
|
|
34910
|
+
} catch (err) {
|
|
34911
|
+
if (err instanceof MirrorDestinationOccupiedError) {
|
|
34912
|
+
return {
|
|
34913
|
+
kind: "refused",
|
|
34914
|
+
writeSlug: fromSlug,
|
|
34915
|
+
fromSlug,
|
|
34916
|
+
reason: "destination_occupied",
|
|
34917
|
+
stashPath
|
|
34918
|
+
};
|
|
34919
|
+
}
|
|
34920
|
+
if (err instanceof MirrorPathError) {
|
|
34921
|
+
return {
|
|
34922
|
+
kind: "skipped",
|
|
34923
|
+
writeSlug: toSlug,
|
|
34924
|
+
fromSlug,
|
|
34925
|
+
reason: "unresolvable"
|
|
34926
|
+
};
|
|
34927
|
+
}
|
|
34928
|
+
throw err;
|
|
34929
|
+
}
|
|
34930
|
+
}
|
|
34931
|
+
function writeMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, suppressor, pathSlug) {
|
|
34932
|
+
const targetSlug = pathSlug ?? doc2.slug;
|
|
34933
|
+
const content = renderMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, targetSlug);
|
|
34934
|
+
if (suppressor) {
|
|
34935
|
+
const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
|
|
34936
|
+
suppressor.registerWrite(target, computeMirrorHashes(strategy, content));
|
|
34937
|
+
}
|
|
34938
|
+
writeMirrorFile(workspaceSlug, strategy.type, targetSlug, content);
|
|
34939
|
+
return content;
|
|
34940
|
+
}
|
|
34941
|
+
function renderMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, targetSlug) {
|
|
34942
|
+
let content = strategy.serialise(doc2);
|
|
34943
|
+
if (strategy.preserveLocalBodyOnPull) {
|
|
34944
|
+
const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
|
|
34945
|
+
let existing = null;
|
|
34946
|
+
try {
|
|
34947
|
+
existing = readFileSync15(target, "utf8");
|
|
34948
|
+
} catch (err) {
|
|
34949
|
+
if (err?.code !== "ENOENT")
|
|
34950
|
+
throw err;
|
|
34951
|
+
}
|
|
34952
|
+
if (existing !== null) {
|
|
34953
|
+
const existingParsed = matter(existing);
|
|
34954
|
+
const cloudParsed = matter(content);
|
|
34955
|
+
const stitched = stringifyFrontmatter(existingParsed.content, cloudParsed.data);
|
|
34956
|
+
content = stitched.endsWith(`
|
|
34957
|
+
`) ? stitched : stitched + `
|
|
34958
|
+
`;
|
|
34959
|
+
}
|
|
34960
|
+
}
|
|
34961
|
+
return content;
|
|
34962
|
+
}
|
|
34963
|
+
|
|
34964
|
+
// src/lib/destructive-gate.ts
|
|
34965
|
+
var GUTTING_RATIO_DEFAULT = 0.5;
|
|
34966
|
+
var GUTTING_MIN_PRIOR_BYTES = 200;
|
|
34967
|
+
function envFloat(name, def) {
|
|
34968
|
+
const v = process.env[name];
|
|
34969
|
+
if (v === undefined || v === "")
|
|
34970
|
+
return def;
|
|
34971
|
+
const n = Number.parseFloat(v);
|
|
34972
|
+
return Number.isFinite(n) && n > 0 ? n : def;
|
|
34973
|
+
}
|
|
34974
|
+
function envInt(name, def) {
|
|
34975
|
+
const v = process.env[name];
|
|
34976
|
+
if (v === undefined || v === "")
|
|
34977
|
+
return def;
|
|
34978
|
+
const n = Number.parseInt(v, 10);
|
|
34979
|
+
return Number.isFinite(n) && n > 0 ? n : def;
|
|
34980
|
+
}
|
|
34981
|
+
function guttingRatio() {
|
|
34982
|
+
return envFloat("PLINTH_GATE_GUTTING_RATIO", GUTTING_RATIO_DEFAULT);
|
|
34983
|
+
}
|
|
34984
|
+
function isGutting(priorLen, newLen) {
|
|
34985
|
+
if (priorLen === undefined)
|
|
34986
|
+
return false;
|
|
34987
|
+
if (priorLen < GUTTING_MIN_PRIOR_BYTES)
|
|
34988
|
+
return false;
|
|
34989
|
+
return newLen <= guttingRatio() * priorLen;
|
|
34990
|
+
}
|
|
34991
|
+
function classifyDestructive(event, entityType, state, deps = {}) {
|
|
34992
|
+
if (event.kind === "renamed")
|
|
34993
|
+
return null;
|
|
34994
|
+
if (event.kind === "unlink") {
|
|
34995
|
+
const entry3 = event.stateEntry;
|
|
34996
|
+
if (!entry3)
|
|
34997
|
+
return null;
|
|
34998
|
+
return {
|
|
34999
|
+
kind: "delete",
|
|
35000
|
+
entityType,
|
|
35001
|
+
slug: entry3.slug,
|
|
35002
|
+
id: entry3.id,
|
|
35003
|
+
updatedAt: entry3.updated_at,
|
|
35004
|
+
absolutePath: event.absolutePath
|
|
35005
|
+
};
|
|
35006
|
+
}
|
|
35007
|
+
const readFile = deps.readFile ?? ((p) => readFileSync16(p, "utf8"));
|
|
35008
|
+
const statInode = deps.statInode ?? ((p) => {
|
|
35009
|
+
try {
|
|
35010
|
+
return statSync6(p).ino;
|
|
35011
|
+
} catch {
|
|
35012
|
+
return;
|
|
35013
|
+
}
|
|
35014
|
+
});
|
|
35015
|
+
let raw;
|
|
35016
|
+
try {
|
|
35017
|
+
raw = readFile(event.absolutePath);
|
|
35018
|
+
} catch {
|
|
35019
|
+
return null;
|
|
35020
|
+
}
|
|
35021
|
+
const read = readFrontmatter(raw);
|
|
35022
|
+
if (!read.ok)
|
|
35023
|
+
return null;
|
|
35024
|
+
const body = read.parsed.content;
|
|
35025
|
+
const newLen = (getStrategy(entityType)?.stripLocalBlocks?.(body) ?? body).length;
|
|
35026
|
+
const liveInode = statInode(event.absolutePath);
|
|
35027
|
+
const slug = basename4(event.absolutePath, ".md");
|
|
35028
|
+
const entry2 = (liveInode !== undefined ? getEntryByInode(state, entityType, liveInode) : undefined) ?? getEntryBySlug(state, entityType, slug);
|
|
35029
|
+
if (!entry2)
|
|
35030
|
+
return null;
|
|
35031
|
+
if (!isGutting(entry2.body_len, newLen))
|
|
35032
|
+
return null;
|
|
35033
|
+
return {
|
|
35034
|
+
kind: "overwrite",
|
|
35035
|
+
entityType,
|
|
35036
|
+
slug: entry2.slug,
|
|
35037
|
+
id: entry2.id,
|
|
35038
|
+
updatedAt: entry2.updated_at,
|
|
35039
|
+
absolutePath: event.absolutePath,
|
|
35040
|
+
priorLen: entry2.body_len ?? 0,
|
|
35041
|
+
newLen
|
|
35042
|
+
};
|
|
35043
|
+
}
|
|
35044
|
+
var LONE_MIN_REMOVED_DEFAULT = 5000;
|
|
35045
|
+
var LONE_MIN_REMOVED_BYTES_DEFAULT = 100 * 1024;
|
|
35046
|
+
function defaultThresholds() {
|
|
35047
|
+
return {
|
|
35048
|
+
absCount: envInt("PLINTH_GATE_ABS_COUNT", 3),
|
|
35049
|
+
propMinOps: envInt("PLINTH_GATE_PROP_MIN_OPS", 2),
|
|
35050
|
+
propPct: envFloat("PLINTH_GATE_PROP_PCT", 0.5),
|
|
35051
|
+
loneMinRemoved: envInt("PLINTH_GATE_LONE_MIN_REMOVED", LONE_MIN_REMOVED_DEFAULT),
|
|
35052
|
+
loneMinRemovedBytes: envInt("PLINTH_GATE_LONE_MIN_REMOVED_BYTES", LONE_MIN_REMOVED_BYTES_DEFAULT)
|
|
35053
|
+
};
|
|
35054
|
+
}
|
|
35055
|
+
function tripReasonToken(reason) {
|
|
35056
|
+
return reason.loneRemoved !== undefined ? "size" : reason.kind;
|
|
35057
|
+
}
|
|
35058
|
+
function evaluateLoneOverwrite(count, removed, floor) {
|
|
35059
|
+
const loneRemoved = Math.max(0, ...removed);
|
|
35060
|
+
if (loneRemoved < floor)
|
|
35061
|
+
return { tripped: false };
|
|
35062
|
+
return { tripped: true, reason: { kind: "count", count, loneRemoved } };
|
|
35063
|
+
}
|
|
35064
|
+
function evaluateBatch(ops, typePopulations, thresholds = defaultThresholds()) {
|
|
35065
|
+
const count = ops.length;
|
|
35066
|
+
if (count === 0)
|
|
35067
|
+
return { tripped: false };
|
|
35068
|
+
if (count >= thresholds.absCount) {
|
|
35069
|
+
return { tripped: true, reason: { kind: "count", count } };
|
|
35070
|
+
}
|
|
35071
|
+
const byType = new Map;
|
|
35072
|
+
for (const op of ops) {
|
|
35073
|
+
byType.set(op.entityType, (byType.get(op.entityType) ?? 0) + 1);
|
|
35074
|
+
}
|
|
35075
|
+
for (const [entityType, typeCount] of byType) {
|
|
35076
|
+
const typePopulation = typePopulations[entityType] ?? typeCount;
|
|
35077
|
+
if (typeCount >= thresholds.propMinOps && typePopulation > 0 && typeCount >= thresholds.propPct * typePopulation) {
|
|
35078
|
+
return {
|
|
35079
|
+
tripped: true,
|
|
35080
|
+
reason: {
|
|
35081
|
+
kind: "proportion",
|
|
35082
|
+
count,
|
|
35083
|
+
entityType,
|
|
35084
|
+
typeCount,
|
|
35085
|
+
typePopulation,
|
|
35086
|
+
pct: typeCount / typePopulation
|
|
35087
|
+
}
|
|
35088
|
+
};
|
|
35089
|
+
}
|
|
35090
|
+
}
|
|
35091
|
+
return evaluateLoneOverwrite(count, ops.map((op) => op.kind === "overwrite" ? op.priorLen - op.newLen : 0), thresholds.loneMinRemoved);
|
|
35092
|
+
}
|
|
35093
|
+
function dedupeOps(ops) {
|
|
35094
|
+
const byKey = new Map;
|
|
35095
|
+
for (const op of ops)
|
|
35096
|
+
byKey.set(`${op.entityType}:${op.id}`, op);
|
|
35097
|
+
return Array.from(byKey.values());
|
|
35098
|
+
}
|
|
35099
|
+
var DEFAULT_SETTLE_MS = 600;
|
|
35100
|
+
var DEFAULT_MAX_BATCH = 500;
|
|
35101
|
+
|
|
35102
|
+
class DestructiveGate {
|
|
35103
|
+
pending = [];
|
|
35104
|
+
timer = null;
|
|
35105
|
+
settleMs;
|
|
35106
|
+
maxBatch;
|
|
35107
|
+
onSettle;
|
|
35108
|
+
onError;
|
|
35109
|
+
constructor(opts) {
|
|
35110
|
+
this.settleMs = opts.settleMs ?? DEFAULT_SETTLE_MS;
|
|
35111
|
+
this.maxBatch = opts.maxBatch ?? DEFAULT_MAX_BATCH;
|
|
35112
|
+
this.onSettle = opts.onSettle;
|
|
35113
|
+
this.onError = opts.onError;
|
|
35114
|
+
}
|
|
35115
|
+
consider(op) {
|
|
35116
|
+
this.pending.push(op);
|
|
35117
|
+
if (this.pending.length >= this.maxBatch) {
|
|
35118
|
+
this.flush();
|
|
35119
|
+
return;
|
|
35120
|
+
}
|
|
35121
|
+
if (this.timer)
|
|
35122
|
+
clearTimeout(this.timer);
|
|
35123
|
+
this.timer = setTimeout(() => {
|
|
35124
|
+
this.flush();
|
|
35125
|
+
}, this.settleMs);
|
|
35126
|
+
}
|
|
35127
|
+
async drain() {
|
|
35128
|
+
await this.flush();
|
|
35129
|
+
}
|
|
35130
|
+
size() {
|
|
35131
|
+
return this.pending.length;
|
|
35132
|
+
}
|
|
35133
|
+
async flush() {
|
|
35134
|
+
if (this.timer) {
|
|
35135
|
+
clearTimeout(this.timer);
|
|
35136
|
+
this.timer = null;
|
|
35137
|
+
}
|
|
35138
|
+
if (this.pending.length === 0)
|
|
35139
|
+
return;
|
|
35140
|
+
const batch = this.pending;
|
|
35141
|
+
this.pending = [];
|
|
35142
|
+
try {
|
|
35143
|
+
await this.onSettle(batch);
|
|
35144
|
+
} catch (err) {
|
|
35145
|
+
this.onError?.(err);
|
|
35146
|
+
}
|
|
35147
|
+
}
|
|
35148
|
+
}
|
|
35149
|
+
|
|
35150
|
+
// src/lib/resolve-pending.ts
|
|
35151
|
+
import { readFileSync as readFileSync22 } from "node:fs";
|
|
35152
|
+
|
|
34694
35153
|
// src/lib/files/push.ts
|
|
34695
|
-
import { readFileSync as
|
|
35154
|
+
import { readFileSync as readFileSync19, statSync as statSync7 } from "node:fs";
|
|
34696
35155
|
|
|
34697
35156
|
// src/lib/permanent-deletions.ts
|
|
34698
|
-
import { createHash as
|
|
35157
|
+
import { createHash as createHash4, randomUUID } from "node:crypto";
|
|
34699
35158
|
import {
|
|
34700
35159
|
chmodSync,
|
|
34701
|
-
closeSync as
|
|
35160
|
+
closeSync as closeSync5,
|
|
34702
35161
|
constants as constants2,
|
|
34703
35162
|
existsSync as existsSync4,
|
|
34704
35163
|
fsyncSync as fsyncSync2,
|
|
34705
|
-
fstatSync as
|
|
35164
|
+
fstatSync as fstatSync3,
|
|
34706
35165
|
lstatSync as lstatSync4,
|
|
34707
35166
|
mkdirSync as mkdirSync11,
|
|
34708
|
-
openSync as
|
|
34709
|
-
readFileSync as
|
|
34710
|
-
renameSync as
|
|
34711
|
-
unlinkSync as
|
|
35167
|
+
openSync as openSync5,
|
|
35168
|
+
readFileSync as readFileSync17,
|
|
35169
|
+
renameSync as renameSync9,
|
|
35170
|
+
unlinkSync as unlinkSync9,
|
|
34712
35171
|
writeSync as writeSync2
|
|
34713
35172
|
} from "node:fs";
|
|
34714
35173
|
import { dirname as dirname10, join as join13 } from "node:path";
|
|
@@ -34806,13 +35265,13 @@ function canonical(value) {
|
|
|
34806
35265
|
return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${canonical(obj[key])}`).join(",")}}`;
|
|
34807
35266
|
}
|
|
34808
35267
|
function canonicalPayloadDigest(value) {
|
|
34809
|
-
return
|
|
35268
|
+
return createHash4("sha256").update(canonical(value), "utf8").digest("hex");
|
|
34810
35269
|
}
|
|
34811
35270
|
function fileCreatePayloadDigest(contentHash, mimeType) {
|
|
34812
35271
|
return canonicalPayloadDigest({ content_hash: contentHash, mime_type: mimeType });
|
|
34813
35272
|
}
|
|
34814
35273
|
function checksum(body) {
|
|
34815
|
-
return
|
|
35274
|
+
return createHash4("sha256").update(canonical(body), "utf8").digest("hex");
|
|
34816
35275
|
}
|
|
34817
35276
|
function emptyJournal(workspaceSlug) {
|
|
34818
35277
|
return { version: 1, revision: 0, workspace_slug: workspaceSlug, records: [], refresh: [] };
|
|
@@ -34884,11 +35343,11 @@ function parseJournal(workspaceSlug, raw) {
|
|
|
34884
35343
|
return body;
|
|
34885
35344
|
}
|
|
34886
35345
|
function fsyncDirectory(path5) {
|
|
34887
|
-
const fd =
|
|
35346
|
+
const fd = openSync5(path5, constants2.O_RDONLY);
|
|
34888
35347
|
try {
|
|
34889
35348
|
fsyncSync2(fd);
|
|
34890
35349
|
} finally {
|
|
34891
|
-
|
|
35350
|
+
closeSync5(fd);
|
|
34892
35351
|
}
|
|
34893
35352
|
}
|
|
34894
35353
|
function loadPermanentDeletionJournal(workspaceSlug) {
|
|
@@ -34897,7 +35356,7 @@ function loadPermanentDeletionJournal(workspaceSlug) {
|
|
|
34897
35356
|
return emptyJournal(workspaceSlug);
|
|
34898
35357
|
assertPrivateDirectory(dirname10(target));
|
|
34899
35358
|
assertOwnerOnlyFile(target);
|
|
34900
|
-
const journal = parseJournal(workspaceSlug,
|
|
35359
|
+
const journal = parseJournal(workspaceSlug, readFileSync17(target, "utf8"));
|
|
34901
35360
|
try {
|
|
34902
35361
|
fsyncDirectory(dirname10(target));
|
|
34903
35362
|
} catch (error52) {
|
|
@@ -34932,8 +35391,8 @@ function writePermanentDeletionJournal(prior, next) {
|
|
|
34932
35391
|
`;
|
|
34933
35392
|
let fd;
|
|
34934
35393
|
try {
|
|
34935
|
-
fd =
|
|
34936
|
-
const stat =
|
|
35394
|
+
fd = openSync5(tmp, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | (constants2.O_NOFOLLOW ?? 0), 384);
|
|
35395
|
+
const stat = fstatSync3(fd);
|
|
34937
35396
|
if (!stat.isFile() || (stat.mode & 511) !== 384) {
|
|
34938
35397
|
throw new PermanentDeletionJournalError("journal temporary file is not owner-only", "prepare");
|
|
34939
35398
|
}
|
|
@@ -34942,16 +35401,16 @@ function writePermanentDeletionJournal(prior, next) {
|
|
|
34942
35401
|
while (offset < bytes.length)
|
|
34943
35402
|
offset += writeSync2(fd, bytes, offset, bytes.length - offset);
|
|
34944
35403
|
fsyncSync2(fd);
|
|
34945
|
-
|
|
35404
|
+
closeSync5(fd);
|
|
34946
35405
|
fd = undefined;
|
|
34947
35406
|
} catch (error52) {
|
|
34948
35407
|
if (fd !== undefined) {
|
|
34949
35408
|
try {
|
|
34950
|
-
|
|
35409
|
+
closeSync5(fd);
|
|
34951
35410
|
} catch {}
|
|
34952
35411
|
}
|
|
34953
35412
|
try {
|
|
34954
|
-
|
|
35413
|
+
unlinkSync9(tmp);
|
|
34955
35414
|
} catch {}
|
|
34956
35415
|
if (error52 instanceof PermanentDeletionJournalError)
|
|
34957
35416
|
throw error52;
|
|
@@ -34959,18 +35418,18 @@ function writePermanentDeletionJournal(prior, next) {
|
|
|
34959
35418
|
throw new PermanentDeletionJournalError(`could not prepare journal write: ${detail}`, "write");
|
|
34960
35419
|
}
|
|
34961
35420
|
try {
|
|
34962
|
-
|
|
35421
|
+
renameSync9(tmp, target);
|
|
34963
35422
|
} catch (error52) {
|
|
34964
35423
|
let installed2 = false;
|
|
34965
35424
|
try {
|
|
34966
35425
|
assertOwnerOnlyFile(target);
|
|
34967
|
-
const candidate = parseJournal(body.workspace_slug,
|
|
35426
|
+
const candidate = parseJournal(body.workspace_slug, readFileSync17(target, "utf8"));
|
|
34968
35427
|
installed2 = candidate.revision === body.revision && checksum(candidate) === checksum(body);
|
|
34969
35428
|
} catch {
|
|
34970
35429
|
installed2 = false;
|
|
34971
35430
|
}
|
|
34972
35431
|
try {
|
|
34973
|
-
|
|
35432
|
+
unlinkSync9(tmp);
|
|
34974
35433
|
} catch {}
|
|
34975
35434
|
if (!installed2) {
|
|
34976
35435
|
const detail = error52 instanceof Error ? error52.message : String(error52);
|
|
@@ -34978,7 +35437,7 @@ function writePermanentDeletionJournal(prior, next) {
|
|
|
34978
35437
|
}
|
|
34979
35438
|
}
|
|
34980
35439
|
assertOwnerOnlyFile(target);
|
|
34981
|
-
const installed = parseJournal(body.workspace_slug,
|
|
35440
|
+
const installed = parseJournal(body.workspace_slug, readFileSync17(target, "utf8"));
|
|
34982
35441
|
if (installed.revision !== body.revision || checksum(installed) !== checksum(body)) {
|
|
34983
35442
|
throw new PermanentDeletionJournalError("renamed journal revision did not validate", "durability_indeterminate");
|
|
34984
35443
|
}
|
|
@@ -34986,7 +35445,7 @@ function writePermanentDeletionJournal(prior, next) {
|
|
|
34986
35445
|
fsyncDirectory(parent);
|
|
34987
35446
|
} catch (firstError) {
|
|
34988
35447
|
assertOwnerOnlyFile(target);
|
|
34989
|
-
const reopened = parseJournal(body.workspace_slug,
|
|
35448
|
+
const reopened = parseJournal(body.workspace_slug, readFileSync17(target, "utf8"));
|
|
34990
35449
|
if (reopened.revision !== body.revision || checksum(reopened) !== checksum(body)) {
|
|
34991
35450
|
throw new PermanentDeletionJournalError("journal changed after rename", "durability_indeterminate");
|
|
34992
35451
|
}
|
|
@@ -35329,9 +35788,109 @@ async function prepareOrResolveGenerationCreateLocked(args) {
|
|
|
35329
35788
|
}
|
|
35330
35789
|
|
|
35331
35790
|
// src/lib/files/content-hash.ts
|
|
35332
|
-
import { createHash as
|
|
35791
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
35333
35792
|
function sha256Bytes(bytes) {
|
|
35334
|
-
return
|
|
35793
|
+
return createHash5("sha256").update(bytes).digest("hex");
|
|
35794
|
+
}
|
|
35795
|
+
|
|
35796
|
+
// src/lib/files/keep-local.ts
|
|
35797
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
35798
|
+
import { lstatSync as lstatSync5, readFileSync as readFileSync18, unlinkSync as unlinkSync10, writeFileSync as writeFileSync8 } from "node:fs";
|
|
35799
|
+
import { dirname as dirname11, join as join14 } from "node:path";
|
|
35800
|
+
function keepUnsentLocalCopy(args) {
|
|
35801
|
+
const read = readKeepable(args.workspaceSlug, args.abs);
|
|
35802
|
+
if (read.kind === "failed")
|
|
35803
|
+
return read.failure;
|
|
35804
|
+
if (read.kind === "absent")
|
|
35805
|
+
return null;
|
|
35806
|
+
const localHash = sha256Bytes(read.bytes);
|
|
35807
|
+
if (args.safeHashes.includes(localHash))
|
|
35808
|
+
return null;
|
|
35809
|
+
let stashed;
|
|
35810
|
+
try {
|
|
35811
|
+
stashed = stashFileConflictOnce({
|
|
35812
|
+
workspaceSlug: args.workspaceSlug,
|
|
35813
|
+
relPath: args.relPath,
|
|
35814
|
+
timestamp: args.now(),
|
|
35815
|
+
bytes: read.bytes,
|
|
35816
|
+
side: "local"
|
|
35817
|
+
});
|
|
35818
|
+
} catch (err) {
|
|
35819
|
+
return { reason: "stash_io", code: errCode(err) };
|
|
35820
|
+
}
|
|
35821
|
+
const key = `reused:${stashed.path}:${args.versionKey}`;
|
|
35822
|
+
if (stashed.reused && args.loggedStashNotices?.has(key))
|
|
35823
|
+
return null;
|
|
35824
|
+
if (stashed.reused)
|
|
35825
|
+
args.loggedStashNotices?.add(key);
|
|
35826
|
+
args.output?.log(fileConflictLine({ path: args.relPath, stash: stashed.path, via: args.via, reused: stashed.reused }));
|
|
35827
|
+
args.output?.warn(stashed.reused ? `Conflict on files/${args.relPath} → your local edit is already kept at ${stashed.path}.` : `Conflict on files/${args.relPath} → stashed local edit to ${stashed.path} (cloud-wins${args.onBoot ? " on boot" : ""}).`);
|
|
35828
|
+
return null;
|
|
35829
|
+
}
|
|
35830
|
+
function probeKeep(args) {
|
|
35831
|
+
const read = readKeepable(args.workspaceSlug, args.abs);
|
|
35832
|
+
if (read.kind === "failed")
|
|
35833
|
+
return read.failure;
|
|
35834
|
+
if (read.kind === "absent")
|
|
35835
|
+
return null;
|
|
35836
|
+
if (args.safeHashes.includes(sha256Bytes(read.bytes)))
|
|
35837
|
+
return null;
|
|
35838
|
+
const dir = conflictsRoot(args.workspaceSlug);
|
|
35839
|
+
const probe2 = join14(dir, `.keep-probe-${process.pid}-${randomBytes2(6).toString("hex")}`);
|
|
35840
|
+
try {
|
|
35841
|
+
prepareFileStash(args.workspaceSlug, args.relPath);
|
|
35842
|
+
writeFileSync8(probe2, read.bytes, { mode: 384 });
|
|
35843
|
+
} catch (err) {
|
|
35844
|
+
return { reason: "stash_io", code: errCode(err) };
|
|
35845
|
+
} finally {
|
|
35846
|
+
try {
|
|
35847
|
+
unlinkSync10(probe2);
|
|
35848
|
+
} catch {}
|
|
35849
|
+
}
|
|
35850
|
+
return null;
|
|
35851
|
+
}
|
|
35852
|
+
function unsentBytesAt(args) {
|
|
35853
|
+
const read = readKeepable(args.workspaceSlug, args.abs);
|
|
35854
|
+
if (read.kind === "failed")
|
|
35855
|
+
return read;
|
|
35856
|
+
if (read.kind === "absent")
|
|
35857
|
+
return { kind: "none" };
|
|
35858
|
+
return args.safeHashes.includes(sha256Bytes(read.bytes)) ? { kind: "none" } : { kind: "unsent" };
|
|
35859
|
+
}
|
|
35860
|
+
function readLocalForKeep(workspaceSlug, abs) {
|
|
35861
|
+
return readKeepable(workspaceSlug, abs);
|
|
35862
|
+
}
|
|
35863
|
+
function localFingerprint(abs) {
|
|
35864
|
+
try {
|
|
35865
|
+
const st = lstatSync5(abs);
|
|
35866
|
+
return { size: st.size, mtime_ms: st.mtimeMs, ino: st.ino };
|
|
35867
|
+
} catch (err) {
|
|
35868
|
+
return err?.code === "ENOENT" ? null : undefined;
|
|
35869
|
+
}
|
|
35870
|
+
}
|
|
35871
|
+
function readKeepable(workspaceSlug, abs) {
|
|
35872
|
+
if (hasSymlinkedComponent(workspaceSlug, dirname11(abs))) {
|
|
35873
|
+
return { kind: "failed", failure: { reason: "symlink" } };
|
|
35874
|
+
}
|
|
35875
|
+
let st;
|
|
35876
|
+
try {
|
|
35877
|
+
st = lstatSync5(abs);
|
|
35878
|
+
} catch (err) {
|
|
35879
|
+
if (err?.code === "ENOENT")
|
|
35880
|
+
return { kind: "absent" };
|
|
35881
|
+
return { kind: "failed", failure: { reason: "unreadable", code: errCode(err) } };
|
|
35882
|
+
}
|
|
35883
|
+
if (!st.isFile())
|
|
35884
|
+
return { kind: "failed", failure: { reason: "not_a_file" } };
|
|
35885
|
+
try {
|
|
35886
|
+
return { kind: "read", bytes: readFileSync18(abs) };
|
|
35887
|
+
} catch (err) {
|
|
35888
|
+
return { kind: "failed", failure: { reason: "unreadable", code: errCode(err) } };
|
|
35889
|
+
}
|
|
35890
|
+
}
|
|
35891
|
+
function errCode(err) {
|
|
35892
|
+
const code = err?.code;
|
|
35893
|
+
return typeof code === "string" ? code : undefined;
|
|
35335
35894
|
}
|
|
35336
35895
|
|
|
35337
35896
|
// src/lib/files/mime.ts
|
|
@@ -35403,23 +35962,63 @@ async function restoreFileFromCloud(args) {
|
|
|
35403
35962
|
const { workspaceSlug, baseUrl, pat, id, relPath, state, fetchImpl, suppressor, output } = args;
|
|
35404
35963
|
const got = args.prefetched ?? await getFile({ baseUrl, pat, id, fetchImpl });
|
|
35405
35964
|
const abs = filesSubtreePath(workspaceSlug, relPath);
|
|
35965
|
+
const ack = getFileEntry(state, relPath);
|
|
35966
|
+
const keptHash = args.kept ? sha256Bytes(args.kept) : undefined;
|
|
35967
|
+
const blocked = (cloud) => {
|
|
35968
|
+
const read = readCommittedFileAck(workspaceSlug, relPath);
|
|
35969
|
+
if (read.kind === "unknown")
|
|
35970
|
+
return null;
|
|
35971
|
+
if (read.kind === "absent")
|
|
35972
|
+
return ack ? "removed" : null;
|
|
35973
|
+
const committed = read.entry;
|
|
35974
|
+
if (committed.id !== id)
|
|
35975
|
+
return "replaced";
|
|
35976
|
+
const against = cloud ?? ack;
|
|
35977
|
+
return against && isLaterVersion(committed, against) ? "newer_ack" : null;
|
|
35978
|
+
};
|
|
35979
|
+
const unsent = (cloudHash) => {
|
|
35980
|
+
const committed = readCommittedFileEntry(workspaceSlug, relPath);
|
|
35981
|
+
const disk = unsentBytesAt({
|
|
35982
|
+
workspaceSlug,
|
|
35983
|
+
abs,
|
|
35984
|
+
safeHashes: [
|
|
35985
|
+
cloudHash,
|
|
35986
|
+
ack?.content_hash,
|
|
35987
|
+
keptHash,
|
|
35988
|
+
committed?.id === id ? committed.content_hash : undefined
|
|
35989
|
+
]
|
|
35990
|
+
});
|
|
35991
|
+
if (disk.kind === "failed")
|
|
35992
|
+
return disk.failure.reason;
|
|
35993
|
+
return disk.kind === "unsent" ? "newer_save" : null;
|
|
35994
|
+
};
|
|
35995
|
+
const skipped = (reason2) => {
|
|
35996
|
+
output?.log(fileRestoreSkippedLine({ path: relPath, rowId: id, reason: reason2 }));
|
|
35997
|
+
return { state, outcome: "skipped", reason: reason2 };
|
|
35998
|
+
};
|
|
35406
35999
|
if (!got.ok) {
|
|
36000
|
+
const reason2 = blocked(null) ?? unsent(undefined);
|
|
36001
|
+
if (reason2)
|
|
36002
|
+
return skipped(reason2);
|
|
35407
36003
|
suppressor?.registerDelete(abs);
|
|
35408
36004
|
deleteFilesFile(workspaceSlug, relPath);
|
|
35409
|
-
return dropFiles(state, relPath);
|
|
36005
|
+
return { state: dropFiles(state, relPath), outcome: "removed" };
|
|
35410
36006
|
}
|
|
35411
36007
|
if (got.row.download_url === null) {
|
|
35412
36008
|
output?.warn(`files/${relPath}: Plinth holds no bytes for this file, so there is nothing to restore from cloud — the local copy and its state were left exactly as they are.`);
|
|
35413
|
-
return state;
|
|
36009
|
+
return { state, outcome: "no_bytes" };
|
|
35414
36010
|
}
|
|
35415
36011
|
const bytes = await downloadBytes(got.row.download_url, fetchImpl);
|
|
36012
|
+
const reason = blocked(got.row) ?? unsent(got.row.content_hash);
|
|
36013
|
+
if (reason)
|
|
36014
|
+
return skipped(reason);
|
|
35416
36015
|
suppressor?.registerWrite(abs, {
|
|
35417
36016
|
bodyHash: got.row.content_hash,
|
|
35418
36017
|
frontmatterHash: ""
|
|
35419
36018
|
});
|
|
35420
36019
|
writeFilesBytes(workspaceSlug, relPath, bytes);
|
|
35421
36020
|
deleteExtractedText(workspaceSlug, relPath);
|
|
35422
|
-
|
|
36021
|
+
const restored = persistFiles(state, {
|
|
35423
36022
|
id: got.row.id,
|
|
35424
36023
|
path: relPath,
|
|
35425
36024
|
content_hash: got.row.content_hash,
|
|
@@ -35428,6 +36027,7 @@ async function restoreFileFromCloud(args) {
|
|
|
35428
36027
|
updated_at: got.row.updated_at,
|
|
35429
36028
|
inode: safeInode(abs)
|
|
35430
36029
|
});
|
|
36030
|
+
return { state: restored, outcome: "restored" };
|
|
35431
36031
|
}
|
|
35432
36032
|
async function pushFilesUpsert(args) {
|
|
35433
36033
|
return withPermanentDeletionLock("workspace_file", args.relPath, () => pushFilesUpsertLocked(args));
|
|
@@ -35485,7 +36085,7 @@ async function pushFilesUpsertLocked(args) {
|
|
|
35485
36085
|
state: next
|
|
35486
36086
|
};
|
|
35487
36087
|
}
|
|
35488
|
-
bytes =
|
|
36088
|
+
bytes = readFileSync19(absolutePath);
|
|
35489
36089
|
hash2 = sha256Bytes(bytes);
|
|
35490
36090
|
inode = st.ino;
|
|
35491
36091
|
}
|
|
@@ -35692,8 +36292,9 @@ async function pushFilesUpsertLocked(args) {
|
|
|
35692
36292
|
side: "local"
|
|
35693
36293
|
});
|
|
35694
36294
|
output?.log(fileConflictLine({ path: relPath, stash: stashPath, reused }));
|
|
36295
|
+
let restore;
|
|
35695
36296
|
if (existing) {
|
|
35696
|
-
|
|
36297
|
+
const restored = await restoreFileFromCloud({
|
|
35697
36298
|
workspaceSlug,
|
|
35698
36299
|
baseUrl,
|
|
35699
36300
|
pat,
|
|
@@ -35703,10 +36304,16 @@ async function pushFilesUpsertLocked(args) {
|
|
|
35703
36304
|
fetchImpl,
|
|
35704
36305
|
suppressor,
|
|
35705
36306
|
output,
|
|
35706
|
-
prefetched: cloudRow
|
|
36307
|
+
prefetched: cloudRow,
|
|
36308
|
+
kept: bytes
|
|
35707
36309
|
});
|
|
36310
|
+
nextState = restored.state;
|
|
36311
|
+
restore = restored.outcome;
|
|
35708
36312
|
}
|
|
35709
|
-
return {
|
|
36313
|
+
return {
|
|
36314
|
+
outcome: { kind: "conflict", relPath, stashPath, ...restore ? { restore } : {} },
|
|
36315
|
+
state: nextState
|
|
36316
|
+
};
|
|
35710
36317
|
}
|
|
35711
36318
|
async function pushFilesDelete(args) {
|
|
35712
36319
|
const { workspaceSlug, baseUrl, pat, relPath, state, deps } = args;
|
|
@@ -35732,7 +36339,7 @@ async function pushFilesDelete(args) {
|
|
|
35732
36339
|
output?.log(fileDeleteLine({ path: relPath, id: existing.id, note: "already_absent" }));
|
|
35733
36340
|
return { outcome: { kind: "deleted", relPath, id: existing.id }, state: dropFiles(state, relPath) };
|
|
35734
36341
|
}
|
|
35735
|
-
const
|
|
36342
|
+
const restore = await restoreFileFromCloud({
|
|
35736
36343
|
workspaceSlug,
|
|
35737
36344
|
baseUrl,
|
|
35738
36345
|
pat,
|
|
@@ -35743,16 +36350,71 @@ async function pushFilesDelete(args) {
|
|
|
35743
36350
|
suppressor,
|
|
35744
36351
|
output
|
|
35745
36352
|
});
|
|
35746
|
-
return {
|
|
36353
|
+
return {
|
|
36354
|
+
outcome: { kind: "conflict", relPath, stashPath: "", restore: restore.outcome },
|
|
36355
|
+
state: restore.state
|
|
36356
|
+
};
|
|
35747
36357
|
}
|
|
35748
36358
|
|
|
35749
36359
|
// src/lib/push.ts
|
|
35750
|
-
|
|
35751
|
-
import { existsSync as existsSync5, readFileSync as readFileSync19, statSync as statSync8 } from "node:fs";
|
|
36360
|
+
import { existsSync as existsSync5, readFileSync as readFileSync21, statSync as statSync8 } from "node:fs";
|
|
35752
36361
|
import { basename as basename5, relative as relative5 } from "node:path";
|
|
35753
36362
|
|
|
36363
|
+
// src/lib/frontmatter-held.ts
|
|
36364
|
+
import { lstatSync as lstatSync6, readFileSync as readFileSync20 } from "node:fs";
|
|
36365
|
+
import { resolve as resolve4, sep as sep5 } from "node:path";
|
|
36366
|
+
function holdUnreadableFrontmatter(args) {
|
|
36367
|
+
const next = recordEntityFrontmatterUnreadable(args);
|
|
36368
|
+
args.output?.log(entityPushHeldLine({
|
|
36369
|
+
entityType: args.entityType,
|
|
36370
|
+
slug: args.slug,
|
|
36371
|
+
reason: FRONTMATTER_UNREADABLE_REASON
|
|
36372
|
+
}));
|
|
36373
|
+
if (next !== args.state) {
|
|
36374
|
+
args.output?.warn(`${args.entityType}/${args.slug}: ${frontmatterUnreadableSentence(args.why)}`);
|
|
36375
|
+
}
|
|
36376
|
+
return next;
|
|
36377
|
+
}
|
|
36378
|
+
function settleFrontmatterHolds(state, workspaceSlug) {
|
|
36379
|
+
const held = listRejected(state).filter(isFrontmatterHold);
|
|
36380
|
+
if (held.length === 0)
|
|
36381
|
+
return state;
|
|
36382
|
+
let root;
|
|
36383
|
+
try {
|
|
36384
|
+
root = workspaceMirrorPath(workspaceSlug);
|
|
36385
|
+
} catch {
|
|
36386
|
+
return state;
|
|
36387
|
+
}
|
|
36388
|
+
const settled = new Set(held.map((record2) => record2.path).filter((path5) => !stillHeld(workspaceSlug, root, path5)));
|
|
36389
|
+
if (settled.size === 0)
|
|
36390
|
+
return state;
|
|
36391
|
+
return commitState(state, {
|
|
36392
|
+
...state,
|
|
36393
|
+
rejected: listRejected(state).filter((o) => !(isFrontmatterHold(o) && settled.has(o.path)))
|
|
36394
|
+
});
|
|
36395
|
+
}
|
|
36396
|
+
function isFrontmatterHold(record2) {
|
|
36397
|
+
return record2.target === "entity" && record2.reason === FRONTMATTER_UNREADABLE_REASON;
|
|
36398
|
+
}
|
|
36399
|
+
function stillHeld(workspaceSlug, root, recordPath) {
|
|
36400
|
+
const absolutePath = resolve4(root, recordPath);
|
|
36401
|
+
if (!absolutePath.startsWith(root + sep5))
|
|
36402
|
+
return true;
|
|
36403
|
+
if (hasSymlinkedComponent(workspaceSlug, absolutePath))
|
|
36404
|
+
return true;
|
|
36405
|
+
let raw;
|
|
36406
|
+
try {
|
|
36407
|
+
if (!lstatSync6(absolutePath).isFile())
|
|
36408
|
+
return true;
|
|
36409
|
+
raw = readFileSync20(absolutePath, "utf8");
|
|
36410
|
+
} catch (err) {
|
|
36411
|
+
const code = err.code;
|
|
36412
|
+
return code !== "ENOENT" && code !== "ENOTDIR";
|
|
36413
|
+
}
|
|
36414
|
+
return frontmatterParseError(raw) !== null;
|
|
36415
|
+
}
|
|
36416
|
+
|
|
35754
36417
|
// src/lib/frontmatter-id.ts
|
|
35755
|
-
var import_gray_matter8 = __toESM(require_gray_matter(), 1);
|
|
35756
36418
|
function eolOf(content) {
|
|
35757
36419
|
return /\r\n/.test(content) ? `\r
|
|
35758
36420
|
` : `
|
|
@@ -35789,158 +36451,16 @@ function withFrontmatterId(raw, parsed, id) {
|
|
|
35789
36451
|
}
|
|
35790
36452
|
function frontmatterIdWriteIsSound(next, id, expectedBody) {
|
|
35791
36453
|
try {
|
|
35792
|
-
const check2 =
|
|
36454
|
+
const check2 = matter(next);
|
|
35793
36455
|
return check2.data?.["id"] === id && check2.content === expectedBody;
|
|
35794
36456
|
} catch {
|
|
35795
36457
|
return false;
|
|
35796
36458
|
}
|
|
35797
36459
|
}
|
|
35798
36460
|
|
|
35799
|
-
// src/lib/pull-write.ts
|
|
35800
|
-
var import_gray_matter9 = __toESM(require_gray_matter(), 1);
|
|
35801
|
-
import { readFileSync as readFileSync18 } from "node:fs";
|
|
35802
|
-
function computeMirrorHashes(strategy, content) {
|
|
35803
|
-
const parsed = import_gray_matter9.default(content);
|
|
35804
|
-
const fm = parsed.data ?? {};
|
|
35805
|
-
const lifted = splitUserFrontmatter(strategy, fm);
|
|
35806
|
-
const userFm = userFrontmatter(strategy, lifted.frontmatter);
|
|
35807
|
-
const hashBody = hashableBody(strategy, parsed.content);
|
|
35808
|
-
return {
|
|
35809
|
-
bodyHash: stableHash(hashBody),
|
|
35810
|
-
frontmatterHash: stableHash({ ...userFm, ...lifted.topLevel }),
|
|
35811
|
-
bodyLen: hashBody.length,
|
|
35812
|
-
frontmatterId: fm["id"]
|
|
35813
|
-
};
|
|
35814
|
-
}
|
|
35815
|
-
function hashableBody(strategy, body) {
|
|
35816
|
-
return strategy.stripLocalBlocks?.(body) ?? body;
|
|
35817
|
-
}
|
|
35818
|
-
function relocateForSlugChange(args) {
|
|
35819
|
-
const { workspaceSlug, strategy, fromSlug, toSlug, acked, echo, now } = args;
|
|
35820
|
-
if (fromSlug === toSlug)
|
|
35821
|
-
return { kind: "not_needed", writeSlug: toSlug };
|
|
35822
|
-
if (!echo) {
|
|
35823
|
-
return { kind: "skipped", writeSlug: toSlug, fromSlug, reason: "unguarded" };
|
|
35824
|
-
}
|
|
35825
|
-
if (strategy.mirrorRelativePath(fromSlug).split("/").length !== 2) {
|
|
35826
|
-
return { kind: "skipped", writeSlug: toSlug, fromSlug, reason: "foldered" };
|
|
35827
|
-
}
|
|
35828
|
-
let fromPath;
|
|
35829
|
-
try {
|
|
35830
|
-
fromPath = entityFilePath(workspaceSlug, strategy.type, fromSlug);
|
|
35831
|
-
entityFilePath(workspaceSlug, strategy.type, toSlug);
|
|
35832
|
-
} catch (err) {
|
|
35833
|
-
if (err instanceof MirrorPathError) {
|
|
35834
|
-
return {
|
|
35835
|
-
kind: "skipped",
|
|
35836
|
-
writeSlug: toSlug,
|
|
35837
|
-
fromSlug,
|
|
35838
|
-
reason: "unresolvable"
|
|
35839
|
-
};
|
|
35840
|
-
}
|
|
35841
|
-
throw err;
|
|
35842
|
-
}
|
|
35843
|
-
let raw = null;
|
|
35844
|
-
try {
|
|
35845
|
-
raw = readFileSync18(fromPath, "utf8");
|
|
35846
|
-
} catch (err) {
|
|
35847
|
-
const code = err?.code;
|
|
35848
|
-
if (code === "ENOENT") {
|
|
35849
|
-
return { kind: "absent", writeSlug: toSlug, fromSlug };
|
|
35850
|
-
}
|
|
35851
|
-
return {
|
|
35852
|
-
kind: "refused",
|
|
35853
|
-
writeSlug: fromSlug,
|
|
35854
|
-
fromSlug,
|
|
35855
|
-
reason: "source_unreadable"
|
|
35856
|
-
};
|
|
35857
|
-
}
|
|
35858
|
-
let diverged = true;
|
|
35859
|
-
try {
|
|
35860
|
-
const hashes = computeMirrorHashes(strategy, raw);
|
|
35861
|
-
diverged = hashes.bodyHash !== acked.bodyHash || hashes.frontmatterHash !== acked.frontmatterHash;
|
|
35862
|
-
} catch {
|
|
35863
|
-
diverged = true;
|
|
35864
|
-
}
|
|
35865
|
-
const stashPath = diverged ? stashConflict({
|
|
35866
|
-
workspaceSlug,
|
|
35867
|
-
entityType: strategy.type,
|
|
35868
|
-
entitySlug: fromSlug,
|
|
35869
|
-
timestamp: now(),
|
|
35870
|
-
contents: raw,
|
|
35871
|
-
side: "local"
|
|
35872
|
-
}) : undefined;
|
|
35873
|
-
try {
|
|
35874
|
-
let registered = false;
|
|
35875
|
-
const result = moveEntityFile(workspaceSlug, strategy.type, fromSlug, toSlug, () => {
|
|
35876
|
-
if (echo.watcher === "live") {
|
|
35877
|
-
echo.suppressor.registerDelete(fromPath);
|
|
35878
|
-
registered = true;
|
|
35879
|
-
}
|
|
35880
|
-
});
|
|
35881
|
-
if (result === "moved") {
|
|
35882
|
-
return { kind: "moved", writeSlug: toSlug, fromSlug, stashPath };
|
|
35883
|
-
}
|
|
35884
|
-
if (registered && echo.watcher === "live") {
|
|
35885
|
-
echo.suppressor.cancelDelete(fromPath);
|
|
35886
|
-
}
|
|
35887
|
-
return { kind: "absent", writeSlug: toSlug, fromSlug };
|
|
35888
|
-
} catch (err) {
|
|
35889
|
-
if (err instanceof MirrorDestinationOccupiedError) {
|
|
35890
|
-
return {
|
|
35891
|
-
kind: "refused",
|
|
35892
|
-
writeSlug: fromSlug,
|
|
35893
|
-
fromSlug,
|
|
35894
|
-
reason: "destination_occupied",
|
|
35895
|
-
stashPath
|
|
35896
|
-
};
|
|
35897
|
-
}
|
|
35898
|
-
if (err instanceof MirrorPathError) {
|
|
35899
|
-
return {
|
|
35900
|
-
kind: "skipped",
|
|
35901
|
-
writeSlug: toSlug,
|
|
35902
|
-
fromSlug,
|
|
35903
|
-
reason: "unresolvable"
|
|
35904
|
-
};
|
|
35905
|
-
}
|
|
35906
|
-
throw err;
|
|
35907
|
-
}
|
|
35908
|
-
}
|
|
35909
|
-
function writeMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, suppressor, pathSlug) {
|
|
35910
|
-
const targetSlug = pathSlug ?? doc2.slug;
|
|
35911
|
-
const content = renderMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, targetSlug);
|
|
35912
|
-
if (suppressor) {
|
|
35913
|
-
const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
|
|
35914
|
-
suppressor.registerWrite(target, computeMirrorHashes(strategy, content));
|
|
35915
|
-
}
|
|
35916
|
-
writeMirrorFile(workspaceSlug, strategy.type, targetSlug, content);
|
|
35917
|
-
return content;
|
|
35918
|
-
}
|
|
35919
|
-
function renderMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, targetSlug) {
|
|
35920
|
-
let content = strategy.serialise(doc2);
|
|
35921
|
-
if (strategy.preserveLocalBodyOnPull) {
|
|
35922
|
-
const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
|
|
35923
|
-
let existing = null;
|
|
35924
|
-
try {
|
|
35925
|
-
existing = readFileSync18(target, "utf8");
|
|
35926
|
-
} catch (err) {
|
|
35927
|
-
if (err?.code !== "ENOENT")
|
|
35928
|
-
throw err;
|
|
35929
|
-
}
|
|
35930
|
-
if (existing !== null) {
|
|
35931
|
-
const existingParsed = import_gray_matter9.default(existing);
|
|
35932
|
-
const cloudParsed = import_gray_matter9.default(content);
|
|
35933
|
-
const stitched = import_gray_matter9.default.stringify(existingParsed.content, cloudParsed.data);
|
|
35934
|
-
content = stitched.endsWith(`
|
|
35935
|
-
`) ? stitched : stitched + `
|
|
35936
|
-
`;
|
|
35937
|
-
}
|
|
35938
|
-
}
|
|
35939
|
-
return content;
|
|
35940
|
-
}
|
|
35941
|
-
|
|
35942
36461
|
// src/lib/push.ts
|
|
35943
36462
|
var DELETE_DECLINED_PATH_EXISTS = "the file is on disk again — a newer write owns this row";
|
|
36463
|
+
var FRONTMATTER_PARSE_ERROR = "frontmatter parse error";
|
|
35944
36464
|
function deleteDeclinedSentence(relPath) {
|
|
35945
36465
|
return `Not deleted: ${relPath} is on disk again, so the row was left alone. If you meant to delete it, remove the file and it will be retried.`;
|
|
35946
36466
|
}
|
|
@@ -36049,8 +36569,8 @@ function stampStateFromMirror(args) {
|
|
|
36049
36569
|
const { strategy, workspaceSlug, doc: doc2, state, priorMemoryHash } = args;
|
|
36050
36570
|
const targetSlug = args.pathSlug ?? doc2.slug;
|
|
36051
36571
|
const mirrorPath = entityFilePath(workspaceSlug, strategy.type, targetSlug);
|
|
36052
|
-
const mirrorRaw =
|
|
36053
|
-
const mirrorParsed =
|
|
36572
|
+
const mirrorRaw = readFileSync21(mirrorPath, "utf8");
|
|
36573
|
+
const mirrorParsed = matter(mirrorRaw);
|
|
36054
36574
|
const mirrorFm = mirrorParsed.data ?? {};
|
|
36055
36575
|
const mirrorLifted = splitUserFrontmatter(strategy, mirrorFm);
|
|
36056
36576
|
const mirrorUserFm = userFrontmatter(strategy, mirrorLifted.frontmatter);
|
|
@@ -36202,7 +36722,7 @@ async function pushFile(args) {
|
|
|
36202
36722
|
}
|
|
36203
36723
|
let raw;
|
|
36204
36724
|
try {
|
|
36205
|
-
raw =
|
|
36725
|
+
raw = readFileSync21(absolutePath, "utf8");
|
|
36206
36726
|
} catch (err) {
|
|
36207
36727
|
if (err?.code === "ENOENT") {
|
|
36208
36728
|
return runPushDelete({
|
|
@@ -36222,23 +36742,32 @@ async function pushFile(args) {
|
|
|
36222
36742
|
}
|
|
36223
36743
|
throw err;
|
|
36224
36744
|
}
|
|
36225
|
-
let parsed;
|
|
36226
|
-
try {
|
|
36227
|
-
parsed = import_gray_matter10.default(raw);
|
|
36228
|
-
} catch {
|
|
36229
|
-
return {
|
|
36230
|
-
outcome: { kind: "ignored", absolutePath, reason: "frontmatter parse error" },
|
|
36231
|
-
state
|
|
36232
|
-
};
|
|
36233
|
-
}
|
|
36234
|
-
const fm = parsed.data ?? {};
|
|
36235
|
-
const slugFromFm = typeof fm.slug === "string" && fm.slug.length > 0 ? fm.slug : null;
|
|
36236
36745
|
let parsedSlug = null;
|
|
36237
36746
|
try {
|
|
36238
36747
|
const rel = relative5(workspaceMirrorPath(workspaceSlug), absolutePath);
|
|
36239
36748
|
parsedSlug = strategy.parseMirrorPath(rel)?.slug ?? null;
|
|
36240
36749
|
} catch {}
|
|
36241
36750
|
const slugFromPath = parsedSlug ?? basename5(absolutePath, ".md");
|
|
36751
|
+
const read = readFrontmatter(raw);
|
|
36752
|
+
if (!read.ok) {
|
|
36753
|
+
return {
|
|
36754
|
+
outcome: { kind: "ignored", absolutePath, reason: FRONTMATTER_PARSE_ERROR },
|
|
36755
|
+
state: holdUnreadableFrontmatter({
|
|
36756
|
+
state,
|
|
36757
|
+
workspaceSlug,
|
|
36758
|
+
absolutePath,
|
|
36759
|
+
entityType,
|
|
36760
|
+
slug: slugFromPath,
|
|
36761
|
+
why: read.why,
|
|
36762
|
+
now,
|
|
36763
|
+
output
|
|
36764
|
+
}),
|
|
36765
|
+
base: state
|
|
36766
|
+
};
|
|
36767
|
+
}
|
|
36768
|
+
let parsed = read.parsed;
|
|
36769
|
+
const fm = parsed.data ?? {};
|
|
36770
|
+
const slugFromFm = typeof fm.slug === "string" && fm.slug.length > 0 ? fm.slug : null;
|
|
36242
36771
|
const slug = identity === "path" ? slugFromPath : slugFromFm ?? slugFromPath;
|
|
36243
36772
|
if (!slug) {
|
|
36244
36773
|
return {
|
|
@@ -36252,14 +36781,14 @@ async function pushFile(args) {
|
|
|
36252
36781
|
if (strategy.classifyFieldErrors) {
|
|
36253
36782
|
const desiredBody = spliceSyncErrorBlock(parsed.content, fieldErrors);
|
|
36254
36783
|
if (desiredBody.replace(/\s+$/, "") !== parsed.content.replace(/\s+$/, "")) {
|
|
36255
|
-
let rewritten =
|
|
36784
|
+
let rewritten = stringifyFrontmatter(desiredBody, parsed.data ?? {});
|
|
36256
36785
|
if (!rewritten.endsWith(`
|
|
36257
36786
|
`))
|
|
36258
36787
|
rewritten += `
|
|
36259
36788
|
`;
|
|
36260
36789
|
writeMirrorFile(workspaceSlug, entityType, slug, rewritten);
|
|
36261
36790
|
raw = rewritten;
|
|
36262
|
-
parsed =
|
|
36791
|
+
parsed = parseUncached(rewritten);
|
|
36263
36792
|
}
|
|
36264
36793
|
}
|
|
36265
36794
|
const liveInode = safeInode2(absolutePath);
|
|
@@ -36537,7 +37066,7 @@ async function runPushUpsert(args) {
|
|
|
36537
37066
|
}
|
|
36538
37067
|
const wireProjection = (md) => {
|
|
36539
37068
|
try {
|
|
36540
|
-
const p =
|
|
37069
|
+
const p = matter(md);
|
|
36541
37070
|
const f = p.data ?? {};
|
|
36542
37071
|
const l = splitUserFrontmatter(strategy, f);
|
|
36543
37072
|
const u = userFrontmatter(strategy, l.frontmatter);
|
|
@@ -36552,7 +37081,7 @@ async function runPushUpsert(args) {
|
|
|
36552
37081
|
if (liveEntry !== snapshotEntry) {
|
|
36553
37082
|
let onDisk = null;
|
|
36554
37083
|
try {
|
|
36555
|
-
onDisk =
|
|
37084
|
+
onDisk = readFileSync21(args.absolutePath, "utf8");
|
|
36556
37085
|
} catch {
|
|
36557
37086
|
onDisk = null;
|
|
36558
37087
|
}
|
|
@@ -37014,7 +37543,7 @@ function writeCreatedIdBack(args) {
|
|
|
37014
37543
|
if (entityFilePath(args.workspaceSlug, args.strategy.type, args.slug) !== args.absolutePath) {
|
|
37015
37544
|
return;
|
|
37016
37545
|
}
|
|
37017
|
-
const readBack = args.readBack ?? ((p) =>
|
|
37546
|
+
const readBack = args.readBack ?? ((p) => readFileSync21(p, "utf8"));
|
|
37018
37547
|
if (readBack(args.absolutePath) !== args.raw)
|
|
37019
37548
|
return;
|
|
37020
37549
|
args.suppressor?.registerWrite(args.absolutePath, computeMirrorHashes(args.strategy, write.content));
|
|
@@ -37486,8 +38015,8 @@ async function pushRename(args) {
|
|
|
37486
38015
|
};
|
|
37487
38016
|
if (!updated.body_hash || !updated.frontmatter_hash) {
|
|
37488
38017
|
try {
|
|
37489
|
-
const mirrorRaw =
|
|
37490
|
-
const mirrorParsed =
|
|
38018
|
+
const mirrorRaw = readFileSync21(toPath, "utf8");
|
|
38019
|
+
const mirrorParsed = matter(mirrorRaw);
|
|
37491
38020
|
const renameFm = mirrorParsed.data ?? {};
|
|
37492
38021
|
const renameLifted = splitUserFrontmatter(strategy, renameFm);
|
|
37493
38022
|
const renameUserFm = userFrontmatter(strategy, renameLifted.frontmatter);
|
|
@@ -37523,7 +38052,7 @@ async function pushRename(args) {
|
|
|
37523
38052
|
if (patchResult.reason === "slug_conflict") {
|
|
37524
38053
|
let contents2 = "";
|
|
37525
38054
|
try {
|
|
37526
|
-
contents2 =
|
|
38055
|
+
contents2 = readFileSync21(toPath, "utf8");
|
|
37527
38056
|
} catch {}
|
|
37528
38057
|
const stashPath2 = stashConflict({
|
|
37529
38058
|
workspaceSlug,
|
|
@@ -37571,7 +38100,7 @@ async function pushRename(args) {
|
|
|
37571
38100
|
}
|
|
37572
38101
|
let contents = "";
|
|
37573
38102
|
try {
|
|
37574
|
-
contents =
|
|
38103
|
+
contents = readFileSync21(toPath, "utf8");
|
|
37575
38104
|
} catch {}
|
|
37576
38105
|
const stashPath = stashConflict({
|
|
37577
38106
|
workspaceSlug,
|
|
@@ -37619,7 +38148,7 @@ async function drainResolvedBatches(deps) {
|
|
|
37619
38148
|
deps.output?.log(`[DESTRUCTIVE_APPLIED batch=${batch.batch_id} ops=${batch.ops.length}]`);
|
|
37620
38149
|
applied++;
|
|
37621
38150
|
} else {
|
|
37622
|
-
await discardBatchOps(batch, deps);
|
|
38151
|
+
await discardBatchOps(batch, deps, mutex2);
|
|
37623
38152
|
deps.output?.log(`[DESTRUCTIVE_DISCARDED batch=${batch.batch_id} ops=${batch.ops.length}]`);
|
|
37624
38153
|
discarded++;
|
|
37625
38154
|
}
|
|
@@ -37692,11 +38221,11 @@ async function applyFileOp(op, deps) {
|
|
|
37692
38221
|
});
|
|
37693
38222
|
deps.applyState(base2, result.state);
|
|
37694
38223
|
}
|
|
37695
|
-
async function discardBatchOps(batch, deps) {
|
|
38224
|
+
async function discardBatchOps(batch, deps, mutex2) {
|
|
37696
38225
|
for (const op of batch.ops) {
|
|
37697
38226
|
try {
|
|
37698
38227
|
if (op.target === "file") {
|
|
37699
|
-
await discardFileOp(op, deps);
|
|
38228
|
+
await discardFileOp(op, deps, mutex2);
|
|
37700
38229
|
continue;
|
|
37701
38230
|
}
|
|
37702
38231
|
const strategy = requireStrategy(op.entityType);
|
|
@@ -37704,7 +38233,7 @@ async function discardBatchOps(batch, deps) {
|
|
|
37704
38233
|
const path5 = opPath(deps.workspaceSlug, op);
|
|
37705
38234
|
let local = null;
|
|
37706
38235
|
try {
|
|
37707
|
-
local =
|
|
38236
|
+
local = readFileSync22(path5, "utf8");
|
|
37708
38237
|
} catch {
|
|
37709
38238
|
local = null;
|
|
37710
38239
|
}
|
|
@@ -37739,35 +38268,50 @@ async function discardBatchOps(batch, deps) {
|
|
|
37739
38268
|
}
|
|
37740
38269
|
}
|
|
37741
38270
|
}
|
|
37742
|
-
async function discardFileOp(op, deps) {
|
|
37743
|
-
|
|
37744
|
-
|
|
37745
|
-
|
|
37746
|
-
|
|
37747
|
-
|
|
37748
|
-
|
|
37749
|
-
|
|
37750
|
-
|
|
37751
|
-
|
|
38271
|
+
async function discardFileOp(op, deps, mutex2) {
|
|
38272
|
+
await mutex2.run(`files:${op.path}`, async () => {
|
|
38273
|
+
const abs = filesSubtreePath(deps.workspaceSlug, op.path);
|
|
38274
|
+
const skip = (reason, why) => {
|
|
38275
|
+
deps.output?.log(fileRestoreSkippedLine({ path: op.path, rowId: op.id, reason }));
|
|
38276
|
+
deps.output?.warn(`Discard of files/${op.path} left the file as it is: ${why}.`);
|
|
38277
|
+
};
|
|
38278
|
+
const committed = readCommittedFileEntry(deps.workspaceSlug, op.path);
|
|
38279
|
+
if (committed && committed.id !== op.id) {
|
|
38280
|
+
skip("replaced", "another file now has that path");
|
|
38281
|
+
return;
|
|
38282
|
+
}
|
|
38283
|
+
const read = readLocalForKeep(deps.workspaceSlug, abs);
|
|
38284
|
+
if (read.kind === "failed") {
|
|
38285
|
+
skip(read.failure.reason, "the file there could not be read safely");
|
|
38286
|
+
return;
|
|
38287
|
+
}
|
|
38288
|
+
const local = read.kind === "read" ? read.bytes : null;
|
|
38289
|
+
if (local !== null) {
|
|
38290
|
+
stashFileConflict({
|
|
38291
|
+
workspaceSlug: deps.workspaceSlug,
|
|
38292
|
+
relPath: op.path,
|
|
38293
|
+
timestamp: (deps.now ?? (() => new Date))(),
|
|
38294
|
+
bytes: local,
|
|
38295
|
+
side: "local"
|
|
38296
|
+
});
|
|
38297
|
+
}
|
|
38298
|
+
const base2 = deps.getState();
|
|
38299
|
+
const restore = await restoreFileFromCloud({
|
|
37752
38300
|
workspaceSlug: deps.workspaceSlug,
|
|
38301
|
+
baseUrl: deps.baseUrl,
|
|
38302
|
+
pat: deps.pat,
|
|
38303
|
+
id: op.id,
|
|
37753
38304
|
relPath: op.path,
|
|
37754
|
-
|
|
37755
|
-
|
|
37756
|
-
|
|
38305
|
+
state: base2,
|
|
38306
|
+
fetchImpl: deps.fetchImpl,
|
|
38307
|
+
output: deps.output,
|
|
38308
|
+
kept: local
|
|
37757
38309
|
});
|
|
37758
|
-
|
|
37759
|
-
|
|
37760
|
-
|
|
37761
|
-
|
|
37762
|
-
baseUrl: deps.baseUrl,
|
|
37763
|
-
pat: deps.pat,
|
|
37764
|
-
id: op.id,
|
|
37765
|
-
relPath: op.path,
|
|
37766
|
-
state: base2,
|
|
37767
|
-
fetchImpl: deps.fetchImpl,
|
|
37768
|
-
output: deps.output
|
|
38310
|
+
if (restore.outcome === "skipped") {
|
|
38311
|
+
deps.output?.warn(`Discard of files/${op.path} left the file as it is (${restore.reason}); daemon.log has the line.`);
|
|
38312
|
+
}
|
|
38313
|
+
deps.applyState(base2, restore.state);
|
|
37769
38314
|
});
|
|
37770
|
-
deps.applyState(base2, next);
|
|
37771
38315
|
}
|
|
37772
38316
|
function opPath(workspaceSlug, op) {
|
|
37773
38317
|
try {
|
|
@@ -37780,7 +38324,7 @@ function opPath(workspaceSlug, op) {
|
|
|
37780
38324
|
}
|
|
37781
38325
|
|
|
37782
38326
|
// src/lib/files/classify.ts
|
|
37783
|
-
import { relative as relative6, sep as
|
|
38327
|
+
import { relative as relative6, sep as sep6 } from "node:path";
|
|
37784
38328
|
var FILES_SUBTREE = "files";
|
|
37785
38329
|
function classifyFilesPath(args) {
|
|
37786
38330
|
let root;
|
|
@@ -37790,20 +38334,20 @@ function classifyFilesPath(args) {
|
|
|
37790
38334
|
return null;
|
|
37791
38335
|
}
|
|
37792
38336
|
const rel = relative6(root, args.absolutePath);
|
|
37793
|
-
if (rel.startsWith("..") || rel.startsWith(
|
|
38337
|
+
if (rel.startsWith("..") || rel.startsWith(sep6))
|
|
37794
38338
|
return null;
|
|
37795
|
-
const prefix = FILES_SUBTREE +
|
|
38339
|
+
const prefix = FILES_SUBTREE + sep6;
|
|
37796
38340
|
if (!rel.startsWith(prefix))
|
|
37797
38341
|
return null;
|
|
37798
38342
|
const sub = rel.slice(prefix.length);
|
|
37799
38343
|
if (sub.length === 0)
|
|
37800
38344
|
return null;
|
|
37801
|
-
const posix =
|
|
38345
|
+
const posix = sep6 === "/" ? sub : sub.split(sep6).join("/");
|
|
37802
38346
|
return validateFilesRelPath(posix) ? { relPath: posix } : null;
|
|
37803
38347
|
}
|
|
37804
38348
|
|
|
37805
38349
|
// src/lib/memory-facts/sync.ts
|
|
37806
|
-
import { isAbsolute as isAbsolute2, relative as relative7, sep as
|
|
38350
|
+
import { isAbsolute as isAbsolute2, relative as relative7, sep as sep7 } from "node:path";
|
|
37807
38351
|
|
|
37808
38352
|
// src/lib/memory-facts/api.ts
|
|
37809
38353
|
var FactCommonSchema = exports_external.object({
|
|
@@ -38547,7 +39091,7 @@ function classifyMemoryFactPath(args) {
|
|
|
38547
39091
|
const rel = relative7(root, args.absolutePath);
|
|
38548
39092
|
if (rel === "" || rel.startsWith("..") || isAbsolute2(rel))
|
|
38549
39093
|
return null;
|
|
38550
|
-
const mirrorRel = `${MEMORY_FACTS_DIR}/${rel.split(
|
|
39094
|
+
const mirrorRel = `${MEMORY_FACTS_DIR}/${rel.split(sep7).join("/")}`;
|
|
38551
39095
|
return { relPath: mirrorRel, fact: parseMemoryFactRelPath(mirrorRel) };
|
|
38552
39096
|
}
|
|
38553
39097
|
function classifyMemoryFactEvent(args) {
|
|
@@ -38644,7 +39188,7 @@ function evaluateFileBatch(ops, population, thresholds = defaultThresholds()) {
|
|
|
38644
39188
|
}
|
|
38645
39189
|
};
|
|
38646
39190
|
}
|
|
38647
|
-
return
|
|
39191
|
+
return evaluateLoneOverwrite(count, ops.map((op) => op.kind === "overwrite" ? op.priorSize - op.newSize : 0), thresholds.loneMinRemovedBytes);
|
|
38648
39192
|
}
|
|
38649
39193
|
function dedupeFileOps(ops) {
|
|
38650
39194
|
const byKey = new Map;
|
|
@@ -38715,10 +39259,10 @@ class FileDestructiveGate {
|
|
|
38715
39259
|
}
|
|
38716
39260
|
|
|
38717
39261
|
// src/lib/files/reconcile.ts
|
|
38718
|
-
import { readFileSync as
|
|
39262
|
+
import { readFileSync as readFileSync23, statSync as statSync10 } from "node:fs";
|
|
38719
39263
|
|
|
38720
39264
|
// src/lib/unsynced.ts
|
|
38721
|
-
import { join as
|
|
39265
|
+
import { join as join15, relative as relative8 } from "node:path";
|
|
38722
39266
|
var RECORDABLE_REASONS = new Set([
|
|
38723
39267
|
"rate_limit",
|
|
38724
39268
|
"server",
|
|
@@ -38780,7 +39324,7 @@ async function drainUnsyncedOps(args) {
|
|
|
38780
39324
|
attempted += 1;
|
|
38781
39325
|
try {
|
|
38782
39326
|
if (op.target === "entity") {
|
|
38783
|
-
const absolutePath =
|
|
39327
|
+
const absolutePath = join15(workspaceMirrorPath(workspaceSlug), op.path);
|
|
38784
39328
|
const classified = classifyMirrorPath({ workspaceSlug, absolutePath });
|
|
38785
39329
|
if (!classified) {
|
|
38786
39330
|
clear("entity", op.path);
|
|
@@ -39593,9 +40137,9 @@ class NodeFsHandler {
|
|
|
39593
40137
|
if (this.fsw.closed) {
|
|
39594
40138
|
return;
|
|
39595
40139
|
}
|
|
39596
|
-
const
|
|
40140
|
+
const dirname13 = sp.dirname(file2);
|
|
39597
40141
|
const basename7 = sp.basename(file2);
|
|
39598
|
-
const parent = this.fsw._getWatchedDir(
|
|
40142
|
+
const parent = this.fsw._getWatchedDir(dirname13);
|
|
39599
40143
|
let prevStats = stats;
|
|
39600
40144
|
if (parent.has(basename7))
|
|
39601
40145
|
return;
|
|
@@ -39622,7 +40166,7 @@ class NodeFsHandler {
|
|
|
39622
40166
|
prevStats = newStats2;
|
|
39623
40167
|
}
|
|
39624
40168
|
} catch (error52) {
|
|
39625
|
-
this.fsw._remove(
|
|
40169
|
+
this.fsw._remove(dirname13, basename7);
|
|
39626
40170
|
}
|
|
39627
40171
|
} else if (parent.has(basename7)) {
|
|
39628
40172
|
const at = newStats.atimeMs;
|
|
@@ -39711,7 +40255,7 @@ class NodeFsHandler {
|
|
|
39711
40255
|
this._addToNodeFs(path5, initialAdd, wh, depth + 1);
|
|
39712
40256
|
}
|
|
39713
40257
|
}).on(EV.ERROR, this._boundHandleError);
|
|
39714
|
-
return new Promise((
|
|
40258
|
+
return new Promise((resolve6, reject) => {
|
|
39715
40259
|
if (!stream)
|
|
39716
40260
|
return reject();
|
|
39717
40261
|
stream.once(STR_END, () => {
|
|
@@ -39720,7 +40264,7 @@ class NodeFsHandler {
|
|
|
39720
40264
|
return;
|
|
39721
40265
|
}
|
|
39722
40266
|
const wasThrottled = throttler ? throttler.clear() : false;
|
|
39723
|
-
|
|
40267
|
+
resolve6(undefined);
|
|
39724
40268
|
previous.getChildren().filter((item) => {
|
|
39725
40269
|
return item !== directory && !current.has(item);
|
|
39726
40270
|
}).forEach((item) => {
|
|
@@ -40479,7 +41023,7 @@ var chokidar_default = { watch, FSWatcher };
|
|
|
40479
41023
|
|
|
40480
41024
|
// src/lib/watcher.ts
|
|
40481
41025
|
import { statSync as statSync9 } from "node:fs";
|
|
40482
|
-
import { sep as
|
|
41026
|
+
import { sep as sep8 } from "node:path";
|
|
40483
41027
|
|
|
40484
41028
|
// src/lib/rename-classifier.ts
|
|
40485
41029
|
class RenameClassifier {
|
|
@@ -40610,7 +41154,7 @@ function watcherIgnoreMatcher(workspaceRoot) {
|
|
|
40610
41154
|
return (absolutePath) => patterns.some((re) => re.test(absolutePath));
|
|
40611
41155
|
}
|
|
40612
41156
|
function escSep() {
|
|
40613
|
-
return
|
|
41157
|
+
return sep8 === "/" ? "/" : "\\\\";
|
|
40614
41158
|
}
|
|
40615
41159
|
|
|
40616
41160
|
class DebounceMap {
|
|
@@ -40660,6 +41204,12 @@ class DebounceMap {
|
|
|
40660
41204
|
this.pending.delete(oldestKey);
|
|
40661
41205
|
this.handler(entry2.event);
|
|
40662
41206
|
}
|
|
41207
|
+
nudge(absolutePath) {
|
|
41208
|
+
if (this.pending.has(absolutePath))
|
|
41209
|
+
return false;
|
|
41210
|
+
this.schedule({ kind: "upsert", absolutePath });
|
|
41211
|
+
return true;
|
|
41212
|
+
}
|
|
40663
41213
|
async drain() {
|
|
40664
41214
|
const entries = Array.from(this.pending.entries());
|
|
40665
41215
|
this.pending.clear();
|
|
@@ -40724,6 +41274,9 @@ function startWatcher(opts, handler) {
|
|
|
40724
41274
|
drainPending() {
|
|
40725
41275
|
return debounce.drain();
|
|
40726
41276
|
},
|
|
41277
|
+
nudge(absolutePath) {
|
|
41278
|
+
return debounce.nudge(absolutePath);
|
|
41279
|
+
},
|
|
40727
41280
|
pendingCount() {
|
|
40728
41281
|
return debounce.size();
|
|
40729
41282
|
}
|
|
@@ -40754,7 +41307,7 @@ function readBlobForPush(args) {
|
|
|
40754
41307
|
}
|
|
40755
41308
|
let bytes;
|
|
40756
41309
|
try {
|
|
40757
|
-
bytes =
|
|
41310
|
+
bytes = readFileSync23(absolutePath);
|
|
40758
41311
|
} catch (err) {
|
|
40759
41312
|
if (err?.code === "ENOENT")
|
|
40760
41313
|
return { kind: "gone" };
|
|
@@ -40897,7 +41450,7 @@ async function reconcileFilesSubtree(args) {
|
|
|
40897
41450
|
ops: pendingOps
|
|
40898
41451
|
});
|
|
40899
41452
|
quarantined = ops.length;
|
|
40900
|
-
output?.log(`[DESTRUCTIVE_HELD batch=${batchId} ops=${ops.length} reason=${result.reason
|
|
41453
|
+
output?.log(`[DESTRUCTIVE_HELD batch=${batchId} ops=${ops.length} reason=${tripReasonToken(result.reason)} source=files-reconcile]`);
|
|
40901
41454
|
} else {
|
|
40902
41455
|
for (const c of guttingCandidates) {
|
|
40903
41456
|
await pushDivergedBlob(c.op.relPath, c.op.absolutePath, c.prepared);
|
|
@@ -41064,7 +41617,7 @@ function applyArtistMemoryOnPull(args) {
|
|
|
41064
41617
|
const cloudMemory = live.memory;
|
|
41065
41618
|
const generated = live.memory_generated === true;
|
|
41066
41619
|
const local = readArtistMemoryFile(workspaceSlug, slug);
|
|
41067
|
-
const
|
|
41620
|
+
const refuse2 = (localContent, canonical2) => {
|
|
41068
41621
|
const refused = refuseGeneratedMemoryEdit({
|
|
41069
41622
|
state,
|
|
41070
41623
|
workspaceSlug,
|
|
@@ -41093,7 +41646,7 @@ function applyArtistMemoryOnPull(args) {
|
|
|
41093
41646
|
const cloudHash = stableHash(cloudMemory);
|
|
41094
41647
|
if (cloudHash === ackedMemoryHash && local !== null) {
|
|
41095
41648
|
if (generated && stableHash(local) !== ackedMemoryHash) {
|
|
41096
|
-
|
|
41649
|
+
refuse2(local, cloudMemory);
|
|
41097
41650
|
return { memoryHash: cloudHash, state };
|
|
41098
41651
|
}
|
|
41099
41652
|
if (generated) {
|
|
@@ -41105,7 +41658,7 @@ function applyArtistMemoryOnPull(args) {
|
|
|
41105
41658
|
const localHash = stableHash(local);
|
|
41106
41659
|
if (localHash !== ackedMemoryHash && localHash !== cloudHash) {
|
|
41107
41660
|
if (generated) {
|
|
41108
|
-
|
|
41661
|
+
refuse2(local, cloudMemory);
|
|
41109
41662
|
return { memoryHash: cloudHash, state };
|
|
41110
41663
|
}
|
|
41111
41664
|
const stashPath = stashConflict({
|
|
@@ -41815,10 +42368,10 @@ function anyUnknown(state) {
|
|
|
41815
42368
|
}
|
|
41816
42369
|
|
|
41817
42370
|
// src/lib/memory/write.ts
|
|
41818
|
-
import { readFileSync as
|
|
42371
|
+
import { readFileSync as readFileSync24, statSync as statSync11 } from "node:fs";
|
|
41819
42372
|
|
|
41820
42373
|
// src/lib/files/extracted-text.ts
|
|
41821
|
-
import { createHash as
|
|
42374
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
41822
42375
|
var DERIVED_TEXT_VERSION = 2;
|
|
41823
42376
|
function yamlString3(value) {
|
|
41824
42377
|
return JSON.stringify(value);
|
|
@@ -41892,11 +42445,10 @@ ${doc2.text}
|
|
|
41892
42445
|
`;
|
|
41893
42446
|
}
|
|
41894
42447
|
function extractedTextHash(rendered) {
|
|
41895
|
-
return
|
|
42448
|
+
return createHash6("sha256").update(rendered, "utf8").digest("hex");
|
|
41896
42449
|
}
|
|
41897
42450
|
|
|
41898
42451
|
// src/lib/memory/splice.ts
|
|
41899
|
-
var import_gray_matter11 = __toESM(require_gray_matter(), 1);
|
|
41900
42452
|
var OPEN_RE = /^---\r?\n/;
|
|
41901
42453
|
function findFrontmatter(content) {
|
|
41902
42454
|
const open2 = OPEN_RE.exec(content);
|
|
@@ -41982,7 +42534,7 @@ function spliceAnnotation(content, lines) {
|
|
|
41982
42534
|
}
|
|
41983
42535
|
function parsesAsFrontmatter(content) {
|
|
41984
42536
|
try {
|
|
41985
|
-
|
|
42537
|
+
matter(content, { cache: false });
|
|
41986
42538
|
return true;
|
|
41987
42539
|
} catch {
|
|
41988
42540
|
return false;
|
|
@@ -42010,7 +42562,7 @@ function writeDocumentAnnotation(args) {
|
|
|
42010
42562
|
}
|
|
42011
42563
|
let current;
|
|
42012
42564
|
try {
|
|
42013
|
-
current =
|
|
42565
|
+
current = readFileSync24(target, "utf8");
|
|
42014
42566
|
} catch {
|
|
42015
42567
|
return { kind: "absent" };
|
|
42016
42568
|
}
|
|
@@ -42048,18 +42600,32 @@ function writeFileTextAnnotation(args) {
|
|
|
42048
42600
|
const current = readExtractedText(args.workspaceSlug, args.relPath);
|
|
42049
42601
|
if (current === null)
|
|
42050
42602
|
return { kind: "absent" };
|
|
42051
|
-
|
|
42603
|
+
const onDisk = extractedTextHash(current);
|
|
42604
|
+
const acked = args.textHash;
|
|
42605
|
+
const offAck = acked !== undefined && onDisk !== acked;
|
|
42606
|
+
if (acked !== undefined && offAck && !isOwnRender(current, acked, args.priorLines)) {
|
|
42052
42607
|
return { kind: "diverged", relPath: extractedTextRelPath(args.relPath) };
|
|
42053
42608
|
}
|
|
42054
42609
|
const spliced = spliceAnnotation(current, args.lines);
|
|
42055
|
-
if (!spliced.changed)
|
|
42056
|
-
return { kind: "unchanged" };
|
|
42610
|
+
if (!spliced.changed) {
|
|
42611
|
+
return offAck ? { kind: "unchanged", textHash: onDisk } : { kind: "unchanged" };
|
|
42612
|
+
}
|
|
42057
42613
|
writeExtractedText(args.workspaceSlug, args.relPath, spliced.content);
|
|
42058
42614
|
return { kind: "written", textHash: extractedTextHash(spliced.content) };
|
|
42059
42615
|
}
|
|
42616
|
+
function isOwnRender(current, ackedHash, priorLines) {
|
|
42617
|
+
if (extractedTextHash(spliceAnnotation(current, []).content) === ackedHash)
|
|
42618
|
+
return true;
|
|
42619
|
+
if (priorLines === undefined)
|
|
42620
|
+
return false;
|
|
42621
|
+
return extractedTextHash(spliceAnnotation(current, priorLines).content) === ackedHash;
|
|
42622
|
+
}
|
|
42060
42623
|
function divergedNotice(relPath, state) {
|
|
42061
42624
|
return `${relPath}: its memory context is now "${state}", but the file has local ` + `edits so the marker was not written into it. Your edits are untouched. ` + `Resolve the edit and the next sync writes the marker.`;
|
|
42062
42625
|
}
|
|
42626
|
+
function generatedDivergedNotice(relPath) {
|
|
42627
|
+
return `${relPath}: this generated file does not match Plinth's record of it, ` + `so its memory marker was not written. Plinth rewrites the file when its ` + `source next changes; changes made to it here are not synced.`;
|
|
42628
|
+
}
|
|
42063
42629
|
function unreadableNotice(relPath, why) {
|
|
42064
42630
|
const reason = why.replace(/\s+/g, " ").trim().slice(0, 120);
|
|
42065
42631
|
return `${relPath}: its memory context could not be written because the file's ` + `frontmatter could not be read (${reason}). The file is untouched. Fix the ` + `frontmatter and the next sync writes the marker.`;
|
|
@@ -42089,6 +42655,10 @@ function heldFileTexts(state) {
|
|
|
42089
42655
|
textHash: f.text_hash
|
|
42090
42656
|
}));
|
|
42091
42657
|
}
|
|
42658
|
+
function priorFileLines(state, id) {
|
|
42659
|
+
return getMemorySourceEntries(state, "workspace_files")[id]?.rendered.split(`
|
|
42660
|
+
`);
|
|
42661
|
+
}
|
|
42092
42662
|
async function writeChanged(args) {
|
|
42093
42663
|
const docs = new Map(heldDocuments(args.state).map((d) => [d.id, d.slug]));
|
|
42094
42664
|
const texts = new Map(heldFileTexts(args.state).map((f) => [f.id, f]));
|
|
@@ -42188,7 +42758,8 @@ async function writeChanged(args) {
|
|
|
42188
42758
|
workspaceSlug: args.workspaceSlug,
|
|
42189
42759
|
relPath: file2.relPath,
|
|
42190
42760
|
textHash: args.freshTextHashes?.get(answer2.id) ?? file2.textHash,
|
|
42191
|
-
lines: answer2.lines
|
|
42761
|
+
lines: answer2.lines,
|
|
42762
|
+
priorLines: priorFileLines(args.state, answer2.id)
|
|
42192
42763
|
});
|
|
42193
42764
|
if (res.kind !== "diverged" && res.kind !== "absent") {
|
|
42194
42765
|
args.onWritten?.("workspace_files", answer2.id, res.textHash);
|
|
@@ -42215,7 +42786,7 @@ async function writeChanged(args) {
|
|
|
42215
42786
|
}
|
|
42216
42787
|
const textPath = extractedTextRelPath(file2.relPath);
|
|
42217
42788
|
if (outcome.kind === "diverged") {
|
|
42218
|
-
report.push({ path: textPath, record:
|
|
42789
|
+
report.push({ path: textPath, record: generatedDivergedNotice(textPath) });
|
|
42219
42790
|
} else {
|
|
42220
42791
|
clear.push(textPath);
|
|
42221
42792
|
}
|
|
@@ -42224,9 +42795,11 @@ async function writeChanged(args) {
|
|
|
42224
42795
|
if (outcome.textHash !== undefined) {
|
|
42225
42796
|
textHashes.set(file2.relPath, { to: outcome.textHash });
|
|
42226
42797
|
}
|
|
42798
|
+
} else if (outcome.kind === "unchanged" && outcome.textHash !== undefined) {
|
|
42799
|
+
textHashes.set(file2.relPath, { to: outcome.textHash });
|
|
42227
42800
|
} else if (outcome.kind === "diverged") {
|
|
42228
42801
|
diverged++;
|
|
42229
|
-
notices.push(
|
|
42802
|
+
notices.push(generatedDivergedNotice(outcome.relPath));
|
|
42230
42803
|
}
|
|
42231
42804
|
}
|
|
42232
42805
|
if (args.abortIfStale?.() === true) {
|
|
@@ -43948,8 +44521,7 @@ class PullSuppressor {
|
|
|
43948
44521
|
}
|
|
43949
44522
|
|
|
43950
44523
|
// src/lib/discover-entities.ts
|
|
43951
|
-
|
|
43952
|
-
import { readFileSync as readFileSync23, statSync as statSync12 } from "node:fs";
|
|
44524
|
+
import { readFileSync as readFileSync25, statSync as statSync12 } from "node:fs";
|
|
43953
44525
|
var DISCOVERY_WRITE_CAP_DEFAULT = 10;
|
|
43954
44526
|
function discoveryWriteCap() {
|
|
43955
44527
|
const raw = process.env.PLINTH_DISCOVERY_WRITE_CAP;
|
|
@@ -43976,6 +44548,11 @@ function classifyUntrackedEntity(args) {
|
|
|
43976
44548
|
}
|
|
43977
44549
|
return { kind: "create" };
|
|
43978
44550
|
}
|
|
44551
|
+
function holdsUnreadableUntracked(args) {
|
|
44552
|
+
if (args.readable)
|
|
44553
|
+
return false;
|
|
44554
|
+
return classifyUntrackedEntity({ ...args, frontmatter: {} }).kind === "create";
|
|
44555
|
+
}
|
|
43979
44556
|
function classifyRenameDestination(args) {
|
|
43980
44557
|
const occupant = getEntryBySlug(args.state, args.entityType, args.entitySlug);
|
|
43981
44558
|
if (occupant && occupant.id !== args.movedEntryId) {
|
|
@@ -44046,13 +44623,20 @@ function planDiscoveryWrites(args) {
|
|
|
44046
44623
|
if (getEntryBySlug(state, entityType, entitySlug))
|
|
44047
44624
|
continue;
|
|
44048
44625
|
counts.discovered++;
|
|
44049
|
-
let
|
|
44626
|
+
let raw;
|
|
44050
44627
|
try {
|
|
44051
|
-
|
|
44628
|
+
raw = readFileSync25(absolutePath, "utf8");
|
|
44052
44629
|
} catch {
|
|
44053
44630
|
counts.discovered--;
|
|
44054
44631
|
continue;
|
|
44055
44632
|
}
|
|
44633
|
+
const read = readFrontmatter(raw);
|
|
44634
|
+
if (!read.ok && holdsUnreadableUntracked({ entityType, entitySlug, readable: false })) {
|
|
44635
|
+
counts.discovered--;
|
|
44636
|
+
planned.push({ kind: "unreadable", entityType, entitySlug, absolutePath, why: read.why });
|
|
44637
|
+
continue;
|
|
44638
|
+
}
|
|
44639
|
+
const frontmatter = read.ok ? read.parsed.data : {};
|
|
44056
44640
|
const verdict = classifyUntrackedEntity({ entityType, entitySlug, frontmatter });
|
|
44057
44641
|
if (verdict.kind === "hold") {
|
|
44058
44642
|
if (verdict.held === "artist")
|
|
@@ -44093,7 +44677,7 @@ async function discoverUntrackedEntities(args) {
|
|
|
44093
44677
|
}
|
|
44094
44678
|
const cap = discoveryWriteCap();
|
|
44095
44679
|
const creates = planned.filter((w) => w.kind === "create");
|
|
44096
|
-
const toRun = creates.length > cap ? planned.filter((w) => w.kind
|
|
44680
|
+
const toRun = creates.length > cap ? planned.filter((w) => w.kind !== "create") : planned;
|
|
44097
44681
|
if (creates.length > cap) {
|
|
44098
44682
|
counts.heldCap = creates.length;
|
|
44099
44683
|
output?.log(`[DISCOVERY_HELD reason=cap creates=${creates.length} cap=${cap} source=entity-discovery]`);
|
|
@@ -44116,6 +44700,19 @@ async function runPlannedWrites(args) {
|
|
|
44116
44700
|
let state = args.state;
|
|
44117
44701
|
for (const write of planned) {
|
|
44118
44702
|
try {
|
|
44703
|
+
if (write.kind === "unreadable") {
|
|
44704
|
+
state = holdUnreadableFrontmatter({
|
|
44705
|
+
state,
|
|
44706
|
+
workspaceSlug,
|
|
44707
|
+
absolutePath: write.absolutePath,
|
|
44708
|
+
entityType: write.entityType,
|
|
44709
|
+
slug: write.entitySlug,
|
|
44710
|
+
why: write.why,
|
|
44711
|
+
now,
|
|
44712
|
+
output
|
|
44713
|
+
});
|
|
44714
|
+
continue;
|
|
44715
|
+
}
|
|
44119
44716
|
if (write.kind === "rename") {
|
|
44120
44717
|
state = await runDiscoveredRename({
|
|
44121
44718
|
write,
|
|
@@ -44276,8 +44873,7 @@ function recordDiscoveryFailure(args) {
|
|
|
44276
44873
|
}
|
|
44277
44874
|
|
|
44278
44875
|
// src/lib/frontmatter-id-backfill.ts
|
|
44279
|
-
|
|
44280
|
-
import { readFileSync as readFileSync24, statSync as statSync13 } from "node:fs";
|
|
44876
|
+
import { readFileSync as readFileSync26, statSync as statSync13 } from "node:fs";
|
|
44281
44877
|
var FRONTMATTER_ID_BACKFILL_VERSION = 1;
|
|
44282
44878
|
function backfillWorthReporting(r) {
|
|
44283
44879
|
return r.written > 0 || r.failed > 0 || r.skipped.foreignId > 0 || r.skipped.unwritable > 0 || r.skipped.slugCollision > 0;
|
|
@@ -44320,7 +44916,7 @@ function safeInode3(path5) {
|
|
|
44320
44916
|
function backfillFrontmatterIds(args) {
|
|
44321
44917
|
const { workspaceSlug, state } = args;
|
|
44322
44918
|
const compose = args.compose ?? withFrontmatterId;
|
|
44323
|
-
const readRaw = args.readFile ?? ((p) =>
|
|
44919
|
+
const readRaw = args.readFile ?? ((p) => readFileSync26(p, "utf8"));
|
|
44324
44920
|
const result = emptyResult();
|
|
44325
44921
|
if (state.frontmatter_id_backfill_version === FRONTMATTER_ID_BACKFILL_VERSION) {
|
|
44326
44922
|
return result;
|
|
@@ -44369,7 +44965,7 @@ function backfillFrontmatterIds(args) {
|
|
|
44369
44965
|
let rawBody;
|
|
44370
44966
|
let parsedData;
|
|
44371
44967
|
try {
|
|
44372
|
-
const parsed =
|
|
44968
|
+
const parsed = parseUncached(raw);
|
|
44373
44969
|
parsedData = parsed.data ?? {};
|
|
44374
44970
|
existing = parsedData["id"];
|
|
44375
44971
|
rawBody = parsed.content;
|
|
@@ -44448,11 +45044,11 @@ function backfillFrontmatterIds(args) {
|
|
|
44448
45044
|
}
|
|
44449
45045
|
|
|
44450
45046
|
// src/lib/reconcile.ts
|
|
44451
|
-
import { readFileSync as
|
|
45047
|
+
import { readFileSync as readFileSync27, statSync as statSync14 } from "node:fs";
|
|
44452
45048
|
async function reconcileDivergedRows(args) {
|
|
44453
45049
|
const { workspaceSlug, baseUrl, pat, fetchImpl, output, isFrozen } = args;
|
|
44454
45050
|
const now = args.now ?? (() => new Date);
|
|
44455
|
-
let state = args.state;
|
|
45051
|
+
let state = settleFrontmatterHolds(args.state, workspaceSlug);
|
|
44456
45052
|
let scanned = 0;
|
|
44457
45053
|
let diverged = 0;
|
|
44458
45054
|
let pushed = 0;
|
|
@@ -44560,7 +45156,7 @@ async function reconcileDivergedRows(args) {
|
|
|
44560
45156
|
}
|
|
44561
45157
|
let content;
|
|
44562
45158
|
try {
|
|
44563
|
-
content =
|
|
45159
|
+
content = readFileSync27(absolutePath, "utf8");
|
|
44564
45160
|
} catch (err) {
|
|
44565
45161
|
if (err?.code === "ENOENT") {
|
|
44566
45162
|
state = clearEntityCloudRowDeleted(state, strategy.mirrorRelativePath(entry2.slug));
|
|
@@ -44568,6 +45164,20 @@ async function reconcileDivergedRows(args) {
|
|
|
44568
45164
|
}
|
|
44569
45165
|
throw err;
|
|
44570
45166
|
}
|
|
45167
|
+
const unreadable = frontmatterParseError(content);
|
|
45168
|
+
if (unreadable !== null) {
|
|
45169
|
+
state = holdUnreadableFrontmatter({
|
|
45170
|
+
state,
|
|
45171
|
+
workspaceSlug,
|
|
45172
|
+
absolutePath,
|
|
45173
|
+
entityType,
|
|
45174
|
+
slug: entry2.slug,
|
|
45175
|
+
why: unreadable,
|
|
45176
|
+
now,
|
|
45177
|
+
output
|
|
45178
|
+
});
|
|
45179
|
+
continue;
|
|
45180
|
+
}
|
|
44571
45181
|
const hashes = computeMirrorHashes(strategy, content);
|
|
44572
45182
|
if (hashes.frontmatterId === undefined)
|
|
44573
45183
|
sawTrackedFileWithoutId = true;
|
|
@@ -44618,7 +45228,7 @@ async function reconcileDivergedRows(args) {
|
|
|
44618
45228
|
ops: pendingOps
|
|
44619
45229
|
});
|
|
44620
45230
|
quarantined = ops.length;
|
|
44621
|
-
output?.log(`[DESTRUCTIVE_HELD batch=${batchId} ops=${ops.length} reason=${result.reason
|
|
45231
|
+
output?.log(`[DESTRUCTIVE_HELD batch=${batchId} ops=${ops.length} reason=${tripReasonToken(result.reason)} source=reconcile]`);
|
|
44622
45232
|
} else {
|
|
44623
45233
|
for (const c of guttingCandidates) {
|
|
44624
45234
|
const op = c.op;
|
|
@@ -45849,7 +46459,7 @@ class Semaphore {
|
|
|
45849
46459
|
this.active += 1;
|
|
45850
46460
|
return;
|
|
45851
46461
|
}
|
|
45852
|
-
await new Promise((
|
|
46462
|
+
await new Promise((resolve7) => this.waiters.push(resolve7));
|
|
45853
46463
|
}
|
|
45854
46464
|
release() {
|
|
45855
46465
|
const next = this.waiters.shift();
|
|
@@ -45911,17 +46521,17 @@ function createThrottledFetch(baseFetch, opts = {}) {
|
|
|
45911
46521
|
// src/commands/sync.ts
|
|
45912
46522
|
import {
|
|
45913
46523
|
existsSync as existsSync9,
|
|
45914
|
-
lstatSync as
|
|
46524
|
+
lstatSync as lstatSync8,
|
|
45915
46525
|
readdirSync as readdirSync7,
|
|
45916
|
-
readFileSync as
|
|
46526
|
+
readFileSync as readFileSync32,
|
|
45917
46527
|
statSync as statSync17,
|
|
45918
|
-
unlinkSync as
|
|
46528
|
+
unlinkSync as unlinkSync11
|
|
45919
46529
|
} from "node:fs";
|
|
45920
|
-
import { join as
|
|
46530
|
+
import { join as join19 } from "node:path";
|
|
45921
46531
|
|
|
45922
46532
|
// src/lib/conflicts-view.ts
|
|
45923
46533
|
import { existsSync as existsSync7 } from "node:fs";
|
|
45924
|
-
import { join as
|
|
46534
|
+
import { join as join18 } from "node:path";
|
|
45925
46535
|
var NO_GENERATED_MEMORY = {
|
|
45926
46536
|
artistMemoryGenerated: () => false,
|
|
45927
46537
|
workspaceMemoryGenerated: () => false,
|
|
@@ -45963,11 +46573,13 @@ function resolveConflictRows(workspaceSlug, stashes, generatedMemory = NO_GENERA
|
|
|
45963
46573
|
if (root === null || !stash.label.startsWith("files/")) {
|
|
45964
46574
|
return { kind: "entity", stash };
|
|
45965
46575
|
}
|
|
46576
|
+
if (stash.pathUnrecorded)
|
|
46577
|
+
return { kind: "unsafe-path", stash };
|
|
45966
46578
|
const rel = stash.label.slice("files/".length);
|
|
45967
46579
|
if (validateFilesRelPath(rel) === null) {
|
|
45968
46580
|
return { kind: "unsafe-path", stash };
|
|
45969
46581
|
}
|
|
45970
|
-
const abs =
|
|
46582
|
+
const abs = join18(root, "files", rel);
|
|
45971
46583
|
if (hasSymlinkedComponent(workspaceSlug, abs)) {
|
|
45972
46584
|
return { kind: "unsafe-path", stash };
|
|
45973
46585
|
}
|
|
@@ -46034,7 +46646,9 @@ function renderRow(row) {
|
|
|
46034
46646
|
`);
|
|
46035
46647
|
}
|
|
46036
46648
|
out.push("To put your version back, run this and then delete the stash:", "", "```sh", `cp ${shellQuote2(stash.absPath)} ${shellQuote2(row.mirrorPath)}`, "```");
|
|
46037
|
-
if (!row.mirrorPathExists) {
|
|
46649
|
+
if (!row.mirrorPathExists && stash.pathRecorded) {
|
|
46650
|
+
out.push("", "**Nothing is at that path right now**, so the cloud copy was probably", "deleted. Open the stash and check before running it.");
|
|
46651
|
+
} else if (!row.mirrorPathExists) {
|
|
46038
46652
|
out.push("", "**Nothing is at that path right now.** It is derived from the stash", "filename, which cannot represent a `/` and a `__` differently, so either", "the cloud copy was deleted, or the original path contained `__` and the", "line above names the wrong file. Open the stash and check before running it.");
|
|
46039
46653
|
}
|
|
46040
46654
|
return out.join(`
|
|
@@ -46097,17 +46711,16 @@ function refreshConflictsView(workspaceSlug) {
|
|
|
46097
46711
|
}
|
|
46098
46712
|
|
|
46099
46713
|
// src/lib/files/pull.ts
|
|
46100
|
-
import { existsSync as existsSync8,
|
|
46101
|
-
import { dirname as dirname13 } from "node:path";
|
|
46714
|
+
import { existsSync as existsSync8, readFileSync as readFileSync29, statSync as statSync15 } from "node:fs";
|
|
46102
46715
|
|
|
46103
46716
|
// src/lib/permanent-disposition.ts
|
|
46104
|
-
import { createHash as
|
|
46105
|
-
import { readFileSync as
|
|
46717
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
46718
|
+
import { readFileSync as readFileSync28 } from "node:fs";
|
|
46106
46719
|
function sha256(value) {
|
|
46107
|
-
return
|
|
46720
|
+
return createHash7("sha256").update(value).digest("hex");
|
|
46108
46721
|
}
|
|
46109
46722
|
function verifyStash(path5, expectedHash) {
|
|
46110
|
-
const actual = sha256(
|
|
46723
|
+
const actual = sha256(readFileSync28(path5));
|
|
46111
46724
|
if (actual !== expectedHash) {
|
|
46112
46725
|
throw new Error(`permanent deletion stash verification failed for ${path5}`);
|
|
46113
46726
|
}
|
|
@@ -46151,7 +46764,7 @@ function disposePermanentDocumentLocked(args) {
|
|
|
46151
46764
|
const target = entityFilePath(args.workspaceSlug, args.strategy.type, retiredEntry?.slug ?? t.logicalKey);
|
|
46152
46765
|
let raw;
|
|
46153
46766
|
try {
|
|
46154
|
-
raw =
|
|
46767
|
+
raw = readFileSync28(target, "utf8");
|
|
46155
46768
|
} catch (error52) {
|
|
46156
46769
|
if (error52?.code !== "ENOENT")
|
|
46157
46770
|
throw error52;
|
|
@@ -46249,7 +46862,7 @@ function disposePermanentFileLocked(args) {
|
|
|
46249
46862
|
const target = filesSubtreePath(args.workspaceSlug, t.logicalKey);
|
|
46250
46863
|
let bytes;
|
|
46251
46864
|
try {
|
|
46252
|
-
bytes =
|
|
46865
|
+
bytes = readFileSync28(target);
|
|
46253
46866
|
} catch (error52) {
|
|
46254
46867
|
if (error52?.code !== "ENOENT")
|
|
46255
46868
|
throw error52;
|
|
@@ -46296,6 +46909,34 @@ function disposePermanentFileLocked(args) {
|
|
|
46296
46909
|
return { state, deleted, textDeleted, ...stashPath ? { stashPath } : {} };
|
|
46297
46910
|
}
|
|
46298
46911
|
|
|
46912
|
+
// src/lib/files/pull-ack.ts
|
|
46913
|
+
function currentFileAck(args) {
|
|
46914
|
+
const read = readCommittedFileAck(args.workspaceSlug, args.relPath);
|
|
46915
|
+
if (read.kind === "unknown")
|
|
46916
|
+
return args.snapshot;
|
|
46917
|
+
if (read.kind === "absent") {
|
|
46918
|
+
const retired = args.committedBase !== undefined && sameFileVersion(args.snapshot, args.committedBase);
|
|
46919
|
+
return retired ? undefined : args.snapshot;
|
|
46920
|
+
}
|
|
46921
|
+
const committed = read.entry;
|
|
46922
|
+
const known = args.snapshot ?? args.committedBase;
|
|
46923
|
+
if (!known)
|
|
46924
|
+
return committed;
|
|
46925
|
+
if (committed.id !== known.id) {
|
|
46926
|
+
if (isLaterVersion(known, committed))
|
|
46927
|
+
return args.snapshot;
|
|
46928
|
+
if (isLaterVersion(committed, known))
|
|
46929
|
+
return committed;
|
|
46930
|
+
return args.snapshot !== undefined && !sameFileVersion(args.snapshot, args.committedBase) ? args.snapshot : committed;
|
|
46931
|
+
}
|
|
46932
|
+
return isLaterVersion(committed, known) ? committed : args.snapshot;
|
|
46933
|
+
}
|
|
46934
|
+
function sameFileVersion(a, b) {
|
|
46935
|
+
if (!a || !b)
|
|
46936
|
+
return a === b;
|
|
46937
|
+
return a.id === b.id && a.updated_at === b.updated_at && a.content_hash === b.content_hash;
|
|
46938
|
+
}
|
|
46939
|
+
|
|
46299
46940
|
// src/lib/files/pull.ts
|
|
46300
46941
|
function dueFileRefreshMarkers(workspaceSlug, now) {
|
|
46301
46942
|
try {
|
|
@@ -46422,7 +47063,9 @@ async function replayPermanentFiles(args, markers) {
|
|
|
46422
47063
|
loggedTombstoneSkips: args.loggedTombstoneSkips,
|
|
46423
47064
|
forceTextRewalk: args.forceTextRewalk,
|
|
46424
47065
|
loggedStashNotices: args.loggedStashNotices,
|
|
46425
|
-
recentUnlinks: args.recentUnlinks
|
|
47066
|
+
recentUnlinks: args.recentUnlinks,
|
|
47067
|
+
committedBase: lastCommittedState,
|
|
47068
|
+
commit: args.commit
|
|
46426
47069
|
});
|
|
46427
47070
|
state = outcome.state;
|
|
46428
47071
|
if (outcome.action === "written")
|
|
@@ -46495,6 +47138,7 @@ async function pullFiles(args) {
|
|
|
46495
47138
|
let finalHighWater = null;
|
|
46496
47139
|
let markRefused = false;
|
|
46497
47140
|
let cursorBlocked = permanentJournalBlocked;
|
|
47141
|
+
const deliveredPaths = new Set;
|
|
46498
47142
|
while (true) {
|
|
46499
47143
|
let page;
|
|
46500
47144
|
try {
|
|
@@ -46512,6 +47156,7 @@ async function pullFiles(args) {
|
|
|
46512
47156
|
let pageTextFailed = false;
|
|
46513
47157
|
for (const row of page.rows) {
|
|
46514
47158
|
fetched++;
|
|
47159
|
+
deliveredPaths.add(row.path);
|
|
46515
47160
|
if (row.deleted_at !== null && row.permanent_delete === true && row.purge_generation !== undefined) {
|
|
46516
47161
|
if (permanentJournalBlocked) {
|
|
46517
47162
|
pageTextFailed = true;
|
|
@@ -46542,7 +47187,9 @@ async function pullFiles(args) {
|
|
|
46542
47187
|
loggedTombstoneSkips: args.loggedTombstoneSkips,
|
|
46543
47188
|
forceTextRewalk: args.forceTextRewalk,
|
|
46544
47189
|
loggedStashNotices: args.loggedStashNotices,
|
|
46545
|
-
recentUnlinks: args.recentUnlinks
|
|
47190
|
+
recentUnlinks: args.recentUnlinks,
|
|
47191
|
+
committedBase: pageBase,
|
|
47192
|
+
commit: args.commit
|
|
46546
47193
|
});
|
|
46547
47194
|
state = outcome.state;
|
|
46548
47195
|
if (!outcome.symlinkRefused) {
|
|
@@ -46596,6 +47243,14 @@ async function pullFiles(args) {
|
|
|
46596
47243
|
return replayPermanentFiles({ ...args, state }, markers);
|
|
46597
47244
|
}
|
|
46598
47245
|
}
|
|
47246
|
+
const pruned = pruneFilePullHolds({
|
|
47247
|
+
state,
|
|
47248
|
+
workspaceSlug,
|
|
47249
|
+
deliveredPaths,
|
|
47250
|
+
cursorPass: markRefused || args.cursor === baseCursor
|
|
47251
|
+
});
|
|
47252
|
+
if (pruned !== state)
|
|
47253
|
+
state = args.commit ? args.commit(state, pruned) : pruned;
|
|
46599
47254
|
if (textUnknown > 0) {
|
|
46600
47255
|
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.`);
|
|
46601
47256
|
}
|
|
@@ -46668,6 +47323,9 @@ async function materialiseExtractedText(args) {
|
|
|
46668
47323
|
});
|
|
46669
47324
|
} else {
|
|
46670
47325
|
const got = await getFileText({ baseUrl, pat, id: row.id, fetchImpl });
|
|
47326
|
+
if (args.stillCurrent && !args.stillCurrent()) {
|
|
47327
|
+
return { textHash: undefined, written: 0, deleted: 0, stale: true };
|
|
47328
|
+
}
|
|
46671
47329
|
if (!got.ok)
|
|
46672
47330
|
return { textHash: undefined, written: 0, deleted: 0 };
|
|
46673
47331
|
if (got.row.text === null) {
|
|
@@ -46729,7 +47387,10 @@ async function applyFileRow(args) {
|
|
|
46729
47387
|
symlinkRefused: true
|
|
46730
47388
|
};
|
|
46731
47389
|
}
|
|
46732
|
-
const
|
|
47390
|
+
const snapshotEntry = getFileEntry(state, relPath);
|
|
47391
|
+
const baseEntry = getFileEntry(args.committedBase, relPath);
|
|
47392
|
+
const ackNow = () => currentFileAck({ workspaceSlug, relPath, snapshot: snapshotEntry, committedBase: baseEntry });
|
|
47393
|
+
const existing = ackNow();
|
|
46733
47394
|
if (row.deleted_at !== null) {
|
|
46734
47395
|
if (existing && existing.id !== row.id) {
|
|
46735
47396
|
const key = `${row.id}:${existing.id}:${relPath}`;
|
|
@@ -46737,40 +47398,63 @@ async function applyFileRow(args) {
|
|
|
46737
47398
|
args.loggedTombstoneSkips?.add(key);
|
|
46738
47399
|
output?.log(`[FILE_TOMBSTONE_SKIP path=${relPath} row_id=${row.id} tracked_id=${existing.id} reason=id_mismatch]`);
|
|
46739
47400
|
}
|
|
46740
|
-
return { state, action: "skipped" };
|
|
47401
|
+
return { state: clearHoldsOfRow(state, relPath, row.id), action: "skipped" };
|
|
46741
47402
|
}
|
|
46742
47403
|
let localBytes;
|
|
46743
47404
|
try {
|
|
46744
|
-
localBytes =
|
|
47405
|
+
localBytes = readFileSync29(abs);
|
|
46745
47406
|
} catch {
|
|
46746
47407
|
localBytes = undefined;
|
|
46747
47408
|
}
|
|
46748
47409
|
const isOurs = localBytes === undefined || existing !== undefined && sha256Bytes(localBytes) === existing.content_hash;
|
|
46749
47410
|
if (!isOurs) {
|
|
46750
47411
|
output?.log(`[FILE_TOMBSTONE_KEEP path=${relPath} row_id=${row.id} reason=${existing === undefined ? "untracked" : "diverged"}]`);
|
|
46751
|
-
|
|
46752
|
-
|
|
47412
|
+
if (!existing)
|
|
47413
|
+
return { state: clearHoldsOfRow(state, relPath, row.id), action: "skipped" };
|
|
47414
|
+
const kept = upsertFilePullHold(clearRowHolds(dropAck(state, relPath, existing, snapshotEntry, args.commit), relPath), {
|
|
47415
|
+
target: "file",
|
|
47416
|
+
path: relPath,
|
|
47417
|
+
kind: "kept_after_delete",
|
|
47418
|
+
row_id: row.id,
|
|
47419
|
+
row_updated_at: row.deleted_at,
|
|
47420
|
+
row_hash: existing.content_hash,
|
|
47421
|
+
local: localFingerprint(abs) ?? null,
|
|
47422
|
+
attempts: 1
|
|
47423
|
+
});
|
|
47424
|
+
return { state: kept, action: "skipped" };
|
|
46753
47425
|
}
|
|
46754
47426
|
suppressor?.registerDelete(abs);
|
|
46755
47427
|
const didUnlink = deleteFilesFile(workspaceSlug, relPath);
|
|
46756
47428
|
const removedText = safeDeleteExtractedText(workspaceSlug, relPath, output);
|
|
46757
47429
|
const textDeleted = removedText.removed ? 1 : 0;
|
|
46758
47430
|
if (existing) {
|
|
46759
|
-
const { state: next2 } = removeFileEntry(state, relPath);
|
|
46760
47431
|
return {
|
|
46761
|
-
state:
|
|
47432
|
+
state: clearRowHolds(dropAck(state, relPath, existing, snapshotEntry, args.commit), relPath),
|
|
46762
47433
|
action: "deleted",
|
|
46763
47434
|
textDeleted,
|
|
46764
47435
|
textFailed: removedText.failed
|
|
46765
47436
|
};
|
|
46766
47437
|
}
|
|
46767
47438
|
return {
|
|
46768
|
-
state,
|
|
47439
|
+
state: clearRowHolds(state, relPath),
|
|
46769
47440
|
action: didUnlink ? "deleted" : "skipped",
|
|
46770
47441
|
textDeleted,
|
|
46771
47442
|
textFailed: removedText.failed
|
|
46772
47443
|
};
|
|
46773
47444
|
}
|
|
47445
|
+
if (existing && isLaterVersion(existing, row)) {
|
|
47446
|
+
const key = `skip:${relPath}:${row.id}:${row.updated_at}`;
|
|
47447
|
+
if (!args.loggedStashNotices?.has(key)) {
|
|
47448
|
+
args.loggedStashNotices?.add(key);
|
|
47449
|
+
output?.log(filePullSkippedLine({
|
|
47450
|
+
path: relPath,
|
|
47451
|
+
rowId: row.id,
|
|
47452
|
+
reason: existing.id === row.id ? "older" : "replaced"
|
|
47453
|
+
}));
|
|
47454
|
+
}
|
|
47455
|
+
return { state: clearRowHolds(state, relPath), action: "skipped" };
|
|
47456
|
+
}
|
|
47457
|
+
const stillAt = (decided) => () => sameFileVersion(ackNow(), decided);
|
|
46774
47458
|
const textSignal = extractedTextSignal(row);
|
|
46775
47459
|
const textUnknown = textSignal === "unknown";
|
|
46776
47460
|
if (storedBytesSignal(row) === "absent") {
|
|
@@ -46784,7 +47468,8 @@ async function applyFileRow(args) {
|
|
|
46784
47468
|
existing,
|
|
46785
47469
|
textSignal,
|
|
46786
47470
|
fetchImpl,
|
|
46787
|
-
output
|
|
47471
|
+
output,
|
|
47472
|
+
stillCurrent: stillAt(existing)
|
|
46788
47473
|
});
|
|
46789
47474
|
}
|
|
46790
47475
|
if (existing && existing.content_hash === row.content_hash && !existsSync8(abs)) {
|
|
@@ -46798,9 +47483,9 @@ async function applyFileRow(args) {
|
|
|
46798
47483
|
} else {
|
|
46799
47484
|
presentAgain(args.recentUnlinks, abs);
|
|
46800
47485
|
}
|
|
46801
|
-
if (existing && existing.content_hash === row.content_hash && existsSync8(abs)) {
|
|
47486
|
+
if (existing && existing.id === row.id && existing.content_hash === row.content_hash && existsSync8(abs)) {
|
|
46802
47487
|
if (!args.forceTextRewalk && textIsCurrent(existing, row, workspaceSlug, relPath)) {
|
|
46803
|
-
return { state, action: "noop", textUnknown };
|
|
47488
|
+
return { state: clearRowHolds(state, relPath), action: "noop", textUnknown };
|
|
46804
47489
|
}
|
|
46805
47490
|
const text2 = await applyExtractedText({
|
|
46806
47491
|
workspaceSlug,
|
|
@@ -46810,9 +47495,13 @@ async function applyFileRow(args) {
|
|
|
46810
47495
|
relPath,
|
|
46811
47496
|
bytesInMirror: true,
|
|
46812
47497
|
fetchImpl,
|
|
46813
|
-
output
|
|
47498
|
+
output,
|
|
47499
|
+
stillCurrent: stillAt(existing)
|
|
46814
47500
|
});
|
|
46815
|
-
|
|
47501
|
+
if (text2.stale) {
|
|
47502
|
+
return { state, action: "noop", textUnknown };
|
|
47503
|
+
}
|
|
47504
|
+
const { state: next2 } = upsertFileEntry(clearRowHolds(state, relPath), {
|
|
46816
47505
|
...existing,
|
|
46817
47506
|
updated_at: row.updated_at,
|
|
46818
47507
|
text_hash: textUnknown ? existing.text_hash : text2.textHash
|
|
@@ -46826,6 +47515,23 @@ async function applyFileRow(args) {
|
|
|
46826
47515
|
textUnknown
|
|
46827
47516
|
};
|
|
46828
47517
|
}
|
|
47518
|
+
const held = stillHeld2({
|
|
47519
|
+
hold: getFilePullHold(state, relPath),
|
|
47520
|
+
row,
|
|
47521
|
+
abs,
|
|
47522
|
+
relPath,
|
|
47523
|
+
workspaceSlug,
|
|
47524
|
+
ackHash: existing?.content_hash,
|
|
47525
|
+
now
|
|
47526
|
+
});
|
|
47527
|
+
if (held) {
|
|
47528
|
+
const key = `held:${relPath}:${row.updated_at}:${held.kind}`;
|
|
47529
|
+
if (!args.loggedStashNotices?.has(key)) {
|
|
47530
|
+
args.loggedStashNotices?.add(key);
|
|
47531
|
+
output?.log(filePullHeldLine({ path: relPath, kind: held.kind, retryInSeconds: held.retryInSeconds }));
|
|
47532
|
+
}
|
|
47533
|
+
return held.kind === "stash_failed" ? { state, action: "skipped", stashFailed: true } : { state, action: "skipped", integrityFailed: true };
|
|
47534
|
+
}
|
|
46829
47535
|
const got = await getFile({ baseUrl, pat, id: row.id, fetchImpl });
|
|
46830
47536
|
if (!got.ok) {
|
|
46831
47537
|
return { state, action: "skipped" };
|
|
@@ -46844,22 +47550,51 @@ async function applyFileRow(args) {
|
|
|
46844
47550
|
existing,
|
|
46845
47551
|
textSignal,
|
|
46846
47552
|
fetchImpl,
|
|
46847
|
-
output
|
|
47553
|
+
output,
|
|
47554
|
+
stillCurrent: stillAt(existing)
|
|
46848
47555
|
});
|
|
46849
47556
|
}
|
|
46850
47557
|
const bytes = await downloadBytes(got.row.download_url, fetchImpl);
|
|
46851
47558
|
if (sha256Bytes(bytes) !== row.content_hash) {
|
|
46852
47559
|
output?.warn(`Integrity check failed for files/${relPath} (hash mismatch) — skipped this pull.`);
|
|
46853
|
-
return {
|
|
47560
|
+
return {
|
|
47561
|
+
state: holdRow(state, getFilePullHold(state, relPath), {
|
|
47562
|
+
row,
|
|
47563
|
+
relPath,
|
|
47564
|
+
kind: "integrity_failed",
|
|
47565
|
+
abs,
|
|
47566
|
+
now
|
|
47567
|
+
}),
|
|
47568
|
+
action: "skipped",
|
|
47569
|
+
integrityFailed: true
|
|
47570
|
+
};
|
|
47571
|
+
}
|
|
47572
|
+
const ackAtWrite = ackNow();
|
|
47573
|
+
if (ackAtWrite && isLaterVersion(ackAtWrite, row)) {
|
|
47574
|
+
const key = `skip:${relPath}:${row.id}:${row.updated_at}`;
|
|
47575
|
+
if (!args.loggedStashNotices?.has(key)) {
|
|
47576
|
+
args.loggedStashNotices?.add(key);
|
|
47577
|
+
output?.log(filePullSkippedLine({
|
|
47578
|
+
path: relPath,
|
|
47579
|
+
rowId: row.id,
|
|
47580
|
+
reason: ackAtWrite.id === row.id ? "older" : "replaced"
|
|
47581
|
+
}));
|
|
47582
|
+
}
|
|
47583
|
+
return { state: clearRowHolds(state, relPath), action: "skipped" };
|
|
47584
|
+
}
|
|
47585
|
+
const ackIsRow = ackAtWrite !== undefined && ackAtWrite.id === row.id && ackAtWrite.updated_at === row.updated_at && ackAtWrite.content_hash === row.content_hash;
|
|
47586
|
+
if (ackIsRow && (!sameFileVersion(ackAtWrite, existing) || unsentBytesAt({ workspaceSlug, abs, safeHashes: [row.content_hash] }).kind === "unsent")) {
|
|
47587
|
+
return { state: clearRowHolds(state, relPath), action: "noop", textUnknown };
|
|
46854
47588
|
}
|
|
46855
|
-
const stashFailure =
|
|
47589
|
+
const stashFailure = keepUnsentLocalCopy({
|
|
46856
47590
|
workspaceSlug,
|
|
46857
47591
|
relPath,
|
|
46858
47592
|
abs,
|
|
46859
|
-
|
|
46860
|
-
rowHash: row.content_hash,
|
|
46861
|
-
ambient: suppressor !== undefined,
|
|
47593
|
+
safeHashes: [row.content_hash, ackAtWrite?.content_hash],
|
|
46862
47594
|
now,
|
|
47595
|
+
via: "pull",
|
|
47596
|
+
versionKey: row.content_hash,
|
|
47597
|
+
onBoot: suppressor === undefined,
|
|
46863
47598
|
output,
|
|
46864
47599
|
loggedStashNotices: args.loggedStashNotices
|
|
46865
47600
|
});
|
|
@@ -46869,7 +47604,18 @@ async function applyFileRow(args) {
|
|
|
46869
47604
|
args.loggedStashNotices?.add(key);
|
|
46870
47605
|
output?.log(fileStashFailedLine({ path: relPath, ...stashFailure }));
|
|
46871
47606
|
}
|
|
46872
|
-
return {
|
|
47607
|
+
return {
|
|
47608
|
+
state: holdRow(state, getFilePullHold(state, relPath), {
|
|
47609
|
+
row,
|
|
47610
|
+
relPath,
|
|
47611
|
+
kind: "stash_failed",
|
|
47612
|
+
abs,
|
|
47613
|
+
now,
|
|
47614
|
+
failure: stashFailure
|
|
47615
|
+
}),
|
|
47616
|
+
action: "skipped",
|
|
47617
|
+
stashFailed: true
|
|
47618
|
+
};
|
|
46873
47619
|
}
|
|
46874
47620
|
suppressor?.registerWrite(abs, { bodyHash: row.content_hash, frontmatterHash: "" });
|
|
46875
47621
|
writeFilesBytes(workspaceSlug, relPath, bytes);
|
|
@@ -46895,7 +47641,8 @@ async function applyFileRow(args) {
|
|
|
46895
47641
|
relPath,
|
|
46896
47642
|
bytesInMirror: true,
|
|
46897
47643
|
fetchImpl,
|
|
46898
|
-
output
|
|
47644
|
+
output,
|
|
47645
|
+
stillCurrent: stillAt(ackAtWrite)
|
|
46899
47646
|
});
|
|
46900
47647
|
}
|
|
46901
47648
|
let inode;
|
|
@@ -46904,7 +47651,7 @@ async function applyFileRow(args) {
|
|
|
46904
47651
|
} catch {
|
|
46905
47652
|
inode = undefined;
|
|
46906
47653
|
}
|
|
46907
|
-
const { state: next } = upsertFileEntry(state, {
|
|
47654
|
+
const { state: next } = upsertFileEntry(clearRowHolds(state, relPath), {
|
|
46908
47655
|
id: row.id,
|
|
46909
47656
|
path: relPath,
|
|
46910
47657
|
content_hash: row.content_hash,
|
|
@@ -46940,7 +47687,8 @@ async function applyPointerRow(args) {
|
|
|
46940
47687
|
relPath,
|
|
46941
47688
|
bytesInMirror,
|
|
46942
47689
|
fetchImpl: args.fetchImpl,
|
|
46943
|
-
output: args.output
|
|
47690
|
+
output: args.output,
|
|
47691
|
+
stillCurrent: args.stillCurrent
|
|
46944
47692
|
});
|
|
46945
47693
|
return {
|
|
46946
47694
|
state,
|
|
@@ -46952,53 +47700,87 @@ async function applyPointerRow(args) {
|
|
|
46952
47700
|
pointer: true
|
|
46953
47701
|
};
|
|
46954
47702
|
}
|
|
46955
|
-
function
|
|
46956
|
-
|
|
46957
|
-
|
|
46958
|
-
|
|
46959
|
-
|
|
46960
|
-
|
|
46961
|
-
|
|
46962
|
-
|
|
46963
|
-
|
|
46964
|
-
|
|
46965
|
-
|
|
46966
|
-
}
|
|
46967
|
-
if (!st.isFile())
|
|
46968
|
-
return { reason: "not_a_file" };
|
|
46969
|
-
let localBytes;
|
|
46970
|
-
try {
|
|
46971
|
-
localBytes = readFileSync27(abs);
|
|
46972
|
-
} catch (err) {
|
|
46973
|
-
return { reason: "unreadable", code: errCode(err) };
|
|
47703
|
+
function clearRowHolds(state, relPath) {
|
|
47704
|
+
return removeFilePullHold(removeFilePullHold(state, relPath, "stash_failed"), relPath, "integrity_failed");
|
|
47705
|
+
}
|
|
47706
|
+
function clearHoldsOfRow(state, relPath, rowId) {
|
|
47707
|
+
const hold = getFilePullHold(state, relPath);
|
|
47708
|
+
return hold && hold.row_id === rowId ? clearRowHolds(state, relPath) : state;
|
|
47709
|
+
}
|
|
47710
|
+
function dropAck(state, relPath, ack, snapshotEntry, commit2) {
|
|
47711
|
+
const next = removeFileEntry(state, relPath).state;
|
|
47712
|
+
if (commit2 && !sameFileVersion(snapshotEntry, ack)) {
|
|
47713
|
+
commit2(upsertFileEntry(state, ack).state, next);
|
|
46974
47714
|
}
|
|
46975
|
-
|
|
46976
|
-
|
|
47715
|
+
return next;
|
|
47716
|
+
}
|
|
47717
|
+
function holdRow(state, prior, args) {
|
|
47718
|
+
const { row, relPath, kind, now } = args;
|
|
47719
|
+
const sameVersion = prior?.kind === kind && prior.row_id === row.id && prior.row_updated_at === row.updated_at && prior.row_hash === row.content_hash;
|
|
47720
|
+
const attempts = sameVersion ? prior.attempts + 1 : 1;
|
|
47721
|
+
const at = now();
|
|
47722
|
+
return upsertFilePullHold(clearRowHolds(state, relPath), {
|
|
47723
|
+
target: "file",
|
|
47724
|
+
path: relPath,
|
|
47725
|
+
kind,
|
|
47726
|
+
row_id: row.id,
|
|
47727
|
+
row_updated_at: row.updated_at,
|
|
47728
|
+
row_hash: row.content_hash,
|
|
47729
|
+
...args.failure ? { reason: args.failure.reason } : {},
|
|
47730
|
+
...args.failure?.code ? { code: args.failure.code } : {},
|
|
47731
|
+
local: localFingerprint(args.abs) ?? null,
|
|
47732
|
+
attempts,
|
|
47733
|
+
...kind === "integrity_failed" ? { next_retry_at: new Date(at.getTime() + integrityBackoffMs(attempts)).toISOString() } : {}
|
|
47734
|
+
});
|
|
47735
|
+
}
|
|
47736
|
+
var INTEGRITY_BACKOFF_MS = 60000;
|
|
47737
|
+
var INTEGRITY_BACKOFF_CAP_MS = 3600000;
|
|
47738
|
+
function integrityBackoffMs(attempts) {
|
|
47739
|
+
return Math.min(INTEGRITY_BACKOFF_MS * 2 ** Math.max(0, attempts - 1), INTEGRITY_BACKOFF_CAP_MS);
|
|
47740
|
+
}
|
|
47741
|
+
function stillHeld2(args) {
|
|
47742
|
+
const { hold, row } = args;
|
|
47743
|
+
if (!hold || hold.row_id !== row.id || hold.row_updated_at !== row.updated_at || hold.row_hash !== row.content_hash) {
|
|
46977
47744
|
return null;
|
|
46978
|
-
let stashed;
|
|
46979
|
-
try {
|
|
46980
|
-
stashed = stashFileConflictOnce({
|
|
46981
|
-
workspaceSlug,
|
|
46982
|
-
relPath,
|
|
46983
|
-
timestamp: args.now(),
|
|
46984
|
-
bytes: localBytes,
|
|
46985
|
-
side: "local"
|
|
46986
|
-
});
|
|
46987
|
-
} catch (err) {
|
|
46988
|
-
return { reason: "stash_io", code: errCode(err) };
|
|
46989
47745
|
}
|
|
46990
|
-
|
|
46991
|
-
|
|
47746
|
+
if (hold.kind === "integrity_failed") {
|
|
47747
|
+
const due = hold.next_retry_at ? Date.parse(hold.next_retry_at) : Number.NaN;
|
|
47748
|
+
const wait = due - args.now().getTime();
|
|
47749
|
+
return Number.isFinite(wait) && wait > 0 ? { kind: "integrity_failed", retryInSeconds: Math.ceil(wait / 1000) } : null;
|
|
47750
|
+
}
|
|
47751
|
+
if (hold.kind !== "stash_failed")
|
|
46992
47752
|
return null;
|
|
46993
|
-
|
|
46994
|
-
|
|
46995
|
-
|
|
46996
|
-
|
|
46997
|
-
|
|
47753
|
+
const local = localFingerprint(args.abs);
|
|
47754
|
+
const unchanged = local !== undefined && (local === null ? hold.local === null : hold.local != null && hold.local.size === local.size && hold.local.mtime_ms === local.mtime_ms && hold.local.ino === local.ino);
|
|
47755
|
+
if (!unchanged)
|
|
47756
|
+
return null;
|
|
47757
|
+
const failure = probeKeep({
|
|
47758
|
+
workspaceSlug: args.workspaceSlug,
|
|
47759
|
+
relPath: args.relPath,
|
|
47760
|
+
abs: args.abs,
|
|
47761
|
+
safeHashes: [row.content_hash, args.ackHash]
|
|
47762
|
+
});
|
|
47763
|
+
return failure ? { kind: "stash_failed" } : null;
|
|
46998
47764
|
}
|
|
46999
|
-
function
|
|
47000
|
-
|
|
47001
|
-
|
|
47765
|
+
function pruneFilePullHolds(args) {
|
|
47766
|
+
let state = args.state;
|
|
47767
|
+
for (const hold of listFilePullHolds(state)) {
|
|
47768
|
+
if (hold.kind === "kept_after_delete") {
|
|
47769
|
+
let gone = getFileEntry(state, hold.path) !== undefined;
|
|
47770
|
+
if (!gone) {
|
|
47771
|
+
try {
|
|
47772
|
+
gone = localFingerprint(filesSubtreePath(args.workspaceSlug, hold.path)) === null;
|
|
47773
|
+
} catch {
|
|
47774
|
+
gone = false;
|
|
47775
|
+
}
|
|
47776
|
+
}
|
|
47777
|
+
if (gone)
|
|
47778
|
+
state = removeFilePullHold(state, hold.path, hold.kind);
|
|
47779
|
+
} else if ((hold.kind === "stash_failed" || hold.kind === "integrity_failed") && args.cursorPass && !args.deliveredPaths.has(hold.path)) {
|
|
47780
|
+
state = removeFilePullHold(state, hold.path, hold.kind);
|
|
47781
|
+
}
|
|
47782
|
+
}
|
|
47783
|
+
return state;
|
|
47002
47784
|
}
|
|
47003
47785
|
function textIsCurrent(existing, row, workspaceSlug, relPath) {
|
|
47004
47786
|
const signal = extractedTextSignal(row);
|
|
@@ -47018,7 +47800,7 @@ function textIsCurrent(existing, row, workspaceSlug, relPath) {
|
|
|
47018
47800
|
}
|
|
47019
47801
|
|
|
47020
47802
|
// src/lib/files/rejection-sync.ts
|
|
47021
|
-
import { readFileSync as
|
|
47803
|
+
import { readFileSync as readFileSync30, statSync as statSync16 } from "node:fs";
|
|
47022
47804
|
|
|
47023
47805
|
// src/lib/files/rejections-api.ts
|
|
47024
47806
|
var RejectionsFeedSchema = exports_external.object({
|
|
@@ -47151,7 +47933,7 @@ function settledDisposition(args) {
|
|
|
47151
47933
|
return null;
|
|
47152
47934
|
let bytes;
|
|
47153
47935
|
try {
|
|
47154
|
-
bytes =
|
|
47936
|
+
bytes = readFileSync30(absolute2);
|
|
47155
47937
|
} catch {
|
|
47156
47938
|
return null;
|
|
47157
47939
|
}
|
|
@@ -47216,6 +47998,49 @@ function rearmMissingReports(state) {
|
|
|
47216
47998
|
|
|
47217
47999
|
// src/commands/sync.ts
|
|
47218
48000
|
init_output();
|
|
48001
|
+
|
|
48002
|
+
// src/lib/pull-live-gate.ts
|
|
48003
|
+
import { lstatSync as lstatSync7, readFileSync as readFileSync31 } from "node:fs";
|
|
48004
|
+
function decideLivePull(args) {
|
|
48005
|
+
const { workspaceSlug, strategy, live, snapshotEntry, writeSlug } = args;
|
|
48006
|
+
if (strategy.preserveLocalBodyOnPull)
|
|
48007
|
+
return { kind: "write" };
|
|
48008
|
+
if (strategy.type === "artists")
|
|
48009
|
+
return { kind: "write" };
|
|
48010
|
+
const absolutePath = entityFilePath(workspaceSlug, strategy.type, writeSlug);
|
|
48011
|
+
const ack = snapshotEntry ? committedEntryIfNewer(workspaceSlug, strategy.type, snapshotEntry) ?? snapshotEntry : readCommittedEntry(workspaceSlug, strategy.type, live.id);
|
|
48012
|
+
if (!ack)
|
|
48013
|
+
return { kind: "write" };
|
|
48014
|
+
const older = isLaterVersion(ack, live);
|
|
48015
|
+
if (ack.slug !== writeSlug)
|
|
48016
|
+
return older ? { kind: "skip" } : { kind: "write" };
|
|
48017
|
+
const newer = !older && live.updated_at !== ack.updated_at;
|
|
48018
|
+
let raw = null;
|
|
48019
|
+
try {
|
|
48020
|
+
raw = lstatSync7(absolutePath).isFile() ? readFileSync31(absolutePath, "utf8") : null;
|
|
48021
|
+
} catch {
|
|
48022
|
+
raw = null;
|
|
48023
|
+
}
|
|
48024
|
+
if (raw === null)
|
|
48025
|
+
return older ? { kind: "skip" } : { kind: "write" };
|
|
48026
|
+
if (frontmatterParseError(raw) !== null) {
|
|
48027
|
+
return { kind: "defer", reason: "unreadable", absolutePath };
|
|
48028
|
+
}
|
|
48029
|
+
const disk = computeMirrorHashes(strategy, raw);
|
|
48030
|
+
const matchesAck = disk.bodyHash === ack.body_hash && disk.frontmatterHash === ack.frontmatter_hash;
|
|
48031
|
+
if (!newer) {
|
|
48032
|
+
return matchesAck ? { kind: "skip" } : { kind: "defer", reason: "local_edit", absolutePath };
|
|
48033
|
+
}
|
|
48034
|
+
if (matchesAck)
|
|
48035
|
+
return { kind: "write" };
|
|
48036
|
+
const incoming = computeMirrorHashes(strategy, renderMirrorWithBodyPreserve(workspaceSlug, strategy, live, writeSlug));
|
|
48037
|
+
if (disk.bodyHash === incoming.bodyHash && disk.frontmatterHash === incoming.frontmatterHash) {
|
|
48038
|
+
return { kind: "adopt", raw };
|
|
48039
|
+
}
|
|
48040
|
+
return { kind: "defer", reason: "local_edit", absolutePath };
|
|
48041
|
+
}
|
|
48042
|
+
|
|
48043
|
+
// src/commands/sync.ts
|
|
47219
48044
|
var memberRefreshFailedAt = new Map;
|
|
47220
48045
|
var MEMBER_REFRESH_RETRY_AFTER_MS = 60 * 60 * 1000;
|
|
47221
48046
|
function classifyApiError(err) {
|
|
@@ -47237,7 +48062,7 @@ function hasExistingContent(workspacePath) {
|
|
|
47237
48062
|
if (!e.isDirectory())
|
|
47238
48063
|
return true;
|
|
47239
48064
|
try {
|
|
47240
|
-
return readdirSync7(
|
|
48065
|
+
return readdirSync7(join19(workspacePath, e.name)).length > 0;
|
|
47241
48066
|
} catch {
|
|
47242
48067
|
return true;
|
|
47243
48068
|
}
|
|
@@ -47267,7 +48092,7 @@ function unlinkIfProvablyOurs(workspaceSlug, strategy, entry2, suppressor) {
|
|
|
47267
48092
|
}
|
|
47268
48093
|
let raw;
|
|
47269
48094
|
try {
|
|
47270
|
-
raw =
|
|
48095
|
+
raw = readFileSync32(target, "utf8");
|
|
47271
48096
|
} catch (err) {
|
|
47272
48097
|
if (err?.code === "ENOENT") {
|
|
47273
48098
|
return { didUnlink: false };
|
|
@@ -47285,7 +48110,7 @@ function unlinkIfProvablyOurs(workspaceSlug, strategy, entry2, suppressor) {
|
|
|
47285
48110
|
}
|
|
47286
48111
|
try {
|
|
47287
48112
|
suppressor?.registerDelete(target);
|
|
47288
|
-
|
|
48113
|
+
unlinkSync11(target);
|
|
47289
48114
|
return { didUnlink: true };
|
|
47290
48115
|
} catch (err) {
|
|
47291
48116
|
if (err?.code === "ENOENT") {
|
|
@@ -47392,7 +48217,7 @@ async function replayPermanentDocuments(args) {
|
|
|
47392
48217
|
for (const row of rows) {
|
|
47393
48218
|
if (row.deleted_at !== null && row.permanent_delete === true)
|
|
47394
48219
|
continue;
|
|
47395
|
-
const outcome = applyRow(args.workspaceSlug, args.strategy, row, state, args.output, args.suppressor, args.now, args.isFrozen, args.loggedTombstoneSkips, args.recentUnlinks);
|
|
48220
|
+
const outcome = applyRow(args.workspaceSlug, args.strategy, row, state, args.output, args.suppressor, args.now, args.isFrozen, args.loggedTombstoneSkips, args.recentUnlinks, args.deferral);
|
|
47396
48221
|
state = outcome.state;
|
|
47397
48222
|
if (outcome.action === "written")
|
|
47398
48223
|
counters.written++;
|
|
@@ -47529,6 +48354,10 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
47529
48354
|
committed = state;
|
|
47530
48355
|
};
|
|
47531
48356
|
const counters = {};
|
|
48357
|
+
const deferral = {
|
|
48358
|
+
onPushDeferred: deps.onPushDeferred,
|
|
48359
|
+
logged: deps.loggedPullDeferrals
|
|
48360
|
+
};
|
|
47532
48361
|
let restoresDeferred = 0;
|
|
47533
48362
|
try {
|
|
47534
48363
|
for (const entityType of allEntityTypes()) {
|
|
@@ -47552,6 +48381,7 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
47552
48381
|
isFrozen: deps.isFrozen,
|
|
47553
48382
|
loggedTombstoneSkips: deps.loggedTombstoneSkips,
|
|
47554
48383
|
recentUnlinks: deps.recentUnlinks,
|
|
48384
|
+
deferral,
|
|
47555
48385
|
commit: (before, after) => {
|
|
47556
48386
|
const next = commitState(before, after);
|
|
47557
48387
|
committed = next;
|
|
@@ -47637,7 +48467,7 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
47637
48467
|
}
|
|
47638
48468
|
let outcome;
|
|
47639
48469
|
try {
|
|
47640
|
-
outcome = applyRow(slug, strategy, row, state, out, deps.suppressor, now, deps.isFrozen, deps.loggedTombstoneSkips, deps.recentUnlinks);
|
|
48470
|
+
outcome = applyRow(slug, strategy, row, state, out, deps.suppressor, now, deps.isFrozen, deps.loggedTombstoneSkips, deps.recentUnlinks, deferral);
|
|
47641
48471
|
} catch (err) {
|
|
47642
48472
|
if (!(err instanceof MirrorSymlinkError))
|
|
47643
48473
|
throw err;
|
|
@@ -47697,6 +48527,7 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
47697
48527
|
isFrozen: deps.isFrozen,
|
|
47698
48528
|
loggedTombstoneSkips: deps.loggedTombstoneSkips,
|
|
47699
48529
|
recentUnlinks: deps.recentUnlinks,
|
|
48530
|
+
deferral,
|
|
47700
48531
|
commit: (before, after) => {
|
|
47701
48532
|
const next = commitState(before, after);
|
|
47702
48533
|
committed = next;
|
|
@@ -47990,7 +48821,7 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
47990
48821
|
out.success(`Done in ${elapsed}s.`);
|
|
47991
48822
|
return { exitCode: 0, applied: totalWritten + totalMoved + totalDeleted, restoresDeferred };
|
|
47992
48823
|
}
|
|
47993
|
-
function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = () => new Date, isFrozen, loggedTombstoneSkips, recentUnlinks) {
|
|
48824
|
+
function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = () => new Date, isFrozen, loggedTombstoneSkips, recentUnlinks, deferral) {
|
|
47994
48825
|
if (isFrozen?.(strategy.type, row.id)) {
|
|
47995
48826
|
return { state, action: "skipped" };
|
|
47996
48827
|
}
|
|
@@ -48141,16 +48972,47 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
48141
48972
|
return { state, action: "noop_overwrite" };
|
|
48142
48973
|
}
|
|
48143
48974
|
}
|
|
48975
|
+
let adoptRaw;
|
|
48976
|
+
if (suppressor && relocation.kind !== "moved") {
|
|
48977
|
+
const gate = decideLivePull({
|
|
48978
|
+
workspaceSlug,
|
|
48979
|
+
strategy,
|
|
48980
|
+
live,
|
|
48981
|
+
snapshotEntry: existingEntry,
|
|
48982
|
+
writeSlug
|
|
48983
|
+
});
|
|
48984
|
+
if (gate.kind === "skip")
|
|
48985
|
+
return { state, action: "noop_overwrite" };
|
|
48986
|
+
if (gate.kind === "defer") {
|
|
48987
|
+
const key = `${strategy.type}:${live.id}:${live.updated_at}`;
|
|
48988
|
+
if (!deferral?.logged?.has(key)) {
|
|
48989
|
+
deferral?.logged?.add(key);
|
|
48990
|
+
out.log(entityPullDeferredLine({
|
|
48991
|
+
entityType: strategy.type,
|
|
48992
|
+
slug: writeSlug,
|
|
48993
|
+
id: live.id,
|
|
48994
|
+
reason: gate.reason
|
|
48995
|
+
}));
|
|
48996
|
+
deferral?.onPushDeferred?.(gate.absolutePath);
|
|
48997
|
+
}
|
|
48998
|
+
return { state, action: "deferred" };
|
|
48999
|
+
}
|
|
49000
|
+
if (gate.kind === "adopt")
|
|
49001
|
+
adoptRaw = gate.raw;
|
|
49002
|
+
}
|
|
48144
49003
|
if (!suppressor && existingEntry && existingEntry.updated_at !== live.updated_at && relocation.kind !== "moved") {
|
|
48145
49004
|
let localRaw = null;
|
|
48146
49005
|
try {
|
|
48147
|
-
localRaw =
|
|
49006
|
+
localRaw = readFileSync32(entityFilePath(workspaceSlug, strategy.type, writeSlug), "utf8");
|
|
48148
49007
|
} catch {
|
|
48149
49008
|
localRaw = null;
|
|
48150
49009
|
}
|
|
48151
49010
|
if (localRaw !== null) {
|
|
48152
|
-
|
|
48153
|
-
|
|
49011
|
+
let diverged = !strategy.preserveLocalBodyOnPull && frontmatterParseError(localRaw) !== null;
|
|
49012
|
+
if (!diverged) {
|
|
49013
|
+
const localHashes = computeMirrorHashes(strategy, localRaw);
|
|
49014
|
+
diverged = localHashes.bodyHash !== existingEntry.body_hash || localHashes.frontmatterHash !== existingEntry.frontmatter_hash;
|
|
49015
|
+
}
|
|
48154
49016
|
if (diverged) {
|
|
48155
49017
|
const stashPath = stashConflict({
|
|
48156
49018
|
workspaceSlug,
|
|
@@ -48168,7 +49030,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
48168
49030
|
const target = entityFilePath(workspaceSlug, strategy.type, writeSlug);
|
|
48169
49031
|
let localRaw = null;
|
|
48170
49032
|
try {
|
|
48171
|
-
localRaw =
|
|
49033
|
+
localRaw = lstatSync8(target).isFile() ? readFileSync32(target, "utf8") : null;
|
|
48172
49034
|
} catch {
|
|
48173
49035
|
localRaw = null;
|
|
48174
49036
|
}
|
|
@@ -48196,7 +49058,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
48196
49058
|
}
|
|
48197
49059
|
}
|
|
48198
49060
|
}
|
|
48199
|
-
const content = writeMirrorWithBodyPreserve(workspaceSlug, strategy, live, suppressor, writeSlug);
|
|
49061
|
+
const content = adoptRaw ?? writeMirrorWithBodyPreserve(workspaceSlug, strategy, live, suppressor, writeSlug);
|
|
48200
49062
|
const hashes = computeMirrorHashes(strategy, content);
|
|
48201
49063
|
let inode;
|
|
48202
49064
|
try {
|
|
@@ -48233,7 +49095,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
48233
49095
|
memory_hash: memoryHash,
|
|
48234
49096
|
memory_generated: memoryGenerated
|
|
48235
49097
|
});
|
|
48236
|
-
const action = relocation.kind === "moved" ? "moved" : "written";
|
|
49098
|
+
const action = adoptRaw !== undefined ? "noop_overwrite" : relocation.kind === "moved" ? "moved" : "written";
|
|
48237
49099
|
return { state: nextState, action };
|
|
48238
49100
|
}
|
|
48239
49101
|
var syncCommand = defineCommand({
|
|
@@ -48334,7 +49196,7 @@ function backfillStateInodes(workspaceSlug, state, statFn = (p) => statSync18(p)
|
|
|
48334
49196
|
return { state, changed: false };
|
|
48335
49197
|
return { state: { ...state, entries: nextEntries }, changed: true };
|
|
48336
49198
|
}
|
|
48337
|
-
function backfillBodyLengths(workspaceSlug, state, readFn = (p) =>
|
|
49199
|
+
function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync33(p, "utf8")) {
|
|
48338
49200
|
let changed = false;
|
|
48339
49201
|
const nextEntries = {};
|
|
48340
49202
|
for (const [entityType, bucket] of Object.entries(state.entries)) {
|
|
@@ -48346,7 +49208,7 @@ function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync3
|
|
|
48346
49208
|
}
|
|
48347
49209
|
try {
|
|
48348
49210
|
const path5 = entityFilePath(workspaceSlug, entityType, entry2.slug);
|
|
48349
|
-
const bodyLen =
|
|
49211
|
+
const bodyLen = parseUncached(readFn(path5)).content.length;
|
|
48350
49212
|
nextBucket[id] = { ...entry2, body_len: bodyLen };
|
|
48351
49213
|
changed = true;
|
|
48352
49214
|
} catch {
|
|
@@ -48359,6 +49221,18 @@ function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync3
|
|
|
48359
49221
|
return { state, changed: false };
|
|
48360
49222
|
return { state: { ...state, entries: nextEntries }, changed: true };
|
|
48361
49223
|
}
|
|
49224
|
+
function isOwnWriteEcho(suppressor, absolutePath, strategy, raw) {
|
|
49225
|
+
if (frontmatterParseError(raw) !== null)
|
|
49226
|
+
return false;
|
|
49227
|
+
return suppressor.shouldSuppressUpsert(absolutePath, computeMirrorHashes(strategy, raw));
|
|
49228
|
+
}
|
|
49229
|
+
function destinationIsUnreadable(absolutePath) {
|
|
49230
|
+
try {
|
|
49231
|
+
return frontmatterParseError(readFileSync33(absolutePath, "utf8")) !== null;
|
|
49232
|
+
} catch {
|
|
49233
|
+
return false;
|
|
49234
|
+
}
|
|
49235
|
+
}
|
|
48362
49236
|
function makeWatchHandler(deps) {
|
|
48363
49237
|
const {
|
|
48364
49238
|
workspaceSlug,
|
|
@@ -48475,7 +49349,7 @@ function makeWatchHandler(deps) {
|
|
|
48475
49349
|
spinner.update(`Watching ${workspaceSlug} · ${watchingSummary(getState())}`);
|
|
48476
49350
|
return;
|
|
48477
49351
|
}
|
|
48478
|
-
const bytes =
|
|
49352
|
+
const bytes = readFileSync33(absolutePath);
|
|
48479
49353
|
const hash2 = sha256Bytes(bytes);
|
|
48480
49354
|
if (suppressor?.shouldSuppressUpsert(absolutePath, {
|
|
48481
49355
|
bodyHash: hash2,
|
|
@@ -48539,7 +49413,7 @@ function makeWatchHandler(deps) {
|
|
|
48539
49413
|
if (suppressor) {
|
|
48540
49414
|
let raw = null;
|
|
48541
49415
|
try {
|
|
48542
|
-
raw =
|
|
49416
|
+
raw = readFileSync33(absolutePath, "utf8");
|
|
48543
49417
|
} catch {
|
|
48544
49418
|
raw = null;
|
|
48545
49419
|
}
|
|
@@ -48578,7 +49452,7 @@ function makeWatchHandler(deps) {
|
|
|
48578
49452
|
if (suppressor) {
|
|
48579
49453
|
let raw = null;
|
|
48580
49454
|
try {
|
|
48581
|
-
raw =
|
|
49455
|
+
raw = readFileSync33(absolutePath, "utf8");
|
|
48582
49456
|
} catch {
|
|
48583
49457
|
raw = null;
|
|
48584
49458
|
}
|
|
@@ -48617,7 +49491,7 @@ function makeWatchHandler(deps) {
|
|
|
48617
49491
|
if (suppressor) {
|
|
48618
49492
|
let raw = null;
|
|
48619
49493
|
try {
|
|
48620
|
-
raw =
|
|
49494
|
+
raw = readFileSync33(absolutePath, "utf8");
|
|
48621
49495
|
} catch {
|
|
48622
49496
|
raw = null;
|
|
48623
49497
|
}
|
|
@@ -48656,7 +49530,7 @@ function makeWatchHandler(deps) {
|
|
|
48656
49530
|
if (suppressor) {
|
|
48657
49531
|
let raw = null;
|
|
48658
49532
|
try {
|
|
48659
|
-
raw =
|
|
49533
|
+
raw = readFileSync33(absolutePath, "utf8");
|
|
48660
49534
|
} catch {
|
|
48661
49535
|
raw = null;
|
|
48662
49536
|
}
|
|
@@ -48695,7 +49569,7 @@ function makeWatchHandler(deps) {
|
|
|
48695
49569
|
if (suppressor) {
|
|
48696
49570
|
let raw = null;
|
|
48697
49571
|
try {
|
|
48698
|
-
raw =
|
|
49572
|
+
raw = readFileSync33(absolutePath, "utf8");
|
|
48699
49573
|
} catch {
|
|
48700
49574
|
raw = null;
|
|
48701
49575
|
}
|
|
@@ -48741,11 +49615,11 @@ function makeWatchHandler(deps) {
|
|
|
48741
49615
|
const strategy = getStrategy(classifiedFrom.entityType);
|
|
48742
49616
|
let toRaw = null;
|
|
48743
49617
|
try {
|
|
48744
|
-
toRaw =
|
|
49618
|
+
toRaw = readFileSync33(event.toPath, "utf8");
|
|
48745
49619
|
} catch {
|
|
48746
49620
|
toRaw = null;
|
|
48747
49621
|
}
|
|
48748
|
-
const isEcho = strategy !== undefined && toRaw !== null && suppressor
|
|
49622
|
+
const isEcho = strategy !== undefined && toRaw !== null && isOwnWriteEcho(suppressor, event.toPath, strategy, toRaw);
|
|
48749
49623
|
if (isEcho) {
|
|
48750
49624
|
output?.log(`[RENAME_SUPPRESSED entityType=${classifiedFrom.entityType} ` + `from=${event.fromSlug} id=${event.id} reason=pull_echo]`);
|
|
48751
49625
|
return;
|
|
@@ -48753,6 +49627,9 @@ function makeWatchHandler(deps) {
|
|
|
48753
49627
|
await handle({ kind: "upsert", absolutePath: event.toPath });
|
|
48754
49628
|
return;
|
|
48755
49629
|
}
|
|
49630
|
+
if (destinationIsUnreadable(event.toPath)) {
|
|
49631
|
+
await handle({ kind: "upsert", absolutePath: event.toPath });
|
|
49632
|
+
}
|
|
48756
49633
|
const base3 = getState();
|
|
48757
49634
|
const result2 = await pushRename({
|
|
48758
49635
|
workspaceSlug,
|
|
@@ -48810,7 +49687,7 @@ function makeWatchHandler(deps) {
|
|
|
48810
49687
|
if (event.kind === "upsert") {
|
|
48811
49688
|
try {
|
|
48812
49689
|
ownEcho = suppressor?.shouldSuppressUpsert(absolutePath, {
|
|
48813
|
-
bodyHash: stableHash(
|
|
49690
|
+
bodyHash: stableHash(readFileSync33(absolutePath, "utf8")),
|
|
48814
49691
|
frontmatterHash: ""
|
|
48815
49692
|
}) === true;
|
|
48816
49693
|
} catch {
|
|
@@ -48865,11 +49742,11 @@ function makeWatchHandler(deps) {
|
|
|
48865
49742
|
if (strategy) {
|
|
48866
49743
|
let raw = null;
|
|
48867
49744
|
try {
|
|
48868
|
-
raw =
|
|
49745
|
+
raw = readFileSync33(absolutePath, "utf8");
|
|
48869
49746
|
} catch {
|
|
48870
49747
|
raw = null;
|
|
48871
49748
|
}
|
|
48872
|
-
if (raw !== null && suppressor
|
|
49749
|
+
if (raw !== null && isOwnWriteEcho(suppressor, absolutePath, strategy, raw)) {
|
|
48873
49750
|
return;
|
|
48874
49751
|
}
|
|
48875
49752
|
}
|
|
@@ -48920,7 +49797,8 @@ function makeWatchHandler(deps) {
|
|
|
48920
49797
|
echo: suppressor ? { watcher: "live", suppressor } : undefined
|
|
48921
49798
|
}
|
|
48922
49799
|
});
|
|
48923
|
-
|
|
49800
|
+
const heldUnreadable = result.outcome.kind === "ignored" && result.outcome.reason === FRONTMATTER_PARSE_ERROR;
|
|
49801
|
+
applyState(result.base ?? base2, heldUnreadable ? result.state : clearEntityRejected(result.state, workspaceSlug, absolutePath));
|
|
48924
49802
|
reportOutcome(spinner, workspaceSlug, getState(), result.outcome);
|
|
48925
49803
|
} catch (err) {
|
|
48926
49804
|
const msg = err instanceof Error ? err.message : "unknown push error";
|
|
@@ -49311,6 +50189,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
49311
50189
|
}
|
|
49312
50190
|
const loggedTombstoneSkips = new Set;
|
|
49313
50191
|
const loggedStashNotices = new Set;
|
|
50192
|
+
const loggedPullDeferrals = new Set;
|
|
49314
50193
|
const initial = await runSync(workspaceSlug, {
|
|
49315
50194
|
fetchImpl,
|
|
49316
50195
|
keychain,
|
|
@@ -49468,7 +50347,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
49468
50347
|
return;
|
|
49469
50348
|
}
|
|
49470
50349
|
refreshFrozen();
|
|
49471
|
-
out.log(`[DESTRUCTIVE_HELD batch=${batchId} ops=${batch.length} reason=${result.reason
|
|
50350
|
+
out.log(`[DESTRUCTIVE_HELD batch=${batchId} ops=${batch.length} reason=${tripReasonToken(result.reason)}]`);
|
|
49472
50351
|
spinner.warn(`Held ${batch.length} destructive op${batch.length === 1 ? "" : "s"} pending confirm → ${stashPath}. Run \`plinth confirm\` to review.`);
|
|
49473
50352
|
spinner.update(`Watching ${slug} · ${watchingSummary(state)}`);
|
|
49474
50353
|
}
|
|
@@ -49524,7 +50403,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
49524
50403
|
return;
|
|
49525
50404
|
}
|
|
49526
50405
|
refreshFrozen();
|
|
49527
|
-
out.log(`[DESTRUCTIVE_HELD batch=${batchId} ops=${batch.length} reason=${result.reason
|
|
50406
|
+
out.log(`[DESTRUCTIVE_HELD batch=${batchId} ops=${batch.length} reason=${tripReasonToken(result.reason)} source=files-watch]`);
|
|
49528
50407
|
spinner.warn(`Held ${batch.length} destructive file op${batch.length === 1 ? "" : "s"} pending confirm → ${stashPath}. Run \`plinth confirm\` to review.`);
|
|
49529
50408
|
spinner.update(`Watching ${slug} · ${watchingSummary(state)}`);
|
|
49530
50409
|
}
|
|
@@ -49576,7 +50455,11 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
49576
50455
|
isFrozen,
|
|
49577
50456
|
isFileFrozen,
|
|
49578
50457
|
loggedTombstoneSkips,
|
|
49579
|
-
loggedStashNotices
|
|
50458
|
+
loggedStashNotices,
|
|
50459
|
+
onPushDeferred: (absolutePath) => {
|
|
50460
|
+
watcher.nudge(absolutePath);
|
|
50461
|
+
},
|
|
50462
|
+
loggedPullDeferrals
|
|
49580
50463
|
}));
|
|
49581
50464
|
if (result.exitCode === 0) {
|
|
49582
50465
|
const applied = result.applied ?? 0;
|
|
@@ -49616,6 +50499,9 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
49616
50499
|
afterPoll: () => {
|
|
49617
50500
|
(async () => {
|
|
49618
50501
|
state = loadState(slug);
|
|
50502
|
+
const unsettled = state;
|
|
50503
|
+
const settled = settleFrontmatterHolds(unsettled, slug);
|
|
50504
|
+
state = rebaseState(state, unsettled, settled);
|
|
49619
50505
|
if (draining)
|
|
49620
50506
|
return;
|
|
49621
50507
|
draining = true;
|
|
@@ -49837,12 +50723,12 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
49837
50723
|
try {
|
|
49838
50724
|
await Promise.race([
|
|
49839
50725
|
watcher.drainPending().then(() => Promise.all([gate.drain(), fileGate.drain()])),
|
|
49840
|
-
new Promise((
|
|
50726
|
+
new Promise((resolve7) => setTimeout(() => resolve7(-1), SHUTDOWN_DRAIN_TIMEOUT_MS))
|
|
49841
50727
|
]);
|
|
49842
50728
|
} finally {
|
|
49843
50729
|
await Promise.race([
|
|
49844
50730
|
watcher.stop(),
|
|
49845
|
-
new Promise((
|
|
50731
|
+
new Promise((resolve7) => setTimeout(resolve7, SHUTDOWN_DRAIN_TIMEOUT_MS))
|
|
49846
50732
|
]);
|
|
49847
50733
|
clearDaemonPid(slug);
|
|
49848
50734
|
releaseDaemonLock(lockHandle);
|
|
@@ -49938,6 +50824,21 @@ function reportOutcome(spinner, slug, state, outcome) {
|
|
|
49938
50824
|
return;
|
|
49939
50825
|
}
|
|
49940
50826
|
}
|
|
50827
|
+
function fileConflictMessage(outcome) {
|
|
50828
|
+
const head = `Conflict on files/${outcome.relPath}${outcome.stashPath ? ` → ${outcome.stashPath}` : ""}`;
|
|
50829
|
+
switch (outcome.restore) {
|
|
50830
|
+
case "skipped":
|
|
50831
|
+
return `${head} (Plinth's version was not applied here; daemon.log says why)`;
|
|
50832
|
+
case "removed":
|
|
50833
|
+
return outcome.stashPath ? `${head} (deleted in Plinth, so removed here; your copy is stashed)` : `${head} (deleted in Plinth too, and gone here)`;
|
|
50834
|
+
case "no_bytes":
|
|
50835
|
+
return `${head} (Plinth holds no bytes for it; the local file was left as it is)`;
|
|
50836
|
+
case "restored":
|
|
50837
|
+
return outcome.stashPath ? `${head} (cloud-wins; local copy stashed)` : `${head} (cloud-wins; cloud copy restored)`;
|
|
50838
|
+
default:
|
|
50839
|
+
return `${head} (already in Plinth; your copy is stashed)`;
|
|
50840
|
+
}
|
|
50841
|
+
}
|
|
49941
50842
|
function reportFileOutcome(spinner, slug, state, outcome) {
|
|
49942
50843
|
const summary = watchingSummary(state);
|
|
49943
50844
|
switch (outcome.kind) {
|
|
@@ -49954,7 +50855,7 @@ function reportFileOutcome(spinner, slug, state, outcome) {
|
|
|
49954
50855
|
spinner.update(`Watching ${slug} · ${summary}`);
|
|
49955
50856
|
return;
|
|
49956
50857
|
case "conflict":
|
|
49957
|
-
spinner.warn(
|
|
50858
|
+
spinner.warn(fileConflictMessage(outcome));
|
|
49958
50859
|
spinner.update(`Watching ${slug} · ${summary}`);
|
|
49959
50860
|
return;
|
|
49960
50861
|
case "over_cap":
|
|
@@ -50094,7 +50995,7 @@ var startCommand = defineCommand({
|
|
|
50094
50995
|
});
|
|
50095
50996
|
|
|
50096
50997
|
// src/commands/status.ts
|
|
50097
|
-
import { statSync as statSync20 } from "node:fs";
|
|
50998
|
+
import { existsSync as existsSync12, statSync as statSync20 } from "node:fs";
|
|
50098
50999
|
|
|
50099
51000
|
// src/lib/context/context-walls.ts
|
|
50100
51001
|
function prepare(raw) {
|
|
@@ -50395,14 +51296,69 @@ function assessContextWalls(workspaceSlug, deps = {}) {
|
|
|
50395
51296
|
return { walls, played, asOf, unreadable };
|
|
50396
51297
|
}
|
|
50397
51298
|
|
|
51299
|
+
// src/commands/status-holds.ts
|
|
51300
|
+
function filePullHoldLines(args) {
|
|
51301
|
+
const details = [];
|
|
51302
|
+
for (const hold of args.holds) {
|
|
51303
|
+
const line = holdLine(hold, args);
|
|
51304
|
+
if (line !== null)
|
|
51305
|
+
details.push(line);
|
|
51306
|
+
}
|
|
51307
|
+
const n = details.length;
|
|
51308
|
+
if (n === 0)
|
|
51309
|
+
return { headline: "No files held.", details };
|
|
51310
|
+
return {
|
|
51311
|
+
headline: `${n} file${n === 1 ? "" : "s"} held - this folder and Plinth differ. Each line says why and what clears it.`,
|
|
51312
|
+
details
|
|
51313
|
+
};
|
|
51314
|
+
}
|
|
51315
|
+
function holdLine(hold, args) {
|
|
51316
|
+
const name = `files/${hold.path}`;
|
|
51317
|
+
if (hold.kind === "kept_after_delete") {
|
|
51318
|
+
if (!args.fileExists(hold.path) || args.isTracked(hold.path))
|
|
51319
|
+
return null;
|
|
51320
|
+
return `${name}: deleted in Plinth, but your copy here had changes that were never uploaded, so it was kept. ` + `It goes back into Plinth as a new file the next time the daemon starts; delete it here to accept the deletion.`;
|
|
51321
|
+
}
|
|
51322
|
+
if (hold.kind === "stash_failed") {
|
|
51323
|
+
return `${name}: Plinth has a newer version, but your copy here could not be kept first, so it was not replaced. ` + `${keepRemedy(hold, args.conflictsDir)} It updates on the next sync after that.`;
|
|
51324
|
+
}
|
|
51325
|
+
if (hold.kind === "integrity_failed") {
|
|
51326
|
+
return `${name}: Plinth's copy did not match its checksum when it was downloaded, so it was not written. ` + `${retryPhrase(hold, args.now)}`;
|
|
51327
|
+
}
|
|
51328
|
+
return `${name}: held by a newer version of Plinth (${hold.kind}); update Plinth on this Mac to see why.`;
|
|
51329
|
+
}
|
|
51330
|
+
function keepRemedy(hold, conflictsDir) {
|
|
51331
|
+
const code = hold.code ? ` (${hold.code})` : "";
|
|
51332
|
+
switch (hold.reason) {
|
|
51333
|
+
case "symlink":
|
|
51334
|
+
return "A folder on the way to it is a link; replace the link with a real folder.";
|
|
51335
|
+
case "not_a_file":
|
|
51336
|
+
return "Something other than a file is at that path; move it aside.";
|
|
51337
|
+
case "unreadable":
|
|
51338
|
+
return `Your copy could not be read${code}; check its permissions.`;
|
|
51339
|
+
case "stash_io":
|
|
51340
|
+
return `A recovery copy could not be written to ${conflictsDir}${code}; free some disk space or check that folder's permissions.`;
|
|
51341
|
+
default:
|
|
51342
|
+
return `Your copy could not be kept (${hold.reason ?? "no reason recorded"}${code}); update Plinth on this Mac to see why.`;
|
|
51343
|
+
}
|
|
51344
|
+
}
|
|
51345
|
+
function retryPhrase(hold, now) {
|
|
51346
|
+
const due = hold.next_retry_at ? Date.parse(hold.next_retry_at) : Number.NaN;
|
|
51347
|
+
const wait = due - now.getTime();
|
|
51348
|
+
if (!Number.isFinite(wait) || wait <= 0)
|
|
51349
|
+
return "It is tried again on the next sync.";
|
|
51350
|
+
const minutes = Math.max(1, Math.round(wait / 60000));
|
|
51351
|
+
return `It is tried again in about ${minutes} minute${minutes === 1 ? "" : "s"}, or at once if the file changes in Plinth.`;
|
|
51352
|
+
}
|
|
51353
|
+
|
|
50398
51354
|
// src/lib/build-stamp.ts
|
|
50399
51355
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
50400
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as
|
|
50401
|
-
import { dirname as dirname14, join as
|
|
51356
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as readFileSync34, writeFileSync as writeFileSync9 } from "node:fs";
|
|
51357
|
+
import { dirname as dirname14, join as join20 } from "node:path";
|
|
50402
51358
|
import { fileURLToPath } from "node:url";
|
|
50403
51359
|
var BUILD_STAMP_FILENAME = "build-stamp.json";
|
|
50404
51360
|
function stampPath(distDir) {
|
|
50405
|
-
return
|
|
51361
|
+
return join20(distDir, BUILD_STAMP_FILENAME);
|
|
50406
51362
|
}
|
|
50407
51363
|
function readHead(repoDir) {
|
|
50408
51364
|
try {
|
|
@@ -50421,7 +51377,7 @@ function readHead(repoDir) {
|
|
|
50421
51377
|
}
|
|
50422
51378
|
function readBuildStamp(distDir) {
|
|
50423
51379
|
try {
|
|
50424
|
-
const parsed = JSON.parse(
|
|
51380
|
+
const parsed = JSON.parse(readFileSync34(stampPath(distDir), "utf8"));
|
|
50425
51381
|
if (parsed && typeof parsed.sha === "string" && parsed.sha.length > 0 && typeof parsed.dirty === "boolean" && typeof parsed.built_at === "string") {
|
|
50426
51382
|
return { sha: parsed.sha, dirty: parsed.dirty, built_at: parsed.built_at };
|
|
50427
51383
|
}
|
|
@@ -50433,7 +51389,7 @@ function readBuildStamp(distDir) {
|
|
|
50433
51389
|
function findRepoRoot(fromDir) {
|
|
50434
51390
|
let dir = fromDir;
|
|
50435
51391
|
for (;; ) {
|
|
50436
|
-
if (existsSync11(
|
|
51392
|
+
if (existsSync11(join20(dir, ".git")))
|
|
50437
51393
|
return dir;
|
|
50438
51394
|
const parent = dirname14(dir);
|
|
50439
51395
|
if (parent === dir)
|
|
@@ -50482,12 +51438,12 @@ function checkBuildStaleness(deps = {}) {
|
|
|
50482
51438
|
}
|
|
50483
51439
|
|
|
50484
51440
|
// src/lib/memory-links.ts
|
|
50485
|
-
import { readFileSync as
|
|
50486
|
-
import { relative as relative15, sep as
|
|
51441
|
+
import { readFileSync as readFileSync35 } from "node:fs";
|
|
51442
|
+
import { relative as relative15, sep as sep9 } from "node:path";
|
|
50487
51443
|
var MEMORY_FILE_RE = /^(workspace-memory\.md|user-memory\.md|artists\/[^/]+\/memory\.md|memory\/[^/]+\.md|memory\/private\/[^/]+\.md)$/i;
|
|
50488
51444
|
function scanMemoryLinks(deps) {
|
|
50489
51445
|
const listFiles = deps.listFiles ?? ((r) => walkSubtreeFiles(r, isVcsInternal));
|
|
50490
|
-
const readFile = deps.readFile ?? ((p) =>
|
|
51446
|
+
const readFile = deps.readFile ?? ((p) => readFileSync35(p, "utf8"));
|
|
50491
51447
|
let root;
|
|
50492
51448
|
let absPaths;
|
|
50493
51449
|
try {
|
|
@@ -50496,7 +51452,7 @@ function scanMemoryLinks(deps) {
|
|
|
50496
51452
|
} catch {
|
|
50497
51453
|
return { scanned: 0, unreadable: [], dead: [] };
|
|
50498
51454
|
}
|
|
50499
|
-
const relPaths = absPaths.map((p) => relative15(root, p).split(
|
|
51455
|
+
const relPaths = absPaths.map((p) => relative15(root, p).split(sep9).join("/"));
|
|
50500
51456
|
const artists = buildArtistIndex(relPaths, root, (abs) => readFile(abs));
|
|
50501
51457
|
const dead = [];
|
|
50502
51458
|
const unreadable = [];
|
|
@@ -50531,9 +51487,8 @@ function scanMemoryLinks(deps) {
|
|
|
50531
51487
|
init_output();
|
|
50532
51488
|
|
|
50533
51489
|
// src/lib/pending-writes.ts
|
|
50534
|
-
import { readFileSync as
|
|
51490
|
+
import { readFileSync as readFileSync36, statSync as statSync19 } from "node:fs";
|
|
50535
51491
|
import { relative as relative16 } from "node:path";
|
|
50536
|
-
var import_gray_matter15 = __toESM(require_gray_matter(), 1);
|
|
50537
51492
|
var EMPTY3 = { writes: [], count: 0 };
|
|
50538
51493
|
function reportedElsewhere(state) {
|
|
50539
51494
|
const entities = new Set;
|
|
@@ -50579,7 +51534,7 @@ function scanPendingWrites(args) {
|
|
|
50579
51534
|
continue;
|
|
50580
51535
|
let hash2;
|
|
50581
51536
|
try {
|
|
50582
|
-
hash2 = sha256Bytes(
|
|
51537
|
+
hash2 = sha256Bytes(readFileSync36(absolutePath));
|
|
50583
51538
|
} catch {
|
|
50584
51539
|
continue;
|
|
50585
51540
|
}
|
|
@@ -50610,17 +51565,15 @@ function scanPendingWrites(args) {
|
|
|
50610
51565
|
continue;
|
|
50611
51566
|
let content;
|
|
50612
51567
|
try {
|
|
50613
|
-
content =
|
|
51568
|
+
content = readFileSync36(absolutePath, "utf8");
|
|
50614
51569
|
} catch {
|
|
50615
51570
|
continue;
|
|
50616
51571
|
}
|
|
50617
51572
|
if (!entry2) {
|
|
50618
|
-
|
|
50619
|
-
|
|
50620
|
-
frontmatter = import_gray_matter15.default(content).data ?? {};
|
|
50621
|
-
} catch {
|
|
51573
|
+
const read = readFrontmatter(content);
|
|
51574
|
+
if (holdsUnreadableUntracked({ entityType, entitySlug, readable: read.ok }))
|
|
50622
51575
|
continue;
|
|
50623
|
-
}
|
|
51576
|
+
const frontmatter = read.ok ? read.parsed.data : {};
|
|
50624
51577
|
const verdict = classifyUntrackedEntity({
|
|
50625
51578
|
entityType,
|
|
50626
51579
|
entitySlug,
|
|
@@ -51055,6 +52008,23 @@ async function runStatus(args, deps = {}) {
|
|
|
51055
52008
|
}
|
|
51056
52009
|
out.info("");
|
|
51057
52010
|
}
|
|
52011
|
+
const pullHolds = filePullHoldLines({
|
|
52012
|
+
holds: listFilePullHolds(localState),
|
|
52013
|
+
conflictsDir: conflictsRoot(slug),
|
|
52014
|
+
now,
|
|
52015
|
+
fileExists: (rel) => {
|
|
52016
|
+
try {
|
|
52017
|
+
return existsSync12(filesSubtreePath(slug, rel));
|
|
52018
|
+
} catch {
|
|
52019
|
+
return true;
|
|
52020
|
+
}
|
|
52021
|
+
},
|
|
52022
|
+
isTracked: (rel) => getFileEntry(localState, rel) !== undefined
|
|
52023
|
+
});
|
|
52024
|
+
out.info(row("Held", pullHolds.headline));
|
|
52025
|
+
for (const line of pullHolds.details)
|
|
52026
|
+
out.info(cont(line));
|
|
52027
|
+
out.info("");
|
|
51058
52028
|
const rejected = listRejected(localState);
|
|
51059
52029
|
const freshness = Object.values(localState.memory_sources?.unwritten ?? {}).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
51060
52030
|
const localRejectedPaths = new Set(rejected.filter((r) => r.target === "file").map((r) => r.path));
|
|
@@ -51076,7 +52046,7 @@ async function runStatus(args, deps = {}) {
|
|
|
51076
52046
|
}
|
|
51077
52047
|
}
|
|
51078
52048
|
if (freshness.length > 0) {
|
|
51079
|
-
out.info(row("Memory context", `${freshness.length} file${freshness.length === 1 ? "" : "s"} could not be marked with ${freshness.length === 1 ? "its" : "their"} current memory state
|
|
52049
|
+
out.info(row("Memory context", `${freshness.length} file${freshness.length === 1 ? "" : "s"} could not be marked with ${freshness.length === 1 ? "its" : "their"} current memory state.`));
|
|
51080
52050
|
for (const r of freshness.slice(0, MAX_SHOWN)) {
|
|
51081
52051
|
out.info(cont(r.message));
|
|
51082
52052
|
}
|
|
@@ -51521,7 +52491,7 @@ var versionCommand = defineCommand({
|
|
|
51521
52491
|
});
|
|
51522
52492
|
|
|
51523
52493
|
// src/commands/voice-gate.ts
|
|
51524
|
-
import { readFileSync as
|
|
52494
|
+
import { readFileSync as readFileSync39 } from "node:fs";
|
|
51525
52495
|
|
|
51526
52496
|
// src/lib/voice-gate/api.ts
|
|
51527
52497
|
var VoiceGateSurfaces = [
|
|
@@ -51623,15 +52593,15 @@ async function callVoiceGate(req) {
|
|
|
51623
52593
|
}
|
|
51624
52594
|
|
|
51625
52595
|
// src/lib/voice-gate/attempts.ts
|
|
51626
|
-
import { createHash as
|
|
52596
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
51627
52597
|
import {
|
|
51628
52598
|
mkdirSync as mkdirSync13,
|
|
51629
|
-
readFileSync as
|
|
51630
|
-
renameSync as
|
|
51631
|
-
unlinkSync as
|
|
51632
|
-
writeFileSync as
|
|
52599
|
+
readFileSync as readFileSync37,
|
|
52600
|
+
renameSync as renameSync10,
|
|
52601
|
+
unlinkSync as unlinkSync12,
|
|
52602
|
+
writeFileSync as writeFileSync10
|
|
51633
52603
|
} from "node:fs";
|
|
51634
|
-
import { join as
|
|
52604
|
+
import { join as join21 } from "node:path";
|
|
51635
52605
|
var MAX_REGENS = 2;
|
|
51636
52606
|
var ENTRY_TTL_MS = 24 * 60 * 60 * 1000;
|
|
51637
52607
|
var AttemptEntrySchema = exports_external.object({
|
|
@@ -51644,20 +52614,20 @@ var AttemptsFileSchema = exports_external.object({
|
|
|
51644
52614
|
entries: exports_external.record(exports_external.string(), AttemptEntrySchema)
|
|
51645
52615
|
});
|
|
51646
52616
|
function attemptsDir(workspaceSlug) {
|
|
51647
|
-
return
|
|
52617
|
+
return join21(plinthHome(), "_voice_gate", workspaceSlug);
|
|
51648
52618
|
}
|
|
51649
52619
|
function attemptsPath(workspaceSlug) {
|
|
51650
|
-
return
|
|
52620
|
+
return join21(attemptsDir(workspaceSlug), "attempts.json");
|
|
51651
52621
|
}
|
|
51652
52622
|
function attemptKey(identity) {
|
|
51653
|
-
return
|
|
52623
|
+
return createHash8("sha256").update(identity, "utf8").digest("hex").slice(0, 32);
|
|
51654
52624
|
}
|
|
51655
52625
|
function emptyFile() {
|
|
51656
52626
|
return { version: 1, entries: {} };
|
|
51657
52627
|
}
|
|
51658
52628
|
function load2(workspaceSlug) {
|
|
51659
52629
|
try {
|
|
51660
|
-
const raw =
|
|
52630
|
+
const raw = readFileSync37(attemptsPath(workspaceSlug), "utf8");
|
|
51661
52631
|
const parsed = AttemptsFileSchema.safeParse(JSON.parse(raw));
|
|
51662
52632
|
return parsed.success ? parsed.data : emptyFile();
|
|
51663
52633
|
} catch {
|
|
@@ -51678,13 +52648,13 @@ function save(workspaceSlug, file2) {
|
|
|
51678
52648
|
const target = attemptsPath(workspaceSlug);
|
|
51679
52649
|
const tmp = `${target}.${process.pid}.${tmpCounter2++}.tmp`;
|
|
51680
52650
|
mkdirSync13(dir, { recursive: true, mode: 448 });
|
|
51681
|
-
|
|
52651
|
+
writeFileSync10(tmp, `${JSON.stringify(file2, null, 2)}
|
|
51682
52652
|
`, { mode: 384 });
|
|
51683
52653
|
try {
|
|
51684
|
-
|
|
52654
|
+
renameSync10(tmp, target);
|
|
51685
52655
|
} catch (err) {
|
|
51686
52656
|
try {
|
|
51687
|
-
|
|
52657
|
+
unlinkSync12(tmp);
|
|
51688
52658
|
} catch {}
|
|
51689
52659
|
throw err;
|
|
51690
52660
|
}
|
|
@@ -51820,9 +52790,9 @@ function denyUngated(detail) {
|
|
|
51820
52790
|
}
|
|
51821
52791
|
|
|
51822
52792
|
// src/lib/voice-gate/payload.ts
|
|
51823
|
-
import { readFileSync as
|
|
52793
|
+
import { readFileSync as readFileSync38, statSync as statSync21 } from "node:fs";
|
|
51824
52794
|
import { homedir as homedir2 } from "node:os";
|
|
51825
|
-
import { isAbsolute as isAbsolute4, resolve as
|
|
52795
|
+
import { isAbsolute as isAbsolute4, resolve as resolve7 } from "node:path";
|
|
51826
52796
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
51827
52797
|
var HELPER_BASENAME = "push_gmail_draft.py";
|
|
51828
52798
|
var DRAFT_FLAGS = ["--to", "--body-file", "--body-stdin"];
|
|
@@ -51981,7 +52951,7 @@ function defaultReadBodyFile(path5) {
|
|
|
51981
52951
|
if (stat4.size > MAX_BODY_BYTES) {
|
|
51982
52952
|
throw new Error(`draft body is ${stat4.size} bytes, over the ${MAX_BODY_BYTES}-byte gate limit`);
|
|
51983
52953
|
}
|
|
51984
|
-
return
|
|
52954
|
+
return readFileSync38(path5, "utf8");
|
|
51985
52955
|
}
|
|
51986
52956
|
function parseRecipients(...raw) {
|
|
51987
52957
|
const all = raw.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0);
|
|
@@ -52016,13 +52986,13 @@ function applyCd(tokens, state) {
|
|
|
52016
52986
|
return { dir: expanded, known: true };
|
|
52017
52987
|
if (!state.known)
|
|
52018
52988
|
return state;
|
|
52019
|
-
return { dir:
|
|
52989
|
+
return { dir: resolve7(state.dir, expanded), known: true };
|
|
52020
52990
|
}
|
|
52021
52991
|
function expandHome(path5) {
|
|
52022
52992
|
if (path5 === "~")
|
|
52023
52993
|
return homedir2();
|
|
52024
52994
|
if (path5.startsWith("~/"))
|
|
52025
|
-
return
|
|
52995
|
+
return resolve7(homedir2(), path5.slice(2));
|
|
52026
52996
|
return path5;
|
|
52027
52997
|
}
|
|
52028
52998
|
function classifyHelperSegment(tokens, dirState, readBodyFile) {
|
|
@@ -52048,7 +53018,7 @@ function classifyHelperSegment(tokens, dirState, readBodyFile) {
|
|
|
52048
53018
|
unreadable: `the draft push uses a relative \`--body-file\` (${bodyFile}) after a \`cd\` ` + `the voice gate cannot resolve, so it cannot tell which file ships. Pass an ` + `absolute \`--body-file\` path.`
|
|
52049
53019
|
};
|
|
52050
53020
|
}
|
|
52051
|
-
const resolved = isAbsolute4(expanded) ? expanded :
|
|
53021
|
+
const resolved = isAbsolute4(expanded) ? expanded : resolve7(dirState.dir, expanded);
|
|
52052
53022
|
let body;
|
|
52053
53023
|
try {
|
|
52054
53024
|
body = readBodyFile(resolved);
|
|
@@ -52341,7 +53311,7 @@ async function runVoiceGateDirect(args, deps = {}) {
|
|
|
52341
53311
|
let text;
|
|
52342
53312
|
if (args.file) {
|
|
52343
53313
|
try {
|
|
52344
|
-
text =
|
|
53314
|
+
text = readFileSync39(args.file, "utf8");
|
|
52345
53315
|
} catch (err) {
|
|
52346
53316
|
const msg = err instanceof Error ? err.message : "unknown error";
|
|
52347
53317
|
out.error(`Could not read ${args.file}: ${msg}`);
|