@fluado/enmetu 0.0.0-stage → 0.2.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 (209) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +209 -2
  3. package/dist/adapters/clock/systemClockAdapter.d.ts +6 -0
  4. package/dist/adapters/clock/systemClockAdapter.js +12 -0
  5. package/dist/adapters/fs/nodeFileSystemAdapter.d.ts +18 -0
  6. package/dist/adapters/fs/nodeFileSystemAdapter.js +230 -0
  7. package/dist/adapters/git/gitFacade.d.ts +148 -0
  8. package/dist/adapters/git/gitFacade.js +822 -0
  9. package/dist/adapters/git/gitLocatorFacade.d.ts +8 -0
  10. package/dist/adapters/git/gitLocatorFacade.js +75 -0
  11. package/dist/adapters/git/gitProcessAdapter.d.ts +46 -0
  12. package/dist/adapters/git/gitProcessAdapter.js +261 -0
  13. package/dist/adapters/log/nullLogger.d.ts +9 -0
  14. package/dist/adapters/log/nullLogger.js +10 -0
  15. package/dist/adapters/log/stderrLoggerAdapter.d.ts +10 -0
  16. package/dist/adapters/log/stderrLoggerAdapter.js +13 -0
  17. package/dist/adapters/process/nodeProcessAdapter.d.ts +9 -0
  18. package/dist/adapters/process/nodeProcessAdapter.js +56 -0
  19. package/dist/adapters/watch/fsWatchAdapter.d.ts +24 -0
  20. package/dist/adapters/watch/fsWatchAdapter.js +47 -0
  21. package/dist/cli/commands/hookCommand.d.ts +29 -0
  22. package/dist/cli/commands/hookCommand.js +92 -0
  23. package/dist/cli/commands/resumeCommand.d.ts +48 -0
  24. package/dist/cli/commands/resumeCommand.js +124 -0
  25. package/dist/cli/commands/runCommand.d.ts +34 -0
  26. package/dist/cli/commands/runCommand.js +79 -0
  27. package/dist/cli/commands/statusCommand.d.ts +71 -0
  28. package/dist/cli/commands/statusCommand.js +249 -0
  29. package/dist/cli/exitCodes.d.ts +7 -0
  30. package/dist/cli/exitCodes.js +38 -0
  31. package/dist/cli/helpText.d.ts +1 -0
  32. package/dist/cli/helpText.js +16 -0
  33. package/dist/cli/main.d.ts +8 -0
  34. package/dist/cli/main.js +49 -0
  35. package/dist/cli/parseArgs.d.ts +19 -0
  36. package/dist/cli/parseArgs.js +106 -0
  37. package/dist/cli.d.ts +2 -0
  38. package/dist/cli.js +32 -0
  39. package/dist/commit/commitLocalChangesController.d.ts +132 -0
  40. package/dist/commit/commitLocalChangesController.js +357 -0
  41. package/dist/commit/gitIndexLock.d.ts +36 -0
  42. package/dist/commit/gitIndexLock.js +90 -0
  43. package/dist/commit/repairStaleIndexController.d.ts +90 -0
  44. package/dist/commit/repairStaleIndexController.js +150 -0
  45. package/dist/commit/stageGuardedChanges.d.ts +30 -0
  46. package/dist/commit/stageGuardedChanges.js +52 -0
  47. package/dist/contracts/IClock.d.ts +7 -0
  48. package/dist/contracts/IClock.js +1 -0
  49. package/dist/contracts/IDaemon.d.ts +26 -0
  50. package/dist/contracts/IDaemon.js +1 -0
  51. package/dist/contracts/IFileSystem.d.ts +50 -0
  52. package/dist/contracts/IFileSystem.js +1 -0
  53. package/dist/contracts/IGitCommitter.d.ts +60 -0
  54. package/dist/contracts/IGitCommitter.js +1 -0
  55. package/dist/contracts/IGitConfigReader.d.ts +5 -0
  56. package/dist/contracts/IGitConfigReader.js +1 -0
  57. package/dist/contracts/IGitIndexLock.d.ts +44 -0
  58. package/dist/contracts/IGitIndexLock.js +1 -0
  59. package/dist/contracts/IGitReader.d.ts +81 -0
  60. package/dist/contracts/IGitReader.js +1 -0
  61. package/dist/contracts/IGitRunner.d.ts +49 -0
  62. package/dist/contracts/IGitRunner.js +1 -0
  63. package/dist/contracts/IGitWriter.d.ts +39 -0
  64. package/dist/contracts/IGitWriter.js +1 -0
  65. package/dist/contracts/IHaltStore.d.ts +14 -0
  66. package/dist/contracts/IHaltStore.js +1 -0
  67. package/dist/contracts/IHookInstaller.d.ts +14 -0
  68. package/dist/contracts/IHookInstaller.js +1 -0
  69. package/dist/contracts/IInstanceLock.d.ts +26 -0
  70. package/dist/contracts/IInstanceLock.js +1 -0
  71. package/dist/contracts/ILogger.d.ts +11 -0
  72. package/dist/contracts/ILogger.js +1 -0
  73. package/dist/contracts/IOperationJournal.d.ts +12 -0
  74. package/dist/contracts/IOperationJournal.js +1 -0
  75. package/dist/contracts/IPathQuarantine.d.ts +67 -0
  76. package/dist/contracts/IPathQuarantine.js +1 -0
  77. package/dist/contracts/IProcessHost.d.ts +11 -0
  78. package/dist/contracts/IProcessHost.js +1 -0
  79. package/dist/contracts/IRepositoryLocator.d.ts +32 -0
  80. package/dist/contracts/IRepositoryLocator.js +1 -0
  81. package/dist/contracts/IStateRecorder.d.ts +68 -0
  82. package/dist/contracts/IStateRecorder.js +1 -0
  83. package/dist/contracts/IStateStore.d.ts +88 -0
  84. package/dist/contracts/IStateStore.js +1 -0
  85. package/dist/contracts/ISyncCycle.d.ts +23 -0
  86. package/dist/contracts/ISyncCycle.js +1 -0
  87. package/dist/contracts/ISyncCycleRunner.d.ts +4 -0
  88. package/dist/contracts/ISyncCycleRunner.js +1 -0
  89. package/dist/contracts/ISyncScheduler.d.ts +6 -0
  90. package/dist/contracts/ISyncScheduler.js +1 -0
  91. package/dist/contracts/ITextSink.d.ts +3 -0
  92. package/dist/contracts/ITextSink.js +1 -0
  93. package/dist/contracts/IWatcher.d.ts +9 -0
  94. package/dist/contracts/IWatcher.js +1 -0
  95. package/dist/contracts/config.d.ts +26 -0
  96. package/dist/contracts/config.js +1 -0
  97. package/dist/contracts/errors.d.ts +39 -0
  98. package/dist/contracts/errors.js +64 -0
  99. package/dist/contracts/exitCode.d.ts +10 -0
  100. package/dist/contracts/exitCode.js +17 -0
  101. package/dist/contracts/gitTypes.d.ts +113 -0
  102. package/dist/contracts/gitTypes.js +1 -0
  103. package/dist/contracts/library.d.ts +35 -0
  104. package/dist/contracts/library.js +1 -0
  105. package/dist/contracts/logCodes.d.ts +1 -0
  106. package/dist/contracts/logCodes.js +1 -0
  107. package/dist/contracts/runtime.d.ts +16 -0
  108. package/dist/contracts/runtime.js +1 -0
  109. package/dist/contracts/syncTypes.d.ts +75 -0
  110. package/dist/contracts/syncTypes.js +6 -0
  111. package/dist/core/commit/commitMemory.d.ts +3 -0
  112. package/dist/core/commit/commitMemory.js +9 -0
  113. package/dist/core/commit/commitMessage.d.ts +6 -0
  114. package/dist/core/commit/commitMessage.js +12 -0
  115. package/dist/core/config/parseConfig.d.ts +8 -0
  116. package/dist/core/config/parseConfig.js +176 -0
  117. package/dist/core/content/contentRules.d.ts +11 -0
  118. package/dist/core/content/contentRules.js +50 -0
  119. package/dist/core/errors/errorMessage.d.ts +6 -0
  120. package/dist/core/errors/errorMessage.js +14 -0
  121. package/dist/core/errors/withCycleId.d.ts +7 -0
  122. package/dist/core/errors/withCycleId.js +38 -0
  123. package/dist/core/git/enmetuRefs.d.ts +9 -0
  124. package/dist/core/git/enmetuRefs.js +22 -0
  125. package/dist/core/git/porcelain.d.ts +52 -0
  126. package/dist/core/git/porcelain.js +219 -0
  127. package/dist/core/git/rebaseOwnership.d.ts +28 -0
  128. package/dist/core/git/rebaseOwnership.js +29 -0
  129. package/dist/core/git/stoppedRebaseWrites.d.ts +10 -0
  130. package/dist/core/git/stoppedRebaseWrites.js +22 -0
  131. package/dist/core/halt/resumeHint.d.ts +6 -0
  132. package/dist/core/halt/resumeHint.js +9 -0
  133. package/dist/core/log/formatLogLine.d.ts +3 -0
  134. package/dist/core/log/formatLogLine.js +11 -0
  135. package/dist/core/quarantine/keptBlobMessage.d.ts +5 -0
  136. package/dist/core/quarantine/keptBlobMessage.js +8 -0
  137. package/dist/core/quarantine/restoreWarnings.d.ts +16 -0
  138. package/dist/core/quarantine/restoreWarnings.js +36 -0
  139. package/dist/core/schedule/schedulerStep.d.ts +63 -0
  140. package/dist/core/schedule/schedulerStep.js +144 -0
  141. package/dist/core/state/stateGuards.d.ts +21 -0
  142. package/dist/core/state/stateGuards.js +295 -0
  143. package/dist/core/state/stateLiveness.d.ts +10 -0
  144. package/dist/core/state/stateLiveness.js +21 -0
  145. package/dist/core/state/stateRecordStep.d.ts +28 -0
  146. package/dist/core/state/stateRecordStep.js +203 -0
  147. package/dist/core/status/conflictGuide.d.ts +30 -0
  148. package/dist/core/status/conflictGuide.js +86 -0
  149. package/dist/core/status/orphanedCommits.d.ts +15 -0
  150. package/dist/core/status/orphanedCommits.js +24 -0
  151. package/dist/core/status/statusHeadline.d.ts +46 -0
  152. package/dist/core/status/statusHeadline.js +130 -0
  153. package/dist/core/watch/watchFilter.d.ts +2 -0
  154. package/dist/core/watch/watchFilter.js +34 -0
  155. package/dist/daemon/daemon.d.ts +55 -0
  156. package/dist/daemon/daemon.js +277 -0
  157. package/dist/enmetuInstances.d.ts +65 -0
  158. package/dist/enmetuInstances.js +332 -0
  159. package/dist/halt/haltGuardGitWriter.d.ts +23 -0
  160. package/dist/halt/haltGuardGitWriter.js +61 -0
  161. package/dist/halt/haltStore.d.ts +13 -0
  162. package/dist/halt/haltStore.js +49 -0
  163. package/dist/halt/recordHalt.d.ts +19 -0
  164. package/dist/halt/recordHalt.js +20 -0
  165. package/dist/hooks/hookScript.d.ts +22 -0
  166. package/dist/hooks/hookScript.js +52 -0
  167. package/dist/hooks/nullHookInstaller.d.ts +5 -0
  168. package/dist/hooks/nullHookInstaller.js +6 -0
  169. package/dist/hooks/preCommitCheckController.d.ts +23 -0
  170. package/dist/hooks/preCommitCheckController.js +22 -0
  171. package/dist/hooks/preCommitHookInstaller.d.ts +32 -0
  172. package/dist/hooks/preCommitHookInstaller.js +123 -0
  173. package/dist/index.d.ts +18 -0
  174. package/dist/index.js +38 -0
  175. package/dist/library/enmetuLibrary.d.ts +16 -0
  176. package/dist/library/enmetuLibrary.js +62 -0
  177. package/dist/library/readStateController.d.ts +28 -0
  178. package/dist/library/readStateController.js +32 -0
  179. package/dist/library/requestedStopProcessHost.d.ts +28 -0
  180. package/dist/library/requestedStopProcessHost.js +58 -0
  181. package/dist/lock/instanceLock.d.ts +26 -0
  182. package/dist/lock/instanceLock.js +125 -0
  183. package/dist/preflight/repositoryPreflight.d.ts +20 -0
  184. package/dist/preflight/repositoryPreflight.js +42 -0
  185. package/dist/quarantine/pathQuarantine.d.ts +113 -0
  186. package/dist/quarantine/pathQuarantine.js +504 -0
  187. package/dist/recovery/enmetuRebaseAbort.d.ts +108 -0
  188. package/dist/recovery/enmetuRebaseAbort.js +302 -0
  189. package/dist/recovery/operationJournal.d.ts +13 -0
  190. package/dist/recovery/operationJournal.js +37 -0
  191. package/dist/recovery/recoveryController.d.ts +48 -0
  192. package/dist/recovery/recoveryController.js +112 -0
  193. package/dist/state/stateFileStore.d.ts +18 -0
  194. package/dist/state/stateFileStore.js +73 -0
  195. package/dist/state/stateRecorder.d.ts +26 -0
  196. package/dist/state/stateRecorder.js +69 -0
  197. package/dist/state/stateRecordingSyncCycle.d.ts +24 -0
  198. package/dist/state/stateRecordingSyncCycle.js +71 -0
  199. package/dist/sync/hookAbortingSyncScheduler.d.ts +16 -0
  200. package/dist/sync/hookAbortingSyncScheduler.js +27 -0
  201. package/dist/sync/integrateUpstreamController.d.ts +95 -0
  202. package/dist/sync/integrateUpstreamController.js +353 -0
  203. package/dist/sync/syncCycleController.d.ts +71 -0
  204. package/dist/sync/syncCycleController.js +345 -0
  205. package/dist/sync/syncCycleRunner.d.ts +24 -0
  206. package/dist/sync/syncCycleRunner.js +169 -0
  207. package/dist/sync/syncScheduler.d.ts +32 -0
  208. package/dist/sync/syncScheduler.js +97 -0
  209. package/package.json +42 -4
@@ -0,0 +1,113 @@
1
+ export type StatusCode = ' ' | 'M' | 'T' | 'A' | 'D' | 'R' | 'C' | 'U' | '?' | '!';
2
+ export interface StatusEntry {
3
+ readonly index: StatusCode;
4
+ readonly worktree: StatusCode;
5
+ /** Root-relative, byte for byte what git reported (NUL-separated, never quoted). */
6
+ readonly path: string;
7
+ }
8
+ export interface StagedFile {
9
+ readonly path: string;
10
+ /** null: not a regular file (symlink, gitlink) or larger than the scan limit. */
11
+ readonly content: Buffer | null;
12
+ }
13
+ export interface Upstream {
14
+ /** refs/heads/main */
15
+ readonly branchRef: string;
16
+ /** origin */
17
+ readonly remote: string;
18
+ /** refs/heads/main, the ref on the remote */
19
+ readonly remoteRef: string;
20
+ /** refs/remotes/origin/main */
21
+ readonly trackingRef: string;
22
+ }
23
+ export type ForeignOperation = 'rebase' | 'merge' | 'cherry-pick' | 'revert' | 'bisect';
24
+ export interface RepositoryState {
25
+ readonly indexLocked: boolean;
26
+ readonly operation: ForeignOperation | null;
27
+ /** null when HEAD is detached. */
28
+ readonly branchRef: string | null;
29
+ }
30
+ export interface Divergence {
31
+ readonly ahead: number;
32
+ readonly behind: number;
33
+ }
34
+ export type RemoteHead = {
35
+ readonly kind: 'found';
36
+ readonly sha: string;
37
+ } | {
38
+ readonly kind: 'absent';
39
+ } | {
40
+ readonly kind: 'unreachable';
41
+ readonly detail: string;
42
+ };
43
+ export interface StageResult {
44
+ /** true when `git add --ignore-errors` skipped files it could not read. */
45
+ readonly partial: boolean;
46
+ readonly detail: string;
47
+ }
48
+ export type FetchResult = {
49
+ readonly kind: 'fetched';
50
+ } | {
51
+ readonly kind: 'failed';
52
+ readonly detail: string;
53
+ };
54
+ /**
55
+ * HEAD's newest "(start)" reflog entry, which names who started the rebase
56
+ * in progress (design 13.2, Decision 104).
57
+ */
58
+ export type RebaseStart =
59
+ /** HEAD has no reflog (`git reflog exists HEAD` exits 1): ownership falls back to orig-head and onto. */
60
+ {
61
+ readonly kind: 'unlogged';
62
+ }
63
+ /** HEAD's reflog holds no start entry. */
64
+ | {
65
+ readonly kind: 'none';
66
+ }
67
+ /**
68
+ * action: the text before " (start): ", which is GIT_REFLOG_ACTION when the
69
+ * starter set it (enmetu's attempt token) and otherwise git's own label of
70
+ * the command ("rebase", "pull -q --rebase --no-autostash").
71
+ */
72
+ | {
73
+ readonly kind: 'entry';
74
+ readonly action: string;
75
+ };
76
+ /**
77
+ * HEAD moved by another command after enmetu's rebase last moved it (design
78
+ * 14.14, Decision 124): from is the commit of HEAD's newest reflog entry the
79
+ * rebase wrote under its own action, to is HEAD's newest entry's commit.
80
+ */
81
+ export interface HeadMove {
82
+ readonly from: string;
83
+ readonly to: string;
84
+ }
85
+ export type RebaseResult = {
86
+ readonly kind: 'rebased';
87
+ } | {
88
+ readonly kind: 'stopped';
89
+ readonly detail: string;
90
+ }
91
+ /** git found a rebase already in progress and started nothing; never aborted. */
92
+ | {
93
+ readonly kind: 'refused';
94
+ readonly detail: string;
95
+ }
96
+ /**
97
+ * git exited non-zero and left no rebase in progress: it refused before
98
+ * starting, typically because a person wrote or staged a file after the
99
+ * commit step (design 12.7). Nothing to abort or verify; retried next cycle.
100
+ */
101
+ | {
102
+ readonly kind: 'not-started';
103
+ readonly detail: string;
104
+ };
105
+ export type PushResult = {
106
+ readonly kind: 'pushed';
107
+ } | {
108
+ readonly kind: 'rejected';
109
+ readonly detail: string;
110
+ } | {
111
+ readonly kind: 'failed';
112
+ readonly detail: string;
113
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ import type { ExitCode } from './exitCode.js';
2
+ import type { StateRecord } from './IStateStore.js';
3
+ import type { ITextSink } from './ITextSink.js';
4
+ /** The public library entry (design 12.5): start, stop and read state inside a host program. */
5
+ export interface EnmetuStartOptions {
6
+ /** Any directory inside the work tree; the root is resolved through git (D9). */
7
+ readonly repo: string;
8
+ /** Where enmetu's log lines go; default the process's stderr. */
9
+ readonly log?: ITextSink | undefined;
10
+ /** The environment git children start from; default the process environment. */
11
+ readonly env?: Readonly<Record<string, string | undefined>> | undefined;
12
+ }
13
+ export interface EnmetuInstance {
14
+ /**
15
+ * Resolves once the run ends and never rejects: 0 stopped, 1 unexpected
16
+ * error (its line written to the log sink the way the CLI writes it to stderr),
17
+ * 2 invalid config, 3 halted, 4 locked, 5 not eligible (design 1.18, 12.5).
18
+ */
19
+ readonly done: Promise<ExitCode>;
20
+ /** Stops after the running cycle, like the first SIGTERM for the CLI; idempotent; resolves with done. */
21
+ stop(): Promise<ExitCode>;
22
+ }
23
+ /**
24
+ * How the state record relates to a live process (design 12.1):
25
+ * running: the lock's live holder wrote the record;
26
+ * unrecorded: an instance holds the lock but has not written a record yet;
27
+ * stopped: no live holder and the record says stopped;
28
+ * crashed: no live holder and the record says anything else;
29
+ * never: no live holder and no readable record.
30
+ */
31
+ export type StateLiveness = 'running' | 'unrecorded' | 'stopped' | 'crashed' | 'never';
32
+ export interface EnmetuStateView {
33
+ readonly liveness: StateLiveness;
34
+ readonly record: StateRecord | null;
35
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export type LogCode = 'STARTED' | 'STOPPING' | 'STOPPED' | 'LOCK_STALE_TAKEN' | 'RECOVERED' | 'HOOK_INSTALLED' | 'HOOK_UPDATED' | 'HOOK_SKIPPED' | 'HOOK_FAILED' | 'WATCHER_FAILED' | 'COMMITTED' | 'STAGE_PARTIAL' | 'EXCLUDED' | 'PULLED' | 'PUSHED' | 'PUSH_REJECTED' | 'PUSH_FAILED' | 'OFFLINE' | 'CONFLICT' | 'UNPUSHED' | 'PAUSED' | 'BUSY' | 'RESUMED' | 'QUARANTINE_KEPT' | 'HALTED' | 'CYCLE_FAILED' | 'NOT_ELIGIBLE' | 'LOCKED' | 'CONFIG_INVALID' | 'CONFIG_KEY_IGNORED' | 'STATE_WRITE_FAILED' | 'WAITING' | 'SELECTION_WAIT_ENDED' | 'HOOK_REJECTED' | 'COMMIT_DISCARDED' | 'INDEX_REPAIRED' | 'QUARANTINE_UPSTREAM_CHANGED' | 'REBASE_NOT_STARTED' | 'STAGED_RESET' | 'STAGED_REVERT' | 'REBASE_BRANCH_MOVED' | 'COMMIT_ORPHANED';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ import type { ITextSink } from './ITextSink.js';
2
+ /** Everything the process gives enmetu, gathered once by src/cli.ts. */
3
+ export interface Runtime {
4
+ /** Arguments after the node binary and the script. */
5
+ readonly argv: readonly string[];
6
+ /** Base environment for every git child. */
7
+ readonly env: Readonly<Record<string, string | undefined>>;
8
+ readonly cwd: string;
9
+ /** Absolute node binary, recorded in the hook. */
10
+ readonly execPath: string;
11
+ /** Absolute path of the CLI script (Node resolves argv[1]), recorded in the hook. */
12
+ readonly cliPath: string;
13
+ readonly version: string;
14
+ readonly stdout: ITextSink;
15
+ readonly stderr: ITextSink;
16
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,75 @@
1
+ import type { HaltCode } from './IHaltStore.js';
2
+ /** The inputs of a rebase that stopped; an identical pair is not re-run (Decision 11). */
3
+ export interface ConflictKey {
4
+ readonly head: string;
5
+ readonly upstream: string;
6
+ }
7
+ export type PauseReason = 'FOREIGN_OPERATION' | 'DETACHED_HEAD' | 'NO_UPSTREAM' | 'UPSTREAM_NOT_REMOTE' | 'UPSTREAM_GONE' | 'BRANCH_CHANGED' | 'QUARANTINE_UNSUPPORTED'
8
+ /**
9
+ * git refused to start enmetu's rebase on REBASE_REFUSED_AFTER consecutive
10
+ * cycles (design 12.7, Decision 82): a cause that does not go away by itself.
11
+ */
12
+ | 'REBASE_REFUSED';
13
+ /**
14
+ * Why a cycle committed nothing although the tree has changes (design 12.7):
15
+ * a person has a selection staged and partialStagingWaitMs has not passed
16
+ * (design 14.17), or a repository commit hook rejected enmetu's commit.
17
+ */
18
+ export type WaitReason = 'PARTIAL_SELECTION_STAGED' | 'COMMIT_HOOK_REJECTED';
19
+ export type CycleOutcome = {
20
+ readonly kind: 'synced';
21
+ } | {
22
+ readonly kind: 'paused';
23
+ readonly reason: PauseReason;
24
+ } | {
25
+ readonly kind: 'busy';
26
+ } | {
27
+ readonly kind: 'offline';
28
+ } | {
29
+ readonly kind: 'conflict';
30
+ } | {
31
+ readonly kind: 'push-failed';
32
+ } | {
33
+ readonly kind: 'halted';
34
+ readonly code: HaltCode;
35
+ } | {
36
+ readonly kind: 'waiting';
37
+ readonly reason: WaitReason;
38
+ };
39
+ /** What the scheduler sees of a cycle; 'failed' is a cycle that threw. */
40
+ export type CycleOutcomeKind = CycleOutcome['kind'] | 'failed';
41
+ /** A commit a repository hook rejected: the tree it was built from and when (epoch ms). */
42
+ export interface HookRejection {
43
+ readonly tree: string;
44
+ readonly at: number;
45
+ }
46
+ /**
47
+ * The last move of HEAD enmetu made within one cycle, its commit, its pull or
48
+ * both (design 12.7, Decision 98): from is HEAD before the first of them
49
+ * (the commit's parent when the cycle committed), to is HEAD after the last.
50
+ */
51
+ export interface HeadMove {
52
+ readonly from: string;
53
+ readonly to: string;
54
+ }
55
+ /**
56
+ * What the commit step remembers across cycles (design 12.7): plain state the
57
+ * cycle returns and SyncCycleRunner hands to the next cycle, like the conflict
58
+ * key. git cannot tell how long a selection has been staged, which tree a hook
59
+ * rejected, what a post-commit hook left, or which commit an index written
60
+ * back by a person's git command is stale against.
61
+ */
62
+ export interface CommitMemory {
63
+ /** Epoch ms of the first cycle that found the real index differing from HEAD; null when it does not. */
64
+ readonly partialStagingSince: number | null;
65
+ readonly rejection: HookRejection | null;
66
+ /** The tree a fresh build gave right after enmetu's last commit with hooks; null when hooks left nothing. */
67
+ readonly residueTree: string | null;
68
+ /**
69
+ * Kept while HEAD is still lastMove.to and no repair was applied for it,
70
+ * so a later cycle can set back index entries a person's git command wrote
71
+ * back from before the move (Decision 98); null otherwise.
72
+ */
73
+ readonly lastMove: HeadMove | null;
74
+ }
75
+ export declare const EMPTY_COMMIT_MEMORY: CommitMemory;
@@ -0,0 +1,6 @@
1
+ export const EMPTY_COMMIT_MEMORY = {
2
+ partialStagingSince: null,
3
+ rejection: null,
4
+ residueTree: null,
5
+ lastMove: null,
6
+ };
@@ -0,0 +1,3 @@
1
+ import type { CommitMemory, HeadMove } from '../../contracts/syncTypes.js';
2
+ /** The memory with lastMove replaced, every other field kept (Decision 98); the commit step and the cycle both set it. */
3
+ export declare function withLastMove(memory: CommitMemory, lastMove: HeadMove | null): CommitMemory;
@@ -0,0 +1,9 @@
1
+ /** The memory with lastMove replaced, every other field kept (Decision 98); the commit step and the cycle both set it. */
2
+ export function withLastMove(memory, lastMove) {
3
+ return {
4
+ partialStagingSince: memory.partialStagingSince,
5
+ rejection: memory.rejection,
6
+ residueTree: memory.residueTree,
7
+ lastMove,
8
+ };
9
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * git's boolean config reading for commit.gpgSign: true, yes, on, 1 (any
3
+ * case) and '' (a key written without a value) are true; null (unset) and
4
+ * everything else are false.
5
+ */
6
+ export declare function gitBoolean(value: string | null): boolean;
@@ -0,0 +1,12 @@
1
+ const GIT_TRUE = ['true', 'yes', 'on', '1', ''];
2
+ /**
3
+ * git's boolean config reading for commit.gpgSign: true, yes, on, 1 (any
4
+ * case) and '' (a key written without a value) are true; null (unset) and
5
+ * everything else are false.
6
+ */
7
+ export function gitBoolean(value) {
8
+ if (value === null) {
9
+ return false;
10
+ }
11
+ return GIT_TRUE.includes(value.toLowerCase());
12
+ }
@@ -0,0 +1,8 @@
1
+ import type { EnmetuConfig, ParsedConfig } from '../../contracts/config.js';
2
+ export declare const CONFIG_FILE_NAME = "enmetu.config.json";
3
+ export declare const DEFAULT_CONFIG: EnmetuConfig;
4
+ /**
5
+ * text null: the file is absent, every default applies. Throws ConfigError
6
+ * for text that does not parse and for an invalid value of a known key.
7
+ */
8
+ export declare function parseConfig(text: string | null): ParsedConfig;
@@ -0,0 +1,176 @@
1
+ import { ConfigError } from '../../contracts/errors.js';
2
+ import { errorMessage } from '../errors/errorMessage.js';
3
+ import { MAX_STATE_HISTORY } from '../state/stateRecordStep.js';
4
+ export const CONFIG_FILE_NAME = 'enmetu.config.json';
5
+ export const DEFAULT_CONFIG = {
6
+ quietMs: 5000,
7
+ maxDelayMs: 60000,
8
+ pollMs: 5000,
9
+ commitSubject: 'enmetu: local changes',
10
+ installHook: true,
11
+ skipJsonCheck: [],
12
+ gitTimeoutMs: 120000,
13
+ stateHistory: 20,
14
+ partialStagingWaitMs: 300000,
15
+ commitHooks: true,
16
+ hookTimeoutMs: 600000,
17
+ };
18
+ const KNOWN_KEYS = [
19
+ 'quietMs',
20
+ 'maxDelayMs',
21
+ 'pollMs',
22
+ 'commitSubject',
23
+ 'installHook',
24
+ 'skipJsonCheck',
25
+ 'gitTimeoutMs',
26
+ 'stateHistory',
27
+ 'partialStagingWaitMs',
28
+ 'commitHooks',
29
+ 'hookTimeoutMs',
30
+ ];
31
+ const MAX_SUBJECT_CHARACTERS = 100;
32
+ /** The range of hookTimeoutMs: one second to one day (design 12.7). */
33
+ const MIN_HOOK_TIMEOUT_MS = 1000;
34
+ const MAX_HOOK_TIMEOUT_MS = 86400000;
35
+ /**
36
+ * text null: the file is absent, every default applies. Throws ConfigError
37
+ * for text that does not parse and for an invalid value of a known key.
38
+ */
39
+ export function parseConfig(text) {
40
+ if (text === null) {
41
+ return { config: DEFAULT_CONFIG, unknownKeys: [] };
42
+ }
43
+ const parsed = parseJson(text);
44
+ if (!isRecord(parsed)) {
45
+ throw new ConfigError('the top level must be a JSON object', 'CONFIG_INVALID_VALUE', null);
46
+ }
47
+ // Ignored rather than refused, so a key only a newer enmetu knows never
48
+ // stops an older one; the caller names each, so a typo is not silent
49
+ // (design 14.17).
50
+ const unknownKeys = Object.keys(parsed).filter((key) => !KNOWN_KEYS.includes(key));
51
+ const quietMs = readInteger(parsed, 'quietMs', 100, 600000);
52
+ const config = {
53
+ quietMs,
54
+ maxDelayMs: readInteger(parsed, 'maxDelayMs', quietMs, 3600000),
55
+ pollMs: readInteger(parsed, 'pollMs', 100, 3600000),
56
+ commitSubject: readCommitSubject(parsed),
57
+ installHook: readBoolean(parsed, 'installHook'),
58
+ skipJsonCheck: readSkipJsonCheck(parsed),
59
+ gitTimeoutMs: readInteger(parsed, 'gitTimeoutMs', 1000, 3600000),
60
+ stateHistory: readStateHistory(parsed),
61
+ partialStagingWaitMs: readPartialStagingWaitMs(parsed),
62
+ commitHooks: readBoolean(parsed, 'commitHooks'),
63
+ hookTimeoutMs: readHookTimeoutMs(parsed),
64
+ };
65
+ return { config, unknownKeys };
66
+ }
67
+ function parseJson(text) {
68
+ try {
69
+ const parsed = JSON.parse(text);
70
+ return parsed;
71
+ }
72
+ catch (error) {
73
+ const detail = errorMessage(error);
74
+ throw new ConfigError(detail, 'CONFIG_INVALID_JSON', null);
75
+ }
76
+ }
77
+ function isRecord(value) {
78
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
79
+ }
80
+ function invalid(key, rule) {
81
+ return new ConfigError(`${key} ${rule}`, 'CONFIG_INVALID_VALUE', key);
82
+ }
83
+ function readInteger(parsed, key, min, max) {
84
+ const value = parsed[key];
85
+ if (value === undefined) {
86
+ // A default can fall outside a range that depends on another key
87
+ // (maxDelayMs below a raised quietMs), so defaults are checked too.
88
+ return checkedInteger(DEFAULT_CONFIG[key], key, min, max);
89
+ }
90
+ if (typeof value !== 'number') {
91
+ throw invalid(key, `must be an integer from ${min} to ${max}`);
92
+ }
93
+ return checkedInteger(value, key, min, max);
94
+ }
95
+ function checkedInteger(value, key, min, max) {
96
+ if (!Number.isInteger(value) || value < min || value > max) {
97
+ throw invalid(key, `must be an integer from ${min} to ${max}`);
98
+ }
99
+ return value;
100
+ }
101
+ function readCommitSubject(parsed) {
102
+ const value = parsed.commitSubject;
103
+ if (value === undefined) {
104
+ return DEFAULT_CONFIG.commitSubject;
105
+ }
106
+ const rule = `must be one line of 1 to ${MAX_SUBJECT_CHARACTERS} characters, not blank and not starting with #`;
107
+ if (typeof value !== 'string') {
108
+ throw invalid('commitSubject', rule);
109
+ }
110
+ const characters = Array.from(value).length;
111
+ // git aborts a commit whose message is empty after cleanup, and a person's
112
+ // commit.cleanup=strip removes a line starting with #; either would make
113
+ // every daemon commit fail.
114
+ if (characters < 1 ||
115
+ characters > MAX_SUBJECT_CHARACTERS ||
116
+ /[\r\n]/.test(value) ||
117
+ value.startsWith('#') ||
118
+ value.trim() === '') {
119
+ throw invalid('commitSubject', rule);
120
+ }
121
+ return value;
122
+ }
123
+ function readBoolean(parsed, key) {
124
+ const value = parsed[key];
125
+ if (value === undefined) {
126
+ return DEFAULT_CONFIG[key];
127
+ }
128
+ if (typeof value !== 'boolean') {
129
+ throw invalid(key, 'must be true or false');
130
+ }
131
+ return value;
132
+ }
133
+ function readSkipJsonCheck(parsed) {
134
+ const value = parsed.skipJsonCheck;
135
+ if (value === undefined) {
136
+ return DEFAULT_CONFIG.skipJsonCheck;
137
+ }
138
+ if (!Array.isArray(value)) {
139
+ throw invalid('skipJsonCheck', 'must be an array of non-empty strings');
140
+ }
141
+ const items = value;
142
+ const entries = [];
143
+ for (const item of items) {
144
+ if (typeof item !== 'string' || item === '') {
145
+ throw invalid('skipJsonCheck', 'must be an array of non-empty strings');
146
+ }
147
+ entries.push(item);
148
+ }
149
+ return entries;
150
+ }
151
+ /**
152
+ * stateHistory: integer 0..MAX_STATE_HISTORY (100); 0 keeps no transitions
153
+ * (design 12.1). Refused above the maximum rather than clamped, so the file
154
+ * never silently keeps fewer transitions than the person asked for.
155
+ */
156
+ function readStateHistory(parsed) {
157
+ return readInteger(parsed, 'stateHistory', 0, MAX_STATE_HISTORY);
158
+ }
159
+ /**
160
+ * partialStagingWaitMs: an integer of 0 or more (design 14.17). There is no
161
+ * wait without an end, so null is refused like any other non-integer.
162
+ */
163
+ function readPartialStagingWaitMs(parsed) {
164
+ const value = parsed.partialStagingWaitMs;
165
+ if (value === undefined) {
166
+ return DEFAULT_CONFIG.partialStagingWaitMs;
167
+ }
168
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
169
+ throw invalid('partialStagingWaitMs', 'must be an integer of 0 or more');
170
+ }
171
+ return value;
172
+ }
173
+ /** hookTimeoutMs: integer 1000..86400000 (design 12.7, Decision 83). */
174
+ function readHookTimeoutMs(parsed) {
175
+ return readInteger(parsed, 'hookTimeoutMs', MIN_HOOK_TIMEOUT_MS, MAX_HOOK_TIMEOUT_MS);
176
+ }
@@ -0,0 +1,11 @@
1
+ import type { StagedFile } from '../../contracts/gitTypes.js';
2
+ /** 32 MiB, used by the guard and the hook alike (D11). */
3
+ export declare const MAX_SCAN_BYTES = 33554432;
4
+ export type ContentRule = 'CONFLICT_MARKERS' | 'INVALID_JSON';
5
+ export interface ContentViolation {
6
+ readonly path: string;
7
+ readonly rule: ContentRule;
8
+ readonly detail: string;
9
+ }
10
+ /** One staged file at a time, so a caller never holds more than the reader's chunk. */
11
+ export declare function findContentViolation(file: StagedFile, skipJsonCheck: readonly string[]): ContentViolation | null;
@@ -0,0 +1,50 @@
1
+ import { errorMessage } from '../errors/errorMessage.js';
2
+ /** 32 MiB, used by the guard and the hook alike (D11). */
3
+ export const MAX_SCAN_BYTES = 33554432;
4
+ // Exactly seven markers at the start of a line, then a space, a tab or the
5
+ // end of the line (CRLF included). Eight are not a marker, and the lone
6
+ // ======= separator is not checked because it is also a setext heading.
7
+ const OPENING_MARKER = /(?:^|\n)<{7}(?:[ \t]|\r?\n|\r?$)/;
8
+ const CLOSING_MARKER = /(?:^|\n)>{7}(?:[ \t]|\r?\n|\r?$)/;
9
+ const BYTE_ORDER_MARK = String.fromCharCode(0xfeff);
10
+ /** One staged file at a time, so a caller never holds more than the reader's chunk. */
11
+ export function findContentViolation(file, skipJsonCheck) {
12
+ const content = file.content;
13
+ if (content === null || content.includes(0)) {
14
+ return null;
15
+ }
16
+ const text = content.toString('utf8');
17
+ if (OPENING_MARKER.test(text) && CLOSING_MARKER.test(text)) {
18
+ return {
19
+ path: file.path,
20
+ rule: 'CONFLICT_MARKERS',
21
+ detail: 'opening and closing marker lines',
22
+ };
23
+ }
24
+ if (!isJsonPath(file.path) || isExempt(file.path, skipJsonCheck)) {
25
+ return null;
26
+ }
27
+ const detail = jsonParseFailure(text);
28
+ if (detail === null) {
29
+ return null;
30
+ }
31
+ return { path: file.path, rule: 'INVALID_JSON', detail };
32
+ }
33
+ function isJsonPath(path) {
34
+ return path.toLowerCase().endsWith('.json');
35
+ }
36
+ /** An entry with a slash names one root-relative path; one without names a basename anywhere. */
37
+ function isExempt(path, skipJsonCheck) {
38
+ const basename = path.slice(path.lastIndexOf('/') + 1);
39
+ return skipJsonCheck.some((entry) => entry.includes('/') ? entry === path : entry === basename);
40
+ }
41
+ function jsonParseFailure(text) {
42
+ const body = text.startsWith(BYTE_ORDER_MARK) ? text.slice(1) : text;
43
+ try {
44
+ JSON.parse(body);
45
+ return null;
46
+ }
47
+ catch (error) {
48
+ return errorMessage(error);
49
+ }
50
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The message of anything thrown. Not instanceof Error: an error from Node's
3
+ * core can come from another realm (Jest's test context), where instanceof
4
+ * is false.
5
+ */
6
+ export declare function errorMessage(error: unknown): string;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The message of anything thrown. Not instanceof Error: an error from Node's
3
+ * core can come from another realm (Jest's test context), where instanceof
4
+ * is false.
5
+ */
6
+ export function errorMessage(error) {
7
+ if (typeof error === 'object' &&
8
+ error !== null &&
9
+ 'message' in error &&
10
+ typeof error.message === 'string') {
11
+ return error.message;
12
+ }
13
+ return String(error);
14
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The cycle id threaded into an error that escapes a controller. Errors raised
3
+ * below the controllers (adapters, stores) do not know the cycle; the classes
4
+ * that carry one are rebuilt with it and keep their stack, anything else
5
+ * passes unchanged.
6
+ */
7
+ export declare function withCycleId(error: unknown, cycleId: string): unknown;
@@ -0,0 +1,38 @@
1
+ import { EnmetuError, GitCommandError, GitOutputError, HaltedError, } from '../../contracts/errors.js';
2
+ /**
3
+ * The cycle id threaded into an error that escapes a controller. Errors raised
4
+ * below the controllers (adapters, stores) do not know the cycle; the classes
5
+ * that carry one are rebuilt with it and keep their stack, anything else
6
+ * passes unchanged.
7
+ */
8
+ export function withCycleId(error, cycleId) {
9
+ if (!(error instanceof EnmetuError) || error.cycleId !== undefined) {
10
+ return error;
11
+ }
12
+ let threaded;
13
+ if (error instanceof GitCommandError && isGitCommandErrorCode(error.code)) {
14
+ threaded = new GitCommandError(error.message, error.code, error.args, error.exitCode, error.stderr, cycleId);
15
+ }
16
+ else if (error instanceof HaltedError) {
17
+ threaded = new HaltedError(error.message, error.haltCode, cycleId);
18
+ }
19
+ else if (error instanceof GitOutputError) {
20
+ threaded = new GitOutputError(error.message, cycleId);
21
+ }
22
+ else if (error.constructor === EnmetuError) {
23
+ threaded = new EnmetuError(error.message, error.code, cycleId);
24
+ }
25
+ else {
26
+ return error;
27
+ }
28
+ if (error.stack !== undefined) {
29
+ threaded.stack = error.stack;
30
+ }
31
+ return threaded;
32
+ }
33
+ function isGitCommandErrorCode(code) {
34
+ return (code === 'GIT_NOT_FOUND' ||
35
+ code === 'GIT_TIMEOUT' ||
36
+ code === 'GIT_FAILED' ||
37
+ code === 'GIT_INDEX_LOCKED');
38
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * enmetu's own refs. They live outside refs/heads, refs/remotes and refs/tags,
3
+ * so no fetch refspec, push or person's command moves them, and a push that
4
+ * names branchRef never sends them.
5
+ */
6
+ /** The last upstream sha enmetu verified to be a fast-forward or pushed itself: refs/enmetu/verified/<branch>. */
7
+ export declare function verifiedRefFor(branchRef: string): string;
8
+ /** Where enmetu fetches before verifying: refs/enmetu/incoming/<branch>. */
9
+ export declare function incomingRefFor(branchRef: string): string;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * enmetu's own refs. They live outside refs/heads, refs/remotes and refs/tags,
3
+ * so no fetch refspec, push or person's command moves them, and a push that
4
+ * names branchRef never sends them.
5
+ */
6
+ const BRANCH_PREFIX = 'refs/heads/';
7
+ /** The last upstream sha enmetu verified to be a fast-forward or pushed itself: refs/enmetu/verified/<branch>. */
8
+ export function verifiedRefFor(branchRef) {
9
+ return `refs/enmetu/verified/${branchName(branchRef)}`;
10
+ }
11
+ /** Where enmetu fetches before verifying: refs/enmetu/incoming/<branch>. */
12
+ export function incomingRefFor(branchRef) {
13
+ return `refs/enmetu/incoming/${branchName(branchRef)}`;
14
+ }
15
+ // Callers pass the branchRef of `git symbolic-ref HEAD`, always under
16
+ // refs/heads/. Anything else keeps its full name, so two inputs never map
17
+ // to the same enmetu ref.
18
+ function branchName(branchRef) {
19
+ return branchRef.startsWith(BRANCH_PREFIX)
20
+ ? branchRef.slice(BRANCH_PREFIX.length)
21
+ : branchRef;
22
+ }