@plinth-music/cli 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/README.md +2 -2
- package/dist/build-stamp.json +2 -2
- package/dist/cli.js +547 -134
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,85 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes to `@plinth-music/cli`. Grouped by what a user notices, not by PR.
|
|
4
4
|
|
|
5
|
+
## 0.9.1 — 2026-08-25
|
|
6
|
+
|
|
7
|
+
Seven PRs since `0.9.0` (`v0.9.0..1efa7d7`, #98–#104). Every one of them is the
|
|
8
|
+
same bug seen from a different side: the daemon treated a failed compare-and-swap
|
|
9
|
+
as proof that the cloud content had changed, and then let the cloud win. It had
|
|
10
|
+
not changed. Fixes only, all client-side; there is no server prerequisite and a
|
|
11
|
+
0.9.0 client talks to the same API. Upgrading is strongly recommended — the
|
|
12
|
+
defects below lose user work silently, and `plinth status` said "All local writes
|
|
13
|
+
synced" the whole time.
|
|
14
|
+
|
|
15
|
+
### ⚠️ A cloud write that touched only a timestamp no longer costs you your edit
|
|
16
|
+
|
|
17
|
+
This is the one to read. The CAS token is `updated_at`, so **any** write that moves
|
|
18
|
+
that column fails the precondition — including a server-side backfill that changes
|
|
19
|
+
no bytes at all. On 24 Aug a backfill moved `updated_at` on 161 of 307 live rows to
|
|
20
|
+
the same microsecond; every later local edit to one of those rows was stashed out
|
|
21
|
+
of sight into `~/.plinth/_conflicts/` and overwritten with a cloud copy that was
|
|
22
|
+
byte-identical to what the daemon had already acknowledged.
|
|
23
|
+
|
|
24
|
+
A 409 is now a question rather than a verdict: the daemon asks the row what its
|
|
25
|
+
content hash is, compares it with the hash it last acked, and re-pushes when they
|
|
26
|
+
match. Nothing is stashed and nothing is overwritten, because there was no conflict
|
|
27
|
+
to reconcile. A genuine cloud edit still conflicts, still stashes, and still wins —
|
|
28
|
+
that half is unchanged and is pinned by its own tests.
|
|
29
|
+
|
|
30
|
+
Fixed on every path, not just the one that bit: `files/` rows, entity rows, tasks,
|
|
31
|
+
and the three singleton lanes (`user-memory.md`, `workspace-rules.md`,
|
|
32
|
+
`workspace-memory.md`), which shared an engine that had been missed.
|
|
33
|
+
|
|
34
|
+
### ⚠️ A local status, priority or due-date edit is no longer silently discarded
|
|
35
|
+
|
|
36
|
+
Found while fixing the above. On an entity conflict the retry dropped the
|
|
37
|
+
structured fields the first attempt had carried, as "cloud's win" — so a metadata
|
|
38
|
+
bump on a row you had just re-prioritised threw the re-prioritisation away with no
|
|
39
|
+
stash and no record. A bump is not a conflict, so it no longer gets a conflict's
|
|
40
|
+
resolution.
|
|
41
|
+
|
|
42
|
+
### ⚠️ `plinth login` no longer wipes your sync cursors, and `--reset-sync` is new
|
|
43
|
+
|
|
44
|
+
Logging in again to the same workspace is a token rotation, not a re-registration,
|
|
45
|
+
and it now keeps your cursors — the next sync resumes instead of re-pulling the
|
|
46
|
+
world. Cursors are still cleared when the token belongs to a different workspace or
|
|
47
|
+
user, and login now says which happened rather than leaving you to guess.
|
|
48
|
+
|
|
49
|
+
That removed the only way to force a full re-pull, so `--reset-sync` restores it as
|
|
50
|
+
something you ask for. Use it after rebuilding a mirror from scratch; without it, a
|
|
51
|
+
mirror rebuilt from an empty ledger would never refill, silently, behind a green
|
|
52
|
+
sync.
|
|
53
|
+
|
|
54
|
+
### Two files saved at the same time no longer lose one file's bookkeeping
|
|
55
|
+
|
|
56
|
+
Every write of `state.json` used to land on a snapshot taken before the network
|
|
57
|
+
call that preceded it, so two saves a moment apart — or a scheduled pull arriving
|
|
58
|
+
while a push was in flight — could leave the file missing one of the two
|
|
59
|
+
acknowledgements. The losing write's record simply vanished, and the next edit to
|
|
60
|
+
that file hit the defect above through a door the fix could not close.
|
|
61
|
+
|
|
62
|
+
State is now merged at write time rather than overwritten, an acknowledgement can
|
|
63
|
+
never be committed over a newer one, and a cursor is never advanced ahead of the
|
|
64
|
+
rows it describes — so an interrupted pull leaves cursors, state and disk agreeing
|
|
65
|
+
rather than stranding rows that would never be delivered again. HTTP concurrency is
|
|
66
|
+
untouched; the cost is about 2 ms per state write.
|
|
67
|
+
|
|
68
|
+
### `plinth status` shows conflict stashes
|
|
69
|
+
|
|
70
|
+
The Conflicts block lists how many local copies are set aside in
|
|
71
|
+
`~/.plinth/_conflicts/<workspace>/`, names the newest, and says what to do with
|
|
72
|
+
them. They had been accumulating unseen since May.
|
|
73
|
+
|
|
74
|
+
### Proven against production
|
|
75
|
+
|
|
76
|
+
The changes above were reproduced end to end against a live workspace before this
|
|
77
|
+
release rather than only in tests: a metadata-only bump followed by a local edit
|
|
78
|
+
survives on both the file and entity paths, two overlapping pushes both keep their
|
|
79
|
+
acknowledgement, and — the part that makes the rest mean anything — a genuine cloud
|
|
80
|
+
content change still conflicts and still stashes under otherwise identical
|
|
81
|
+
conditions. The pull-ordering half and the three singleton lanes remain
|
|
82
|
+
test-proven only; both are named in the repo's `CLAUDE.md`.
|
|
83
|
+
|
|
5
84
|
## 0.9.0 — 2026-08-24
|
|
6
85
|
|
|
7
86
|
Two PRs since `0.8.0` (`v0.8.0..4842d40`). An agent is now told to read a draft
|
package/README.md
CHANGED
|
@@ -22,10 +22,10 @@ plinth start # run the daemon: continuous pull + watch + pu
|
|
|
22
22
|
|
|
23
23
|
## Commands
|
|
24
24
|
|
|
25
|
-
- `plinth login --workspace <slug
|
|
25
|
+
- `plinth login --workspace <slug> [--reset-sync]` — issue a personal access token in the browser, paste it back, store it in the OS keychain (macOS Keychain / Linux Secret Service / Windows Credential Manager). Logging in again to the same workspace is a token rotation and **keeps your sync cursors**, so the next sync picks up where the last one left off; it clears them only when the token belongs to a different workspace or user. `--reset-sync` discards them deliberately, making the next sync a full re-pull — use it after rebuilding a mirror from scratch, which is otherwise unreachable now that rotation preserves them.
|
|
26
26
|
- `plinth sync [--workspace <slug>]` — one-shot pull into `~/Plinth/<workspace>/`. Also generates the workspace-root `CLAUDE.md` (a thin TOC + entity model + skill pointers), an empty `CLAUDE.local.md` stub on first sync, and the two session-boundary commands (below), and silently refreshes `CLAUDE.md` on a schema-version change. Defaults to the active workspace if `--workspace` is omitted.
|
|
27
27
|
- `plinth start` — run the long-running sync daemon: pull the latest, then watch the mirror for local edits and push them. It also projects each mapped artist's Google calendar into `views/calendar/` in the mirror — a file per artist plus an `upcoming.md` — on its own 15-minute timer (`PLINTH_CALENDAR_INTERVAL_MS`), with one run at boot. That is deliberately not the 15-second pull cadence: one tick makes a Google call per mapped calendar, which would breach the server route's rate limit inside a minute. A refresh that fails keeps the last known bodies and rewrites only the header, so a stale view says it is stale rather than reading as an empty diary. Logs to `~/.plinth/daemon.log`.
|
|
28
|
-
- `plinth status` — local sync state: daemon liveness, last sync and per-type counts,
|
|
28
|
+
- `plinth status` — local sync state: daemon liveness, last sync and per-type counts, any destructive batches being held, writes awaiting retry or permanently rejected, and any conflict stashes in `~/.plinth/_conflicts/<workspace>/` — local copies that were set aside when the cloud version won. Warns when a source install's `dist/` is behind its checkout.
|
|
29
29
|
- `plinth confirm` — review and release destructive batches the daemon has quarantined (apply, or `--discard`).
|
|
30
30
|
- `plinth refresh-context [--workspace <slug>]` — force-regenerate the workspace-root `CLAUDE.md` **and the two generated commands** from the current Plinth schema + workspace. The user-customisable block between the `<!-- BEGIN: user-customisable -->` / `<!-- END -->` markers is always preserved verbatim in every generated file; `CLAUDE.local.md` is never touched.
|
|
31
31
|
- `plinth grounding` — print the session-start grounding block (current date, entity resolution, write confirmation, workspace rules, workspace memory, user memory). Invoked by the generated Claude Code SessionStart hook.
|
package/dist/build-stamp.json
CHANGED
package/dist/cli.js
CHANGED
|
@@ -9193,7 +9193,7 @@ async function runMain(cmd, opts = {}) {
|
|
|
9193
9193
|
// package.json
|
|
9194
9194
|
var package_default = {
|
|
9195
9195
|
name: "@plinth-music/cli",
|
|
9196
|
-
version: "0.9.
|
|
9196
|
+
version: "0.9.1",
|
|
9197
9197
|
description: "Plinth capstone sync daemon and CLI",
|
|
9198
9198
|
type: "module",
|
|
9199
9199
|
private: false,
|
|
@@ -26007,12 +26007,28 @@ function writeAtomic(config2) {
|
|
|
26007
26007
|
throw err;
|
|
26008
26008
|
}
|
|
26009
26009
|
}
|
|
26010
|
-
function
|
|
26010
|
+
function sameWorkspaceIdentity(a2, b2) {
|
|
26011
|
+
return a2.workspace_id === b2.workspace_id && a2.user_id === b2.user_id && a2.api_base_url === b2.api_base_url;
|
|
26012
|
+
}
|
|
26013
|
+
function carryForwardSyncState(prior, entry) {
|
|
26014
|
+
if (!prior || !sameWorkspaceIdentity(prior, entry))
|
|
26015
|
+
return entry;
|
|
26016
|
+
return {
|
|
26017
|
+
...entry,
|
|
26018
|
+
last_synced_at: prior.last_synced_at,
|
|
26019
|
+
...prior.sync_cursors !== undefined ? { sync_cursors: prior.sync_cursors } : {}
|
|
26020
|
+
};
|
|
26021
|
+
}
|
|
26022
|
+
function upsertWorkspace(slug, entry, options2 = {}) {
|
|
26011
26023
|
const existing = loadConfig() ?? emptyConfig(slug);
|
|
26024
|
+
const prior = options2.resetSyncState === true ? undefined : existing.workspaces[slug];
|
|
26012
26025
|
const next = {
|
|
26013
26026
|
...existing,
|
|
26014
26027
|
active_workspace: slug,
|
|
26015
|
-
workspaces: {
|
|
26028
|
+
workspaces: {
|
|
26029
|
+
...existing.workspaces,
|
|
26030
|
+
[slug]: carryForwardSyncState(prior, entry)
|
|
26031
|
+
}
|
|
26016
26032
|
};
|
|
26017
26033
|
writeAtomic(next);
|
|
26018
26034
|
return next;
|
|
@@ -27102,6 +27118,140 @@ function writeState(state) {
|
|
|
27102
27118
|
throw err;
|
|
27103
27119
|
}
|
|
27104
27120
|
}
|
|
27121
|
+
function isLaterVersion(a2, b2) {
|
|
27122
|
+
if (!a2 || !b2)
|
|
27123
|
+
return false;
|
|
27124
|
+
const ta = Date.parse(a2.updated_at);
|
|
27125
|
+
const tb = Date.parse(b2.updated_at);
|
|
27126
|
+
if (Number.isNaN(ta) || Number.isNaN(tb))
|
|
27127
|
+
return false;
|
|
27128
|
+
return ta > tb;
|
|
27129
|
+
}
|
|
27130
|
+
function mergeKey(fresh, before, after) {
|
|
27131
|
+
if (before === after)
|
|
27132
|
+
return { value: fresh, overruled: false };
|
|
27133
|
+
if (after === undefined) {
|
|
27134
|
+
if (isLaterVersion(fresh, before))
|
|
27135
|
+
return { value: fresh, overruled: true };
|
|
27136
|
+
return { value: undefined, overruled: false };
|
|
27137
|
+
}
|
|
27138
|
+
if (isLaterVersion(fresh, after))
|
|
27139
|
+
return { value: fresh, overruled: true };
|
|
27140
|
+
return { value: after, overruled: false };
|
|
27141
|
+
}
|
|
27142
|
+
function mergeKeyed(fresh, before, after) {
|
|
27143
|
+
const merged = { ...fresh };
|
|
27144
|
+
const overruled = [];
|
|
27145
|
+
const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
27146
|
+
for (const key of keys) {
|
|
27147
|
+
const decision = mergeKey(fresh[key], before[key], after[key]);
|
|
27148
|
+
if (decision.overruled)
|
|
27149
|
+
overruled.push(key);
|
|
27150
|
+
if (decision.value === undefined)
|
|
27151
|
+
delete merged[key];
|
|
27152
|
+
else
|
|
27153
|
+
merged[key] = decision.value;
|
|
27154
|
+
}
|
|
27155
|
+
return { merged, overruled };
|
|
27156
|
+
}
|
|
27157
|
+
function recordKey(r3) {
|
|
27158
|
+
return `${r3.target} ${r3.path}`;
|
|
27159
|
+
}
|
|
27160
|
+
function mergeRecords(fresh, before, after) {
|
|
27161
|
+
const index = (list) => new Map(list.map((r3) => [recordKey(r3), r3]));
|
|
27162
|
+
const beforeIdx = index(before);
|
|
27163
|
+
const afterIdx = index(after);
|
|
27164
|
+
const merged = index(fresh);
|
|
27165
|
+
for (const key of new Set([...beforeIdx.keys(), ...afterIdx.keys()])) {
|
|
27166
|
+
const b2 = beforeIdx.get(key);
|
|
27167
|
+
const a2 = afterIdx.get(key);
|
|
27168
|
+
if (b2 === a2)
|
|
27169
|
+
continue;
|
|
27170
|
+
if (a2 === undefined)
|
|
27171
|
+
merged.delete(key);
|
|
27172
|
+
else
|
|
27173
|
+
merged.set(key, a2);
|
|
27174
|
+
}
|
|
27175
|
+
return Array.from(merged.values());
|
|
27176
|
+
}
|
|
27177
|
+
function mergedList(fresh, before, after) {
|
|
27178
|
+
const merged = mergeRecords(fresh ?? [], before ?? [], after ?? []);
|
|
27179
|
+
if (merged.length > 0)
|
|
27180
|
+
return merged;
|
|
27181
|
+
return fresh === undefined && after === undefined ? undefined : merged;
|
|
27182
|
+
}
|
|
27183
|
+
function mergeScalar(fresh, before, after) {
|
|
27184
|
+
return before === after ? fresh : after;
|
|
27185
|
+
}
|
|
27186
|
+
function mergeVersioned(fresh, before, after) {
|
|
27187
|
+
if (before === after)
|
|
27188
|
+
return { value: fresh, overruled: false };
|
|
27189
|
+
if (fresh?.updated_at && after?.updated_at) {
|
|
27190
|
+
const tf = Date.parse(fresh.updated_at);
|
|
27191
|
+
const ta = Date.parse(after.updated_at);
|
|
27192
|
+
if (!Number.isNaN(tf) && !Number.isNaN(ta) && tf > ta) {
|
|
27193
|
+
return { value: fresh, overruled: true };
|
|
27194
|
+
}
|
|
27195
|
+
}
|
|
27196
|
+
return { value: after, overruled: false };
|
|
27197
|
+
}
|
|
27198
|
+
function commitState(before, after) {
|
|
27199
|
+
const fresh = loadState(after.workspace_slug);
|
|
27200
|
+
const entityTypes = new Set([
|
|
27201
|
+
...Object.keys(fresh.entries),
|
|
27202
|
+
...Object.keys(before.entries),
|
|
27203
|
+
...Object.keys(after.entries)
|
|
27204
|
+
]);
|
|
27205
|
+
const entries = {};
|
|
27206
|
+
const overruledEntries = [];
|
|
27207
|
+
for (const entityType of entityTypes) {
|
|
27208
|
+
const { merged: merged2, overruled } = mergeKeyed(fresh.entries[entityType] ?? {}, before.entries[entityType] ?? {}, after.entries[entityType] ?? {});
|
|
27209
|
+
if (Object.keys(merged2).length > 0 || fresh.entries[entityType] !== undefined) {
|
|
27210
|
+
entries[entityType] = merged2;
|
|
27211
|
+
}
|
|
27212
|
+
for (const id of overruled)
|
|
27213
|
+
overruledEntries.push({ entityType, id });
|
|
27214
|
+
}
|
|
27215
|
+
const files = mergeKeyed(fresh.files ?? {}, before.files ?? {}, after.files ?? {});
|
|
27216
|
+
const mergedFiles = Object.keys(files.merged).length > 0 || fresh.files !== undefined || after.files !== undefined ? files.merged : undefined;
|
|
27217
|
+
const rules = mergeVersioned(fresh.workspace_rules, before.workspace_rules, after.workspace_rules);
|
|
27218
|
+
const memory = mergeVersioned(fresh.workspace_memory, before.workspace_memory, after.workspace_memory);
|
|
27219
|
+
const userMemory = mergeVersioned(fresh.user_memory, before.user_memory, after.user_memory);
|
|
27220
|
+
const merged = {
|
|
27221
|
+
...fresh,
|
|
27222
|
+
version: 2,
|
|
27223
|
+
workspace_slug: after.workspace_slug,
|
|
27224
|
+
entries,
|
|
27225
|
+
files: mergedFiles,
|
|
27226
|
+
unsynced: mergedList(fresh.unsynced, before.unsynced, after.unsynced),
|
|
27227
|
+
rejected: mergedList(fresh.rejected, before.rejected, after.rejected),
|
|
27228
|
+
workspace_rules: rules.value,
|
|
27229
|
+
workspace_memory: memory.value,
|
|
27230
|
+
user_memory: userMemory.value,
|
|
27231
|
+
derived_text_version: mergeScalar(fresh.derived_text_version, before.derived_text_version, after.derived_text_version)
|
|
27232
|
+
};
|
|
27233
|
+
writeState(merged);
|
|
27234
|
+
const singletonOverruled = rules.overruled || memory.overruled || userMemory.overruled;
|
|
27235
|
+
if (overruledEntries.length === 0 && files.overruled.length === 0 && !singletonOverruled) {
|
|
27236
|
+
return after;
|
|
27237
|
+
}
|
|
27238
|
+
let corrected = after;
|
|
27239
|
+
if (rules.overruled)
|
|
27240
|
+
corrected = { ...corrected, workspace_rules: rules.value };
|
|
27241
|
+
if (memory.overruled)
|
|
27242
|
+
corrected = { ...corrected, workspace_memory: memory.value };
|
|
27243
|
+
if (userMemory.overruled)
|
|
27244
|
+
corrected = { ...corrected, user_memory: userMemory.value };
|
|
27245
|
+
for (const { entityType, id } of overruledEntries) {
|
|
27246
|
+
const committed = merged.entries[entityType]?.[id];
|
|
27247
|
+
corrected = committed ? upsertEntry(corrected, entityType, committed).state : removeEntry(corrected, entityType, id).state;
|
|
27248
|
+
}
|
|
27249
|
+
for (const path3 of files.overruled) {
|
|
27250
|
+
const committed = merged.files?.[path3];
|
|
27251
|
+
corrected = committed ? upsertFileEntry(corrected, committed).state : removeFileEntry(corrected, path3).state;
|
|
27252
|
+
}
|
|
27253
|
+
return corrected;
|
|
27254
|
+
}
|
|
27105
27255
|
function upsertEntry(state, entityType, entry) {
|
|
27106
27256
|
const bucket = state.entries[entityType] ?? {};
|
|
27107
27257
|
const existing = bucket[entry.id];
|
|
@@ -27798,12 +27948,22 @@ var macKeychain = {
|
|
|
27798
27948
|
|
|
27799
27949
|
// src/commands/login.ts
|
|
27800
27950
|
var DEFAULT_API_BASE_URL = "https://app.plinthmusic.com";
|
|
27951
|
+
function identityDifference(prior, entry2) {
|
|
27952
|
+
if (prior.workspace_id !== entry2.workspace_id)
|
|
27953
|
+
return "a different workspace";
|
|
27954
|
+
if (prior.user_id !== entry2.user_id)
|
|
27955
|
+
return "a different user";
|
|
27956
|
+
return "a different server";
|
|
27957
|
+
}
|
|
27958
|
+
function storedCursorCount(entry2) {
|
|
27959
|
+
return Object.keys(entry2.sync_cursors ?? {}).length;
|
|
27960
|
+
}
|
|
27801
27961
|
function defaultOpenBrowser(url2) {
|
|
27802
27962
|
const child = spawn("open", [url2], { stdio: "ignore", detached: true });
|
|
27803
27963
|
child.on("error", () => {});
|
|
27804
27964
|
child.unref();
|
|
27805
27965
|
}
|
|
27806
|
-
async function runLogin(workspaceSlug, apiBaseUrl, deps = {}) {
|
|
27966
|
+
async function runLogin(workspaceSlug, apiBaseUrl, deps = {}, options2 = {}) {
|
|
27807
27967
|
const out = deps.stdout ?? process.stdout;
|
|
27808
27968
|
const err = deps.stderr ?? process.stderr;
|
|
27809
27969
|
const keychain = deps.keychain ?? macKeychain;
|
|
@@ -27852,13 +28012,22 @@ async function runLogin(workspaceSlug, apiBaseUrl, deps = {}) {
|
|
|
27852
28012
|
`);
|
|
27853
28013
|
return 1;
|
|
27854
28014
|
}
|
|
28015
|
+
const entry2 = {
|
|
28016
|
+
workspace_id: principal.workspace_id,
|
|
28017
|
+
user_id: principal.user_id,
|
|
28018
|
+
api_base_url: base2,
|
|
28019
|
+
last_synced_at: null,
|
|
28020
|
+
workspace_name: principal.workspace_name
|
|
28021
|
+
};
|
|
28022
|
+
let prior;
|
|
27855
28023
|
try {
|
|
27856
|
-
|
|
27857
|
-
|
|
27858
|
-
|
|
27859
|
-
|
|
27860
|
-
|
|
27861
|
-
|
|
28024
|
+
prior = loadConfig()?.workspaces[workspaceSlug];
|
|
28025
|
+
} catch {
|
|
28026
|
+
prior = undefined;
|
|
28027
|
+
}
|
|
28028
|
+
try {
|
|
28029
|
+
upsertWorkspace(workspaceSlug, entry2, {
|
|
28030
|
+
resetSyncState: options2.resetSync === true
|
|
27862
28031
|
});
|
|
27863
28032
|
} catch (e2) {
|
|
27864
28033
|
keychain.delete(workspaceSlug, principal.user_id);
|
|
@@ -27867,7 +28036,15 @@ async function runLogin(workspaceSlug, apiBaseUrl, deps = {}) {
|
|
|
27867
28036
|
`);
|
|
27868
28037
|
return 1;
|
|
27869
28038
|
}
|
|
27870
|
-
|
|
28039
|
+
let note = "Run `plinth sync` to begin syncing.";
|
|
28040
|
+
if (prior !== undefined && storedCursorCount(prior) > 0) {
|
|
28041
|
+
if (options2.resetSync === true) {
|
|
28042
|
+
note = "Sync cursors cleared as asked - the next `plinth sync` is a full re-pull.";
|
|
28043
|
+
} else {
|
|
28044
|
+
note = sameWorkspaceIdentity(prior, entry2) ? "Sync cursors kept - the next sync picks up where the last one left off." : `The stored entry was for ${identityDifference(prior, entry2)}, so sync ` + "cursors were cleared - the next `plinth sync` is a full re-pull.";
|
|
28045
|
+
}
|
|
28046
|
+
}
|
|
28047
|
+
out.write(`Logged in to ${workspaceSlug} as ${principal.profile.email} ` + `(token ending …${patLastFour(pat)}). ${note}
|
|
27871
28048
|
`);
|
|
27872
28049
|
return 0;
|
|
27873
28050
|
}
|
|
@@ -27881,11 +28058,17 @@ var loginCommand = defineCommand({
|
|
|
27881
28058
|
type: "string",
|
|
27882
28059
|
description: "Workspace slug to log in to",
|
|
27883
28060
|
required: true
|
|
28061
|
+
},
|
|
28062
|
+
"reset-sync": {
|
|
28063
|
+
type: "boolean",
|
|
28064
|
+
description: "Discard stored sync cursors so the next sync re-pulls everything. " + "Use after rebuilding a mirror from scratch."
|
|
27884
28065
|
}
|
|
27885
28066
|
},
|
|
27886
28067
|
async run({ args }) {
|
|
27887
28068
|
const baseUrl = process.env.PLINTH_API_URL ?? DEFAULT_API_BASE_URL;
|
|
27888
|
-
const code = await runLogin(args.workspace, baseUrl
|
|
28069
|
+
const code = await runLogin(args.workspace, baseUrl, {}, {
|
|
28070
|
+
resetSync: args["reset-sync"] === true
|
|
28071
|
+
});
|
|
27889
28072
|
if (code !== 0)
|
|
27890
28073
|
process.exit(code);
|
|
27891
28074
|
}
|
|
@@ -30362,7 +30545,7 @@ var sessionCommand = defineCommand({
|
|
|
30362
30545
|
|
|
30363
30546
|
// src/commands/start.ts
|
|
30364
30547
|
var import_gray_matter10 = __toESM(require_gray_matter(), 1);
|
|
30365
|
-
import { readFileSync as readFileSync20, statSync as
|
|
30548
|
+
import { readFileSync as readFileSync20, statSync as statSync13 } from "node:fs";
|
|
30366
30549
|
|
|
30367
30550
|
// src/lib/daemon-log.ts
|
|
30368
30551
|
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync8, renameSync as renameSync7, statSync as statSync4 } from "node:fs";
|
|
@@ -30609,7 +30792,7 @@ class DestructiveGate {
|
|
|
30609
30792
|
import { readFileSync as readFileSync15 } from "node:fs";
|
|
30610
30793
|
|
|
30611
30794
|
// src/lib/conflicts.ts
|
|
30612
|
-
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync7 } from "node:fs";
|
|
30795
|
+
import { mkdirSync as mkdirSync9, readdirSync as readdirSync5, statSync as statSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
30613
30796
|
import { dirname as dirname7, join as join10 } from "node:path";
|
|
30614
30797
|
function conflictsRoot(workspaceSlug) {
|
|
30615
30798
|
return join10(plinthHome(), "_conflicts", workspaceSlug);
|
|
@@ -30637,9 +30820,60 @@ function stashFileConflict(args) {
|
|
|
30637
30820
|
writeFileSync7(target, args.bytes, { mode: 384 });
|
|
30638
30821
|
return target;
|
|
30639
30822
|
}
|
|
30823
|
+
var STAMP_RE2 = /\.(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)(\.md)?$/;
|
|
30824
|
+
function parseStamp(stamp) {
|
|
30825
|
+
const iso = stamp.replace(/T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/, "T$1:$2:$3.$4Z");
|
|
30826
|
+
const d2 = new Date(iso);
|
|
30827
|
+
return Number.isNaN(d2.getTime()) ? null : d2;
|
|
30828
|
+
}
|
|
30829
|
+
function labelFor(file2) {
|
|
30830
|
+
const m2 = STAMP_RE2.exec(file2);
|
|
30831
|
+
if (!m2)
|
|
30832
|
+
return file2;
|
|
30833
|
+
const head = file2.slice(0, m2.index);
|
|
30834
|
+
if (head.startsWith("files.")) {
|
|
30835
|
+
return `files/${head.slice("files.".length).replace(/__/g, "/")}`;
|
|
30836
|
+
}
|
|
30837
|
+
const dot = head.indexOf(".");
|
|
30838
|
+
if (dot === -1)
|
|
30839
|
+
return head;
|
|
30840
|
+
return `${head.slice(0, dot)}/${head.slice(dot + 1)}`;
|
|
30841
|
+
}
|
|
30842
|
+
function listConflictStashes(workspaceSlug) {
|
|
30843
|
+
const dir = conflictsRoot(workspaceSlug);
|
|
30844
|
+
let names;
|
|
30845
|
+
try {
|
|
30846
|
+
names = readdirSync5(dir);
|
|
30847
|
+
} catch {
|
|
30848
|
+
return [];
|
|
30849
|
+
}
|
|
30850
|
+
const out = [];
|
|
30851
|
+
for (const file2 of names) {
|
|
30852
|
+
const absPath = join10(dir, file2);
|
|
30853
|
+
let st;
|
|
30854
|
+
try {
|
|
30855
|
+
st = statSync6(absPath);
|
|
30856
|
+
} catch {
|
|
30857
|
+
continue;
|
|
30858
|
+
}
|
|
30859
|
+
if (!st.isFile())
|
|
30860
|
+
continue;
|
|
30861
|
+
if (file2.startsWith("."))
|
|
30862
|
+
continue;
|
|
30863
|
+
const m2 = STAMP_RE2.exec(file2);
|
|
30864
|
+
out.push({
|
|
30865
|
+
file: file2,
|
|
30866
|
+
absPath,
|
|
30867
|
+
label: labelFor(file2),
|
|
30868
|
+
stashedAt: m2 ? parseStamp(m2[1]) : null,
|
|
30869
|
+
mtimeMs: st.mtimeMs
|
|
30870
|
+
});
|
|
30871
|
+
}
|
|
30872
|
+
return out.sort((a2, b2) => (b2.stashedAt?.getTime() ?? b2.mtimeMs) - (a2.stashedAt?.getTime() ?? a2.mtimeMs));
|
|
30873
|
+
}
|
|
30640
30874
|
|
|
30641
30875
|
// src/lib/files/push.ts
|
|
30642
|
-
import { readFileSync as readFileSync12, statSync as
|
|
30876
|
+
import { readFileSync as readFileSync12, statSync as statSync7 } from "node:fs";
|
|
30643
30877
|
|
|
30644
30878
|
// src/lib/log-tokens.ts
|
|
30645
30879
|
function categorizeError(err) {
|
|
@@ -30729,9 +30963,9 @@ function isPermanentReject(err) {
|
|
|
30729
30963
|
return PERMANENT_REJECT_REASONS.has(categorizeError(err).reason);
|
|
30730
30964
|
}
|
|
30731
30965
|
function persist(state, next) {
|
|
30732
|
-
if (next.changed)
|
|
30733
|
-
|
|
30734
|
-
return next.state;
|
|
30966
|
+
if (!next.changed)
|
|
30967
|
+
return state;
|
|
30968
|
+
return commitState(state, next.state);
|
|
30735
30969
|
}
|
|
30736
30970
|
function recordEntityRejected(args) {
|
|
30737
30971
|
const cat = categorizeError(args.err);
|
|
@@ -30796,15 +31030,15 @@ function clearEntityRejected(state, workspaceSlug, absolutePath) {
|
|
|
30796
31030
|
path5 = absolutePath;
|
|
30797
31031
|
}
|
|
30798
31032
|
const next = removeRejected(state, "entity", path5);
|
|
30799
|
-
if (next.changed)
|
|
30800
|
-
|
|
30801
|
-
return next.state;
|
|
31033
|
+
if (!next.changed)
|
|
31034
|
+
return state;
|
|
31035
|
+
return commitState(state, next.state);
|
|
30802
31036
|
}
|
|
30803
31037
|
function clearFileRejected(state, relPath) {
|
|
30804
31038
|
const next = removeRejected(state, "file", relPath);
|
|
30805
|
-
if (next.changed)
|
|
30806
|
-
|
|
30807
|
-
return next.state;
|
|
31039
|
+
if (!next.changed)
|
|
31040
|
+
return state;
|
|
31041
|
+
return commitState(state, next.state);
|
|
30808
31042
|
}
|
|
30809
31043
|
|
|
30810
31044
|
// src/lib/files/api.ts
|
|
@@ -31139,26 +31373,26 @@ function inferMime(relPath) {
|
|
|
31139
31373
|
// src/lib/files/push.ts
|
|
31140
31374
|
function persistFiles(state, entry2) {
|
|
31141
31375
|
const { state: next, changed } = upsertFileEntry(state, entry2);
|
|
31142
|
-
if (changed)
|
|
31143
|
-
|
|
31144
|
-
return next;
|
|
31376
|
+
if (!changed)
|
|
31377
|
+
return state;
|
|
31378
|
+
return commitState(state, next);
|
|
31145
31379
|
}
|
|
31146
31380
|
function dropFiles(state, relPath) {
|
|
31147
31381
|
const { state: next, changed } = removeFileEntry(state, relPath);
|
|
31148
|
-
if (changed)
|
|
31149
|
-
|
|
31150
|
-
return next;
|
|
31382
|
+
if (!changed)
|
|
31383
|
+
return state;
|
|
31384
|
+
return commitState(state, next);
|
|
31151
31385
|
}
|
|
31152
31386
|
function safeInode(absolutePath) {
|
|
31153
31387
|
try {
|
|
31154
|
-
return
|
|
31388
|
+
return statSync7(absolutePath).ino;
|
|
31155
31389
|
} catch {
|
|
31156
31390
|
return;
|
|
31157
31391
|
}
|
|
31158
31392
|
}
|
|
31159
31393
|
async function restoreFileFromCloud(args) {
|
|
31160
31394
|
const { workspaceSlug, baseUrl, pat, id, relPath, state, fetchImpl, suppressor, output } = args;
|
|
31161
|
-
const got = await getFile({ baseUrl, pat, id, fetchImpl });
|
|
31395
|
+
const got = args.prefetched ?? await getFile({ baseUrl, pat, id, fetchImpl });
|
|
31162
31396
|
const abs = filesSubtreePath(workspaceSlug, relPath);
|
|
31163
31397
|
if (!got.ok) {
|
|
31164
31398
|
suppressor?.registerDelete(abs);
|
|
@@ -31204,7 +31438,7 @@ async function pushFilesUpsert(args) {
|
|
|
31204
31438
|
} else {
|
|
31205
31439
|
let st;
|
|
31206
31440
|
try {
|
|
31207
|
-
st =
|
|
31441
|
+
st = statSync7(absolutePath);
|
|
31208
31442
|
} catch (err) {
|
|
31209
31443
|
if (err?.code === "ENOENT") {
|
|
31210
31444
|
return pushFilesDelete({ workspaceSlug, baseUrl, pat, relPath, state, deps });
|
|
@@ -31254,6 +31488,42 @@ async function pushFilesUpsert(args) {
|
|
|
31254
31488
|
return { outcome: { kind: "noop", relPath }, state };
|
|
31255
31489
|
}
|
|
31256
31490
|
const mimeType = inferMime(relPath);
|
|
31491
|
+
const acceptPush = (row, action) => {
|
|
31492
|
+
output?.log(filePushLine({ path: relPath, id: row.id, action }));
|
|
31493
|
+
return {
|
|
31494
|
+
outcome: { kind: action, relPath, id: row.id },
|
|
31495
|
+
state: persistFiles(state, {
|
|
31496
|
+
id: row.id,
|
|
31497
|
+
path: relPath,
|
|
31498
|
+
content_hash: hash2,
|
|
31499
|
+
size_bytes: row.size_bytes,
|
|
31500
|
+
mime_type: row.mime_type,
|
|
31501
|
+
updated_at: row.updated_at,
|
|
31502
|
+
inode
|
|
31503
|
+
})
|
|
31504
|
+
};
|
|
31505
|
+
};
|
|
31506
|
+
const rejectOverCap = (serverMax) => {
|
|
31507
|
+
const max = serverMax ?? MAX_WORKSPACE_FILE_BYTES;
|
|
31508
|
+
const source = serverMax === null ? "client" : "server";
|
|
31509
|
+
output?.log(fileOverCapLine({ path: relPath, size, max, source }));
|
|
31510
|
+
return {
|
|
31511
|
+
outcome: { kind: "over_cap", relPath, size, max, source },
|
|
31512
|
+
state: recordFileOverCap({
|
|
31513
|
+
state,
|
|
31514
|
+
relPath,
|
|
31515
|
+
size,
|
|
31516
|
+
max,
|
|
31517
|
+
source,
|
|
31518
|
+
tracked: existing !== undefined,
|
|
31519
|
+
now
|
|
31520
|
+
})
|
|
31521
|
+
};
|
|
31522
|
+
};
|
|
31523
|
+
const rejectHashMismatch = () => {
|
|
31524
|
+
output?.log(fileFailedLine({ path: relPath, status: 400, reason: "hash_mismatch" }));
|
|
31525
|
+
return { outcome: { kind: "ignored", relPath, reason: "hash_mismatch" }, state };
|
|
31526
|
+
};
|
|
31257
31527
|
const res = await postFile({
|
|
31258
31528
|
baseUrl,
|
|
31259
31529
|
pat,
|
|
@@ -31264,43 +31534,38 @@ async function pushFilesUpsert(args) {
|
|
|
31264
31534
|
expectedUpdatedAt: existing?.updated_at ?? null,
|
|
31265
31535
|
fetchImpl
|
|
31266
31536
|
});
|
|
31267
|
-
if (res.ok)
|
|
31268
|
-
|
|
31269
|
-
|
|
31270
|
-
|
|
31271
|
-
|
|
31272
|
-
|
|
31273
|
-
|
|
31274
|
-
|
|
31275
|
-
|
|
31276
|
-
|
|
31277
|
-
|
|
31278
|
-
|
|
31279
|
-
|
|
31280
|
-
|
|
31281
|
-
|
|
31282
|
-
|
|
31283
|
-
|
|
31284
|
-
|
|
31285
|
-
|
|
31286
|
-
|
|
31287
|
-
|
|
31288
|
-
|
|
31289
|
-
|
|
31290
|
-
|
|
31291
|
-
|
|
31292
|
-
|
|
31293
|
-
|
|
31294
|
-
|
|
31295
|
-
|
|
31296
|
-
}
|
|
31297
|
-
if (res.code === 400) {
|
|
31298
|
-
output?.log(fileFailedLine({ path: relPath, status: 400, reason: "hash_mismatch" }));
|
|
31299
|
-
return { outcome: { kind: "ignored", relPath, reason: "hash_mismatch" }, state };
|
|
31537
|
+
if (res.ok)
|
|
31538
|
+
return acceptPush(res.row, existing ? "updated" : "created");
|
|
31539
|
+
if (res.code === 413)
|
|
31540
|
+
return rejectOverCap(res.maxBytes);
|
|
31541
|
+
if (res.code === 400)
|
|
31542
|
+
return rejectHashMismatch();
|
|
31543
|
+
let nextState = state;
|
|
31544
|
+
let cloudRow;
|
|
31545
|
+
if (existing) {
|
|
31546
|
+
cloudRow = await getFile({ baseUrl, pat, id: existing.id, fetchImpl });
|
|
31547
|
+
if (cloudRow.ok && cloudRow.row.download_url !== null && cloudRow.row.content_hash === existing.content_hash) {
|
|
31548
|
+
const retry = await postFile({
|
|
31549
|
+
baseUrl,
|
|
31550
|
+
pat,
|
|
31551
|
+
relPath,
|
|
31552
|
+
bytes,
|
|
31553
|
+
contentHash: hash2,
|
|
31554
|
+
mimeType,
|
|
31555
|
+
expectedUpdatedAt: cloudRow.row.updated_at,
|
|
31556
|
+
fetchImpl
|
|
31557
|
+
});
|
|
31558
|
+
if (retry.ok)
|
|
31559
|
+
return acceptPush(retry.row, "updated");
|
|
31560
|
+
if (retry.code === 413)
|
|
31561
|
+
return rejectOverCap(retry.maxBytes);
|
|
31562
|
+
if (retry.code === 400)
|
|
31563
|
+
return rejectHashMismatch();
|
|
31564
|
+
cloudRow = undefined;
|
|
31565
|
+
}
|
|
31300
31566
|
}
|
|
31301
31567
|
const stashPath = stashFileConflict({ workspaceSlug, relPath, timestamp: now(), bytes });
|
|
31302
31568
|
output?.log(fileConflictLine({ path: relPath, stash: stashPath }));
|
|
31303
|
-
let nextState = state;
|
|
31304
31569
|
if (existing) {
|
|
31305
31570
|
nextState = await restoreFileFromCloud({
|
|
31306
31571
|
workspaceSlug,
|
|
@@ -31311,7 +31576,8 @@ async function pushFilesUpsert(args) {
|
|
|
31311
31576
|
state,
|
|
31312
31577
|
fetchImpl,
|
|
31313
31578
|
suppressor,
|
|
31314
|
-
output
|
|
31579
|
+
output,
|
|
31580
|
+
prefetched: cloudRow
|
|
31315
31581
|
});
|
|
31316
31582
|
}
|
|
31317
31583
|
return { outcome: { kind: "conflict", relPath, stashPath }, state: nextState };
|
|
@@ -31376,7 +31642,7 @@ class PerIdMutex {
|
|
|
31376
31642
|
|
|
31377
31643
|
// src/lib/push.ts
|
|
31378
31644
|
var import_gray_matter9 = __toESM(require_gray_matter(), 1);
|
|
31379
|
-
import { readFileSync as readFileSync14, statSync as
|
|
31645
|
+
import { readFileSync as readFileSync14, statSync as statSync8 } from "node:fs";
|
|
31380
31646
|
import { basename as basename4, relative as relative4 } from "node:path";
|
|
31381
31647
|
|
|
31382
31648
|
// src/lib/pull-write.ts
|
|
@@ -31474,22 +31740,31 @@ function omitKeys(obj, keys) {
|
|
|
31474
31740
|
}
|
|
31475
31741
|
function safeInode2(absolutePath) {
|
|
31476
31742
|
try {
|
|
31477
|
-
return
|
|
31743
|
+
return statSync8(absolutePath).ino;
|
|
31478
31744
|
} catch {
|
|
31479
31745
|
return;
|
|
31480
31746
|
}
|
|
31481
31747
|
}
|
|
31482
31748
|
function persistEntry(state, entityType, entry2) {
|
|
31483
31749
|
const { state: nextState, changed } = upsertEntry(state, entityType, entry2);
|
|
31484
|
-
if (changed)
|
|
31485
|
-
|
|
31486
|
-
return nextState;
|
|
31750
|
+
if (!changed)
|
|
31751
|
+
return state;
|
|
31752
|
+
return commitState(state, nextState);
|
|
31487
31753
|
}
|
|
31488
31754
|
function dropEntry(state, entityType, id) {
|
|
31489
31755
|
const { state: nextState, changed } = removeEntry(state, entityType, id);
|
|
31490
|
-
if (changed)
|
|
31491
|
-
|
|
31492
|
-
return nextState;
|
|
31756
|
+
if (!changed)
|
|
31757
|
+
return state;
|
|
31758
|
+
return commitState(state, nextState);
|
|
31759
|
+
}
|
|
31760
|
+
function cloudMatchesAcked(strategy, cloud, acked) {
|
|
31761
|
+
const { bodyHash, frontmatterHash } = computeMirrorHashes(strategy, strategy.serialise(cloud));
|
|
31762
|
+
if (cloud.slug !== acked.slug)
|
|
31763
|
+
return false;
|
|
31764
|
+
if (strategy.preserveLocalBodyOnPull) {
|
|
31765
|
+
return frontmatterHash === acked.frontmatter_hash;
|
|
31766
|
+
}
|
|
31767
|
+
return bodyHash === acked.body_hash && frontmatterHash === acked.frontmatter_hash;
|
|
31493
31768
|
}
|
|
31494
31769
|
function stampStateFromMirror(args) {
|
|
31495
31770
|
const { strategy, workspaceSlug, doc: doc2, state, priorMemoryHash } = args;
|
|
@@ -31938,7 +32213,7 @@ async function runPushUpsert(args) {
|
|
|
31938
32213
|
state: nextState2
|
|
31939
32214
|
};
|
|
31940
32215
|
}
|
|
31941
|
-
|
|
32216
|
+
let patchResult = await patchEntity({
|
|
31942
32217
|
strategy,
|
|
31943
32218
|
baseUrl: args.baseUrl,
|
|
31944
32219
|
pat: args.pat,
|
|
@@ -31949,6 +32224,34 @@ async function runPushUpsert(args) {
|
|
|
31949
32224
|
topLevel: wireTopLevel,
|
|
31950
32225
|
fetchImpl: args.fetchImpl
|
|
31951
32226
|
});
|
|
32227
|
+
if (!patchResult.ok && patchResult.code === 409 && patchResult.reason === "precondition_failed") {
|
|
32228
|
+
let cloud = null;
|
|
32229
|
+
try {
|
|
32230
|
+
cloud = await getEntity({
|
|
32231
|
+
strategy,
|
|
32232
|
+
baseUrl: args.baseUrl,
|
|
32233
|
+
pat: args.pat,
|
|
32234
|
+
id: liveEntry.id,
|
|
32235
|
+
fetchImpl: args.fetchImpl
|
|
32236
|
+
});
|
|
32237
|
+
} catch (err) {
|
|
32238
|
+
if (!(err instanceof ApiError) || err.status !== 404)
|
|
32239
|
+
throw err;
|
|
32240
|
+
}
|
|
32241
|
+
if (cloud !== null && cloudMatchesAcked(strategy, cloud, liveEntry)) {
|
|
32242
|
+
patchResult = await patchEntity({
|
|
32243
|
+
strategy,
|
|
32244
|
+
baseUrl: args.baseUrl,
|
|
32245
|
+
pat: args.pat,
|
|
32246
|
+
id: liveEntry.id,
|
|
32247
|
+
expectedUpdatedAt: cloud.updated_at,
|
|
32248
|
+
body: wireBody,
|
|
32249
|
+
frontmatter: split ? wireFrontmatter : undefined,
|
|
32250
|
+
topLevel: wireTopLevel,
|
|
32251
|
+
fetchImpl: args.fetchImpl
|
|
32252
|
+
});
|
|
32253
|
+
}
|
|
32254
|
+
}
|
|
31952
32255
|
if (patchResult.ok) {
|
|
31953
32256
|
const updated = {
|
|
31954
32257
|
id: patchResult.doc.id,
|
|
@@ -32421,7 +32724,7 @@ async function pushRename(args) {
|
|
|
32421
32724
|
return mutex.run(id, async () => {
|
|
32422
32725
|
const live = getEntry(state, entityType, id);
|
|
32423
32726
|
const effectiveExpected = live?.updated_at ?? expectedUpdatedAt;
|
|
32424
|
-
|
|
32727
|
+
let patchResult = await patchEntity({
|
|
32425
32728
|
strategy,
|
|
32426
32729
|
baseUrl,
|
|
32427
32730
|
pat,
|
|
@@ -32430,6 +32733,26 @@ async function pushRename(args) {
|
|
|
32430
32733
|
slug: toSlug,
|
|
32431
32734
|
fetchImpl
|
|
32432
32735
|
});
|
|
32736
|
+
if (!patchResult.ok && patchResult.code === 409 && patchResult.reason === "precondition_failed" && live !== undefined) {
|
|
32737
|
+
let probe2 = null;
|
|
32738
|
+
try {
|
|
32739
|
+
probe2 = await getEntity({ strategy, baseUrl, pat, id, fetchImpl });
|
|
32740
|
+
} catch (err) {
|
|
32741
|
+
if (!(err instanceof ApiError) || err.status !== 404)
|
|
32742
|
+
throw err;
|
|
32743
|
+
}
|
|
32744
|
+
if (probe2 !== null && cloudMatchesAcked(strategy, probe2, live)) {
|
|
32745
|
+
patchResult = await patchEntity({
|
|
32746
|
+
strategy,
|
|
32747
|
+
baseUrl,
|
|
32748
|
+
pat,
|
|
32749
|
+
id,
|
|
32750
|
+
expectedUpdatedAt: probe2.updated_at,
|
|
32751
|
+
slug: toSlug,
|
|
32752
|
+
fetchImpl
|
|
32753
|
+
});
|
|
32754
|
+
}
|
|
32755
|
+
}
|
|
32433
32756
|
if (patchResult.ok) {
|
|
32434
32757
|
const doc2 = patchResult.doc;
|
|
32435
32758
|
const prior = live ?? null;
|
|
@@ -32864,7 +33187,7 @@ class FileDestructiveGate {
|
|
|
32864
33187
|
}
|
|
32865
33188
|
|
|
32866
33189
|
// src/lib/files/reconcile.ts
|
|
32867
|
-
import { readFileSync as readFileSync16, statSync as
|
|
33190
|
+
import { readFileSync as readFileSync16, statSync as statSync10 } from "node:fs";
|
|
32868
33191
|
|
|
32869
33192
|
// src/lib/unsynced.ts
|
|
32870
33193
|
import { join as join11, relative as relative6 } from "node:path";
|
|
@@ -32879,9 +33202,9 @@ function isRecordableFailure(err) {
|
|
|
32879
33202
|
}
|
|
32880
33203
|
function persistUpsert(state, candidate) {
|
|
32881
33204
|
const { state: next, changed } = upsertUnsynced(state, candidate);
|
|
32882
|
-
if (changed)
|
|
32883
|
-
|
|
32884
|
-
return next;
|
|
33205
|
+
if (!changed)
|
|
33206
|
+
return state;
|
|
33207
|
+
return commitState(state, next);
|
|
32885
33208
|
}
|
|
32886
33209
|
function recordEntityUnsynced(args) {
|
|
32887
33210
|
const cat = categorizeError(args.err);
|
|
@@ -32920,11 +33243,10 @@ async function drainUnsyncedOps(args) {
|
|
|
32920
33243
|
let stillFailing = 0;
|
|
32921
33244
|
let rejected = 0;
|
|
32922
33245
|
const clear = (target, path5) => {
|
|
32923
|
-
const
|
|
32924
|
-
|
|
32925
|
-
|
|
32926
|
-
setState(next);
|
|
32927
|
-
}
|
|
33246
|
+
const before = getState();
|
|
33247
|
+
const { state: next, changed } = removeUnsynced(before, target, path5);
|
|
33248
|
+
if (changed)
|
|
33249
|
+
setState(commitState(before, next));
|
|
32928
33250
|
};
|
|
32929
33251
|
for (const op of ops) {
|
|
32930
33252
|
attempted += 1;
|
|
@@ -32976,7 +33298,8 @@ async function drainUnsyncedOps(args) {
|
|
|
32976
33298
|
const cat = categorizeError(err);
|
|
32977
33299
|
if (isPermanentReject(err)) {
|
|
32978
33300
|
rejected += 1;
|
|
32979
|
-
const
|
|
33301
|
+
const beforeReject = getState();
|
|
33302
|
+
const removed = removeUnsynced(beforeReject, op.target, op.path).state;
|
|
32980
33303
|
const { state: next2 } = upsertRejected(removed, {
|
|
32981
33304
|
target: op.target,
|
|
32982
33305
|
op: op.op,
|
|
@@ -32987,22 +33310,20 @@ async function drainUnsyncedOps(args) {
|
|
|
32987
33310
|
message: cat.serverMessage,
|
|
32988
33311
|
rejected_at: now().toISOString()
|
|
32989
33312
|
});
|
|
32990
|
-
|
|
32991
|
-
setState(next2);
|
|
33313
|
+
setState(commitState(beforeReject, next2));
|
|
32992
33314
|
continue;
|
|
32993
33315
|
}
|
|
32994
33316
|
stillFailing += 1;
|
|
32995
|
-
const
|
|
33317
|
+
const beforeRetry = getState();
|
|
33318
|
+
const { state: next, changed } = upsertUnsynced(beforeRetry, {
|
|
32996
33319
|
target: op.target,
|
|
32997
33320
|
op: op.op,
|
|
32998
33321
|
path: op.path,
|
|
32999
33322
|
last_status: cat.status,
|
|
33000
33323
|
last_reason: cat.reason
|
|
33001
33324
|
});
|
|
33002
|
-
if (changed)
|
|
33003
|
-
|
|
33004
|
-
setState(next);
|
|
33005
|
-
}
|
|
33325
|
+
if (changed)
|
|
33326
|
+
setState(commitState(beforeRetry, next));
|
|
33006
33327
|
}
|
|
33007
33328
|
}
|
|
33008
33329
|
return { attempted, resolved, stillFailing, rejected };
|
|
@@ -34621,7 +34942,7 @@ function watch(paths, options2 = {}) {
|
|
|
34621
34942
|
var chokidar_default = { watch, FSWatcher };
|
|
34622
34943
|
|
|
34623
34944
|
// src/lib/watcher.ts
|
|
34624
|
-
import { statSync as
|
|
34945
|
+
import { statSync as statSync9 } from "node:fs";
|
|
34625
34946
|
import { sep as sep5 } from "node:path";
|
|
34626
34947
|
|
|
34627
34948
|
// src/lib/rename-classifier.ts
|
|
@@ -34837,7 +35158,7 @@ function startWatcher(opts, handler) {
|
|
|
34837
35158
|
pairingWindowMs: opts.renamePairingWindowMs ?? DEFAULT_DEBOUNCE_MS,
|
|
34838
35159
|
classifyPath: opts.classifyPath,
|
|
34839
35160
|
getStateEntry: opts.getStateEntry,
|
|
34840
|
-
statSync: opts.statSync ?? ((p) => ({ ino:
|
|
35161
|
+
statSync: opts.statSync ?? ((p) => ({ ino: statSync9(p).ino }))
|
|
34841
35162
|
});
|
|
34842
35163
|
const factory = opts.chokidarFactory ?? ((paths, watcherOpts) => chokidar_default.watch(paths, watcherOpts));
|
|
34843
35164
|
const watcher = factory(workspaceRoot, {
|
|
@@ -34872,7 +35193,7 @@ function readBlobForPush(args) {
|
|
|
34872
35193
|
const { absolutePath, relPath, output } = args;
|
|
34873
35194
|
let st;
|
|
34874
35195
|
try {
|
|
34875
|
-
st =
|
|
35196
|
+
st = statSync10(absolutePath);
|
|
34876
35197
|
} catch (err) {
|
|
34877
35198
|
if (err?.code === "ENOENT")
|
|
34878
35199
|
return { kind: "gone" };
|
|
@@ -35110,9 +35431,9 @@ function conflictSlug(slug) {
|
|
|
35110
35431
|
}
|
|
35111
35432
|
function persistEntry2(state, entry2) {
|
|
35112
35433
|
const { state: next, changed } = upsertEntry(state, ENTITY_TYPE, entry2);
|
|
35113
|
-
if (changed)
|
|
35114
|
-
|
|
35115
|
-
return next;
|
|
35434
|
+
if (!changed)
|
|
35435
|
+
return state;
|
|
35436
|
+
return commitState(state, next);
|
|
35116
35437
|
}
|
|
35117
35438
|
function memoryStub(name) {
|
|
35118
35439
|
return `# ${name} — Long-term memory
|
|
@@ -36574,9 +36895,9 @@ function writeLocal(workspaceSlug, body, suppressor) {
|
|
|
36574
36895
|
}
|
|
36575
36896
|
function persistRules(state, entry2) {
|
|
36576
36897
|
const { state: next, changed } = setWorkspaceRulesState(state, entry2);
|
|
36577
|
-
if (changed)
|
|
36578
|
-
|
|
36579
|
-
return next;
|
|
36898
|
+
if (!changed)
|
|
36899
|
+
return state;
|
|
36900
|
+
return commitState(state, next);
|
|
36580
36901
|
}
|
|
36581
36902
|
function classifyWorkspaceRulesPath(args) {
|
|
36582
36903
|
let root;
|
|
@@ -36692,6 +37013,30 @@ async function pushWorkspaceRules(args) {
|
|
|
36692
37013
|
output?.warn(`${WORKSPACE_RULES_FILENAME} is over the ${Math.round(MAX_WORKSPACE_RULES_BYTES / 1024)}KB cap — not synced.`);
|
|
36693
37014
|
return { state, outcome: { kind: "over_cap" } };
|
|
36694
37015
|
}
|
|
37016
|
+
{
|
|
37017
|
+
const probe2 = await getWorkspaceRules({ baseUrl, pat, fetchImpl });
|
|
37018
|
+
if (acked && stableHash(probe2.body_md) === acked.body_hash) {
|
|
37019
|
+
const retry = await putWorkspaceRules({
|
|
37020
|
+
baseUrl,
|
|
37021
|
+
pat,
|
|
37022
|
+
bodyMd: local,
|
|
37023
|
+
expectedUpdatedAt: probe2.updated_at,
|
|
37024
|
+
fetchImpl
|
|
37025
|
+
});
|
|
37026
|
+
if (retry.ok) {
|
|
37027
|
+
const next2 = persistRules(state, {
|
|
37028
|
+
body_hash: localHash,
|
|
37029
|
+
updated_at: retry.row.updated_at
|
|
37030
|
+
});
|
|
37031
|
+
output?.log(`[WORKSPACE_RULES_PUT ws=${workspaceSlug} is_empty=${retry.row.is_empty}]`);
|
|
37032
|
+
return { state: next2, outcome: { kind: "pushed" } };
|
|
37033
|
+
}
|
|
37034
|
+
if (retry.code === 413) {
|
|
37035
|
+
output?.warn(`${WORKSPACE_RULES_FILENAME} is over the ${Math.round(MAX_WORKSPACE_RULES_BYTES / 1024)}KB cap — not synced.`);
|
|
37036
|
+
return { state, outcome: { kind: "over_cap" } };
|
|
37037
|
+
}
|
|
37038
|
+
}
|
|
37039
|
+
}
|
|
36695
37040
|
const stashPath = stashConflict({
|
|
36696
37041
|
workspaceSlug,
|
|
36697
37042
|
entityType: CONFLICT_ENTITY,
|
|
@@ -36834,9 +37179,9 @@ function writeLocal2(workspaceSlug, body, suppressor) {
|
|
|
36834
37179
|
}
|
|
36835
37180
|
function persistMemory(state, entry2) {
|
|
36836
37181
|
const { state: next, changed } = setWorkspaceMemoryState(state, entry2);
|
|
36837
|
-
if (changed)
|
|
36838
|
-
|
|
36839
|
-
return next;
|
|
37182
|
+
if (!changed)
|
|
37183
|
+
return state;
|
|
37184
|
+
return commitState(state, next);
|
|
36840
37185
|
}
|
|
36841
37186
|
function classifyWorkspaceMemoryPath(args) {
|
|
36842
37187
|
let root;
|
|
@@ -36934,6 +37279,30 @@ async function pushWorkspaceMemory(args) {
|
|
|
36934
37279
|
output?.warn(`${WORKSPACE_MEMORY_FILENAME} is over the ${Math.round(MAX_WORKSPACE_MEMORY_BYTES / 1024)}KB cap — not synced.`);
|
|
36935
37280
|
return { state, outcome: { kind: "over_cap" } };
|
|
36936
37281
|
}
|
|
37282
|
+
{
|
|
37283
|
+
const probe2 = await getWorkspaceMemory({ baseUrl, pat, fetchImpl });
|
|
37284
|
+
if (acked && stableHash(probe2.body_md) === acked.body_hash) {
|
|
37285
|
+
const retry = await putWorkspaceMemory({
|
|
37286
|
+
baseUrl,
|
|
37287
|
+
pat,
|
|
37288
|
+
bodyMd: local,
|
|
37289
|
+
expectedUpdatedAt: probe2.updated_at,
|
|
37290
|
+
fetchImpl
|
|
37291
|
+
});
|
|
37292
|
+
if (retry.ok) {
|
|
37293
|
+
const next2 = persistMemory(state, {
|
|
37294
|
+
body_hash: localHash,
|
|
37295
|
+
updated_at: retry.row.updated_at
|
|
37296
|
+
});
|
|
37297
|
+
output?.log(`[WORKSPACE_MEMORY_PUT ws=${workspaceSlug} is_empty=${retry.row.is_empty}]`);
|
|
37298
|
+
return { state: next2, outcome: { kind: "pushed" } };
|
|
37299
|
+
}
|
|
37300
|
+
if (retry.code === 413) {
|
|
37301
|
+
output?.warn(`${WORKSPACE_MEMORY_FILENAME} is over the ${Math.round(MAX_WORKSPACE_MEMORY_BYTES / 1024)}KB cap — not synced.`);
|
|
37302
|
+
return { state, outcome: { kind: "over_cap" } };
|
|
37303
|
+
}
|
|
37304
|
+
}
|
|
37305
|
+
}
|
|
36937
37306
|
const stashPath = stashConflict({
|
|
36938
37307
|
workspaceSlug,
|
|
36939
37308
|
entityType: CONFLICT_ENTITY2,
|
|
@@ -37078,9 +37447,9 @@ function writeLocal3(descriptor, workspaceSlug, body, suppressor) {
|
|
|
37078
37447
|
}
|
|
37079
37448
|
function persist2(descriptor, state, entry2) {
|
|
37080
37449
|
const { state: next, changed } = descriptor.setState(state, entry2);
|
|
37081
|
-
if (changed)
|
|
37082
|
-
|
|
37083
|
-
return next;
|
|
37450
|
+
if (!changed)
|
|
37451
|
+
return state;
|
|
37452
|
+
return commitState(state, next);
|
|
37084
37453
|
}
|
|
37085
37454
|
function classifySingletonPath(descriptor, args) {
|
|
37086
37455
|
let root;
|
|
@@ -37178,6 +37547,30 @@ async function pushSingleton(descriptor, args) {
|
|
|
37178
37547
|
output?.warn(`${descriptor.filename} is over the ${Math.round(descriptor.maxBytes / 1024)}KB cap — not synced.`);
|
|
37179
37548
|
return { state, outcome: { kind: "over_cap" } };
|
|
37180
37549
|
}
|
|
37550
|
+
{
|
|
37551
|
+
const probe2 = await descriptor.api.get({ baseUrl, pat, fetchImpl });
|
|
37552
|
+
if (acked && stableHash(probe2.body_md) === acked.body_hash) {
|
|
37553
|
+
const retry = await descriptor.api.put({
|
|
37554
|
+
baseUrl,
|
|
37555
|
+
pat,
|
|
37556
|
+
bodyMd: local,
|
|
37557
|
+
expectedUpdatedAt: probe2.updated_at,
|
|
37558
|
+
fetchImpl
|
|
37559
|
+
});
|
|
37560
|
+
if (retry.ok) {
|
|
37561
|
+
const next2 = persist2(descriptor, state, {
|
|
37562
|
+
body_hash: localHash,
|
|
37563
|
+
updated_at: retry.row.updated_at
|
|
37564
|
+
});
|
|
37565
|
+
output?.log(`[${descriptor.logTag}_PUT ws=${workspaceSlug} is_empty=${retry.row.is_empty}]`);
|
|
37566
|
+
return { state: next2, outcome: { kind: "pushed" } };
|
|
37567
|
+
}
|
|
37568
|
+
if (retry.code === 413) {
|
|
37569
|
+
output?.warn(`${descriptor.filename} is over the ${Math.round(descriptor.maxBytes / 1024)}KB cap — not synced.`);
|
|
37570
|
+
return { state, outcome: { kind: "over_cap" } };
|
|
37571
|
+
}
|
|
37572
|
+
}
|
|
37573
|
+
}
|
|
37181
37574
|
const stashPath = stashConflict({
|
|
37182
37575
|
workspaceSlug,
|
|
37183
37576
|
entityType: descriptor.conflictEntity,
|
|
@@ -37326,15 +37719,15 @@ function createThrottledFetch(baseFetch, opts = {}) {
|
|
|
37326
37719
|
// src/commands/sync.ts
|
|
37327
37720
|
import {
|
|
37328
37721
|
existsSync as existsSync6,
|
|
37329
|
-
readdirSync as
|
|
37722
|
+
readdirSync as readdirSync6,
|
|
37330
37723
|
readFileSync as readFileSync19,
|
|
37331
|
-
statSync as
|
|
37724
|
+
statSync as statSync12,
|
|
37332
37725
|
unlinkSync as unlinkSync7
|
|
37333
37726
|
} from "node:fs";
|
|
37334
37727
|
import { join as join14 } from "node:path";
|
|
37335
37728
|
|
|
37336
37729
|
// src/lib/files/pull.ts
|
|
37337
|
-
import { existsSync as existsSync5, readFileSync as readFileSync18, statSync as
|
|
37730
|
+
import { existsSync as existsSync5, readFileSync as readFileSync18, statSync as statSync11 } from "node:fs";
|
|
37338
37731
|
|
|
37339
37732
|
// src/lib/files/extracted-text.ts
|
|
37340
37733
|
import { createHash as createHash3 } from "node:crypto";
|
|
@@ -37430,6 +37823,7 @@ async function pullFiles(args) {
|
|
|
37430
37823
|
let cursorBlocked = false;
|
|
37431
37824
|
while (true) {
|
|
37432
37825
|
const page = await syncFilesPage({ baseUrl, pat, cursor, fetchImpl });
|
|
37826
|
+
const pageBase = state;
|
|
37433
37827
|
let pageTextFailed = false;
|
|
37434
37828
|
for (const row of page.rows) {
|
|
37435
37829
|
fetched++;
|
|
@@ -37460,6 +37854,8 @@ async function pullFiles(args) {
|
|
|
37460
37854
|
if (outcome.textFailed)
|
|
37461
37855
|
pageTextFailed = true;
|
|
37462
37856
|
}
|
|
37857
|
+
if (args.commit && state !== pageBase)
|
|
37858
|
+
state = args.commit(pageBase, state);
|
|
37463
37859
|
if (pageTextFailed)
|
|
37464
37860
|
cursorBlocked = true;
|
|
37465
37861
|
if (page.next_cursor === null)
|
|
@@ -37665,7 +38061,7 @@ async function applyFileRow(args) {
|
|
|
37665
38061
|
}
|
|
37666
38062
|
let inode;
|
|
37667
38063
|
try {
|
|
37668
|
-
inode =
|
|
38064
|
+
inode = statSync11(abs).ino;
|
|
37669
38065
|
} catch {
|
|
37670
38066
|
inode = undefined;
|
|
37671
38067
|
}
|
|
@@ -37747,11 +38143,11 @@ function hasExistingContent(workspacePath) {
|
|
|
37747
38143
|
if (!existsSync6(workspacePath))
|
|
37748
38144
|
return false;
|
|
37749
38145
|
try {
|
|
37750
|
-
return
|
|
38146
|
+
return readdirSync6(workspacePath, { withFileTypes: true }).some((e2) => {
|
|
37751
38147
|
if (!e2.isDirectory())
|
|
37752
38148
|
return true;
|
|
37753
38149
|
try {
|
|
37754
|
-
return
|
|
38150
|
+
return readdirSync6(join14(workspacePath, e2.name)).length > 0;
|
|
37755
38151
|
} catch {
|
|
37756
38152
|
return true;
|
|
37757
38153
|
}
|
|
@@ -37771,7 +38167,7 @@ function unlinkIfInodeMatches(workspaceSlug, entityType, entrySlug, expectedInod
|
|
|
37771
38167
|
}
|
|
37772
38168
|
let liveInode;
|
|
37773
38169
|
try {
|
|
37774
|
-
liveInode =
|
|
38170
|
+
liveInode = statSync12(target).ino;
|
|
37775
38171
|
} catch {
|
|
37776
38172
|
return { didUnlink: false, skippedReason: "absent" };
|
|
37777
38173
|
}
|
|
@@ -37828,7 +38224,13 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
37828
38224
|
out.info(`Syncing ${slug}…`);
|
|
37829
38225
|
const startedAt = now();
|
|
37830
38226
|
let state = loadState(slug);
|
|
37831
|
-
let
|
|
38227
|
+
let committed = state;
|
|
38228
|
+
const commitPending = () => {
|
|
38229
|
+
if (state === committed)
|
|
38230
|
+
return;
|
|
38231
|
+
state = commitState(committed, state);
|
|
38232
|
+
committed = state;
|
|
38233
|
+
};
|
|
37832
38234
|
const counters = {};
|
|
37833
38235
|
try {
|
|
37834
38236
|
for (const entityType of allEntityTypes()) {
|
|
@@ -37850,13 +38252,13 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
37850
38252
|
const outcome = applyRow(slug, strategy, row, state, out, deps.suppressor, now, deps.isFrozen);
|
|
37851
38253
|
if (outcome.state !== state) {
|
|
37852
38254
|
state = outcome.state;
|
|
37853
|
-
stateDirty = true;
|
|
37854
38255
|
}
|
|
37855
38256
|
if (outcome.action === "written")
|
|
37856
38257
|
ctr.written++;
|
|
37857
38258
|
else if (outcome.action === "deleted")
|
|
37858
38259
|
ctr.deleted++;
|
|
37859
38260
|
}
|
|
38261
|
+
commitPending();
|
|
37860
38262
|
if (page.next_cursor === null)
|
|
37861
38263
|
break;
|
|
37862
38264
|
lastAdvancedCursor = page.next_cursor;
|
|
@@ -37883,21 +38285,21 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
37883
38285
|
output: out,
|
|
37884
38286
|
suppressor: deps.suppressor,
|
|
37885
38287
|
isFileFrozen: deps.isFileFrozen,
|
|
38288
|
+
commit: commitState,
|
|
37886
38289
|
forceTextRewalk: needsDerivedTextRewalk
|
|
37887
38290
|
});
|
|
37888
38291
|
if (filesResult.state !== state) {
|
|
37889
38292
|
state = filesResult.state;
|
|
37890
|
-
stateDirty = true;
|
|
37891
38293
|
}
|
|
37892
38294
|
if (needsDerivedTextRewalk && filesResult.textUnknown === 0) {
|
|
37893
38295
|
state = { ...state, derived_text_version: DERIVED_TEXT_VERSION };
|
|
37894
|
-
stateDirty = true;
|
|
37895
38296
|
}
|
|
37896
38297
|
counters["files"] = {
|
|
37897
38298
|
fetched: filesResult.fetched,
|
|
37898
38299
|
written: filesResult.written + filesResult.textWritten,
|
|
37899
38300
|
deleted: filesResult.deleted + filesResult.textDeleted
|
|
37900
38301
|
};
|
|
38302
|
+
commitPending();
|
|
37901
38303
|
if (filesResult.lastCursor !== filesStartCursor) {
|
|
37902
38304
|
updateSyncCursor(slug, "files", filesResult.lastCursor);
|
|
37903
38305
|
}
|
|
@@ -37913,8 +38315,8 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
37913
38315
|
});
|
|
37914
38316
|
if (rulesResult.state !== state) {
|
|
37915
38317
|
state = rulesResult.state;
|
|
37916
|
-
stateDirty = true;
|
|
37917
38318
|
}
|
|
38319
|
+
commitPending();
|
|
37918
38320
|
if (rulesResult.action === "written") {
|
|
37919
38321
|
counters["workspace-rules"] = { fetched: 1, written: 1, deleted: 0 };
|
|
37920
38322
|
}
|
|
@@ -37930,8 +38332,8 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
37930
38332
|
});
|
|
37931
38333
|
if (memoryResult.state !== state) {
|
|
37932
38334
|
state = memoryResult.state;
|
|
37933
|
-
stateDirty = true;
|
|
37934
38335
|
}
|
|
38336
|
+
commitPending();
|
|
37935
38337
|
if (memoryResult.action === "written") {
|
|
37936
38338
|
counters["workspace-memory"] = { fetched: 1, written: 1, deleted: 0 };
|
|
37937
38339
|
}
|
|
@@ -37947,8 +38349,8 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
37947
38349
|
});
|
|
37948
38350
|
if (userMemoryResult.state !== state) {
|
|
37949
38351
|
state = userMemoryResult.state;
|
|
37950
|
-
stateDirty = true;
|
|
37951
38352
|
}
|
|
38353
|
+
commitPending();
|
|
37952
38354
|
if (userMemoryResult.action === "written") {
|
|
37953
38355
|
counters["user-memory"] = { fetched: 1, written: 1, deleted: 0 };
|
|
37954
38356
|
}
|
|
@@ -37964,8 +38366,7 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
37964
38366
|
}
|
|
37965
38367
|
try {
|
|
37966
38368
|
updateLastSyncedAt(slug, now().toISOString());
|
|
37967
|
-
|
|
37968
|
-
writeState(state);
|
|
38369
|
+
commitPending();
|
|
37969
38370
|
} catch (err) {
|
|
37970
38371
|
const msg = err instanceof Error ? err.message : "unknown error";
|
|
37971
38372
|
out.error(`Sync state could not be persisted: ${msg}`);
|
|
@@ -38101,7 +38502,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
38101
38502
|
const hashes = computeMirrorHashes(strategy, content);
|
|
38102
38503
|
let inode;
|
|
38103
38504
|
try {
|
|
38104
|
-
inode =
|
|
38505
|
+
inode = statSync12(entityFilePath(workspaceSlug, strategy.type, live.slug)).ino;
|
|
38105
38506
|
} catch {
|
|
38106
38507
|
inode = undefined;
|
|
38107
38508
|
}
|
|
@@ -38158,7 +38559,7 @@ var syncCommand = defineCommand({
|
|
|
38158
38559
|
// src/commands/start.ts
|
|
38159
38560
|
var SHUTDOWN_DRAIN_TIMEOUT_MS = 5000;
|
|
38160
38561
|
var PULL_HEARTBEAT_MS = 900000;
|
|
38161
|
-
function backfillStateInodes(workspaceSlug, state, statFn = (p) =>
|
|
38562
|
+
function backfillStateInodes(workspaceSlug, state, statFn = (p) => statSync13(p)) {
|
|
38162
38563
|
let changed = false;
|
|
38163
38564
|
const nextEntries = {};
|
|
38164
38565
|
for (const [entityType, bucket] of Object.entries(state.entries)) {
|
|
@@ -38288,7 +38689,7 @@ function makeWatchHandler(deps) {
|
|
|
38288
38689
|
}
|
|
38289
38690
|
let st;
|
|
38290
38691
|
try {
|
|
38291
|
-
st =
|
|
38692
|
+
st = statSync13(absolutePath);
|
|
38292
38693
|
} catch (err) {
|
|
38293
38694
|
if (err?.code === "ENOENT")
|
|
38294
38695
|
return;
|
|
@@ -38944,12 +39345,13 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
38944
39345
|
if (initial.exitCode !== 0)
|
|
38945
39346
|
return initial;
|
|
38946
39347
|
let state = loadState(slug);
|
|
39348
|
+
const beforeBackfill = state;
|
|
38947
39349
|
const inodeBackfill = backfillStateInodes(slug, state);
|
|
38948
39350
|
state = inodeBackfill.state;
|
|
38949
39351
|
const lenBackfill = backfillBodyLengths(slug, state);
|
|
38950
39352
|
state = lenBackfill.state;
|
|
38951
39353
|
if (inodeBackfill.changed || lenBackfill.changed) {
|
|
38952
|
-
|
|
39354
|
+
state = commitState(beforeBackfill, state);
|
|
38953
39355
|
}
|
|
38954
39356
|
const spinner = (deps.spinnerFactory ?? (() => createSpinner({ noColor: deps.noColor })))();
|
|
38955
39357
|
spinner.start(`Watching ${slug} · ${watchingSummary(state)}`);
|
|
@@ -39582,7 +39984,7 @@ var startCommand = defineCommand({
|
|
|
39582
39984
|
});
|
|
39583
39985
|
|
|
39584
39986
|
// src/commands/status.ts
|
|
39585
|
-
import { statSync as
|
|
39987
|
+
import { statSync as statSync14 } from "node:fs";
|
|
39586
39988
|
|
|
39587
39989
|
// src/lib/build-stamp.ts
|
|
39588
39990
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
@@ -39718,7 +40120,7 @@ function renderBuild(out, staleness) {
|
|
|
39718
40120
|
}
|
|
39719
40121
|
function lastActivity(now) {
|
|
39720
40122
|
try {
|
|
39721
|
-
const mtimeMs =
|
|
40123
|
+
const mtimeMs = statSync14(daemonLogPath()).mtimeMs;
|
|
39722
40124
|
return formatRelative(now.getTime() - mtimeMs);
|
|
39723
40125
|
} catch {
|
|
39724
40126
|
return null;
|
|
@@ -39823,6 +40225,17 @@ async function runStatus(args, deps = {}) {
|
|
|
39823
40225
|
out.info(cont(`…and ${rejected.length - MAX_SHOWN} more (see ~/.plinth/daemon.log).`));
|
|
39824
40226
|
}
|
|
39825
40227
|
}
|
|
40228
|
+
out.info("");
|
|
40229
|
+
const stashes = listConflictStashes(slug);
|
|
40230
|
+
if (stashes.length === 0) {
|
|
40231
|
+
out.info(row("Conflicts", "No conflict stashes."));
|
|
40232
|
+
} else {
|
|
40233
|
+
out.info(row("Conflicts", `${stashes.length} stashed local cop${stashes.length === 1 ? "y" : "ies"} in ${conflictsRoot(slug)}`));
|
|
40234
|
+
const newest = stashes[0];
|
|
40235
|
+
const when = newest.stashedAt ? `${formatAbsolute(newest.stashedAt)} (${formatRelative(now.getTime() - newest.stashedAt.getTime())})` : "time unknown";
|
|
40236
|
+
out.info(cont(`Newest: ${newest.label} — ${when}`));
|
|
40237
|
+
out.info(cont("Each is your version of a file that was replaced by the cloud copy. Compare it with the file in your mirror, re-apply anything you still want, then delete the stash."));
|
|
40238
|
+
}
|
|
39826
40239
|
return { exitCode: 0 };
|
|
39827
40240
|
}
|
|
39828
40241
|
var statusCommand = defineCommand({
|
|
@@ -40161,7 +40574,7 @@ function denyUngated(detail) {
|
|
|
40161
40574
|
}
|
|
40162
40575
|
|
|
40163
40576
|
// src/lib/voice-gate/payload.ts
|
|
40164
|
-
import { readFileSync as readFileSync23, statSync as
|
|
40577
|
+
import { readFileSync as readFileSync23, statSync as statSync15 } from "node:fs";
|
|
40165
40578
|
import { homedir as homedir2 } from "node:os";
|
|
40166
40579
|
import { isAbsolute as isAbsolute3, resolve as resolve4 } from "node:path";
|
|
40167
40580
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
@@ -40318,7 +40731,7 @@ function isHelperInvocation(tokens) {
|
|
|
40318
40731
|
return helperIndex === start + 1 && /(^|\/)python[0-9.]*$/.test(prev);
|
|
40319
40732
|
}
|
|
40320
40733
|
function defaultReadBodyFile(path5) {
|
|
40321
|
-
const stat4 =
|
|
40734
|
+
const stat4 = statSync15(path5);
|
|
40322
40735
|
if (stat4.size > MAX_BODY_BYTES) {
|
|
40323
40736
|
throw new Error(`draft body is ${stat4.size} bytes, over the ${MAX_BODY_BYTES}-byte gate limit`);
|
|
40324
40737
|
}
|