gutterpress 0.10.0 → 0.10.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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-7a37g51p.js} +5 -5
- package/dist/{build-6r502chw.js → build-kdaet3pd.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-zfcryxg8.js → cli-ak1tagkr.js} +94 -57
- package/dist/{cli-eq5naw4m.js → cli-df7gcyns.js} +370 -1249
- package/dist/{cli-149edp6b.js → cli-dx6sxj2t.js} +1 -1
- package/dist/{cli-cqtggsng.js → cli-mcb484g3.js} +2 -47
- package/dist/{cli-k1065rkg.js → cli-w692g6dx.js} +75 -29
- package/dist/cli.js +14 -15
- package/dist/{doctor-akvxbtjb.js → doctor-vajd5h45.js} +2 -2
- package/dist/{engine-b159tbns.js → engine-0nt35svz.js} +2 -2
- package/dist/{engine-ft4cr3ep.js → engine-gkcqdye5.js} +1 -1
- package/dist/{gutterpress-viewer-te8g5grx.js → gutterpress-viewer-tma1qwz2.js} +57 -15
- package/dist/{index-ge7q9xj3.js → index-6g72h2n1.js} +771 -1482
- package/dist/{index-ycpvr0am.js → index-b2rqw41a.js} +1 -1
- package/dist/{index-mdefp0y5.js → index-dtrftayr.js} +75 -27
- package/dist/{index-wq3r5pj7.js → index-kvgq4q3q.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-p6a4bkvh.js} +5 -5
- package/dist/{new-hvq0x91q.js → new-4cdzfbsj.js} +5 -5
- package/dist/{plugin-pssmk0dx.js → plugin-a89tqspy.js} +5 -5
- package/dist/{preflight-4j00yd0g.js → preflight-r4y3f8f9.js} +5 -5
- package/dist/{preview-d4s6gk6p.js → preview-k8n2kew7.js} +5 -5
- package/dist/{publish-tkc26en7.js → publish-jnhjp2vd.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-prnybpkx.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
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `node:fs`, with crash-atomic writes for git's MUTABLE metadata.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS. isomorphic-git writes `.git/index`, `HEAD`, `packed-refs`
|
|
5
|
+
* and loose refs with a plain `fs.writeFile` — an open-truncate-write. If the
|
|
6
|
+
* process dies between the truncate and the write (the writer force-quits the
|
|
7
|
+
* app, the OS kills it, the machine sleeps badly) the file is left EMPTY or
|
|
8
|
+
* half-written, and the repository can no longer be read. That is the one
|
|
9
|
+
* class of history damage Gutterpress itself causes; every other class needs
|
|
10
|
+
* an outside actor. Since isomorphic-git takes `fs` as a parameter, replacing
|
|
11
|
+
* the write with temp-file + `rename` removes the truncation window entirely
|
|
12
|
+
* — a reader sees either the old file or the new one, never a torn one.
|
|
13
|
+
*
|
|
14
|
+
* SCOPE. Only git's mutable metadata is redirected. Working-tree files and
|
|
15
|
+
* content-addressed objects keep the plain write: objects are written once
|
|
16
|
+
* under a hash of their contents (a torn object is a NEW file that simply
|
|
17
|
+
* fails to inflate, it never destroys a good one), and the working tree is
|
|
18
|
+
* the author's own files, which `rename` semantics would not improve.
|
|
19
|
+
*
|
|
20
|
+
* NOT A DURABILITY BARRIER. There is deliberately no `fsync`: this closes the
|
|
21
|
+
* PROCESS-DEATH window, where the page cache survives and `rename` is atomic
|
|
22
|
+
* to every reader. Surviving sudden power loss would mean fsync-ing the temp
|
|
23
|
+
* file and its directory on every ref write, which is a real cost on the
|
|
24
|
+
* snapshot hot path and guards a failure the app does not cause.
|
|
25
|
+
*
|
|
26
|
+
* Everything not named below passes through to `node:fs` unchanged, so
|
|
27
|
+
* `import { gitFs as fs }` is a drop-in for `import * as fs from "node:fs"`.
|
|
28
|
+
*/
|
|
29
|
+
import * as nodeFs from "node:fs";
|
|
30
|
+
/** True when `file` is git metadata that must be replaced, never truncated. */
|
|
31
|
+
export declare function needsAtomicWrite(file: unknown): file is string;
|
|
32
|
+
/**
|
|
33
|
+
* `node:fs` with the two writers above swapped in — the object callers import
|
|
34
|
+
* as `fs` and hand to isomorphic-git. Everything else passes through
|
|
35
|
+
* untouched.
|
|
36
|
+
*
|
|
37
|
+
* WHY an object and not `export * from "node:fs"`: bun build with
|
|
38
|
+
* `--packages=external` compiles a star re-export of a BUILTIN into a
|
|
39
|
+
* `__reExport(exports, node_fs)` call whose `node_fs` binding is never
|
|
40
|
+
* emitted, so the built lib throws `ReferenceError: node_fs is not defined`
|
|
41
|
+
* on import. A plain spread has no such hazard.
|
|
42
|
+
*/
|
|
43
|
+
export declare const gitFs: typeof nodeFs;
|
|
@@ -40,6 +40,17 @@ export interface AutoSyncPolicy {
|
|
|
40
40
|
export declare const AUTO_SYNC_MIN_MINUTES = 1;
|
|
41
41
|
export declare const AUTO_SYNC_MAX_MINUTES: number;
|
|
42
42
|
export declare const AUTO_SYNC_DEFAULT_MINUTES = 2;
|
|
43
|
+
/**
|
|
44
|
+
* Minutes between PUSH-enabled auto-sync passes (owner decision 2026-08-23).
|
|
45
|
+
* Every ~2-minute tick still PULLS — a collaborator's work keeps arriving
|
|
46
|
+
* promptly — but only a tick whose push window has elapsed also pushes, and
|
|
47
|
+
* the desktop pushes once more on project close/app exit. This is what keeps
|
|
48
|
+
* an actively-typing author from minting a snapshot-and-push every 2 minutes
|
|
49
|
+
* (the "commit wall"): between push windows, ticks with an unmoved remote
|
|
50
|
+
* commit nothing at all. Deliberately a constant, NOT a settings knob — the
|
|
51
|
+
* sync cadence is hidden policy, same as the tick interval's default.
|
|
52
|
+
*/
|
|
53
|
+
export declare const AUTO_SYNC_PUSH_INTERVAL_MINUTES = 15;
|
|
43
54
|
/**
|
|
44
55
|
* Resolve the debounce delay (ms) for the host's auto-snapshot timer, or
|
|
45
56
|
* `null` when automatic snapshots are disabled. Pure — the testable core of
|
|
@@ -61,8 +72,9 @@ export declare function autoSnapshotDelayMs(policy: Partial<AutoSnapshotPolicy>
|
|
|
61
72
|
* the default and is then clamped into [AUTO_SYNC_MIN_MINUTES,
|
|
62
73
|
* AUTO_SYNC_MAX_MINUTES].
|
|
63
74
|
*
|
|
64
|
-
* Note: the host orchestrator
|
|
65
|
-
*
|
|
75
|
+
* Note: this is the host orchestrator's ONLY sync trigger — the file-change
|
|
76
|
+
* debounce that used to sit beside it was removed (it could never fire before
|
|
77
|
+
* this interval already had).
|
|
66
78
|
*/
|
|
67
79
|
export declare function autoSyncDelayMs(policy: Partial<AutoSyncPolicy> | undefined): number | null;
|
|
68
80
|
/**
|
|
@@ -7,22 +7,6 @@ export interface CloneProgressEvent {
|
|
|
7
7
|
loaded: number;
|
|
8
8
|
total?: number;
|
|
9
9
|
}
|
|
10
|
-
/**
|
|
11
|
-
* Provider provenance recorded next to a cloned project (ADR 0006 D4):
|
|
12
|
-
* metadata for the repo picker / re-auth UX, never consulted by the
|
|
13
|
-
* editing/preview/build paths.
|
|
14
|
-
*/
|
|
15
|
-
export interface ProjectProvenance {
|
|
16
|
-
provider: "github";
|
|
17
|
-
owner: string;
|
|
18
|
-
repo: string;
|
|
19
|
-
/**
|
|
20
|
-
* Legacy GitHub-App installation id. New clones never write it (the OAuth
|
|
21
|
-
* App model has no installations — ADR 0006 D1 amendment 2026-06-10); kept
|
|
22
|
-
* optional so provenance files written by 0.4.x betas still parse.
|
|
23
|
-
*/
|
|
24
|
-
installationId?: string;
|
|
25
|
-
}
|
|
26
10
|
export interface CloneRepositoryOptions {
|
|
27
11
|
/** HTTPS clone URL. Tokens embedded in the URL are stripped (D7). */
|
|
28
12
|
url: string;
|
|
@@ -49,8 +33,6 @@ export interface CloneRepositoryOptions {
|
|
|
49
33
|
onProgress?: (event: CloneProgressEvent) => void;
|
|
50
34
|
/** When provided, credentials embedded in `url` are migrated into it (D7). */
|
|
51
35
|
tokenStore?: TokenStore;
|
|
52
|
-
/** Provider provenance to record beside the clone (ADR 0006 D4). */
|
|
53
|
-
provenance?: ProjectProvenance;
|
|
54
36
|
/**
|
|
55
37
|
* Injectable git HTTP transport for tests (isomorphic-git's `http` client
|
|
56
38
|
* shape). Defaults to isomorphic-git's node client.
|
|
@@ -63,9 +45,6 @@ export interface CloneRepositoryResult {
|
|
|
63
45
|
/** The checked-out branch. */
|
|
64
46
|
branch?: string;
|
|
65
47
|
}
|
|
66
|
-
export declare function provenancePath(projectDir: string): string;
|
|
67
|
-
/** Read recorded provider provenance for a project, if any. Never throws. */
|
|
68
|
-
export declare function readProjectProvenance(projectDir: string): Promise<ProjectProvenance | null>;
|
|
69
48
|
/**
|
|
70
49
|
* Reduce a (possibly renderer-/user-supplied) project folder name to a single
|
|
71
50
|
* safe path segment: path separators become dashes and leading dots are
|
|
@@ -1,5 +1,23 @@
|
|
|
1
|
-
import
|
|
2
|
-
export
|
|
1
|
+
import { onlineSiblingPath, isOnlineSibling } from "./sync-messages.ts";
|
|
2
|
+
export { onlineSiblingPath, isOnlineSibling };
|
|
3
|
+
import type { GitCache, KeptBothFile } from "./sync-types.ts";
|
|
4
|
+
export type { KeptBothFile };
|
|
5
|
+
/** Type guard exposing MergeConflictError's per-file payload. */
|
|
6
|
+
export declare function isMergeConflictError(e: unknown): e is {
|
|
7
|
+
data: {
|
|
8
|
+
filepaths: string[];
|
|
9
|
+
bothModified: string[];
|
|
10
|
+
deleteByUs: string[];
|
|
11
|
+
deleteByTheirs: string[];
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* A non-forced `git.checkout` refused to overwrite working-tree files whose
|
|
16
|
+
* content moved after we committed them. isomorphic-git detects this in its
|
|
17
|
+
* analysis pass and throws BEFORE touching the tree, so the refusal is
|
|
18
|
+
* atomic: nothing on disk has been written.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isCheckoutConflict(e: unknown): boolean;
|
|
3
21
|
export interface ConvergeResult {
|
|
4
22
|
/** The branch tip after the merge (the merge commit, or the ff/no-op tip). */
|
|
5
23
|
oid: string;
|
|
@@ -8,13 +26,13 @@ export interface ConvergeResult {
|
|
|
8
26
|
* the writer should blend these. Empty when everything merged cleanly.
|
|
9
27
|
*/
|
|
10
28
|
combinedFiles: string[];
|
|
11
|
-
/**
|
|
12
|
-
|
|
29
|
+
/** Files kept as a pair: ours at `path`, theirs at `onlinePath`. */
|
|
30
|
+
keptBothFiles: KeptBothFile[];
|
|
13
31
|
}
|
|
14
32
|
/** Snapshot message for the equalization commit (driver-unreachable cases). */
|
|
15
33
|
export declare const CONVERGE_PREPARE_MESSAGE = "Getting your changes ready to combine with the online version";
|
|
16
|
-
/** Snapshot message for the post-merge restore commit (
|
|
17
|
-
export declare const CONVERGE_RESTORE_MESSAGE = "Kept
|
|
34
|
+
/** Snapshot message for the post-merge restore commit (kept-both/edits). */
|
|
35
|
+
export declare const CONVERGE_RESTORE_MESSAGE = "Kept both versions of the files that can't be combined";
|
|
18
36
|
/** The merge commit message (same wording the old sync used). */
|
|
19
37
|
export declare const CONVERGE_MERGE_MESSAGE = "Combined your changes with the online version";
|
|
20
38
|
/**
|
|
@@ -44,5 +62,4 @@ export declare function convergeMerge(params: {
|
|
|
44
62
|
};
|
|
45
63
|
authorName?: string;
|
|
46
64
|
authorEmail?: string;
|
|
47
|
-
allowUnrelatedHistories?: boolean;
|
|
48
65
|
}): Promise<ConvergeResult>;
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
* SECURITY INVARIANT: token values never appear in error messages or logs.
|
|
16
16
|
*/
|
|
17
17
|
import type httpNode from "isomorphic-git/http/node";
|
|
18
|
-
import type { HostCallbacks, RemoteAuthProvider } from "./github-auth.ts";
|
|
19
18
|
import { type HostCredential } from "./token-store.ts";
|
|
20
19
|
/** What the Advanced Setup UI collects for "Connect a Git server". */
|
|
21
20
|
export interface GenericTokenConnectInput {
|
|
@@ -38,14 +37,6 @@ export interface GenericTokenConnectInput {
|
|
|
38
37
|
*/
|
|
39
38
|
repoUrl?: string;
|
|
40
39
|
}
|
|
41
|
-
/**
|
|
42
|
-
* The {@link RemoteAuthProvider} `connect` input for the generic provider:
|
|
43
|
-
* the UI-collected fields plus the standard host callbacks (`onUserCode` is
|
|
44
|
-
* part of the shared contract but unused — there is no device code in the
|
|
45
|
-
* token flow; pass a no-op, or use {@link connectGenericHost} directly).
|
|
46
|
-
*/
|
|
47
|
-
export interface GenericHostCallbacks extends HostCallbacks, GenericTokenConnectInput {
|
|
48
|
-
}
|
|
49
40
|
export interface GenericAuthOptions {
|
|
50
41
|
/** Injectable git HTTP transport for tests. */
|
|
51
42
|
httpClient?: typeof httpNode;
|
|
@@ -82,16 +73,3 @@ export declare function knownForgeTokenUrl(host: string): string | null;
|
|
|
82
73
|
* a full end-to-end verification of the token against a real repository.
|
|
83
74
|
*/
|
|
84
75
|
export declare function connectGenericHost(input: GenericTokenConnectInput, options?: GenericAuthOptions): Promise<HostCredential>;
|
|
85
|
-
/**
|
|
86
|
-
* {@link RemoteAuthProvider} wrapper for the generic token flow, so host apps
|
|
87
|
-
* can route any non-GitHub origin through the shared provider contract.
|
|
88
|
-
*/
|
|
89
|
-
export declare class GenericTokenAuthProvider implements RemoteAuthProvider {
|
|
90
|
-
private readonly options;
|
|
91
|
-
constructor(options?: GenericAuthOptions);
|
|
92
|
-
/** Handles any smart-HTTP(S) host that is not github.com (device flow). */
|
|
93
|
-
matches(origin: URL): boolean;
|
|
94
|
-
connect(callbacks: GenericHostCallbacks): Promise<HostCredential>;
|
|
95
|
-
/** Revalidate a stored credential: false only on a definitive rejection. */
|
|
96
|
-
validate(credential: HostCredential): Promise<boolean>;
|
|
97
|
-
}
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
*
|
|
32
32
|
* One timer serves the whole request (re-armed via `refresh()`; no per-chunk
|
|
33
33
|
* allocation). On timeout the thrown error's message classifies as offline in
|
|
34
|
-
*
|
|
34
|
+
* transport.ts. Known limitation (documented, not fixable at this
|
|
35
35
|
* layer): isomorphic-git's client accepts no AbortSignal, so an abandoned
|
|
36
36
|
* timed-out transfer's socket is left to the OS/agent to reap.
|
|
37
37
|
*
|
|
@@ -17,19 +17,6 @@ export interface HostCallbacks {
|
|
|
17
17
|
/** Optional cancellation (user closed the dialog). */
|
|
18
18
|
signal?: AbortSignal;
|
|
19
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
20
|
/**
|
|
34
21
|
* Client id resolution: explicit option → env var → registered default.
|
|
35
22
|
* Empty/whitespace-only values at any layer are treated as unset (the packaged
|
|
@@ -48,20 +35,13 @@ export interface GitHubAuthProviderOptions {
|
|
|
48
35
|
/** Injectable sleep for tests (so polling tests run instantly). */
|
|
49
36
|
sleepImpl?: (ms: number) => Promise<void>;
|
|
50
37
|
}
|
|
51
|
-
/** GitHub device-flow
|
|
52
|
-
export declare class GitHubAuthProvider
|
|
38
|
+
/** GitHub device-flow credential acquisition (ADR 0006 D1). */
|
|
39
|
+
export declare class GitHubAuthProvider {
|
|
53
40
|
private readonly clientId;
|
|
54
41
|
private readonly fetchImpl;
|
|
55
42
|
private readonly sleep;
|
|
56
43
|
constructor(options?: GitHubAuthProviderOptions);
|
|
57
|
-
matches(origin: URL): boolean;
|
|
58
44
|
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
45
|
/** Best-effort login lookup after auth — failure is non-fatal. */
|
|
66
46
|
private fetchUsername;
|
|
67
47
|
}
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
* [ISO-timestamp] LEVEL operation: step=<step> key=value ... | <message>
|
|
21
21
|
*
|
|
22
22
|
* Example:
|
|
23
|
-
* [2026-06-19T12:34:56.789Z] INFO
|
|
24
|
-
* [2026-06-19T12:34:56.890Z] INFO
|
|
25
|
-
* [2026-06-19T12:34:57.789Z] WARN
|
|
26
|
-
* [2026-06-19T12:34:57.890Z] INFO
|
|
23
|
+
* [2026-06-19T12:34:56.789Z] INFO sync: repo=my-book branch=main | starting sync
|
|
24
|
+
* [2026-06-19T12:34:56.890Z] INFO sync: step=snapshot | committed 3 changed files
|
|
25
|
+
* [2026-06-19T12:34:57.789Z] WARN sync: step=merge | combined files=manifest.yaml,notes.md
|
|
26
|
+
* [2026-06-19T12:34:57.890Z] INFO sync: result=synced | pushed to the online copy
|
|
27
27
|
*
|
|
28
28
|
* Cross-platform: uses `node:fs` appendFileSync + `node:path`. The caller
|
|
29
29
|
* is responsible for providing a valid directory (the logger creates the
|
|
@@ -59,8 +59,3 @@ export declare function createFileLogger(logFile: string, operation: string, min
|
|
|
59
59
|
* don't pass `logFile` see zero behavior change and zero file I/O).
|
|
60
60
|
*/
|
|
61
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;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Author-language copy for sync
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Author-language copy for sync (never raw git words). Extracted from sync.ts
|
|
3
|
+
* so the transport and orchestrator modules share ONE source of these
|
|
4
|
+
* strings — their exact wording is part of the observable contract (the host
|
|
5
|
+
* renders them verbatim).
|
|
6
6
|
*/
|
|
7
7
|
export declare const MSG_UP_TO_DATE = "Everything is in sync.";
|
|
8
8
|
export declare const MSG_UP_TO_DATE_PULLED = "Everything is in sync. The latest online changes were downloaded to this computer.";
|
|
@@ -12,14 +12,37 @@ export declare const MSG_OFFLINE = "Your changes are saved on this computer. gut
|
|
|
12
12
|
export declare const MSG_AUTH = "The online repository didn't accept the saved connection. Reconnect and try again.";
|
|
13
13
|
export declare const MSG_INSECURE_TRANSPORT = "This project's online address isn't secure, so the saved connection wasn't sent \u2014 connections are never sent over an insecure address. Switch the address to a secure one (starting with https), or to a local loopback address for a server on this computer, to sync.";
|
|
14
14
|
export declare const MSG_BUSY = "The online copy is changing very quickly right now. Your work is saved on this computer \u2014 try Sync again in a moment.";
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The project's own version history is unreadable — a damaged or missing
|
|
17
|
+
* `.git`. Says the three things that are actually true and useful: the writing
|
|
18
|
+
* is safe, the HISTORY is what broke, and (sync always has a remote, so this
|
|
19
|
+
* always applies here) a fresh copy from online is the way back.
|
|
20
|
+
*/
|
|
21
|
+
export declare const MSG_HISTORY_UNREADABLE = "This project's version history can't be read, so syncing can't continue. Your writing is safe \u2014 every file is still here on this computer. To get history and syncing working again, download a fresh copy of the project from online, then move any recent changes into it.";
|
|
22
|
+
export declare const MSG_UNRELATED = "This project and the online copy have no history in common, so they can't be combined. Either the online address points at a different project, or this project's own history was lost and started over. Your writing is safe \u2014 every file is still here on this computer. Check the online address; if it is correct, download a fresh copy from online and move any recent changes into it.";
|
|
16
23
|
export declare const MSG_NO_REMOTE = "This project isn't connected to an online repository yet.";
|
|
17
24
|
export declare const MSG_SSH_REMOTE = "This project's online address uses SSH (git@\u2026), which gutterpress can't sync to. Switch it to the web (HTTPS) address to sync from here.";
|
|
18
25
|
export declare const MSG_NO_BRANCH = "This project's version history isn't on a named branch, so it can't be synced right now.";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
export declare const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
/** Message recorded on the automatic pre-sync snapshot (D5 invariant).
|
|
27
|
+
* Pre-0.10.1 history carries the old spelling "Snapshot before syncing". */
|
|
28
|
+
export declare const SYNC_SNAPSHOT_MESSAGE = "Automatic backup of your work";
|
|
29
|
+
/**
|
|
30
|
+
* Message recorded when an edit reached disk while the fetch was in flight —
|
|
31
|
+
* after the pre-sync snapshot, before the merge (see the re-snapshot in
|
|
32
|
+
* `syncProject`). Distinct wording so the race is legible in history; the
|
|
33
|
+
* copy is already writer-voiced, so it deliberately stays unchanged (every
|
|
34
|
+
* rename permanently grows the timeline's superseded-spelling list).
|
|
35
|
+
*/
|
|
36
|
+
export declare const SYNC_LATE_EDIT_MESSAGE = "Saved the edit you made while syncing";
|
|
37
|
+
/**
|
|
38
|
+
* The keep-both sibling contract, kept HERE rather than in converge-merge.ts
|
|
39
|
+
* because every consumer needs it and converge-merge pulls in isomorphic-git:
|
|
40
|
+
* the markdown file resolver must not render a sibling as a chapter, and the
|
|
41
|
+
* merge-marker check must report it. This file imports nothing, so any of
|
|
42
|
+
* them can depend on it.
|
|
43
|
+
*
|
|
44
|
+
* `art/cover.png` -> `art/cover.online.png`; extensionless `NOTES` -> `NOTES.online`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function onlineSiblingPath(filepath: string): string;
|
|
47
|
+
/** True for a path `onlineSiblingPath` produced — Gutterpress's artifact, not the author's. */
|
|
48
|
+
export declare function isOnlineSibling(filepath: string): boolean;
|
|
@@ -16,26 +16,21 @@ import type { HostCredential, TokenStore } from "./token-store.ts";
|
|
|
16
16
|
*/
|
|
17
17
|
export type GitCache = Record<string, unknown>;
|
|
18
18
|
/**
|
|
19
|
-
* One
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* can never go stale.
|
|
19
|
+
* One file that changed on both sides and cannot carry conflict markers (a
|
|
20
|
+
* binary, or an SVG whose XML markers would break). Both versions are on
|
|
21
|
+
* disk and committed: ours stayed at `path`, the online one was written
|
|
22
|
+
* beside it. The host names the pair so the writer can fix it by hand.
|
|
24
23
|
*/
|
|
25
|
-
export interface
|
|
26
|
-
/** Repo-relative path
|
|
24
|
+
export interface KeptBothFile {
|
|
25
|
+
/** Repo-relative path holding OUR version (unchanged). */
|
|
27
26
|
path: string;
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
/** Blob oid of the online version. */
|
|
31
|
-
remoteOid: string;
|
|
32
|
-
/** Which side the automatic newer-wins policy kept on disk. */
|
|
33
|
-
kept: "local" | "online";
|
|
27
|
+
/** Repo-relative path holding the ONLINE version (`name.online.ext`). */
|
|
28
|
+
onlinePath: string;
|
|
34
29
|
}
|
|
35
30
|
/** Outcome of a sync attempt. Sync always converges — there is no "conflict"
|
|
36
31
|
* arm: overlapping text edits land in the file inside standard git conflict
|
|
37
|
-
* markers (`combinedFiles`), clashing binaries keep
|
|
38
|
-
* (`
|
|
32
|
+
* markers (`combinedFiles`), clashing binaries keep BOTH versions as two
|
|
33
|
+
* files (`keptBothFiles`), and every version is always reachable in
|
|
39
34
|
* history. Owner ruling 2026-08-14. */
|
|
40
35
|
export type SyncOutcome = {
|
|
41
36
|
status: "synced";
|
|
@@ -48,13 +43,17 @@ export type SyncOutcome = {
|
|
|
48
43
|
filesChanged?: boolean;
|
|
49
44
|
/** Files whose text now holds BOTH versions inside git conflict markers. */
|
|
50
45
|
combinedFiles?: string[];
|
|
51
|
-
/**
|
|
52
|
-
|
|
46
|
+
/** Files kept as a pair (ours at `path`, theirs at `onlinePath`). */
|
|
47
|
+
keptBothFiles?: KeptBothFile[];
|
|
53
48
|
} | {
|
|
54
49
|
status: "up-to-date";
|
|
55
50
|
message: string;
|
|
56
51
|
snapshotId?: string;
|
|
57
52
|
filesChanged?: boolean;
|
|
53
|
+
/** A pull-merge-only pass (`push: false`) can combine overlapping edits
|
|
54
|
+
* and then hold the push — the converge report still surfaces here. */
|
|
55
|
+
combinedFiles?: string[];
|
|
56
|
+
keptBothFiles?: KeptBothFile[];
|
|
58
57
|
} | {
|
|
59
58
|
status: "auth";
|
|
60
59
|
message: string;
|
|
@@ -68,90 +67,9 @@ export type SyncOutcome = {
|
|
|
68
67
|
} | {
|
|
69
68
|
status: "error";
|
|
70
69
|
message: string;
|
|
71
|
-
/**
|
|
72
|
-
* Stable machine-readable signal so a host UI can route without
|
|
73
|
-
* string-matching `message` (which stays free to reword):
|
|
74
|
-
* "needs-connection-setup" — the project isn't set up right (no
|
|
75
|
-
* remote / SSH remote / no named branch); route to the connect/setup
|
|
76
|
-
* surface.
|
|
77
|
-
*/
|
|
78
|
-
code?: "needs-connection-setup";
|
|
79
70
|
snapshotId?: string;
|
|
80
71
|
filesChanged?: boolean;
|
|
81
72
|
};
|
|
82
|
-
/**
|
|
83
|
-
* Outcome of a pull-only attempt ({@link pullChanges}): fetch + fast-forward/
|
|
84
|
-
* converge-merge of the online changes, NEVER a push. The merge always lands
|
|
85
|
-
* (see converge-merge.ts) — there is no "conflict" arm.
|
|
86
|
-
*/
|
|
87
|
-
export type PullOutcome = {
|
|
88
|
-
status: "pulled";
|
|
89
|
-
message: string;
|
|
90
|
-
/** Snapshot taken of unsaved work before pulling, if any. */
|
|
91
|
-
snapshotId?: string;
|
|
92
|
-
/**
|
|
93
|
-
* True when the pull created a combine (merge) commit — local commits
|
|
94
|
-
* existed alongside the online ones. False for a plain fast-forward.
|
|
95
|
-
*/
|
|
96
|
-
merged: boolean;
|
|
97
|
-
/**
|
|
98
|
-
* True when the working tree CONTENT changed (the tip's tree differs
|
|
99
|
-
* from before) — the host should refresh its preview.
|
|
100
|
-
*/
|
|
101
|
-
filesChanged: boolean;
|
|
102
|
-
/** Files whose text now holds BOTH versions inside git conflict markers. */
|
|
103
|
-
combinedFiles?: string[];
|
|
104
|
-
/** Clashing images (newer kept) for the host's non-blocking picker. */
|
|
105
|
-
imageClashes?: ImageClash[];
|
|
106
|
-
} | {
|
|
107
|
-
status: "up-to-date";
|
|
108
|
-
message: string;
|
|
109
|
-
snapshotId?: string;
|
|
110
|
-
} | {
|
|
111
|
-
status: "auth";
|
|
112
|
-
message: string;
|
|
113
|
-
snapshotId?: string;
|
|
114
|
-
} | {
|
|
115
|
-
status: "offline";
|
|
116
|
-
message: string;
|
|
117
|
-
snapshotId?: string;
|
|
118
|
-
} | {
|
|
119
|
-
status: "error";
|
|
120
|
-
message: string;
|
|
121
|
-
snapshotId?: string;
|
|
122
|
-
};
|
|
123
|
-
/**
|
|
124
|
-
* Outcome of a push-only attempt ({@link pushChanges}): snapshot-if-needed,
|
|
125
|
-
* then push — NEVER a merge. When the online copy has commits this computer
|
|
126
|
-
* doesn't have, the result is the typed `"pull-first"` status (the host shows
|
|
127
|
-
* a plain-language "get the latest changes first" message) — pushChanges
|
|
128
|
-
* never auto-merges.
|
|
129
|
-
*/
|
|
130
|
-
export type PushOutcome = {
|
|
131
|
-
status: "pushed";
|
|
132
|
-
message: string;
|
|
133
|
-
snapshotId?: string;
|
|
134
|
-
} | {
|
|
135
|
-
status: "up-to-date";
|
|
136
|
-
message: string;
|
|
137
|
-
snapshotId?: string;
|
|
138
|
-
} | {
|
|
139
|
-
status: "pull-first";
|
|
140
|
-
message: string;
|
|
141
|
-
snapshotId?: string;
|
|
142
|
-
} | {
|
|
143
|
-
status: "auth";
|
|
144
|
-
message: string;
|
|
145
|
-
snapshotId?: string;
|
|
146
|
-
} | {
|
|
147
|
-
status: "offline";
|
|
148
|
-
message: string;
|
|
149
|
-
snapshotId?: string;
|
|
150
|
-
} | {
|
|
151
|
-
status: "error";
|
|
152
|
-
message: string;
|
|
153
|
-
snapshotId?: string;
|
|
154
|
-
};
|
|
155
73
|
export interface SyncProjectOptions {
|
|
156
74
|
projectDir: string;
|
|
157
75
|
/** Explicit credential; wins over the token store. */
|
|
@@ -162,12 +80,23 @@ export interface SyncProjectOptions {
|
|
|
162
80
|
message?: string;
|
|
163
81
|
authorName?: string;
|
|
164
82
|
authorEmail?: string;
|
|
83
|
+
/**
|
|
84
|
+
* When `false`, run a pull-merge-only pass: fetch + converge-merge exactly
|
|
85
|
+
* as always (remote work still arrives promptly), but hold the network push
|
|
86
|
+
* — and mint no snapshot unless the remote moved and the merge needs one
|
|
87
|
+
* (D5), so a quiet pass creates no commit at all. Local work stays safely
|
|
88
|
+
* committed/on disk for the next push-enabled pass to send. Default `true`:
|
|
89
|
+
* the full snapshot-first → fetch → merge → push pass. Owner decision
|
|
90
|
+
* 2026-08-23 (push cadence): the desktop's 2-minute tick pulls every time
|
|
91
|
+
* and enables the push only every ~15 minutes and on app exit.
|
|
92
|
+
*/
|
|
93
|
+
push?: boolean;
|
|
165
94
|
/** Injectable git HTTP transport for tests. */
|
|
166
95
|
httpClient?: typeof httpNode;
|
|
167
96
|
/**
|
|
168
|
-
* Bounded retry policy for {@link syncProject}'s
|
|
169
|
-
* loop is ALWAYS bounded (never infinite) and the snapshot-first
|
|
170
|
-
* holds on every path. Defaults to {@link DEFAULT_SYNC_RETRY}. `sleep` is
|
|
97
|
+
* Bounded retry policy for {@link syncProject}'s fetch→merge→push race
|
|
98
|
+
* loop. The loop is ALWAYS bounded (never infinite) and the snapshot-first
|
|
99
|
+
* guarantee holds on every path. Defaults to {@link DEFAULT_SYNC_RETRY}. `sleep` is
|
|
171
100
|
* injectable so tests can drive backoff deterministically.
|
|
172
101
|
*/
|
|
173
102
|
retry?: SyncRetryOptions;
|
|
@@ -1,30 +1,28 @@
|
|
|
1
1
|
import { onAuthFor } from "./transport.ts";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SyncOutcome, SyncProjectOptions } from "./sync-types.ts";
|
|
3
3
|
export { onAuthFor };
|
|
4
4
|
export { SYNC_SNAPSHOT_MESSAGE } from "./sync-messages.ts";
|
|
5
|
-
export type {
|
|
5
|
+
export type { KeptBothFile, SyncOutcome, SyncProjectOptions, } from "./sync-types.ts";
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* the push reports pull-first and the pair re-runs (their commits converge in
|
|
10
|
-
* on the next pass), with a short backoff between passes. The loop is ALWAYS
|
|
11
|
-
* bounded by `retry.attempts`; a remote that races every pass surfaces a
|
|
12
|
-
* friendly "try again in a moment" rather than looping forever. Never throws
|
|
13
|
-
* for expected outcomes — everything is reported through {@link SyncOutcome}.
|
|
7
|
+
* A push the remote refused because our branch is behind — the pull-first
|
|
8
|
+
* situation the retry loop exists for.
|
|
14
9
|
*/
|
|
15
|
-
export declare function
|
|
10
|
+
export declare function isPushRejected(e: unknown): boolean;
|
|
16
11
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* function-scoped object cache, released on return.
|
|
12
|
+
* Unrelated histories — the local project and the configured online project
|
|
13
|
+
* share no common starting point. Sync surfaces this as a plain setup error
|
|
14
|
+
* (a wrong online address must never be silently spliced into the book).
|
|
21
15
|
*/
|
|
22
|
-
export declare function
|
|
16
|
+
export declare function isUnrelatedHistories(e: unknown): boolean;
|
|
23
17
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* the
|
|
18
|
+
* Snapshot-first sync (ADR 0006 D5). Serialized on the per-repo lock; one
|
|
19
|
+
* function-scoped object cache, released on return.
|
|
20
|
+
*
|
|
21
|
+
* If someone pushes between our fetch and our push, the push is rejected and
|
|
22
|
+
* the fetch→merge→push pass re-runs (their commits converge in on the next
|
|
23
|
+
* fetch), with a short backoff between passes. The loop is ALWAYS bounded by
|
|
24
|
+
* `retry.attempts`; a remote that races every pass surfaces a friendly "try
|
|
25
|
+
* again in a moment" rather than looping forever. Never throws for expected
|
|
26
|
+
* outcomes — everything is reported through {@link SyncOutcome}.
|
|
29
27
|
*/
|
|
30
|
-
export declare function
|
|
28
|
+
export declare function syncProject(options: SyncProjectOptions): Promise<SyncOutcome>;
|
|
@@ -25,10 +25,6 @@ export interface TokenStore {
|
|
|
25
25
|
/** All stored credentials (used by "connected accounts" UIs). */
|
|
26
26
|
list(): Promise<HostCredential[]>;
|
|
27
27
|
}
|
|
28
|
-
/** A credential with the token value masked — safe for logs/diagnostics. */
|
|
29
|
-
export declare function redactCredential(cred: HostCredential): Omit<HostCredential, "token"> & {
|
|
30
|
-
token: string;
|
|
31
|
-
};
|
|
32
28
|
/**
|
|
33
29
|
* THE canonical credential-store key for a remote host — the ONE derivation
|
|
34
30
|
* every writer (GitHub device flow, generic connect, embedded-URL migration)
|
|
@@ -87,9 +83,3 @@ export interface UrlCredentialExtraction {
|
|
|
87
83
|
* display. Non-HTTP(S) or unparseable URLs pass through unchanged.
|
|
88
84
|
*/
|
|
89
85
|
export declare function extractUrlCredential(url: string): UrlCredentialExtraction;
|
|
90
|
-
/**
|
|
91
|
-
* Migrate any credential embedded in `url` into `store` (only when the store
|
|
92
|
-
* has no existing credential for that host — a stored credential is fresher
|
|
93
|
-
* than one fossilized in a clone URL) and return the sanitized URL.
|
|
94
|
-
*/
|
|
95
|
-
export declare function migrateUrlCredential(url: string, store: TokenStore): Promise<string>;
|