gutterpress 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/README.md +422 -0
  2. package/dist/CGATS21_CRPC1-g0e3k7kr.icc +0 -0
  3. package/dist/api/index.d.ts +76 -0
  4. package/dist/api/index.js +272 -0
  5. package/dist/assets/preview/scripts/pagedjs-bridge.d.ts +1 -0
  6. package/dist/assets/preview/scripts/pagedjs-interface.d.ts +1 -0
  7. package/dist/assets/preview/scripts/preview-shell.d.ts +1 -0
  8. package/dist/audit-1jrwwacz.js +66 -0
  9. package/dist/build-5s92kgpc.js +77 -0
  10. package/dist/chapter-01-6fn9pvd0.md +31 -0
  11. package/dist/chapter-01-7jwqg1ct.md +20 -0
  12. package/dist/chapter-01-jj8ftn4c.md +29 -0
  13. package/dist/chapter-01-rkpyvjna.md +26 -0
  14. package/dist/checks/asset/approved-fonts.d.ts +3 -0
  15. package/dist/checks/asset/extensions.d.ts +41 -0
  16. package/dist/checks/asset/font-license.d.ts +3 -0
  17. package/dist/checks/asset/image-alpha.d.ts +3 -0
  18. package/dist/checks/asset/image-color-space.d.ts +3 -0
  19. package/dist/checks/asset/image-file-size.d.ts +3 -0
  20. package/dist/checks/asset/image-resolution.d.ts +3 -0
  21. package/dist/checks/asset/image-tac.d.ts +3 -0
  22. package/dist/checks/asset/index.d.ts +7 -0
  23. package/dist/checks/formatter.d.ts +3 -0
  24. package/dist/checks/heuristic/index.d.ts +4 -0
  25. package/dist/checks/heuristic/layer-count.d.ts +3 -0
  26. package/dist/checks/heuristic/placement-variance.d.ts +3 -0
  27. package/dist/checks/heuristic/section-density.d.ts +3 -0
  28. package/dist/checks/heuristic/text-density.d.ts +3 -0
  29. package/dist/checks/pdf/bleed.d.ts +3 -0
  30. package/dist/checks/pdf/bookmarks.d.ts +3 -0
  31. package/dist/checks/pdf/color-spaces.d.ts +3 -0
  32. package/dist/checks/pdf/cross-refs.d.ts +3 -0
  33. package/dist/checks/pdf/embedded-fonts.d.ts +3 -0
  34. package/dist/checks/pdf/image-resolution.d.ts +3 -0
  35. package/dist/checks/pdf/index.d.ts +15 -0
  36. package/dist/checks/pdf/ink-coverage.d.ts +3 -0
  37. package/dist/checks/pdf/page-labels.d.ts +3 -0
  38. package/dist/checks/pdf/page-size.d.ts +3 -0
  39. package/dist/checks/pdf/pdfx-markers.d.ts +3 -0
  40. package/dist/checks/pdf/pdfx-metadata.d.ts +3 -0
  41. package/dist/checks/pdf/pdfx-structure.d.ts +6 -0
  42. package/dist/checks/pdf/qpdf-structure.d.ts +3 -0
  43. package/dist/checks/pdf/rasterized-pages.d.ts +3 -0
  44. package/dist/checks/pdf/toc-links.d.ts +3 -0
  45. package/dist/checks/pdf/transparency.d.ts +3 -0
  46. package/dist/checks/policy.d.ts +61 -0
  47. package/dist/checks/register-builtins.d.ts +17 -0
  48. package/dist/checks/registry.d.ts +40 -0
  49. package/dist/checks/runner.d.ts +25 -0
  50. package/dist/checks/source/accessibility-alt-text.d.ts +3 -0
  51. package/dist/checks/source/accessibility-heading-order.d.ts +3 -0
  52. package/dist/checks/source/config-file.d.ts +8 -0
  53. package/dist/checks/source/htmlhint.d.ts +3 -0
  54. package/dist/checks/source/index.d.ts +6 -0
  55. package/dist/checks/source/local-refs.d.ts +3 -0
  56. package/dist/checks/source/markdownlint.d.ts +3 -0
  57. package/dist/checks/source/stylelint.d.ts +3 -0
  58. package/dist/checks/tool-check.d.ts +23 -0
  59. package/dist/checks/types.d.ts +52 -0
  60. package/dist/cli-1tyj4bk3.js +124 -0
  61. package/dist/cli-37x76zdn.js +4 -0
  62. package/dist/cli-506tg37g.js +26 -0
  63. package/dist/cli-5bxx0c4y.js +464 -0
  64. package/dist/cli-7ykx7pp0.js +12378 -0
  65. package/dist/cli-h7phg3ka.js +746 -0
  66. package/dist/cli-hvqjs342.js +284 -0
  67. package/dist/cli-yja077f6.js +92 -0
  68. package/dist/cli.js +157 -0
  69. package/dist/constants.d.ts +17 -0
  70. package/dist/doctor-25zjt8dz.js +57 -0
  71. package/dist/favicon-wkbm9cjn.ico +0 -0
  72. package/dist/git-http-yrb4ag6z.js +17 -0
  73. package/dist/index-1tyj4bk3.js +124 -0
  74. package/dist/index-37x76zdn.js +4 -0
  75. package/dist/index-3h3fef7r.js +15218 -0
  76. package/dist/index-5bxx0c4y.js +464 -0
  77. package/dist/index-yja077f6.js +92 -0
  78. package/dist/index.d.ts +32 -0
  79. package/dist/index.js +310 -0
  80. package/dist/lib/app-heartbeat.d.ts +102 -0
  81. package/dist/lib/asset-inline.d.ts +73 -0
  82. package/dist/lib/browser-pool.d.ts +17 -0
  83. package/dist/lib/build-error.d.ts +52 -0
  84. package/dist/lib/build-fingerprint.d.ts +15 -0
  85. package/dist/lib/build-preflight.d.ts +43 -0
  86. package/dist/lib/build-runner.d.ts +161 -0
  87. package/dist/lib/build-staging.d.ts +64 -0
  88. package/dist/lib/chromium.d.ts +20 -0
  89. package/dist/lib/cli-args.d.ts +45 -0
  90. package/dist/lib/desktop.d.ts +14 -0
  91. package/dist/lib/diagnostics.d.ts +64 -0
  92. package/dist/lib/embedded-assets.d.ts +29 -0
  93. package/dist/lib/exec.d.ts +84 -0
  94. package/dist/lib/fetch-timeout.d.ts +41 -0
  95. package/dist/lib/ghostscript.d.ts +48 -0
  96. package/dist/lib/host-policy.d.ts +75 -0
  97. package/dist/lib/image-inspect.d.ts +32 -0
  98. package/dist/lib/install-hints.d.ts +21 -0
  99. package/dist/lib/lint-runner.d.ts +10 -0
  100. package/dist/lib/manifest-config.d.ts +31 -0
  101. package/dist/lib/manifest-doc.d.ts +25 -0
  102. package/dist/lib/manifest.d.ts +41 -0
  103. package/dist/lib/markdown/assemble.d.ts +78 -0
  104. package/dist/lib/markdown/chapter-id.d.ts +24 -0
  105. package/dist/lib/markdown/images.d.ts +34 -0
  106. package/dist/lib/markdown/index.d.ts +78 -0
  107. package/dist/lib/markdown/markdown-it-paged.d.ts +30 -0
  108. package/dist/lib/markdown/plugins.d.ts +69 -0
  109. package/dist/lib/markdown/renderer.d.ts +111 -0
  110. package/dist/lib/npm-plugin-installer.d.ts +33 -0
  111. package/dist/lib/open-path.d.ts +5 -0
  112. package/dist/lib/output-paths.d.ts +23 -0
  113. package/dist/lib/pagedjs-marker.d.ts +42 -0
  114. package/dist/lib/pagedjs.d.ts +26 -0
  115. package/dist/lib/pagination.d.ts +169 -0
  116. package/dist/lib/pdf-inspect.d.ts +123 -0
  117. package/dist/lib/pdf-parse.d.ts +57 -0
  118. package/dist/lib/plugin-manager.d.ts +108 -0
  119. package/dist/lib/plugin-vendor.d.ts +107 -0
  120. package/dist/lib/presets.d.ts +45 -0
  121. package/dist/lib/printsafe.d.ts +17 -0
  122. package/dist/lib/project-scaffold.d.ts +116 -0
  123. package/dist/lib/project-source.d.ts +140 -0
  124. package/dist/lib/project-templates.d.ts +49 -0
  125. package/dist/lib/publish/butler.d.ts +10 -0
  126. package/dist/lib/publish/command-runner.d.ts +18 -0
  127. package/dist/lib/publish/connect.d.ts +23 -0
  128. package/dist/lib/publish/manifest-publish.d.ts +9 -0
  129. package/dist/lib/publish/providers/azure-swa.d.ts +3 -0
  130. package/dist/lib/publish/providers/drivethrurpg.d.ts +10 -0
  131. package/dist/lib/publish/providers/guided.d.ts +3 -0
  132. package/dist/lib/publish/providers/itch.d.ts +5 -0
  133. package/dist/lib/publish/providers/kdp.d.ts +9 -0
  134. package/dist/lib/publish/providers/shopify.d.ts +5 -0
  135. package/dist/lib/publish/registry.d.ts +5 -0
  136. package/dist/lib/publish/run-publish.d.ts +30 -0
  137. package/dist/lib/publish/selections.d.ts +25 -0
  138. package/dist/lib/publish/types.d.ts +258 -0
  139. package/dist/lib/remote-auth/clone.d.ts +87 -0
  140. package/dist/lib/remote-auth/conflict-resolution.d.ts +29 -0
  141. package/dist/lib/remote-auth/diagnose.d.ts +77 -0
  142. package/dist/lib/remote-auth/generic-auth.d.ts +97 -0
  143. package/dist/lib/remote-auth/git-http.d.ts +85 -0
  144. package/dist/lib/remote-auth/github-auth.d.ts +69 -0
  145. package/dist/lib/remote-auth/github-repos.d.ts +64 -0
  146. package/dist/lib/remote-auth/operation-log.d.ts +66 -0
  147. package/dist/lib/remote-auth/recovery/abort-interrupted-operation.d.ts +103 -0
  148. package/dist/lib/remote-auth/recovery/backup.d.ts +113 -0
  149. package/dist/lib/remote-auth/recovery/classify.d.ts +109 -0
  150. package/dist/lib/remote-auth/recovery/context.d.ts +47 -0
  151. package/dist/lib/remote-auth/recovery/dispatch.d.ts +33 -0
  152. package/dist/lib/remote-auth/recovery/failsafe.d.ts +33 -0
  153. package/dist/lib/remote-auth/recovery/inspect.d.ts +74 -0
  154. package/dist/lib/remote-auth/recovery/manual-guidance.d.ts +28 -0
  155. package/dist/lib/remote-auth/recovery/outcome-mapping.d.ts +58 -0
  156. package/dist/lib/remote-auth/recovery/policy.d.ts +47 -0
  157. package/dist/lib/remote-auth/recovery/recover-auth.d.ts +42 -0
  158. package/dist/lib/remote-auth/recovery/recover-binary-conflict.d.ts +37 -0
  159. package/dist/lib/remote-auth/recovery/recover-corrupt-index.d.ts +40 -0
  160. package/dist/lib/remote-auth/recovery/recover-detached-head.d.ts +70 -0
  161. package/dist/lib/remote-auth/recovery/recover-interrupted-cherry-pick.d.ts +23 -0
  162. package/dist/lib/remote-auth/recovery/recover-interrupted-merge.d.ts +28 -0
  163. package/dist/lib/remote-auth/recovery/recover-interrupted-rebase.d.ts +39 -0
  164. package/dist/lib/remote-auth/recovery/recover-merge-conflict.d.ts +34 -0
  165. package/dist/lib/remote-auth/recovery/recover-missing-git-dir.d.ts +37 -0
  166. package/dist/lib/remote-auth/recovery/recover-missing-objects.d.ts +56 -0
  167. package/dist/lib/remote-auth/recovery/recover-network.d.ts +34 -0
  168. package/dist/lib/remote-auth/recovery/recover-non-fast-forward.d.ts +27 -0
  169. package/dist/lib/remote-auth/recovery/recover-stale-lock.d.ts +76 -0
  170. package/dist/lib/remote-auth/recovery/recover-unrelated-histories.d.ts +44 -0
  171. package/dist/lib/remote-auth/recovery/recover-wrong-remote.d.ts +35 -0
  172. package/dist/lib/remote-auth/recovery/types.d.ts +235 -0
  173. package/dist/lib/remote-auth/resolution-plan.d.ts +64 -0
  174. package/dist/lib/remote-auth/sync-messages.d.ts +26 -0
  175. package/dist/lib/remote-auth/sync-types.d.ts +225 -0
  176. package/dist/lib/remote-auth/sync.d.ts +45 -0
  177. package/dist/lib/remote-auth/test-access.d.ts +47 -0
  178. package/dist/lib/remote-auth/test-support/git-http-server.d.ts +71 -0
  179. package/dist/lib/remote-auth/token-store.d.ts +95 -0
  180. package/dist/lib/remote-auth/transport.d.ts +105 -0
  181. package/dist/lib/slug.d.ts +28 -0
  182. package/dist/lib/snippets.d.ts +39 -0
  183. package/dist/lib/source-provider.d.ts +244 -0
  184. package/dist/lib/static-serve.d.ts +54 -0
  185. package/dist/lib/style-resolver.d.ts +30 -0
  186. package/dist/lib/theme-import.d.ts +70 -0
  187. package/dist/lib/theme-manager.d.ts +128 -0
  188. package/dist/lib/tool-probe.d.ts +37 -0
  189. package/dist/lib/validation-exec.d.ts +47 -0
  190. package/dist/lib/validation-profile.d.ts +18 -0
  191. package/dist/lib/version.d.ts +9 -0
  192. package/dist/lint-f63nxjfj.js +65 -0
  193. package/dist/manifest-n1gh3qw5.yaml +9 -0
  194. package/dist/manifest-ryyg71nf.yaml +9 -0
  195. package/dist/manifest.schema-rn1dpdpn.json +564 -0
  196. package/dist/new-7b08a42h.js +105 -0
  197. package/dist/paged.polyfill-n95pbxfn.js +33288 -0
  198. package/dist/pagedjs-bridge-vn4hk9fx.js +111 -0
  199. package/dist/pagedjs-interface-80g3xa8p.js +498 -0
  200. package/dist/platform.d.ts +100 -0
  201. package/dist/plugin-wf2cbmwc.js +129 -0
  202. package/dist/preflight-tg4mpz1h.js +222 -0
  203. package/dist/preview/file-watcher.d.ts +174 -0
  204. package/dist/preview/http-server.d.ts +45 -0
  205. package/dist/preview/lifecycle.d.ts +67 -0
  206. package/dist/preview/server-context.d.ts +36 -0
  207. package/dist/preview-h2bwbb2x.js +102 -0
  208. package/dist/preview-shell-rghtfmc8.js +155 -0
  209. package/dist/project-source-0tg0r6vm.js +17 -0
  210. package/dist/publish-ztaet53r.js +233 -0
  211. package/dist/render.d.ts +20 -0
  212. package/dist/render.js +682 -0
  213. package/dist/repair-jkp4n3j8.js +171 -0
  214. package/dist/schema/manifest.types.d.ts +218 -0
  215. package/dist/server.d.ts +34 -0
  216. package/dist/source-provider-hp96b5jf.js +46 -0
  217. package/dist/test-helpers/testkit.d.ts +91 -0
  218. package/dist/theme-0yzvthe8.css +65 -0
  219. package/dist/theme-570zmh2t.css +58 -0
  220. package/dist/theme-6a2fngc8.json +6 -0
  221. package/dist/theme-cd6efzjw.json +6 -0
  222. package/dist/theme-nya4nqh6.css +70 -0
  223. package/dist/theme-tqfypc1w.json +6 -0
  224. package/dist/theme-xt0hjb1b.json +6 -0
  225. package/dist/theme-zz2ktzqs.css +60 -0
  226. package/dist/types.d.ts +31 -0
  227. package/dist/utils/file-utils.d.ts +53 -0
  228. package/dist/utils/logger.d.ts +59 -0
  229. package/dist/validate-k2drs19j.js +99 -0
  230. package/package.json +102 -0
@@ -0,0 +1,69 @@
1
+ import type { HostCredential } from "./token-store.ts";
2
+ /** What the host UI needs to show the user during the device flow. */
3
+ export interface DeviceCodeInfo {
4
+ /** The code the user types at the verification page, e.g. "ABCD-1234". */
5
+ userCode: string;
6
+ /** Where the user enters the code (https://github.com/login/device). */
7
+ verificationUri: string;
8
+ /** Seconds until the device code expires. */
9
+ expiresIn: number;
10
+ /** Minimum seconds between token polls. */
11
+ interval: number;
12
+ }
13
+ /** Host-supplied callbacks for the interactive connect flow (ADR 0006 D3). */
14
+ export interface HostCallbacks {
15
+ /** Called once with the code/URL so the UI can display them. */
16
+ onUserCode(info: DeviceCodeInfo): void;
17
+ /** Optional cancellation (user closed the dialog). */
18
+ signal?: AbortSignal;
19
+ }
20
+ /**
21
+ * Per-host auth-acquisition plugin contract (ADR 0006 D3 layer 3). github.com
22
+ * gets the device flow below; every other host gets the generic token flow
23
+ * (#14, not in this change).
24
+ */
25
+ export interface RemoteAuthProvider {
26
+ /** Does this provider handle the given remote host? */
27
+ matches(origin: URL): boolean;
28
+ /** Interactive flow producing a credential for the host. */
29
+ connect(callbacks: HostCallbacks): Promise<HostCredential>;
30
+ /** Cheap revalidation for stored credentials. */
31
+ validate(credential: HostCredential): Promise<boolean>;
32
+ }
33
+ /**
34
+ * Client id resolution: explicit option → env var → registered default.
35
+ * Empty/whitespace-only values at any layer are treated as unset (the packaged
36
+ * desktop bakes `process.env.GUTTERPRESS_GITHUB_CLIENT_ID` in via a vite `define`,
37
+ * which yields `""` when the secret is missing at build time).
38
+ */
39
+ export declare function resolveGitHubClientId(explicit?: string): string;
40
+ export declare const GITHUB_HOST = "github.com";
41
+ /** Author-friendly message for connectivity failures (ADR 0006 D7). */
42
+ export declare const OFFLINE_MESSAGE = "Couldn't reach GitHub. Check your connection and try again.";
43
+ export interface GitHubAuthProviderOptions {
44
+ /** Explicit client id (overrides env + default). */
45
+ clientId?: string;
46
+ /** Injectable fetch for tests. Defaults to global fetch. */
47
+ fetchImpl?: typeof fetch;
48
+ /** Injectable sleep for tests (so polling tests run instantly). */
49
+ sleepImpl?: (ms: number) => Promise<void>;
50
+ }
51
+ /** GitHub device-flow {@link RemoteAuthProvider} (ADR 0006 D1). */
52
+ export declare class GitHubAuthProvider implements RemoteAuthProvider {
53
+ private readonly clientId;
54
+ private readonly fetchImpl;
55
+ private readonly sleep;
56
+ constructor(options?: GitHubAuthProviderOptions);
57
+ matches(origin: URL): boolean;
58
+ connect(callbacks: HostCallbacks): Promise<HostCredential>;
59
+ /**
60
+ * Cheap credential revalidation against `GET /user`. Returns `false` on any
61
+ * non-200 (401 revoked/expired, 403 forbidden/rate-limit-blocked, …);
62
+ * network failures are treated as "can't tell" → `true`.
63
+ */
64
+ validate(credential: HostCredential): Promise<boolean>;
65
+ /** Best-effort login lookup after auth — failure is non-fatal. */
66
+ private fetchUsername;
67
+ }
68
+ /** Standard headers for api.github.com calls. */
69
+ export declare function githubApiHeaders(token: string): Record<string, string>;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * GitHub repo discovery (#15, ADR 0006 D3 layer 4).
3
+ *
4
+ * Plain `fetch` against the GitHub REST API — deliberately no `@octokit`
5
+ * dependency. Lists every repository the user can access (`GET /user/repos`,
6
+ * the OAuth `repo`-scope model — ADR 0006 D1 amendment 2026-06-10), the
7
+ * branches of a chosen repository, and (via the Git Trees API) the gutterpress
8
+ * book projects inside a repo. The two REST listings paginate; the Git Trees
9
+ * call is a single request that handles the API's `truncated` flag instead. All
10
+ * three time out explicitly and map failures to author-friendly messages
11
+ * (401 → "reconnect").
12
+ */
13
+ import type { HostCredential } from "./token-store.ts";
14
+ /** One repository the user can open from GitHub. */
15
+ export interface RemoteRepository {
16
+ owner: string;
17
+ name: string;
18
+ fullName: string;
19
+ private: boolean;
20
+ defaultBranch: string;
21
+ htmlUrl: string;
22
+ }
23
+ /** One branch of a remote repository. */
24
+ export interface RemoteBranch {
25
+ name: string;
26
+ }
27
+ export interface GitHubApiOptions {
28
+ /** Injectable fetch for tests. Defaults to global fetch. */
29
+ fetchImpl?: typeof fetch;
30
+ }
31
+ /** "Reconnect" message for revoked/expired tokens (ADR 0006 D7). */
32
+ export declare const RECONNECT_MESSAGE = "Your GitHub connection has expired. Reconnect GitHub and try again.";
33
+ /**
34
+ * List every repository the user can access — own, collaborator, and org
35
+ * member — via `GET /user/repos` (paginated). The OAuth `repo` scope makes
36
+ * the full set visible with zero install/selection steps (ADR 0006 D1
37
+ * amendment). `sort=pushed` puts recently-active books first; callers must
38
+ * PRESERVE this order (the picker renders it as "most recent first").
39
+ */
40
+ export declare function listGitHubRepositories(credential: HostCredential, options?: GitHubApiOptions): Promise<RemoteRepository[]>;
41
+ /** One Gutterpress book found inside a repository. */
42
+ export interface RepoBook {
43
+ /**
44
+ * Folder of the book relative to the repository root, forward-slash form.
45
+ * Empty string when the manifest sits at the repository root.
46
+ */
47
+ path: string;
48
+ /** Display name: the folder's basename, or the repo name for the root. */
49
+ name: string;
50
+ }
51
+ /**
52
+ * Find the Gutterpress books inside a repository branch: every directory that
53
+ * contains a recognized manifest (from `MANIFEST_FILENAMES`) — the repository
54
+ * root counts, with `path: ""`. Uses one
55
+ * `GET /repos/{owner}/{repo}/git/trees/{branch}
56
+ * ?recursive=1` call; when GitHub truncates the recursive listing (very large
57
+ * repositories) it falls back to scanning the root + each top-level directory
58
+ * with non-recursive tree calls, so the result stays correct for the common
59
+ * "books are top-level folders" layout. Results are sorted by path, root
60
+ * first.
61
+ */
62
+ export declare function listRepoBooks(credential: HostCredential, owner: string, repo: string, branch: string, options?: GitHubApiOptions): Promise<RepoBook[]>;
63
+ /** List a repository's branches via `GET /repos/{owner}/{repo}/branches`. */
64
+ export declare function listGitHubBranches(credential: HostCredential, owner: string, repo: string, options?: GitHubApiOptions): Promise<RemoteBranch[]>;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * operation-log.ts — structured file logger for git / sync / recovery / snapshot
3
+ * operations.
4
+ *
5
+ * WHY this exists: when a sync or recovery operation fails, the user needs a
6
+ * debuggable log file that records what was attempted, what steps ran, and
7
+ * what went wrong — without having to reproduce the failure with verbose
8
+ * console output enabled. The log is written incrementally (append mode) so
9
+ * it survives even if the process crashes mid-operation.
10
+ *
11
+ * SECURITY INVARIANT: this logger NEVER writes secrets, tokens, credentials,
12
+ * or full remote URLs with embedded auth. Callers must pass only sanitized
13
+ * data (repo slug, branch name, short OIDs, error codes, outcome status).
14
+ *
15
+ * The logger is injectable: callers pass a `logFile` path and get a file
16
+ * logger; omit it and a no-op logger is used (backward compatible — existing
17
+ * callers that don't pass `logFile` see zero behavior change).
18
+ *
19
+ * LOG FORMAT (one line per entry, plain text for easy grep/tail):
20
+ * [ISO-timestamp] LEVEL operation: step=<step> key=value ... | <message>
21
+ *
22
+ * Example:
23
+ * [2026-06-19T12:34:56.789Z] INFO recovery: kind=unrelated_histories repo=my-book branch=main | starting recovery
24
+ * [2026-06-19T12:34:56.890Z] INFO recovery: step=backup | backup created at /tmp/print-sync-recovery/my-book/...zip
25
+ * [2026-06-19T12:34:57.789Z] WARN recovery: step=merge | merge conflicted files=manifest.yaml,notes.md
26
+ * [2026-06-19T12:34:57.890Z] INFO recovery: result=needs_user | surfaced 2 conflicted files to user
27
+ *
28
+ * Cross-platform: uses `node:fs` appendFileSync + `node:path`. The caller
29
+ * is responsible for providing a valid directory (the logger creates the
30
+ * file but NOT the parent directory — that's the caller's job, e.g. the
31
+ * desktop ensures `userData/logs/` exists before passing the path).
32
+ *
33
+ * Compatible with `bun build --compile`: no runtime package.json reads, no
34
+ * computed-path dynamic imports, no native bindings — just `fs.appendFileSync`.
35
+ */
36
+ export type LogLevel = "debug" | "info" | "warn" | "error";
37
+ export interface OperationLogger {
38
+ debug(step: string, message: string, data?: LogData): void;
39
+ info(step: string, message: string, data?: LogData): void;
40
+ warn(step: string, message: string, data?: LogData): void;
41
+ error(step: string, message: string, data?: LogData): void;
42
+ }
43
+ /** Structured key-value fields appended after the step. No secrets. */
44
+ export type LogData = Record<string, string | number | boolean | string[] | undefined>;
45
+ /**
46
+ * Create a file-backed operation logger. Each call appends one line to
47
+ * `logFile` (created if it doesn't exist; the parent directory is created
48
+ * if missing). Writes are synchronous so the log survives a process crash
49
+ * mid-operation.
50
+ *
51
+ * @param logFile Absolute path to the log file.
52
+ * @param minLevel Minimum level to write (default: "debug" — write everything).
53
+ * @param operation Operation name shown in each line (e.g. "sync", "recovery").
54
+ */
55
+ export declare function createFileLogger(logFile: string, operation: string, minLevel?: LogLevel): OperationLogger;
56
+ /**
57
+ * Resolve a logger from an optional `logFile` path. Returns a no-op logger
58
+ * when `logFile` is undefined or empty (backward compatible — callers that
59
+ * don't pass `logFile` see zero behavior change and zero file I/O).
60
+ */
61
+ export declare function resolveLogger(logFile: string | undefined, operation: string): OperationLogger;
62
+ /**
63
+ * Shorten a 40-char OID to the first 7 characters for compact log lines.
64
+ * Same convention as `git log --oneline`.
65
+ */
66
+ export declare function shortOid(oid: string): string;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * abort-interrupted-operation.ts — shared skeleton for aborting an unfinished
3
+ * git operation (merge / cherry-pick / rebase) left on disk.
4
+ *
5
+ * WHY (DRY): recover-interrupted-merge, recover-interrupted-cherry-pick, and
6
+ * recover-interrupted-rebase were ~90% identical — same TOCTOU guard, same
7
+ * backup gate, same hadLocalChanges capture, same fault-injection ordering,
8
+ * same force-checkout + marker cleanup + re-verify. The ONLY real differences
9
+ * are: which marker files signal the state, which files to remove, and how to
10
+ * resolve the ref to restore (merge/cherry-pick reset the current branch; a
11
+ * rebase may rewind a named branch to a recorded pre-rebase commit OR restore a
12
+ * detached HEAD). Those three differences are captured in `AbortConfig`; the
13
+ * invariant ordering lives here, once.
14
+ *
15
+ * Abort algorithm (pure isomorphic-git + node:fs — never the system git binary):
16
+ * 1. TOCTOU precondition: if NONE of `markerFiles` exist, the operation was
17
+ * already finished/aborted externally and there is nothing to abort. This
18
+ * is now enforced by the DISPATCHER (dispatch.ts), which calls each
19
+ * kind's exported `stillApplies` — built from `anyMarkerPresent` below —
20
+ * INSIDE withRepoLock, before this function is ever invoked. There used
21
+ * to be a duplicate hand-rolled copy of this same check at the top of
22
+ * this function; it is deleted, not kept alongside the dispatcher probe,
23
+ * because — unlike recover-missing-git-dir.ts — this function has no
24
+ * SECOND re-check later (no re-check after the backup/confirm wait), so
25
+ * the one check and the dispatcher probe cover the exact same window: the
26
+ * dispatcher probe runs immediately before this function's synchronous
27
+ * entry, with no work in between, just as the deleted local check did.
28
+ * 2. withBackupGate (backup → confirm → risky → failsafe). Inside the callback:
29
+ * 3. Capture whether the working tree had in-progress edits (best-effort).
30
+ * 4. Resolve the restore target via `resolveTarget` (default:
31
+ * ctx.branch → git.currentBranch → "HEAD"). resolveTarget MAY throw (e.g.
32
+ * the rebase cannot find its pre-op commit) — that surfaces as
33
+ * failed_backup_available since the backup is already safe.
34
+ * 5. Optionally rewind a named branch ref to a recorded commit, then
35
+ * force-checkout the target (resets index + worktree).
36
+ * 6. Remove the transient `cleanupFiles` inside .git.
37
+ * 7. Verify every `markerFiles` entry is gone; if not → THROW.
38
+ *
39
+ * Inside the risky callback we call ONLY raw git.* / node:fs — never a
40
+ * lock-wrapped lib function — so the dispatcher's per-repo FIFO queue can't
41
+ * deadlock. Re-verification uses direct fs.existsSync, not inspectRepo. All
42
+ * removed paths live INSIDE the repo's own .git and are captured in the verified
43
+ * backup — never user content.
44
+ *
45
+ * Fault injection points (ctx.faults?.before()):
46
+ * after_backup_before_repair — start of the destructive section
47
+ * abort_interrupted_operation — start of the abort proper
48
+ * checkout_branch — before the force checkout
49
+ * remove_operation_state — before deleting the on-disk state files
50
+ */
51
+ import type { RecoveryContext, RecoveryResult, SyncErrorKind } from "./types.ts";
52
+ /**
53
+ * True when any of `markerFiles` (gitDir-relative) still exists. Shared by
54
+ * each interrupted-* handler's exported `stillApplies` (the dispatcher's
55
+ * precondition probe, see types.ts `StillAppliesFn`) so there is ONE
56
+ * implementation of "is this abort still needed", not three copies.
57
+ */
58
+ export declare function anyMarkerPresent(ctx: RecoveryContext, markerFiles: string[]): boolean;
59
+ /** Arguments handed to a config's `resolveTarget`. */
60
+ export interface AbortResolveArgs {
61
+ ctx: RecoveryContext;
62
+ /** The git repository root (ctx.repoDir). */
63
+ dir: string;
64
+ /** The resolved `.git` directory for `dir`. */
65
+ gitDir: string;
66
+ }
67
+ /** Which ref to restore and (optionally) which branch ref to rewind first. */
68
+ export interface AbortTargetPlan {
69
+ /** The ref to force-checkout (a branch name, a commit sha, or "HEAD"). */
70
+ checkoutRef: string;
71
+ /**
72
+ * When set, `refs/heads/<writeRefBranch>` is force-written to `writeRefValue`
73
+ * BEFORE the checkout (used to rewind a named branch to a pre-op commit). Omit
74
+ * to leave all refs untouched (merge/cherry-pick and detached-HEAD rebase).
75
+ */
76
+ writeRefBranch?: string;
77
+ writeRefValue?: string;
78
+ }
79
+ /** The per-operation differences the shared skeleton parameterizes. */
80
+ export interface AbortConfig {
81
+ /** Recovery kind — drives policy (backup/confirm) and guidance copy. */
82
+ kind: SyncErrorKind;
83
+ /**
84
+ * gitDir-relative marker paths whose presence signals the interrupted state.
85
+ * If NONE exist the abort is a benign no-op; after the abort ALL must be gone.
86
+ */
87
+ markerFiles: string[];
88
+ /** gitDir-relative paths removed during the abort (force; missing is fine). */
89
+ cleanupFiles: string[];
90
+ /**
91
+ * Resolve which ref to restore. Defaults to
92
+ * ctx.branch → git.currentBranch → "HEAD". MAY throw to abort the repair
93
+ * (the verified backup makes that safe → failed_backup_available).
94
+ */
95
+ resolveTarget?: (args: AbortResolveArgs) => Promise<AbortTargetPlan>;
96
+ /** Build the success message; `hadLocalChanges` reports whether edits were reset. */
97
+ successMessage: (hadLocalChanges: boolean) => string;
98
+ }
99
+ /**
100
+ * Abort an interrupted git operation per `config`. See the module header for the
101
+ * full algorithm and safety invariants.
102
+ */
103
+ export declare function abortInterruptedOperation(ctx: RecoveryContext, config: AbortConfig): Promise<RecoveryResult>;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Temp-dir zip backup creation and verification for the sync-recovery subsystem.
3
+ *
4
+ * Creates a STORE-method (no compression) ZIP of the project's user files plus
5
+ * .git/, writing it to:
6
+ * <os.tmpdir()>/print-sync-recovery/<repo-slug>/<ISO-timestamp>-<reason>.zip
7
+ *
8
+ * The backup root is `os.tmpdir()` (NOT a hardcoded "/tmp") so it is correct on
9
+ * Windows/macOS/Linux — the desktop ships on all three (CLAUDE.md §8). A literal
10
+ * "/tmp" would make every risky-repair backup throw on Windows.
11
+ *
12
+ * The ZIP bytes are produced by fflate's streaming Zip/ZipPassThrough (STORE
13
+ * method — no compression), replacing the previous hand-rolled writer (#86).
14
+ * fflate is pure JS with zero dependencies, no runtime package.json/data
15
+ * reads, and no computed-path dynamic imports, so it bundles cleanly under
16
+ * bun build --compile (CLAUDE.md §1/§3). It is already a dependency (publish
17
+ * providers use its unzipSync).
18
+ *
19
+ * Verification (assertZipReadable): deliberately NOT fflate. It validates
20
+ * EVERY central-directory entry's signature and bounds (not just the first)
21
+ * via positioned reads of the EOCD + central directory only — never the file
22
+ * data — so it stays memory-safe on multi-GB backups while still catching
23
+ * corruption in any later entry. fflate's unzip needs the whole archive in
24
+ * memory, so it cannot back this check; the small positioned-read parser
25
+ * stays.
26
+ *
27
+ * Retention: createRecoveryZip prunes stale backups (best-effort, never throws)
28
+ * before writing a new one — see pruneOldBackups — so old backups do not fill
29
+ * the disk over time.
30
+ *
31
+ * Exclusions: node_modules/, .print-sync/cache/, and **.git/config**. The git
32
+ * config file is deliberately dropped from the backup because it can carry an
33
+ * embedded credential (e.g. a tokenized remote URL) and is reconstructable on
34
+ * recovery (the remote is reconfigured from the stored connection). All other
35
+ * .git/ contents (objects, refs, HEAD) ARE included so recovery still works.
36
+ * Inclusions: all user-visible files + .git/ (minus config, for full recovery)
37
+ */
38
+ import type { RecoveryBackup, RecoveryContext } from "./types.ts";
39
+ /**
40
+ * Root directory for all sync-recovery backups.
41
+ *
42
+ * Computed from `os.tmpdir()` (NOT hardcoded "/tmp") so it resolves to a real
43
+ * temp dir on Windows, macOS, and Linux. A literal "/tmp" does not exist on
44
+ * Windows and would make every risky-repair backup throw there (CLAUDE.md §8).
45
+ */
46
+ export declare const BACKUP_ROOT: string;
47
+ export interface PruneOldBackupsOptions {
48
+ /** Backup root to prune under. Defaults to {@link BACKUP_ROOT}. */
49
+ root?: string;
50
+ /** Restrict pruning to a single repo-slug subfolder. */
51
+ slug?: string;
52
+ /** Remove zips with an mtime older than this many ms. Default 7 days. */
53
+ ttlMs?: number;
54
+ /** After TTL pruning, keep at most this many newest zips per slug. */
55
+ maxPerSlug?: number;
56
+ /** Clock override (tests). Returns epoch ms. */
57
+ now?: () => number;
58
+ }
59
+ /**
60
+ * Remove stale recovery backups so they do not accumulate forever and fill the
61
+ * disk (BUG 2). Two independent policies are applied per repo-slug folder:
62
+ * 1. TTL — delete any zip whose mtime is older than `ttlMs` (default 7 days).
63
+ * 2. Cap — keep only the newest `maxPerSlug` zips (default 20), delete the rest.
64
+ *
65
+ * BEST-EFFORT and SILENT: this never throws and never blocks backup creation.
66
+ * Any unreadable dir, racing delete, or stat failure is ignored — a failure to
67
+ * prune must never prevent the user's work from being backed up.
68
+ */
69
+ export declare function pruneOldBackups(opts?: PruneOldBackupsOptions): Promise<void>;
70
+ /**
71
+ * Create a temp-dir zip backup of the project directory (user files + .git/,
72
+ * minus .git/config). Calls ctx.faults?.before("backup_create") then
73
+ * ctx.faults?.before("backup_verify") so tests can inject failures at each step.
74
+ *
75
+ * Returns a RecoveryBackup describing the zip, or throws on failure.
76
+ */
77
+ export declare function createRecoveryZip(ctx: Pick<RecoveryContext, "repoDir" | "repoSlug" | "faults" | "now">, reason: string): Promise<RecoveryBackup>;
78
+ export interface ZipEntryInfo {
79
+ name: string;
80
+ size: number;
81
+ /** The extracted file content. */
82
+ data: Uint8Array;
83
+ }
84
+ /**
85
+ * Extract every entry of a ZIP buffer (fflate unzipSync). Returns [] for a
86
+ * buffer that is not a parseable zip — matching the old hand-rolled parser's
87
+ * "no EOCD found" behavior that some assertions rely on.
88
+ */
89
+ export declare function parseZipEntries(buf: Buffer): ZipEntryInfo[];
90
+ /**
91
+ * Assert that a zip file at `zipPath` is readable and parseable.
92
+ * Throws with a descriptive error if not.
93
+ *
94
+ * Validates EVERY central-directory entry — each entry's CD signature
95
+ * (0x02014b50) and that its variable-length fields stay within the central
96
+ * directory bounds. A zip whose FIRST entry is intact but whose LATER entries
97
+ * are corrupt is rejected (BUG 4): checking only entry 0 let truncated/garbled
98
+ * tails slip through.
99
+ *
100
+ * MEMORY-SAFE: reads only the end-of-central-directory record (file tail) and
101
+ * the central directory via positioned reads — NEVER the file data. A multi-GB
102
+ * backup is verified without reading it back into memory.
103
+ */
104
+ export declare function assertZipReadable(zipPath: string): Promise<void>;
105
+ /**
106
+ * Return the list of entries inside a zip file (as ZipEntryInfo objects).
107
+ *
108
+ * @internal TEST-ONLY: reads the WHOLE zip into memory to expose entry content.
109
+ * NEVER call this in a production path — a large backup would OOM. Production
110
+ * verification uses {@link assertZipReadable} (positioned reads, memory-safe).
111
+ * Used by tests to assert which files were backed up and inspect content.
112
+ */
113
+ export declare function zipEntries(zipPath: string): Promise<ZipEntryInfo[]>;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Error-to-kind classifier for the sync-recovery subsystem — the SINGLE
3
+ * source of truth for git error classification.
4
+ *
5
+ * Maps a thrown error (from isomorphic-git or sync.ts) plus an optional
6
+ * RepoHealth preflight to a SyncErrorKind. The building blocks
7
+ * (isPushRejected, isMergeConflictError, classifyTransportFailure) are
8
+ * exported and consumed by sync.ts — there is exactly ONE implementation of
9
+ * each decoder, not parallel copies "kept in sync by spec".
10
+ *
11
+ * classifyFromHealth() is the health-only classifier used by preflight
12
+ * callers (no thrown error yet — e.g. the desktop at project-open): it returns
13
+ * null for a healthy repo so the caller can skip recovery entirely.
14
+ *
15
+ * This module is pure — no I/O, no side effects.
16
+ */
17
+ import type { RepoHealth, SyncErrorKind } from "./types.ts";
18
+ /**
19
+ * Minimum age before a leftover git lock counts as STALE for a preflight
20
+ * classification. recover-stale-lock.ts imports this same constant as its
21
+ * act-or-retry threshold, so preflight and handler can never disagree: a lock
22
+ * young enough to pass preflight is exactly a lock the handler would defer
23
+ * with retry_later ("a live process may still hold it").
24
+ */
25
+ export declare const STALE_LOCK_MIN_AGE_MS: number;
26
+ /**
27
+ * Thrown by syncProject's structural preflight when the repo must be repaired
28
+ * before any sync work can safely run. The `code` string is the STABLE
29
+ * contract hosts may match on across the dynamic-import boundary (where
30
+ * `instanceof` is unreliable); `kind` names the repair to dispatch.
31
+ */
32
+ export declare class RepoNeedsRecoveryError extends Error {
33
+ readonly kind: SyncErrorKind;
34
+ readonly code = "RepoNeedsRecovery";
35
+ constructor(kind: SyncErrorKind);
36
+ }
37
+ /** Type guard for {@link RepoNeedsRecoveryError} (matches on the stable code). */
38
+ export declare function isRepoNeedsRecoveryError(e: unknown): e is RepoNeedsRecoveryError;
39
+ /**
40
+ * Thrown by transport.ts's onAuth when a stored credential EXISTS but the
41
+ * remote URL fails isCredentialTransmissionSafe (non-loopback http). Loud and
42
+ * typed on purpose: the old behavior (silently withholding the credential)
43
+ * surfaced as a 401 → "auth" → "reconnect" loop, and recover-auth then deleted
44
+ * the credential for the whole host. The `code` string is the STABLE contract
45
+ * (matchable across dynamic-import boundaries where `instanceof` fails).
46
+ */
47
+ export declare class InsecureTransportError extends Error {
48
+ readonly code = "InsecureTransport";
49
+ constructor();
50
+ }
51
+ /** Type guard for {@link InsecureTransportError} (matches on the stable code). */
52
+ export declare function isInsecureTransportError(e: unknown): e is InsecureTransportError;
53
+ export declare function isPushRejected(e: unknown): boolean;
54
+ /** Type guard exposing MergeConflictError's per-file payload (used by sync.ts). */
55
+ export declare function isMergeConflictError(e: unknown): e is {
56
+ data: {
57
+ filepaths: string[];
58
+ bothModified: string[];
59
+ deleteByUs: string[];
60
+ deleteByTheirs: string[];
61
+ };
62
+ };
63
+ export declare function classifyTransportFailure(e: unknown): "auth_required" | "network_unavailable" | "insecure_transport" | null;
64
+ /**
65
+ * Classify a structural repo condition from a RepoHealth snapshot alone.
66
+ * Used by preflight callers (no thrown error — e.g. project-open), and by
67
+ * classifyGitError's structural step so there is ONE ordering, not two.
68
+ *
69
+ * Returns null for a healthy repo (nothing to recover).
70
+ *
71
+ * ORDERING: interrupted-operation checks MUST precede the detached-head check
72
+ * (an in-progress rebase usually detaches HEAD — the abort repair must win over
73
+ * the rescue-branch repair), and specific interrupted-op repairs precede the
74
+ * generic stale-lock cleanup.
75
+ *
76
+ * `minLockAgeMs` gates the stale-lock classification: at preflight (the
77
+ * default, STALE_LOCK_MIN_AGE_MS) a younger lock is treated as healthy because
78
+ * a live process may still hold it — the same rule recover-stale-lock.ts
79
+ * applies before acting. Error-path callers pass 0: a lock that just made a
80
+ * sync THROW is worth routing regardless of age (the handler still re-checks
81
+ * and returns retry_later while it is fresh).
82
+ */
83
+ export declare function classifyFromHealth(health: RepoHealth, opts?: {
84
+ minLockAgeMs?: number;
85
+ }): SyncErrorKind | null;
86
+ /**
87
+ * Map a thrown error plus optional repo-health facts to a SyncErrorKind.
88
+ *
89
+ * Called by the recovery dispatcher BEFORE invoking the per-kind handler.
90
+ *
91
+ * ORDERING (BUG 1 — transient transport beats structural health):
92
+ * 1. missing_git_dir — when there is genuinely no repo, a transport error is
93
+ * meaningless (nothing to talk to a remote ABOUT), so the missing-repo
94
+ * guidance must win even over an auth/network error.
95
+ * 2. Clearly transient/transport errors (auth, network) — these are decoded
96
+ * from the THROWN ERROR and win over the remaining structural health
97
+ * flags (detached HEAD, stale lock). Rationale: you cannot repair repo
98
+ * STRUCTURE while you are offline or signed out, and the scary
99
+ * backup+rescue-branch+confirm repair is the wrong first response to a
100
+ * blip — the friendly "reconnect" / "try later" is correct. Once the user
101
+ * is back online, the next sync re-runs preflight (no thrown error) and
102
+ * the structural kind surfaces then (step 3).
103
+ * 3. Structural health flags (detached HEAD, stale lock) — applied when the
104
+ * failure is NOT a transient transport error (e.g. a preflight with no
105
+ * error, or a non-transport error).
106
+ * 4. Remaining error-code/message heuristics (conflicts, corrupt index,
107
+ * unrelated histories, missing objects/ref-store, wrong remote/branch).
108
+ */
109
+ export declare function classifyGitError(err: unknown, health?: RepoHealth): SyncErrorKind;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * buildRecoveryContext — the ONE place a RecoveryContext is resolved from a
3
+ * project directory. Both hosts consume it (the desktop's recovery bridge and
4
+ * `gutterpress repair`); each supplies only its own ConfirmationGate (dialog vs
5
+ * terminal prompt). Keeping the resolution here means repo-root, branch,
6
+ * credential, and slug rules can never drift between hosts.
7
+ *
8
+ * Resolution rules (each learned the hard way — see the audit trail):
9
+ * - repoDir: the project's OWN repo root via detectProjectSource. NEVER
10
+ * findEnclosingRepoDir — it is ancestor-only (skips the project's own
11
+ * .git), so a project that IS its own repo root would resolve to a parent
12
+ * repo (e.g. ~/.git) and the backup step would zip the entire home
13
+ * directory.
14
+ * - branch: remote diagnosis first, then the locally detected branch
15
+ * (local-only repos on non-"main" branches), then "main".
16
+ * - credential: resolved from the token store by remote hostname; stays in
17
+ * the calling process — never serialized to a UI layer.
18
+ * - repoSlug: last path segment, sanitized for backup file naming.
19
+ */
20
+ import { detectProjectSource } from "../../project-source.ts";
21
+ import { diagnoseProjectRemote } from "../diagnose.ts";
22
+ import { type TokenStore } from "../token-store.ts";
23
+ import type { ConfirmationGate, RecoveryContext } from "./types.ts";
24
+ export interface BuildRecoveryContextOptions {
25
+ /** The directory the user opened (may be a subfolder of its repo). */
26
+ projectDir: string;
27
+ /** Host-specific approval gate (dialog, terminal prompt, …). */
28
+ confirmation: ConfirmationGate;
29
+ /** Credential store for the remote host, when the host has one. */
30
+ tokenStore?: TokenStore;
31
+ /** Display name for snapshot commits created during recovery. */
32
+ authorName?: string;
33
+ /** Email for snapshot commits created during recovery. */
34
+ authorEmail?: string;
35
+ /** Operation-log file shared with the sync path. */
36
+ logFile?: string;
37
+ /**
38
+ * Classification override (tests only — omit in production). Injected the
39
+ * same way as RecoveryContext's `now`/`faults`: bun's mock.module leaks
40
+ * across test files, so cross-cutting modules are never module-mocked.
41
+ */
42
+ classify?: typeof detectProjectSource;
43
+ /** Diagnosis override (tests only — omit in production). See `classify`. */
44
+ diagnose?: typeof diagnoseProjectRemote;
45
+ }
46
+ /** Resolve everything a recovery handler needs from a project directory. */
47
+ export declare function buildRecoveryContext(options: BuildRecoveryContextOptions): Promise<RecoveryContext>;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Recovery dispatcher — INTEGRATE PHASE.
3
+ *
4
+ * Maps each SyncErrorKind to its recover-<x>.ts handler.
5
+ * Also exports the index barrel for the recovery subsystem public surface.
6
+ *
7
+ * The dispatcher is the single call-site for all recovery attempts. Callers
8
+ * (e.g. a guardedSync wrapper) obtain a SyncErrorKind from classifyGitError(),
9
+ * build a RecoveryContext, and call recover(kind, ctx, error).
10
+ *
11
+ * Handler lookup is a plain switch — no dynamic import, no registry object —
12
+ * so TypeScript can verify exhaustiveness at compile time and bun build
13
+ * --compile can tree-shake unused handlers.
14
+ */
15
+ import type { RecoveryContext, RecoveryResult, SyncErrorKind } from "./types.ts";
16
+ /**
17
+ * Dispatch a recovery attempt.
18
+ *
19
+ * @param kind - The SyncErrorKind from classifyGitError().
20
+ * @param ctx - Full RecoveryContext (repoDir, branch, remoteUrl, etc.).
21
+ * @param error - The original thrown error, if available.
22
+ * @returns RecoveryResult describing what happened.
23
+ */
24
+ export declare function recover(kind: SyncErrorKind, ctx: RecoveryContext, error?: unknown): Promise<RecoveryResult>;
25
+ export type { RecoverFn, RecoveryContext, RecoveryResult, SyncErrorKind, RecoveryRisk, ManualGuidance, RepoHealth, RecoveryBackup, RepairConfirmation, ConfirmationGate, FaultInjector, FaultPoint, } from "./types.ts";
26
+ export { classifyGitError, classifyFromHealth, RepoNeedsRecoveryError, isRepoNeedsRecoveryError, } from "./classify.ts";
27
+ export { inspectRepo, preflightStructuralReason, buildPreflightDiagnostics, verifyRepoReadable, isUnbornRepo, } from "./inspect.ts";
28
+ export { buildRecoveryContext } from "./context.ts";
29
+ export type { BuildRecoveryContextOptions } from "./context.ts";
30
+ export { recoveryPolicy, policyFor, detachedHeadWithLocalChangesPolicy } from "./policy.ts";
31
+ export { createRecoveryZip, assertZipReadable, zipEntries } from "./backup.ts";
32
+ export { makeManualGuidance } from "./manual-guidance.ts";
33
+ export { failSafeNoRepair, withBackupGate } from "./failsafe.ts";
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Fail-safe wrapper and no-op fallback for the sync-recovery subsystem.
3
+ *
4
+ * withBackupGate() enforces the invariant ordering every risky repair must
5
+ * follow:
6
+ * 1. Look up the policy for the kind.
7
+ * 2. If createBackup → createRecoveryZip; on failure → failSafe
8
+ * "failed_no_changes_made" with NO subsequent writes.
9
+ * 3. If requireConfirmation → confirmRepair; DENIED → "blocked" no-op.
10
+ * 4. Run the risky repair callback.
11
+ * 5. If the risky callback throws AFTER a backup → failSafe
12
+ * "failed_backup_available" (backup is readable, remote is unchanged).
13
+ *
14
+ * failSafeNoRepair() is the terminal no-op: returns
15
+ * - "failed_backup_available" when a backup zip path is supplied
16
+ * - "failed_no_changes_made" otherwise
17
+ * Both branches include ManualGuidance so the host can show useful copy.
18
+ */
19
+ import type { RecoveryContext, RecoveryResult, SyncErrorKind } from "./types.ts";
20
+ /**
21
+ * Terminal no-op: no repair was attempted (or the repair failed after a
22
+ * backup was created). Returns a RecoveryResult with the appropriate status.
23
+ */
24
+ export declare function failSafeNoRepair(ctx: Pick<RecoveryContext, "repoSlug" | "remoteUrl">, kind: SyncErrorKind, backupZipPath?: string, error?: unknown): RecoveryResult;
25
+ /**
26
+ * Enforce the invariant ordering for risky repairs:
27
+ * policy → backup → confirmation → risky → failsafe on throw.
28
+ *
29
+ * The `risky` callback receives the backup zip path (or undefined) and
30
+ * must call ctx.faults?.before("after_backup_before_repair") at the start
31
+ * of its destructive section.
32
+ */
33
+ export declare function withBackupGate(ctx: RecoveryContext, kind: SyncErrorKind, risky: (backupZipPath: string | undefined) => Promise<RecoveryResult>, error?: unknown): Promise<RecoveryResult>;