gutterpress 0.10.0 → 0.10.1-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -16
- package/dist/api/index.d.ts +10 -19
- package/dist/api/index.js +8 -64
- package/dist/{audit-cvarpa72.js → audit-dce99css.js} +5 -5
- package/dist/{build-6r502chw.js → build-bj4x0727.js} +5 -5
- package/dist/checks/source/index.d.ts +1 -0
- package/dist/checks/source/merge-markers.d.ts +3 -0
- package/dist/{cli-cqtggsng.js → cli-0z6zf98d.js} +2 -47
- package/dist/{cli-eq5naw4m.js → cli-7n848af2.js} +370 -1249
- package/dist/{cli-zfcryxg8.js → cli-eewynen9.js} +97 -60
- package/dist/{cli-k1065rkg.js → cli-w692g6dx.js} +75 -29
- package/dist/{cli-149edp6b.js → cli-ydpf09d9.js} +1 -1
- package/dist/cli.js +14 -15
- package/dist/{doctor-akvxbtjb.js → doctor-7nd6n2aa.js} +2 -2
- package/dist/{engine-ft4cr3ep.js → engine-57vw8zpd.js} +1 -1
- package/dist/{engine-b159tbns.js → engine-8kdwy0jz.js} +2 -2
- package/dist/{gutterpress-viewer-te8g5grx.js → gutterpress-viewer-tma1qwz2.js} +57 -15
- package/dist/{index-ycpvr0am.js → index-7hrvksrb.js} +4 -4
- package/dist/{index-mdefp0y5.js → index-dtrftayr.js} +75 -27
- package/dist/{index-ge7q9xj3.js → index-h1x7krkg.js} +771 -1482
- package/dist/{index-wq3r5pj7.js → index-p0ebr7dh.js} +94 -57
- package/dist/index.js +9 -65
- package/dist/lib/git-fs.d.ts +43 -0
- package/dist/lib/host-policy.d.ts +14 -2
- package/dist/lib/remote-auth/clone.d.ts +0 -21
- package/dist/lib/remote-auth/converge-merge.d.ts +24 -7
- package/dist/lib/remote-auth/generic-auth.d.ts +0 -22
- package/dist/lib/remote-auth/git-http.d.ts +1 -1
- package/dist/lib/remote-auth/github-auth.d.ts +2 -22
- package/dist/lib/remote-auth/operation-log.d.ts +4 -9
- package/dist/lib/remote-auth/sync-messages.d.ts +35 -12
- package/dist/lib/remote-auth/sync-types.d.ts +30 -101
- package/dist/lib/remote-auth/sync.d.ts +19 -21
- package/dist/lib/remote-auth/token-store.d.ts +0 -10
- package/dist/lib/remote-auth/transport.d.ts +42 -40
- package/dist/lib/source-provider.d.ts +12 -0
- package/dist/{lint-p2sw53d9.js → lint-c8ze8sqw.js} +5 -5
- package/dist/{new-hvq0x91q.js → new-xy2qc300.js} +5 -5
- package/dist/{plugin-pssmk0dx.js → plugin-g9hwzjsm.js} +5 -5
- package/dist/{preflight-4j00yd0g.js → preflight-prcvdvmk.js} +5 -5
- package/dist/{preview-bridge-fz7vpk8m.js → preview-bridge-2nnrs2hy.js} +15 -0
- package/dist/{preview-d4s6gk6p.js → preview-h479apj9.js} +5 -5
- package/dist/{preview-interface-0ssk8bmm.js → preview-interface-2f5v96hd.js} +411 -88
- package/dist/{preview-shell-c5mfa3q0.js → preview-shell-kyjq59d9.js} +80 -25
- package/dist/{publish-tkc26en7.js → publish-emk644a8.js} +5 -5
- package/dist/{source-provider-3tcj6qg2.js → source-provider-2j5x2hyw.js} +1 -1
- package/dist/{source-provider-vanafrt9.js → source-provider-kn41jmbs.js} +1 -1
- package/dist/{validate-w9vfefrw.js → validate-be54bdc6.js} +5 -5
- package/package.json +1 -1
- package/dist/lib/app-heartbeat.d.ts +0 -102
- package/dist/lib/remote-auth/image-clash.d.ts +0 -17
- package/dist/lib/remote-auth/recovery/classify.d.ts +0 -97
- package/dist/lib/remote-auth/recovery/inspect.d.ts +0 -75
- package/dist/lib/remote-auth/recovery/locks.d.ts +0 -20
- package/dist/lib/remote-auth/recovery/repair.d.ts +0 -29
- package/dist/lib/remote-auth/recovery/types.d.ts +0 -39
- package/dist/repair-2va4w12t.js +0 -137
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
var pendingSwap = null;
|
|
20
20
|
var pendingSwapTimer = null;
|
|
21
21
|
var SCROLL_IDLE_MS = 250;
|
|
22
|
+
// True while the book iframe has a live in-flow block editor open
|
|
23
|
+
// (preview-interface.js's blockEditStateChanged, protocol v8). A swap
|
|
24
|
+
// replaces the whole iframe, so swapping mid-edit would destroy the caret
|
|
25
|
+
// and the author's uncommitted typing with it — see holdForEdit below.
|
|
26
|
+
var blockEditOpen = false;
|
|
22
27
|
if (!active) return;
|
|
23
28
|
|
|
24
29
|
// Transparent bridge relay: forward host-toolbar commands to the active book
|
|
@@ -27,26 +32,36 @@
|
|
|
27
32
|
try {
|
|
28
33
|
if (window.parent !== window && e.source === window.parent) {
|
|
29
34
|
if (active && active.contentWindow) active.contentWindow.postMessage(e.data, '*');
|
|
35
|
+
// Opening the in-flow editor needs KEYBOARD focus in the book, and a
|
|
36
|
+
// postMessage carries no user activation the book could focus itself
|
|
37
|
+
// with. The host focuses this shell frame; this hands it the rest of
|
|
38
|
+
// the way down to the active book iframe, whose own window-focus
|
|
39
|
+
// listener then seats the caret.
|
|
40
|
+
if (e.data && e.data.type === 'gutterpress:cmd' && e.data.cmd === 'beginBlockEdit' && active) {
|
|
41
|
+
try { active.focus(); } catch (_f) {}
|
|
42
|
+
}
|
|
30
43
|
} else if (active && e.source === active.contentWindow) {
|
|
31
44
|
var data = e.data;
|
|
32
45
|
if (data && data.type === 'gutterpress:event' && data.name === 'viewportChanged') {
|
|
33
46
|
lastViewportChangeAt = Date.now();
|
|
34
47
|
if (pendingSwap) armPendingSwap();
|
|
35
48
|
}
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
detail.hotReload = true;
|
|
43
|
-
var startedAt = hotReloadFrame.__gutterpressReloadStartedAt;
|
|
44
|
-
detail.hotReloadMs = typeof startedAt === 'number' ? Math.max(0, Date.now() - startedAt) : 0;
|
|
45
|
-
detail.revision = hotReloadFrame.__gutterpressRevision;
|
|
46
|
-
detail.updateMode = 'full-reload';
|
|
47
|
-
data = { type: data.type, name: data.name, detail: detail };
|
|
48
|
-
hotReloadFrame = null;
|
|
49
|
+
if (data && data.type === 'gutterpress:event' && data.name === 'blockEditStateChanged') {
|
|
50
|
+
blockEditOpen = !!(data.detail && data.detail.open);
|
|
51
|
+
// Releasing the hold: a swap that arrived mid-edit is still queued,
|
|
52
|
+
// so start it now that the caret is gone.
|
|
53
|
+
if (!blockEditOpen && pendingSwap) armPendingSwap();
|
|
49
54
|
}
|
|
55
|
+
// A swapped-in frame's own lifecycle events are the shell's business,
|
|
56
|
+
// not the host's: finish() reports the swap (see reportSwapComplete).
|
|
57
|
+
// Relaying them too would either double-report or — when the frame
|
|
58
|
+
// paginates before its `load` event, which is the common case — report
|
|
59
|
+
// nothing at all, because the message arrives while the frame is still
|
|
60
|
+
// `building` and matches no branch here.
|
|
61
|
+
if (
|
|
62
|
+
active === hotReloadFrame && data && data.type === 'gutterpress:event' &&
|
|
63
|
+
(data.name === 'ready' || data.name === 'renderingComplete')
|
|
64
|
+
) return;
|
|
50
65
|
if (window.parent !== window) window.parent.postMessage(data, '*');
|
|
51
66
|
} else if (retiring && e.source === retiring.contentWindow && window.parent !== window) {
|
|
52
67
|
var retiringData = e.data;
|
|
@@ -135,7 +150,33 @@
|
|
|
135
150
|
building = null;
|
|
136
151
|
}
|
|
137
152
|
|
|
153
|
+
// Tell the host the swap is done. This is a synchronous fact the shell knows
|
|
154
|
+
// — the replacement is paginated (onReady resolved), promoted, and refreshed
|
|
155
|
+
// — rather than a relayed message whose arrival depends on whether the
|
|
156
|
+
// frame's pagination beat its own `load` event.
|
|
157
|
+
function reportSwapComplete(frame, pageState) {
|
|
158
|
+
if (window.parent === window) return;
|
|
159
|
+
var startedAt = frame.__gutterpressReloadStartedAt;
|
|
160
|
+
window.parent.postMessage({
|
|
161
|
+
type: 'gutterpress:event',
|
|
162
|
+
name: 'renderingComplete',
|
|
163
|
+
detail: {
|
|
164
|
+
totalPages: pageState ? pageState.totalPages : 0,
|
|
165
|
+
hotReload: true,
|
|
166
|
+
hotReloadMs: typeof startedAt === 'number' ? Math.max(0, Date.now() - startedAt) : 0,
|
|
167
|
+
revision: frame.__gutterpressRevision,
|
|
168
|
+
updateMode: 'full-reload'
|
|
169
|
+
}
|
|
170
|
+
}, '*');
|
|
171
|
+
}
|
|
172
|
+
|
|
138
173
|
function swap(instance, revision) {
|
|
174
|
+
// The editor lived in the frame we are about to retire, so the hold is
|
|
175
|
+
// void whatever the book document last reported. Every update path reaches
|
|
176
|
+
// swap() through armPendingSwap(), which refuses while an edit is open, so
|
|
177
|
+
// this only fires when the frame goes away some other way — but a flag
|
|
178
|
+
// left stuck true would freeze hot reload for the session.
|
|
179
|
+
blockEditOpen = false;
|
|
139
180
|
discardBuilding();
|
|
140
181
|
var frame = document.createElement('iframe');
|
|
141
182
|
frame.style.visibility = 'hidden';
|
|
@@ -175,7 +216,8 @@
|
|
|
175
216
|
hotReloadFrame = frame;
|
|
176
217
|
building = null;
|
|
177
218
|
var api = fwin(frame) && fwin(frame).previewAPI;
|
|
178
|
-
|
|
219
|
+
var pageState = api && typeof api.refresh === 'function' ? api.refresh() : null;
|
|
220
|
+
reportSwapComplete(frame, pageState);
|
|
179
221
|
frame.style.visibility = 'visible';
|
|
180
222
|
frame.removeAttribute('aria-hidden');
|
|
181
223
|
appliedInstance = instance;
|
|
@@ -229,6 +271,13 @@
|
|
|
229
271
|
|
|
230
272
|
function armPendingSwap() {
|
|
231
273
|
if (pendingSwapTimer !== null) clearTimeout(pendingSwapTimer);
|
|
274
|
+
// An open in-flow editor holds the swap indefinitely — there is no timeout
|
|
275
|
+
// to race, because an author may legitimately sit in one block for
|
|
276
|
+
// minutes. The hold is released by blockEditStateChanged{open:false},
|
|
277
|
+
// which preview-interface.js emits on EVERY close path (commit, cancel,
|
|
278
|
+
// blur, SPA-forced), and defensively by swap() below in case the frame
|
|
279
|
+
// carrying the editor goes away first.
|
|
280
|
+
if (blockEditOpen) return;
|
|
232
281
|
var delay = Math.max(0, SCROLL_IDLE_MS - (Date.now() - lastViewportChangeAt));
|
|
233
282
|
if (delay === 0) beginPendingSwap();
|
|
234
283
|
else pendingSwapTimer = setTimeout(beginPendingSwap, delay);
|
|
@@ -243,17 +292,23 @@
|
|
|
243
292
|
// Paged.js's `.pagedjs_page` DOM to find a chapter's live page range and
|
|
244
293
|
// graft a freshly-paginated replacement into it. Paged.js has been removed
|
|
245
294
|
// (native-only-migration-plan.md Phase 6). A native in-place splice was
|
|
246
|
-
// also tried and removed (2026-08-08 review)
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
295
|
+
// also tried and removed (2026-08-08 review), and the standing reason is
|
|
296
|
+
// PERFORMANCE, not soundness: measured end-to-end (file write -> change
|
|
297
|
+
// visible, 5 samples, 34pp field guide) the plain full reload (`swap`,
|
|
298
|
+
// below) is 509ms avg vs the incremental splice's 998ms avg. Every
|
|
299
|
+
// content-update goes straight to `swap()`.
|
|
300
|
+
//
|
|
301
|
+
// CORRECTED 2026-08-24: the 2026-08-08 review ALSO recorded a soundness
|
|
302
|
+
// objection — that `refresh()` -> `relayout()` "only re-measures the
|
|
303
|
+
// EXISTING strips", silently dropping any page context the edit
|
|
304
|
+
// introduces. That is false against the current `fragment.ts`, whose
|
|
305
|
+
// `relayout()` unwraps the strips and re-runs `buildStrips()` from
|
|
306
|
+
// scratch before re-measuring, precisely so a mutation that adds or
|
|
307
|
+
// removes a page-context run is seen. Do not cite the old objection as a
|
|
308
|
+
// reason a DOM mutation cannot be re-paginated — it can, and the inline
|
|
309
|
+
// editing work depends on it (docs/inline-editing-plan.md, ADR 0009
|
|
310
|
+
// decision 4 as revised). The perf comparison above still stands on its
|
|
311
|
+
// own for the full-document reload path.
|
|
257
312
|
|
|
258
313
|
function markActiveReady() {
|
|
259
314
|
onReady(active, function () {
|
|
@@ -7,22 +7,22 @@ import {
|
|
|
7
7
|
publishCredentialKey,
|
|
8
8
|
publishProviderFor,
|
|
9
9
|
runPublish
|
|
10
|
-
} from "./cli-
|
|
10
|
+
} from "./cli-7n848af2.js";
|
|
11
11
|
import {
|
|
12
12
|
FileTokenStore,
|
|
13
13
|
log
|
|
14
|
-
} from "./cli-
|
|
14
|
+
} from "./cli-0z6zf98d.js";
|
|
15
15
|
import {
|
|
16
16
|
UsageError,
|
|
17
17
|
rejectExtraPositionals,
|
|
18
18
|
rejectUnknownFlags
|
|
19
|
-
} from "./cli-
|
|
20
|
-
import"./cli-
|
|
19
|
+
} from "./cli-ydpf09d9.js";
|
|
20
|
+
import"./cli-eewynen9.js";
|
|
21
21
|
import"./cli-c41yr7he.js";
|
|
22
22
|
import {
|
|
23
23
|
EXIT_CODES
|
|
24
24
|
} from "./cli-46ycxe6r.js";
|
|
25
|
-
import"./cli-
|
|
25
|
+
import"./cli-w692g6dx.js";
|
|
26
26
|
import"./cli-v5mp7a6q.js";
|
|
27
27
|
import"./cli-37x76zdn.js";
|
|
28
28
|
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
executeAndReport
|
|
3
|
-
} from "./cli-
|
|
3
|
+
} from "./cli-7n848af2.js";
|
|
4
4
|
import {
|
|
5
5
|
log
|
|
6
|
-
} from "./cli-
|
|
6
|
+
} from "./cli-0z6zf98d.js";
|
|
7
7
|
import {
|
|
8
8
|
UsageError,
|
|
9
9
|
rejectExtraPositionals,
|
|
10
10
|
rejectUnknownFlags
|
|
11
|
-
} from "./cli-
|
|
12
|
-
import"./cli-
|
|
11
|
+
} from "./cli-ydpf09d9.js";
|
|
12
|
+
import"./cli-eewynen9.js";
|
|
13
13
|
import"./cli-c41yr7he.js";
|
|
14
14
|
import {
|
|
15
15
|
EXIT_CODES
|
|
16
16
|
} from "./cli-46ycxe6r.js";
|
|
17
|
-
import"./cli-
|
|
17
|
+
import"./cli-w692g6dx.js";
|
|
18
18
|
import"./cli-v5mp7a6q.js";
|
|
19
19
|
import"./cli-37x76zdn.js";
|
|
20
20
|
|
package/package.json
CHANGED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* app-heartbeat.ts — detect a running Gutterpress app before a CLI repair
|
|
3
|
-
* mutates the repo.
|
|
4
|
-
*
|
|
5
|
-
* WHY: `gutterpress repair` and the desktop's own recovery path both call into
|
|
6
|
-
* the recovery subsystem, but each runs in its own OS process. The per-repo
|
|
7
|
-
* FIFO lock (source-provider.ts) only serializes operations WITHIN a process,
|
|
8
|
-
* so `gutterpress repair` run from a terminal while the desktop has the same
|
|
9
|
-
* project open can race a live sync/snapshot. Rather than build a
|
|
10
|
-
* cross-process lock manager, the desktop leaves a small liveness marker
|
|
11
|
-
* behind while a project is open, and `repair` checks it before mutating.
|
|
12
|
-
*
|
|
13
|
-
* The marker lives at `<repoDir>/.git/gutterpress-app-heartbeat` (NOT in
|
|
14
|
-
* userData) so `repair`, given only a repo directory, can find it without any
|
|
15
|
-
* knowledge of the desktop's install. It is deliberately NOT a lock:
|
|
16
|
-
* - It never blocks the desktop itself, or any other repair run with --force.
|
|
17
|
-
* - It carries no locking semantics — just "an app touched this repo
|
|
18
|
-
* recently, maybe check before you assume it's idle".
|
|
19
|
-
* - It is written best-effort (a failed write must never surprise the
|
|
20
|
-
* author with a crash) and read best-effort (a missing/corrupt file
|
|
21
|
-
* means "no app appears to have this open", not an error).
|
|
22
|
-
*
|
|
23
|
-
* Two invariants that keep this file inert to the rest of the recovery
|
|
24
|
-
* subsystem:
|
|
25
|
-
* - Filename does not match any pattern `findLockCandidates` scans
|
|
26
|
-
* (recover-stale-lock.ts: fixed top-level `*.lock` names + `refs/**`) —
|
|
27
|
-
* so stale-lock recovery can never see or remove it.
|
|
28
|
-
* - It lives under `.git`, which `hasPendingChanges`/`listWorkdirChanges`
|
|
29
|
-
* (source-provider.ts) always ignores (`git.isIgnored` treats a `.git`
|
|
30
|
-
* path segment as ignored unconditionally) — so it can never look like an
|
|
31
|
-
* uncommitted author change.
|
|
32
|
-
*/
|
|
33
|
-
/**
|
|
34
|
-
* Fallback freshness window used only when a heartbeat carries no `ttlMs`
|
|
35
|
-
* (an older writer, or one with no cadence to report). Prefer a
|
|
36
|
-
* cadence-derived TTL — see {@link heartbeatTtlMs} — since the actual
|
|
37
|
-
* refresh cadence ranges 1 min–24 h (AUTO_SYNC_MIN/MAX_MINUTES,
|
|
38
|
-
* host-policy.ts) and a fixed 2-minute window reads a live app as "closed"
|
|
39
|
-
* for most of any longer cadence.
|
|
40
|
-
*/
|
|
41
|
-
export declare const APP_HEARTBEAT_FRESH_MS: number;
|
|
42
|
-
export interface AppHeartbeat {
|
|
43
|
-
/** PID of the process that wrote the heartbeat (diagnostic only — never
|
|
44
|
-
* used to `kill`/signal anything). */
|
|
45
|
-
pid: number;
|
|
46
|
-
/** Epoch ms the heartbeat was last (re)written. */
|
|
47
|
-
timestamp: number;
|
|
48
|
-
/**
|
|
49
|
-
* Freshness window (ms) the writer says applies to THIS heartbeat, derived
|
|
50
|
-
* from its own refresh cadence (see {@link heartbeatTtlMs}). When present,
|
|
51
|
-
* `isAppHeartbeatFresh` honors it instead of its `maxAgeMs` fallback param —
|
|
52
|
-
* the writer knows how often it actually refreshes; the reader doesn't.
|
|
53
|
-
*/
|
|
54
|
-
ttlMs?: number;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* The freshness window a heartbeat writer should stamp for a given refresh
|
|
58
|
-
* cadence (the actual interval this writer refreshes the marker on — e.g.
|
|
59
|
-
* `autoSyncDelayMs(settings.versionHistory)`). `null` (feature disabled, no
|
|
60
|
-
* periodic refresh at all) falls back to {@link APP_HEARTBEAT_FRESH_MS}.
|
|
61
|
-
*
|
|
62
|
-
* Standard heartbeat rule: TTL should comfortably exceed the refresh period
|
|
63
|
-
* so one missed/delayed tick doesn't read as "closed". Exported so both the
|
|
64
|
-
* desktop (the writer, which knows its own cadence) and tests can compute it
|
|
65
|
-
* consistently.
|
|
66
|
-
*/
|
|
67
|
-
export declare function heartbeatTtlMs(periodicMs: number | null): number;
|
|
68
|
-
/** Absolute path to the heartbeat marker for a repo. */
|
|
69
|
-
export declare function appHeartbeatPath(repoDir: string): string;
|
|
70
|
-
/**
|
|
71
|
-
* Write/refresh the heartbeat marker. Best-effort: a failed write (missing
|
|
72
|
-
* `.git` dir, read-only filesystem, …) is swallowed — it must never surface
|
|
73
|
-
* as an error to the author, since it is only ever a background liveness
|
|
74
|
-
* signal, not a required operation.
|
|
75
|
-
*
|
|
76
|
-
* `ttlMs`, when supplied, should be {@link heartbeatTtlMs} applied to this
|
|
77
|
-
* writer's OWN actual refresh cadence — it is stamped into the marker so a
|
|
78
|
-
* reader with no knowledge of that cadence still judges freshness correctly.
|
|
79
|
-
* Omit it (or pass `undefined`) when the caller has no cadence to report; the
|
|
80
|
-
* reader then falls back to its own `maxAgeMs` default.
|
|
81
|
-
*/
|
|
82
|
-
export declare function writeAppHeartbeat(repoDir: string, now?: number, pid?: number, ttlMs?: number): Promise<void>;
|
|
83
|
-
/**
|
|
84
|
-
* Remove the heartbeat marker (project close / app quit). Best-effort: an
|
|
85
|
-
* already-missing or unreadable file is not an error.
|
|
86
|
-
*/
|
|
87
|
-
export declare function removeAppHeartbeat(repoDir: string): Promise<void>;
|
|
88
|
-
/**
|
|
89
|
-
* Read + parse the heartbeat marker. Returns `null` when absent, unreadable,
|
|
90
|
-
* or corrupt (never throws — a damaged heartbeat is just "no signal").
|
|
91
|
-
*/
|
|
92
|
-
export declare function readAppHeartbeat(repoDir: string): Promise<AppHeartbeat | null>;
|
|
93
|
-
/**
|
|
94
|
-
* True when a FRESH heartbeat exists for this repo — i.e. the gutterpress app
|
|
95
|
-
* appears to have this project open right now. Absent/stale/corrupt all read
|
|
96
|
-
* as "not open" (fail open: `repair` should not block on ambiguous signal).
|
|
97
|
-
*
|
|
98
|
-
* Freshness window: the heartbeat's own stamped `ttlMs` (derived from the
|
|
99
|
-
* writer's actual refresh cadence — see {@link heartbeatTtlMs}) wins when
|
|
100
|
-
* present; `maxAgeMs` is only a fallback for a heartbeat with none.
|
|
101
|
-
*/
|
|
102
|
-
export declare function isAppHeartbeatFresh(repoDir: string, now?: number, maxAgeMs?: number): Promise<boolean>;
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/** Read one version's exact bytes by BLOB oid. */
|
|
2
|
-
export declare function readImageVersion(options: {
|
|
3
|
-
projectDir: string;
|
|
4
|
-
oid: string;
|
|
5
|
-
}): Promise<Uint8Array>;
|
|
6
|
-
/**
|
|
7
|
-
* Keep a specific version of a clashing image: write the blob's exact bytes
|
|
8
|
-
* to the file and snapshot. Serialized on the per-repo lock like every other
|
|
9
|
-
* mutating operation.
|
|
10
|
-
*/
|
|
11
|
-
export declare function keepImageVersion(options: {
|
|
12
|
-
projectDir: string;
|
|
13
|
-
path: string;
|
|
14
|
-
oid: string;
|
|
15
|
-
authorName?: string;
|
|
16
|
-
authorEmail?: string;
|
|
17
|
-
}): Promise<void>;
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Error/health classification for sync + repair — the SINGLE source of truth.
|
|
3
|
-
*
|
|
4
|
-
* 2026-08-14 simplification (owner directive): the 17-kind SyncErrorKind
|
|
5
|
-
* taxonomy and its 16 per-kind handlers are gone. Every structural problem a
|
|
6
|
-
* repo can have now has ONE answer — `repairRepo()` (repair.ts) — so
|
|
7
|
-
* classification collapses to three health verdicts:
|
|
8
|
-
*
|
|
9
|
-
* - null — healthy, nothing to do
|
|
10
|
-
* - "stale_lock" — a leftover lock old enough to sweep
|
|
11
|
-
* - "needs_repair" — anything structural (missing/corrupt `.git`, broken
|
|
12
|
-
* ref store, detached HEAD, interrupted merge/rebase/
|
|
13
|
-
* cherry-pick left by an external tool)
|
|
14
|
-
*
|
|
15
|
-
* The transport decoders (auth/offline/insecure) and the merge/push guards
|
|
16
|
-
* used by sync.ts remain here unchanged — outcome mapping, not repair.
|
|
17
|
-
* This module is pure — no I/O, no side effects.
|
|
18
|
-
*/
|
|
19
|
-
import type { RepoHealth } from "./types.ts";
|
|
20
|
-
/**
|
|
21
|
-
* Minimum age before a leftover git lock counts as STALE. locks.ts imports
|
|
22
|
-
* this same constant as its sweep threshold, so preflight and sweep can never
|
|
23
|
-
* disagree: a lock young enough to pass preflight is exactly a lock the sweep
|
|
24
|
-
* would defer ("a live process may still hold it").
|
|
25
|
-
*/
|
|
26
|
-
export declare const STALE_LOCK_MIN_AGE_MS: number;
|
|
27
|
-
/** The collapsed repair taxonomy — see the module header. */
|
|
28
|
-
export type RepairNeed = "stale_lock" | "needs_repair";
|
|
29
|
-
/**
|
|
30
|
-
* Thrown by sync's structural preflight when the repo must be repaired before
|
|
31
|
-
* any sync work can safely run. The `code` string is the STABLE contract
|
|
32
|
-
* hosts may match on across the dynamic-import boundary (where `instanceof`
|
|
33
|
-
* is unreliable); `kind` says whether a lock sweep suffices or the full
|
|
34
|
-
* repair pipeline is needed.
|
|
35
|
-
*/
|
|
36
|
-
export declare class RepoNeedsRecoveryError extends Error {
|
|
37
|
-
readonly kind: RepairNeed;
|
|
38
|
-
readonly code = "RepoNeedsRecovery";
|
|
39
|
-
constructor(kind: RepairNeed);
|
|
40
|
-
}
|
|
41
|
-
/** Type guard for {@link RepoNeedsRecoveryError} (matches on the stable code). */
|
|
42
|
-
export declare function isRepoNeedsRecoveryError(e: unknown): e is RepoNeedsRecoveryError;
|
|
43
|
-
/**
|
|
44
|
-
* Thrown by transport.ts's onAuth when a stored credential EXISTS but the
|
|
45
|
-
* remote URL fails isCredentialTransmissionSafe (non-loopback http). Loud and
|
|
46
|
-
* typed on purpose: the old behavior (silently withholding the credential)
|
|
47
|
-
* surfaced as a 401 → "auth" → "reconnect" loop. The `code` string is the
|
|
48
|
-
* STABLE contract (matchable across dynamic-import boundaries).
|
|
49
|
-
*/
|
|
50
|
-
export declare class InsecureTransportError extends Error {
|
|
51
|
-
readonly code = "InsecureTransport";
|
|
52
|
-
constructor();
|
|
53
|
-
}
|
|
54
|
-
/** Type guard for {@link InsecureTransportError} (matches on the stable code). */
|
|
55
|
-
export declare function isInsecureTransportError(e: unknown): e is InsecureTransportError;
|
|
56
|
-
export declare function isPushRejected(e: unknown): boolean;
|
|
57
|
-
/** Type guard exposing MergeConflictError's per-file payload (converge-merge). */
|
|
58
|
-
export declare function isMergeConflictError(e: unknown): e is {
|
|
59
|
-
data: {
|
|
60
|
-
filepaths: string[];
|
|
61
|
-
bothModified: string[];
|
|
62
|
-
deleteByUs: string[];
|
|
63
|
-
deleteByTheirs: string[];
|
|
64
|
-
};
|
|
65
|
-
};
|
|
66
|
-
/**
|
|
67
|
-
* Unrelated histories — the local project and the configured online project
|
|
68
|
-
* share no common starting point. Sync surfaces this as a plain setup error
|
|
69
|
-
* (a wrong online address must never be silently spliced into the book).
|
|
70
|
-
*/
|
|
71
|
-
export declare function isUnrelatedHistories(e: unknown): boolean;
|
|
72
|
-
export declare function classifyTransportFailure(e: unknown): "auth_required" | "network_unavailable" | "insecure_transport" | null;
|
|
73
|
-
/**
|
|
74
|
-
* True when a thrown error smells like LOCAL repo corruption (unreadable
|
|
75
|
-
* objects/refs/index) rather than a transport or logic failure — the signal
|
|
76
|
-
* for a host's mid-sync catch to run `repairRepo()`. NOTE on NotFoundError
|
|
77
|
-
* ambiguity: a transport 404 also surfaces as NotFoundError, but
|
|
78
|
-
* transport.ts's fetchRemoteTip rewrites those to an HttpError(401) BEFORE
|
|
79
|
-
* they can reach this heuristic, so a raw NotFoundError here is a LOCAL
|
|
80
|
-
* ref-resolution failure.
|
|
81
|
-
*/
|
|
82
|
-
export declare function isLikelyRepoCorruption(e: unknown): boolean;
|
|
83
|
-
/**
|
|
84
|
-
* Classify a repo's structural condition from a RepoHealth snapshot.
|
|
85
|
-
* Returns null for a healthy repo, "stale_lock" when the only problem is a
|
|
86
|
-
* sweepable lock, and "needs_repair" for everything structural — the repair
|
|
87
|
-
* pipeline (repair.ts) handles every structural case in one ordered pass, so
|
|
88
|
-
* finer distinctions buy nothing.
|
|
89
|
-
*
|
|
90
|
-
* `minLockAgeMs` gates the stale-lock verdict: at preflight (the default) a
|
|
91
|
-
* younger lock is treated as healthy because a live process may still hold
|
|
92
|
-
* it. Error-path callers pass 0: a lock that just made a sync THROW is worth
|
|
93
|
-
* routing regardless of age (the sweep still defers while it is fresh).
|
|
94
|
-
*/
|
|
95
|
-
export declare function classifyFromHealth(health: RepoHealth, opts?: {
|
|
96
|
-
minLockAgeMs?: number;
|
|
97
|
-
}): RepairNeed | null;
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Preflight repo health probe for the sync-recovery subsystem.
|
|
3
|
-
*
|
|
4
|
-
* inspectRepo() reads local filesystem state ONLY — no network, no git objects,
|
|
5
|
-
* never throws. It returns a RepoHealth snapshot that the classifier and policy
|
|
6
|
-
* lookup use to decide which recovery path to take.
|
|
7
|
-
*
|
|
8
|
-
* Reuses:
|
|
9
|
-
* - gitDirFor (source-provider.ts) for the .git dir path
|
|
10
|
-
* - hasPendingChanges (source-provider.ts) for local-changes detection
|
|
11
|
-
*
|
|
12
|
-
* Notes on two health facts:
|
|
13
|
-
* - hasGitDir is true whenever `.git/` EXISTS, even with a missing/corrupt
|
|
14
|
-
* HEAD (a damaged repo is still a repo — see the inline note at the check).
|
|
15
|
-
* - hasStaleLock uses the lock sweep's OWN scanner
|
|
16
|
-
* (findLockCandidates in locks.ts) — one implementation, so
|
|
17
|
-
* health and handler can never disagree about which locks exist.
|
|
18
|
-
*
|
|
19
|
-
* All probes are best-effort and throw-free (the caller must never see an
|
|
20
|
-
* exception from a preflight probe).
|
|
21
|
-
*/
|
|
22
|
-
import type { LogData } from "../operation-log.ts";
|
|
23
|
-
import type { RepairNeed } from "./classify.ts";
|
|
24
|
-
import type { RepoHealth } from "./types.ts";
|
|
25
|
-
/**
|
|
26
|
-
* Probe the local repository and return a RepoHealth snapshot.
|
|
27
|
-
* Never throws — on any error the relevant flag is set conservatively.
|
|
28
|
-
*
|
|
29
|
-
* `checkLocalChanges: false` skips the hasPendingChanges working-tree walk
|
|
30
|
-
* (the one non-trivial probe) and reports hasLocalChanges=false. Use it when
|
|
31
|
-
* only the structural flags matter — e.g. syncProject's preflight, whose
|
|
32
|
-
* pull step immediately performs the same walk anyway (sync-simplicity
|
|
33
|
-
* mandate: no redundant walks on the hot path).
|
|
34
|
-
*/
|
|
35
|
-
export declare function inspectRepo(ctx: {
|
|
36
|
-
repoDir: string;
|
|
37
|
-
source?: import("../../project-source.ts").ProjectSource;
|
|
38
|
-
}, opts?: {
|
|
39
|
-
checkLocalChanges?: boolean;
|
|
40
|
-
}): Promise<RepoHealth>;
|
|
41
|
-
/**
|
|
42
|
-
* Confirm the repo's object store is actually readable: resolve HEAD, read
|
|
43
|
-
* its commit, and read its root tree. Throws when any step fails (missing or
|
|
44
|
-
* corrupt object/ref); resolves when the repo is fully readable.
|
|
45
|
-
*
|
|
46
|
-
* Exported so the repair
|
|
47
|
-
* command's diagnosis step and any host check share ONE implementation. `inspectRepo`'s health flags are
|
|
48
|
-
* all filesystem-presence checks (no object is ever read), so they cannot
|
|
49
|
-
* detect object-store corruption on their own — callers that need to catch
|
|
50
|
-
* unreadable objects/refs must run this probe and feed a caught error through
|
|
51
|
-
* `isLikelyRepoCorruption` (classify.ts) to decide whether to repairRepo().
|
|
52
|
-
*/
|
|
53
|
-
export declare function verifyRepoReadable(dir: string): Promise<void>;
|
|
54
|
-
/**
|
|
55
|
-
* True when HEAD names a branch that simply has no commits yet — a fresh
|
|
56
|
-
* `git init` before the first snapshot. In that state `verifyRepoReadable`
|
|
57
|
-
* throws the SAME NotFoundError as a damaged ref store, but the repo is
|
|
58
|
-
* healthy, not corrupt. The distinguishing signal is the object store: a
|
|
59
|
-
* fresh repo has NO objects at all, while ref damage on a real repo leaves
|
|
60
|
-
* loose objects and/or packfiles behind.
|
|
61
|
-
*/
|
|
62
|
-
export declare function isUnbornRepo(repoDir: string): boolean;
|
|
63
|
-
/**
|
|
64
|
-
* The SINGLE health signal that drove classification. Derived from the KIND
|
|
65
|
-
* classifyFromHealth returned (a pure mapping — it cannot drift from the
|
|
66
|
-
* classifier's decision order, because it never re-implements it).
|
|
67
|
-
*/
|
|
68
|
-
export declare function preflightStructuralReason(kind: RepairNeed | null): string;
|
|
69
|
-
/**
|
|
70
|
-
* Build a flat, secret-free record of the preflight decision inputs for the
|
|
71
|
-
* operation log. Every health boolean is recorded (so support can see the FULL
|
|
72
|
-
* picture, not just the one-word kind), plus the opened dir vs repo root, the
|
|
73
|
-
* chosen kind, and the single reason that drove it.
|
|
74
|
-
*/
|
|
75
|
-
export declare function buildPreflightDiagnostics(openedDir: string, repoDir: string, health: RepoHealth, kind: RepairNeed | null): LogData;
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
export interface LockCandidate {
|
|
2
|
-
/** Absolute path to the lock file. */
|
|
3
|
-
path: string;
|
|
4
|
-
/** Age in ms relative to `now` (now - mtime). */
|
|
5
|
-
ageMs: number;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Gather every candidate lock file (top-level + refs/**) with its age.
|
|
9
|
-
* Locks that vanish between discovery and stat are simply dropped (a racing
|
|
10
|
-
* delete is fine to ignore). Never throws.
|
|
11
|
-
*/
|
|
12
|
-
export declare function findLockCandidates(gitDir: string, now: number): Promise<LockCandidate[]>;
|
|
13
|
-
/**
|
|
14
|
-
* Delete every stale lock under `gitDir` — but ONLY when every candidate is
|
|
15
|
-
* stale (one fresh lock defers the whole sweep; a live process may hold it).
|
|
16
|
-
*
|
|
17
|
-
* Returns "clean" (nothing found), "swept" (all removed), or "fresh"
|
|
18
|
-
* (deferred — the caller should retry later).
|
|
19
|
-
*/
|
|
20
|
-
export declare function sweepStaleLocks(gitDir: string, now?: number, minAgeMs?: number): Promise<"clean" | "swept" | "fresh">;
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { HostCredential, TokenStore } from "../token-store.ts";
|
|
2
|
-
export interface RepairOptions {
|
|
3
|
-
projectDir: string;
|
|
4
|
-
/** Explicit credential; wins over the token store (nuclear re-clone only). */
|
|
5
|
-
credential?: HostCredential;
|
|
6
|
-
/** Host-keyed store used to resolve the credential for the remote's host. */
|
|
7
|
-
tokenStore?: TokenStore;
|
|
8
|
-
authorName?: string;
|
|
9
|
-
authorEmail?: string;
|
|
10
|
-
/** Operation log (same file sync writes to). */
|
|
11
|
-
logFile?: string;
|
|
12
|
-
}
|
|
13
|
-
export interface RepairResult {
|
|
14
|
-
status: "repaired" | "retry_later" | "failed";
|
|
15
|
-
/** One author-language sentence describing the outcome. */
|
|
16
|
-
message: string;
|
|
17
|
-
/** Author-language lines describing what was done (for the log/details). */
|
|
18
|
-
actions: string[];
|
|
19
|
-
/** Present when the re-clone ran: on-disk backup of the old `.git`. */
|
|
20
|
-
damagedGitBackupPath?: string;
|
|
21
|
-
/** Suggested delay before retrying (present on "retry_later"). */
|
|
22
|
-
retryAfterMs?: number;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Repair a damaged repository. See the module header for the pipeline and
|
|
26
|
-
* invariants. Never throws for expected outcomes — everything is reported
|
|
27
|
-
* through {@link RepairResult}.
|
|
28
|
-
*/
|
|
29
|
-
export declare function repairRepo(options: RepairOptions): Promise<RepairResult>;
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared types for the repo health probe + repair pipeline.
|
|
3
|
-
*
|
|
4
|
-
* 2026-08-14 simplification (owner directive): the old handler-dispatch type
|
|
5
|
-
* surface (SyncErrorKind taxonomy, RecoveryContext, RecoveryResult,
|
|
6
|
-
* ManualGuidance, confirmation gates, fault injection) is gone — repair is
|
|
7
|
-
* one automatic pipeline (repair.ts) and needs none of it. What remains is
|
|
8
|
-
* the health snapshot inspect.ts produces.
|
|
9
|
-
*/
|
|
10
|
-
export interface RepoHealth {
|
|
11
|
-
/** True when .git/ exists and looks like a valid git dir. */
|
|
12
|
-
hasGitDir: boolean;
|
|
13
|
-
/** Current branch name, or undefined when HEAD is detached. */
|
|
14
|
-
currentBranch?: string;
|
|
15
|
-
/** True when HEAD is detached (no named branch). */
|
|
16
|
-
isDetachedHead: boolean;
|
|
17
|
-
/**
|
|
18
|
-
* True when HEAD (or the ref store) could not even be READ — distinct from
|
|
19
|
-
* a clean detached HEAD, where HEAD resolves fine but points at a commit
|
|
20
|
-
* instead of a branch. Routes to the re-clone step of repairRepo.
|
|
21
|
-
*/
|
|
22
|
-
headUnreadable: boolean;
|
|
23
|
-
/**
|
|
24
|
-
* True when a leftover git lock file exists (a previous operation may have
|
|
25
|
-
* died). Detects any of index.lock, HEAD.lock, config.lock, packed-refs.lock,
|
|
26
|
-
* or a per-ref refs/**\/*.lock — the same set the lock sweep scans.
|
|
27
|
-
*/
|
|
28
|
-
hasStaleLock: boolean;
|
|
29
|
-
/** How old the YOUNGEST detected lock file is in milliseconds, when present. */
|
|
30
|
-
lockAgeMs?: number;
|
|
31
|
-
/** True when a MERGE_HEAD file exists (interrupted merge). */
|
|
32
|
-
hasInterruptedMerge: boolean;
|
|
33
|
-
/** True when a rebase-in-progress directory exists. */
|
|
34
|
-
hasInterruptedRebase: boolean;
|
|
35
|
-
/** True when CHERRY_PICK_HEAD exists. */
|
|
36
|
-
hasInterruptedCherryPick: boolean;
|
|
37
|
-
/** True when the working tree has uncommitted changes. */
|
|
38
|
-
hasLocalChanges: boolean;
|
|
39
|
-
}
|