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.
Files changed (57) hide show
  1. package/README.md +3 -16
  2. package/dist/api/index.d.ts +10 -19
  3. package/dist/api/index.js +8 -64
  4. package/dist/{audit-cvarpa72.js → audit-dce99css.js} +5 -5
  5. package/dist/{build-6r502chw.js → build-bj4x0727.js} +5 -5
  6. package/dist/checks/source/index.d.ts +1 -0
  7. package/dist/checks/source/merge-markers.d.ts +3 -0
  8. package/dist/{cli-cqtggsng.js → cli-0z6zf98d.js} +2 -47
  9. package/dist/{cli-eq5naw4m.js → cli-7n848af2.js} +370 -1249
  10. package/dist/{cli-zfcryxg8.js → cli-eewynen9.js} +97 -60
  11. package/dist/{cli-k1065rkg.js → cli-w692g6dx.js} +75 -29
  12. package/dist/{cli-149edp6b.js → cli-ydpf09d9.js} +1 -1
  13. package/dist/cli.js +14 -15
  14. package/dist/{doctor-akvxbtjb.js → doctor-7nd6n2aa.js} +2 -2
  15. package/dist/{engine-ft4cr3ep.js → engine-57vw8zpd.js} +1 -1
  16. package/dist/{engine-b159tbns.js → engine-8kdwy0jz.js} +2 -2
  17. package/dist/{gutterpress-viewer-te8g5grx.js → gutterpress-viewer-tma1qwz2.js} +57 -15
  18. package/dist/{index-ycpvr0am.js → index-7hrvksrb.js} +4 -4
  19. package/dist/{index-mdefp0y5.js → index-dtrftayr.js} +75 -27
  20. package/dist/{index-ge7q9xj3.js → index-h1x7krkg.js} +771 -1482
  21. package/dist/{index-wq3r5pj7.js → index-p0ebr7dh.js} +94 -57
  22. package/dist/index.js +9 -65
  23. package/dist/lib/git-fs.d.ts +43 -0
  24. package/dist/lib/host-policy.d.ts +14 -2
  25. package/dist/lib/remote-auth/clone.d.ts +0 -21
  26. package/dist/lib/remote-auth/converge-merge.d.ts +24 -7
  27. package/dist/lib/remote-auth/generic-auth.d.ts +0 -22
  28. package/dist/lib/remote-auth/git-http.d.ts +1 -1
  29. package/dist/lib/remote-auth/github-auth.d.ts +2 -22
  30. package/dist/lib/remote-auth/operation-log.d.ts +4 -9
  31. package/dist/lib/remote-auth/sync-messages.d.ts +35 -12
  32. package/dist/lib/remote-auth/sync-types.d.ts +30 -101
  33. package/dist/lib/remote-auth/sync.d.ts +19 -21
  34. package/dist/lib/remote-auth/token-store.d.ts +0 -10
  35. package/dist/lib/remote-auth/transport.d.ts +42 -40
  36. package/dist/lib/source-provider.d.ts +12 -0
  37. package/dist/{lint-p2sw53d9.js → lint-c8ze8sqw.js} +5 -5
  38. package/dist/{new-hvq0x91q.js → new-xy2qc300.js} +5 -5
  39. package/dist/{plugin-pssmk0dx.js → plugin-g9hwzjsm.js} +5 -5
  40. package/dist/{preflight-4j00yd0g.js → preflight-prcvdvmk.js} +5 -5
  41. package/dist/{preview-bridge-fz7vpk8m.js → preview-bridge-2nnrs2hy.js} +15 -0
  42. package/dist/{preview-d4s6gk6p.js → preview-h479apj9.js} +5 -5
  43. package/dist/{preview-interface-0ssk8bmm.js → preview-interface-2f5v96hd.js} +411 -88
  44. package/dist/{preview-shell-c5mfa3q0.js → preview-shell-kyjq59d9.js} +80 -25
  45. package/dist/{publish-tkc26en7.js → publish-emk644a8.js} +5 -5
  46. package/dist/{source-provider-3tcj6qg2.js → source-provider-2j5x2hyw.js} +1 -1
  47. package/dist/{source-provider-vanafrt9.js → source-provider-kn41jmbs.js} +1 -1
  48. package/dist/{validate-w9vfefrw.js → validate-be54bdc6.js} +5 -5
  49. package/package.json +1 -1
  50. package/dist/lib/app-heartbeat.d.ts +0 -102
  51. package/dist/lib/remote-auth/image-clash.d.ts +0 -17
  52. package/dist/lib/remote-auth/recovery/classify.d.ts +0 -97
  53. package/dist/lib/remote-auth/recovery/inspect.d.ts +0 -75
  54. package/dist/lib/remote-auth/recovery/locks.d.ts +0 -20
  55. package/dist/lib/remote-auth/recovery/repair.d.ts +0 -29
  56. package/dist/lib/remote-auth/recovery/types.d.ts +0 -39
  57. 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 (active === hotReloadFrame && data && data.type === 'gutterpress:event' && data.name === 'ready') return;
37
- if (active === hotReloadFrame && data && data.type === 'gutterpress:event' && data.name === 'renderingComplete') {
38
- var detail = {}, sourceDetail = data.detail || {};
39
- for (var key in sourceDetail) {
40
- if (Object.prototype.hasOwnProperty.call(sourceDetail, key)) detail[key] = sourceDetail[key];
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
- if (api && typeof api.refresh === 'function') api.refresh();
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): grafting a fresh
247
- // `.gutterpress-chapter` node in and calling `Gutterpress.refresh()` is not
248
- // sound, because `refresh()` -> `relayout()` only re-`measure()`s the
249
- // EXISTING strips it never re-runs `buildStrips()`/`explodeChildren()`.
250
- // So any page context the edit introduces (a new `@page`/`page:`
251
- // assignment inside the chapter) is silently dropped: measured 2 preview
252
- // pages where the same content prints 3 a preview<->PDF divergence, the
253
- // worst failure this project can produce. It also bought nothing: measured
254
- // end-to-end (file write -> change visible, 5 samples, 34pp field guide)
255
- // the plain full reload (`swap`, below) is 509ms avg vs the incremental
256
- // splice's 998ms avg. Every content-update now goes straight to `swap()`.
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-eq5naw4m.js";
10
+ } from "./cli-7n848af2.js";
11
11
  import {
12
12
  FileTokenStore,
13
13
  log
14
- } from "./cli-cqtggsng.js";
14
+ } from "./cli-0z6zf98d.js";
15
15
  import {
16
16
  UsageError,
17
17
  rejectExtraPositionals,
18
18
  rejectUnknownFlags
19
- } from "./cli-149edp6b.js";
20
- import"./cli-zfcryxg8.js";
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-k1065rkg.js";
25
+ import"./cli-w692g6dx.js";
26
26
  import"./cli-v5mp7a6q.js";
27
27
  import"./cli-37x76zdn.js";
28
28
 
@@ -16,7 +16,7 @@ import {
16
16
  snapshotWorkingTreeUnlocked,
17
17
  stageChanges,
18
18
  withRepoLock
19
- } from "./cli-k1065rkg.js";
19
+ } from "./cli-w692g6dx.js";
20
20
  import"./cli-v5mp7a6q.js";
21
21
  import"./cli-37x76zdn.js";
22
22
  export {
@@ -16,7 +16,7 @@ import {
16
16
  snapshotWorkingTreeUnlocked,
17
17
  stageChanges,
18
18
  withRepoLock
19
- } from "./index-mdefp0y5.js";
19
+ } from "./index-dtrftayr.js";
20
20
  import"./index-v5mp7a6q.js";
21
21
  import"./index-37x76zdn.js";
22
22
  export {
@@ -1,20 +1,20 @@
1
1
  import {
2
2
  executeAndReport
3
- } from "./cli-eq5naw4m.js";
3
+ } from "./cli-7n848af2.js";
4
4
  import {
5
5
  log
6
- } from "./cli-cqtggsng.js";
6
+ } from "./cli-0z6zf98d.js";
7
7
  import {
8
8
  UsageError,
9
9
  rejectExtraPositionals,
10
10
  rejectUnknownFlags
11
- } from "./cli-149edp6b.js";
12
- import"./cli-zfcryxg8.js";
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-k1065rkg.js";
17
+ import"./cli-w692g6dx.js";
18
18
  import"./cli-v5mp7a6q.js";
19
19
  import"./cli-37x76zdn.js";
20
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gutterpress",
3
- "version": "0.10.0",
3
+ "version": "0.10.1-beta.2",
4
4
  "description": "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
5
5
  "author": "itlackey",
6
6
  "license": "MPL-2.0",
@@ -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
- }