@narumitw/pi-sync 0.50.3 → 0.51.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.
Files changed (218) hide show
  1. package/README.md +4 -1
  2. package/dist/chunks/{chunk-6LP3MCR2.ts → chunk-2JZFHNLM.ts} +3 -5
  3. package/dist/chunks/chunk-2JZFHNLM.ts.map +7 -0
  4. package/dist/chunks/{chunk-LCRI3ABW.ts → chunk-2RZSQWBH.ts} +8 -19
  5. package/dist/chunks/chunk-2RZSQWBH.ts.map +7 -0
  6. package/dist/chunks/{chunk-IJ6FJFA5.ts → chunk-4AIHXRKE.ts} +11 -17
  7. package/dist/chunks/chunk-4AIHXRKE.ts.map +7 -0
  8. package/dist/chunks/{chunk-C7XY5NF5.ts → chunk-4MPIH46N.ts} +29 -51
  9. package/dist/chunks/chunk-4MPIH46N.ts.map +7 -0
  10. package/dist/chunks/{chunk-5B3FXBLE.ts → chunk-6546TS54.ts} +16 -20
  11. package/dist/chunks/chunk-6546TS54.ts.map +7 -0
  12. package/dist/chunks/{chunk-Q2TUM5X6.ts → chunk-6NT3YFGY.ts} +2 -2
  13. package/dist/chunks/chunk-6NT3YFGY.ts.map +7 -0
  14. package/dist/chunks/{chunk-2B2ELUF5.ts → chunk-7D5SF7P3.ts} +1 -1
  15. package/dist/chunks/chunk-7D5SF7P3.ts.map +7 -0
  16. package/dist/chunks/{chunk-YDWUZVPN.ts → chunk-7KHKWL3Q.ts} +28 -47
  17. package/dist/chunks/chunk-7KHKWL3Q.ts.map +7 -0
  18. package/dist/chunks/{chunk-ZI5MXMEX.ts → chunk-BVVP33U5.ts} +2 -4
  19. package/dist/chunks/chunk-BVVP33U5.ts.map +7 -0
  20. package/dist/chunks/{chunk-SZWBGPID.ts → chunk-DM4X4FIA.ts} +8 -13
  21. package/dist/chunks/chunk-DM4X4FIA.ts.map +7 -0
  22. package/dist/chunks/{chunk-SL64RLXR.ts → chunk-DZUSTGQV.ts} +8 -16
  23. package/dist/chunks/chunk-DZUSTGQV.ts.map +7 -0
  24. package/dist/chunks/{chunk-YHEMENKV.ts → chunk-E42GXD6D.ts} +13 -19
  25. package/dist/chunks/chunk-E42GXD6D.ts.map +7 -0
  26. package/dist/chunks/{chunk-Z4DQF6L3.ts → chunk-ECYCNZOE.ts} +1 -1
  27. package/dist/chunks/chunk-ECYCNZOE.ts.map +7 -0
  28. package/dist/chunks/{chunk-OEX4ABY6.ts → chunk-EEJPYTWE.ts} +11 -21
  29. package/dist/chunks/chunk-EEJPYTWE.ts.map +7 -0
  30. package/dist/chunks/{chunk-CPEHGXJV.ts → chunk-G4RUXRWZ.ts} +3 -5
  31. package/dist/chunks/chunk-G4RUXRWZ.ts.map +7 -0
  32. package/dist/chunks/{chunk-T2B5IU74.ts → chunk-HGJH56KR.ts} +4 -4
  33. package/dist/chunks/chunk-HGJH56KR.ts.map +7 -0
  34. package/dist/chunks/{chunk-JAAKLL6M.ts → chunk-IVXEDPAD.ts} +10 -34
  35. package/dist/chunks/chunk-IVXEDPAD.ts.map +7 -0
  36. package/dist/chunks/chunk-K3RIEL6J.ts +20 -0
  37. package/dist/chunks/chunk-K3RIEL6J.ts.map +7 -0
  38. package/dist/chunks/{chunk-LTBWGSBV.ts → chunk-MSSW6F3R.ts} +1 -1
  39. package/dist/chunks/chunk-MSSW6F3R.ts.map +7 -0
  40. package/dist/chunks/{chunk-T3HH3UEJ.ts → chunk-OVDTF5CH.ts} +4 -10
  41. package/dist/chunks/chunk-OVDTF5CH.ts.map +7 -0
  42. package/dist/chunks/{chunk-NJFUIVYL.ts → chunk-PWMWOJP4.ts} +1 -1
  43. package/dist/chunks/chunk-PWMWOJP4.ts.map +7 -0
  44. package/dist/chunks/{chunk-HUDQATCL.ts → chunk-Q32CQWPM.ts} +6 -8
  45. package/dist/chunks/chunk-Q32CQWPM.ts.map +7 -0
  46. package/dist/chunks/{chunk-ETTRJDNP.ts → chunk-QLSN7AGX.ts} +6 -13
  47. package/dist/chunks/chunk-QLSN7AGX.ts.map +7 -0
  48. package/dist/chunks/{chunk-THMF4BVL.ts → chunk-SWUXA3MH.ts} +1 -1
  49. package/dist/chunks/chunk-SWUXA3MH.ts.map +7 -0
  50. package/dist/chunks/{chunk-TEN2ZBJV.ts → chunk-T2SJD3N7.ts} +2 -4
  51. package/dist/chunks/chunk-T2SJD3N7.ts.map +7 -0
  52. package/dist/chunks/{chunk-AUSDLD6F.ts → chunk-T56RNRZJ.ts} +10 -26
  53. package/dist/chunks/chunk-T56RNRZJ.ts.map +7 -0
  54. package/dist/chunks/{chunk-DHVFS4QK.ts → chunk-UA7RXOIT.ts} +37 -90
  55. package/dist/chunks/chunk-UA7RXOIT.ts.map +7 -0
  56. package/dist/chunks/{chunk-D5X27LCA.ts → chunk-UETBKTDI.ts} +1 -1
  57. package/dist/chunks/chunk-UETBKTDI.ts.map +7 -0
  58. package/dist/chunks/{chunk-CHH26D4F.ts → chunk-YFADF5ZQ.ts} +7 -6
  59. package/dist/chunks/chunk-YFADF5ZQ.ts.map +7 -0
  60. package/dist/chunks/{chunk-CCSN3F47.ts → chunk-YPXGU5OR.ts} +4 -4
  61. package/dist/chunks/chunk-YPXGU5OR.ts.map +7 -0
  62. package/dist/chunks/{file-selection-FTTKAOYX.ts → file-selection-RAVHRYFG.ts} +14 -26
  63. package/dist/chunks/file-selection-RAVHRYFG.ts.map +7 -0
  64. package/dist/chunks/{git-backend-GMVS4PD4.ts → git-backend-IAANDL3K.ts} +41 -104
  65. package/dist/chunks/git-backend-IAANDL3K.ts.map +7 -0
  66. package/dist/chunks/manager-result-dispatcher-OTUMQM26.ts +23 -0
  67. package/dist/chunks/{manager-ui-IPAOAWZA.ts → manager-ui-763NEKJY.ts} +124 -251
  68. package/dist/chunks/manager-ui-763NEKJY.ts.map +7 -0
  69. package/dist/chunks/remote-selection-ui-TZTJF32Y.ts +22 -0
  70. package/dist/chunks/{s3-backend-66O4FGBH.ts → s3-backend-XGXUOPUN.ts} +23 -54
  71. package/dist/chunks/s3-backend-XGXUOPUN.ts.map +7 -0
  72. package/dist/chunks/{setup-switch-IS2PEZ3J.ts → setup-switch-5G2VNJHL.ts} +9 -9
  73. package/dist/chunks/{setup-wizard-JUJZYNNM.ts → setup-wizard-YIEWGX32.ts} +12 -12
  74. package/dist/chunks/setup-wizard-YIEWGX32.ts.map +7 -0
  75. package/dist/chunks/{snapshot-HKEWCVK6.ts → snapshot-JL5DFSXI.ts} +5 -5
  76. package/dist/chunks/sync-inspection-P3ENJLNF.ts +20 -0
  77. package/dist/chunks/{sync-operations-LBX3GKCM.ts → sync-operations-AR76EB6C.ts} +85 -201
  78. package/dist/chunks/sync-operations-AR76EB6C.ts.map +7 -0
  79. package/dist/chunks/{sync-state-Q2FTXTW6.ts → sync-state-VHJUZ4YG.ts} +6 -6
  80. package/dist/chunks/{webdav-backend-ECIKD7SO.ts → webdav-backend-3EGUONAT.ts} +20 -51
  81. package/dist/chunks/webdav-backend-3EGUONAT.ts.map +7 -0
  82. package/dist/index.ts +49 -74
  83. package/dist/index.ts.map +3 -3
  84. package/docs/settings.md +9 -0
  85. package/package.json +61 -62
  86. package/src/backends/backend-factory.ts +15 -15
  87. package/src/backends/backend-identity.ts +31 -31
  88. package/src/backends/backend-types.ts +42 -42
  89. package/src/backends/git/git-backend.ts +714 -788
  90. package/src/backends/git/git-config.ts +104 -111
  91. package/src/backends/git/git-runner.ts +230 -247
  92. package/src/backends/git/git-storage.ts +228 -245
  93. package/src/backends/s3/s3-backend.ts +408 -441
  94. package/src/backends/s3/s3-client.ts +288 -333
  95. package/src/backends/sync-backend.ts +65 -65
  96. package/src/backends/webdav/webdav-backend.ts +542 -589
  97. package/src/backends/webdav/webdav-client.ts +292 -316
  98. package/src/backends/webdav/webdav-config.ts +57 -68
  99. package/src/commands/command-execution.ts +270 -289
  100. package/src/commands/command-handler.ts +160 -163
  101. package/src/commands/command-types.ts +13 -13
  102. package/src/commands/command.ts +190 -196
  103. package/src/commands/setup-completions.ts +1 -1
  104. package/src/paths.ts +47 -47
  105. package/src/settings/config-errors.ts +4 -5
  106. package/src/settings/config-file.ts +473 -506
  107. package/src/settings/config.ts +171 -174
  108. package/src/settings/settings-management.ts +191 -216
  109. package/src/settings/settings-store.ts +63 -77
  110. package/src/settings/settings-types.ts +78 -75
  111. package/src/settings/settings-validation.ts +328 -364
  112. package/src/snapshot/session-paths.ts +41 -55
  113. package/src/snapshot/snapshot-apply.ts +197 -229
  114. package/src/snapshot/snapshot-codec.ts +29 -31
  115. package/src/snapshot/snapshot-paths.ts +4 -6
  116. package/src/snapshot/snapshot-transaction.ts +161 -170
  117. package/src/snapshot/snapshot-types.ts +24 -24
  118. package/src/snapshot/snapshot.ts +310 -343
  119. package/src/state/json-file.ts +11 -11
  120. package/src/state/lock.ts +226 -242
  121. package/src/state/lockfile-fs.ts +20 -20
  122. package/src/state/operation-availability.ts +36 -41
  123. package/src/state/state-directory.ts +171 -173
  124. package/src/state/state-types.ts +15 -15
  125. package/src/state/sync-state-store.ts +23 -23
  126. package/src/sync/automatic-sync.ts +75 -80
  127. package/src/sync/remote-snapshot.ts +52 -60
  128. package/src/sync/setup-switch.ts +99 -114
  129. package/src/sync/signals.ts +3 -5
  130. package/src/sync/startup-check.ts +97 -111
  131. package/src/sync/sync-decision.ts +76 -80
  132. package/src/sync/sync-error-guidance.ts +41 -48
  133. package/src/sync/sync-errors.ts +10 -10
  134. package/src/sync/sync-inspection.ts +29 -33
  135. package/src/sync/sync-loaders.ts +28 -36
  136. package/src/sync/sync-mutations.ts +510 -622
  137. package/src/sync/sync-operations.ts +7 -7
  138. package/src/sync/sync-policy.ts +296 -326
  139. package/src/sync/sync-queries.ts +175 -194
  140. package/src/sync/sync-state.ts +74 -100
  141. package/src/sync-extension.ts +89 -101
  142. package/src/sync.ts +26 -26
  143. package/src/types.ts +37 -37
  144. package/src/ui/automatic-sync-summary.ts +1 -3
  145. package/src/ui/cancellable-operation.ts +89 -101
  146. package/src/ui/file-selection.ts +250 -274
  147. package/src/ui/manager-attention.ts +51 -63
  148. package/src/ui/manager-recovery.ts +48 -57
  149. package/src/ui/manager-result-dispatcher.ts +62 -76
  150. package/src/ui/manager-state.ts +200 -232
  151. package/src/ui/manager-ui.ts +264 -362
  152. package/src/ui/remote-selection-ui.ts +547 -588
  153. package/src/ui/secret-input.ts +197 -205
  154. package/src/ui/settings-ui.ts +175 -172
  155. package/src/ui/setup/git-ui.ts +232 -261
  156. package/src/ui/setup/s3-credentials-ui.ts +58 -65
  157. package/src/ui/setup/s3-ui.ts +171 -195
  158. package/src/ui/setup/setup-actions.ts +91 -101
  159. package/src/ui/setup/setup-location-ui.ts +121 -137
  160. package/src/ui/setup/setup-name-ui.ts +27 -27
  161. package/src/ui/setup/setup-prompts.ts +65 -65
  162. package/src/ui/setup/setup-review.ts +58 -59
  163. package/src/ui/setup/setup-switcher.ts +156 -160
  164. package/src/ui/setup/setup-wizard.ts +28 -28
  165. package/src/ui/setup/text-input.ts +54 -58
  166. package/src/ui/setup/webdav-ui.ts +246 -293
  167. package/src/ui/storage-connections-ui.ts +317 -369
  168. package/src/ui/sync-attention.ts +148 -164
  169. package/src/ui/sync-format.ts +110 -121
  170. package/src/ui/sync-resolution-ui.ts +147 -153
  171. package/src/ui/sync-setups-ui.ts +218 -238
  172. package/src/ui/sync-status.ts +17 -0
  173. package/src/ui/terminal-text.ts +2 -2
  174. package/dist/chunks/chunk-2B2ELUF5.ts.map +0 -7
  175. package/dist/chunks/chunk-5B3FXBLE.ts.map +0 -7
  176. package/dist/chunks/chunk-6LP3MCR2.ts.map +0 -7
  177. package/dist/chunks/chunk-AUSDLD6F.ts.map +0 -7
  178. package/dist/chunks/chunk-C7XY5NF5.ts.map +0 -7
  179. package/dist/chunks/chunk-CCSN3F47.ts.map +0 -7
  180. package/dist/chunks/chunk-CHH26D4F.ts.map +0 -7
  181. package/dist/chunks/chunk-CPEHGXJV.ts.map +0 -7
  182. package/dist/chunks/chunk-D5X27LCA.ts.map +0 -7
  183. package/dist/chunks/chunk-DHVFS4QK.ts.map +0 -7
  184. package/dist/chunks/chunk-ETTRJDNP.ts.map +0 -7
  185. package/dist/chunks/chunk-HUDQATCL.ts.map +0 -7
  186. package/dist/chunks/chunk-IJ6FJFA5.ts.map +0 -7
  187. package/dist/chunks/chunk-JAAKLL6M.ts.map +0 -7
  188. package/dist/chunks/chunk-LCRI3ABW.ts.map +0 -7
  189. package/dist/chunks/chunk-LTBWGSBV.ts.map +0 -7
  190. package/dist/chunks/chunk-NJFUIVYL.ts.map +0 -7
  191. package/dist/chunks/chunk-OEX4ABY6.ts.map +0 -7
  192. package/dist/chunks/chunk-Q2TUM5X6.ts.map +0 -7
  193. package/dist/chunks/chunk-SL64RLXR.ts.map +0 -7
  194. package/dist/chunks/chunk-SZWBGPID.ts.map +0 -7
  195. package/dist/chunks/chunk-T2B5IU74.ts.map +0 -7
  196. package/dist/chunks/chunk-T3HH3UEJ.ts.map +0 -7
  197. package/dist/chunks/chunk-TEN2ZBJV.ts.map +0 -7
  198. package/dist/chunks/chunk-THMF4BVL.ts.map +0 -7
  199. package/dist/chunks/chunk-YDWUZVPN.ts.map +0 -7
  200. package/dist/chunks/chunk-YHEMENKV.ts.map +0 -7
  201. package/dist/chunks/chunk-Z4DQF6L3.ts.map +0 -7
  202. package/dist/chunks/chunk-ZI5MXMEX.ts.map +0 -7
  203. package/dist/chunks/file-selection-FTTKAOYX.ts.map +0 -7
  204. package/dist/chunks/git-backend-GMVS4PD4.ts.map +0 -7
  205. package/dist/chunks/manager-result-dispatcher-ASKGOSW5.ts +0 -22
  206. package/dist/chunks/manager-ui-IPAOAWZA.ts.map +0 -7
  207. package/dist/chunks/remote-selection-ui-QDNR7WEI.ts +0 -21
  208. package/dist/chunks/s3-backend-66O4FGBH.ts.map +0 -7
  209. package/dist/chunks/setup-wizard-JUJZYNNM.ts.map +0 -7
  210. package/dist/chunks/sync-inspection-CST2KL65.ts +0 -20
  211. package/dist/chunks/sync-operations-LBX3GKCM.ts.map +0 -7
  212. package/dist/chunks/webdav-backend-ECIKD7SO.ts.map +0 -7
  213. /package/dist/chunks/{manager-result-dispatcher-ASKGOSW5.ts.map → manager-result-dispatcher-OTUMQM26.ts.map} +0 -0
  214. /package/dist/chunks/{remote-selection-ui-QDNR7WEI.ts.map → remote-selection-ui-TZTJF32Y.ts.map} +0 -0
  215. /package/dist/chunks/{setup-switch-IS2PEZ3J.ts.map → setup-switch-5G2VNJHL.ts.map} +0 -0
  216. /package/dist/chunks/{snapshot-HKEWCVK6.ts.map → snapshot-JL5DFSXI.ts.map} +0 -0
  217. /package/dist/chunks/{sync-inspection-CST2KL65.ts.map → sync-inspection-P3ENJLNF.ts.map} +0 -0
  218. /package/dist/chunks/{sync-state-Q2FTXTW6.ts.map → sync-state-VHJUZ4YG.ts.map} +0 -0
@@ -5,272 +5,255 @@ const DEFAULT_TIMEOUT_MS = 30_000;
5
5
  const DEFAULT_OUTPUT_LIMIT = 1024 * 1024;
6
6
 
7
7
  export interface GitRunOptions {
8
- gitDir?: string;
9
- cwd?: string;
10
- input?: Buffer | string;
11
- env?: NodeJS.ProcessEnv;
12
- signal?: AbortSignal;
13
- timeoutMs?: number;
14
- maxOutputBytes?: number;
15
- allowFileProtocol?: boolean;
8
+ gitDir?: string;
9
+ cwd?: string;
10
+ input?: Buffer | string;
11
+ env?: NodeJS.ProcessEnv;
12
+ signal?: AbortSignal;
13
+ timeoutMs?: number;
14
+ maxOutputBytes?: number;
15
+ allowFileProtocol?: boolean;
16
16
  }
17
17
 
18
18
  export interface GitRunResult {
19
- stdout: Buffer;
20
- stderr: Buffer;
19
+ stdout: Buffer;
20
+ stderr: Buffer;
21
21
  }
22
22
 
23
23
  export class GitCommandError extends Error {
24
- readonly code = "GIT_COMMAND_FAILED";
25
- constructor(
26
- message: string,
27
- readonly exitCode: number | null,
28
- readonly stderr: string,
29
- options?: ErrorOptions,
30
- ) {
31
- super(message, options);
32
- this.name = "GitCommandError";
33
- }
24
+ readonly code = "GIT_COMMAND_FAILED";
25
+ constructor(
26
+ message: string,
27
+ readonly exitCode: number | null,
28
+ readonly stderr: string,
29
+ options?: ErrorOptions,
30
+ ) {
31
+ super(message, options);
32
+ this.name = "GitCommandError";
33
+ }
34
34
  }
35
35
 
36
36
  export async function runGit(args: string[], options: GitRunOptions = {}): Promise<GitRunResult> {
37
- throwIfAborted(options.signal);
38
- const hooksPath = process.platform === "win32" ? "NUL" : "/dev/null";
39
- const protocolArgs = [
40
- "-c",
41
- `core.hooksPath=${hooksPath}`,
42
- "-c",
43
- "gc.auto=0",
44
- "-c",
45
- "maintenance.auto=false",
46
- "-c",
47
- "protocol.allow=never",
48
- "-c",
49
- "protocol.https.allow=always",
50
- "-c",
51
- "protocol.ssh.allow=always",
52
- ];
53
- if (options.allowFileProtocol) protocolArgs.push("-c", "protocol.file.allow=always");
54
- const commandArgs = [
55
- ...(options.gitDir ? [`--git-dir=${options.gitDir}`] : []),
56
- ...protocolArgs,
57
- ...args,
58
- ];
59
- const inheritedEnvironment = Object.fromEntries(
60
- Object.entries(process.env).filter(
61
- ([key]) =>
62
- !key.startsWith("GIT_") &&
63
- key !== "PAGER" &&
64
- key !== "EDITOR" &&
65
- key !== "VISUAL" &&
66
- key !== "SSH_ASKPASS" &&
67
- key !== "SSH_ASKPASS_REQUIRE",
68
- ),
69
- );
70
- const allowedGitOverrides = new Set([
71
- "GIT_INDEX_FILE",
72
- "GIT_AUTHOR_NAME",
73
- "GIT_AUTHOR_EMAIL",
74
- "GIT_AUTHOR_DATE",
75
- "GIT_COMMITTER_NAME",
76
- "GIT_COMMITTER_EMAIL",
77
- "GIT_COMMITTER_DATE",
78
- ]);
79
- const suppliedEnvironment = Object.fromEntries(
80
- Object.entries(options.env ?? {}).filter(
81
- ([key]) => !key.startsWith("GIT_") || allowedGitOverrides.has(key),
82
- ),
83
- );
84
- const env: NodeJS.ProcessEnv = {
85
- ...inheritedEnvironment,
86
- ...suppliedEnvironment,
87
- LC_ALL: "C",
88
- LANG: "C",
89
- GIT_CONFIG_NOSYSTEM: "1",
90
- GIT_TERMINAL_PROMPT: "0",
91
- GCM_INTERACTIVE: "Never",
92
- GIT_PAGER: "cat",
93
- PAGER: "cat",
94
- GIT_EDITOR: "true",
95
- EDITOR: "true",
96
- VISUAL: "true",
97
- GIT_ASKPASS: "",
98
- SSH_ASKPASS: "",
99
- SSH_ASKPASS_REQUIRE: "never",
100
- GIT_SSH_COMMAND: "ssh -oBatchMode=yes",
101
- };
102
- const child = spawn("git", commandArgs, {
103
- cwd: options.cwd,
104
- env,
105
- stdio: ["pipe", "pipe", "pipe"],
106
- detached: process.platform !== "win32",
107
- windowsHide: true,
108
- });
109
- const stdout: Buffer[] = [];
110
- const stderr: Buffer[] = [];
111
- let total = 0;
112
- let settled = false;
113
- let terminationError: Error | undefined;
114
- let escalationTimer: NodeJS.Timeout | undefined;
115
- const limit = options.maxOutputBytes ?? DEFAULT_OUTPUT_LIMIT;
37
+ throwIfAborted(options.signal);
38
+ const hooksPath = process.platform === "win32" ? "NUL" : "/dev/null";
39
+ const protocolArgs = [
40
+ "-c",
41
+ `core.hooksPath=${hooksPath}`,
42
+ "-c",
43
+ "gc.auto=0",
44
+ "-c",
45
+ "maintenance.auto=false",
46
+ "-c",
47
+ "protocol.allow=never",
48
+ "-c",
49
+ "protocol.https.allow=always",
50
+ "-c",
51
+ "protocol.ssh.allow=always",
52
+ ];
53
+ if (options.allowFileProtocol) protocolArgs.push("-c", "protocol.file.allow=always");
54
+ const commandArgs = [...(options.gitDir ? [`--git-dir=${options.gitDir}`] : []), ...protocolArgs, ...args];
55
+ const inheritedEnvironment = Object.fromEntries(
56
+ Object.entries(process.env).filter(
57
+ ([key]) =>
58
+ !key.startsWith("GIT_") &&
59
+ key !== "PAGER" &&
60
+ key !== "EDITOR" &&
61
+ key !== "VISUAL" &&
62
+ key !== "SSH_ASKPASS" &&
63
+ key !== "SSH_ASKPASS_REQUIRE",
64
+ ),
65
+ );
66
+ const allowedGitOverrides = new Set([
67
+ "GIT_INDEX_FILE",
68
+ "GIT_AUTHOR_NAME",
69
+ "GIT_AUTHOR_EMAIL",
70
+ "GIT_AUTHOR_DATE",
71
+ "GIT_COMMITTER_NAME",
72
+ "GIT_COMMITTER_EMAIL",
73
+ "GIT_COMMITTER_DATE",
74
+ ]);
75
+ const suppliedEnvironment = Object.fromEntries(
76
+ Object.entries(options.env ?? {}).filter(([key]) => !key.startsWith("GIT_") || allowedGitOverrides.has(key)),
77
+ );
78
+ const env: NodeJS.ProcessEnv = {
79
+ ...inheritedEnvironment,
80
+ ...suppliedEnvironment,
81
+ LC_ALL: "C",
82
+ LANG: "C",
83
+ GIT_CONFIG_NOSYSTEM: "1",
84
+ GIT_TERMINAL_PROMPT: "0",
85
+ GCM_INTERACTIVE: "Never",
86
+ GIT_PAGER: "cat",
87
+ PAGER: "cat",
88
+ GIT_EDITOR: "true",
89
+ EDITOR: "true",
90
+ VISUAL: "true",
91
+ GIT_ASKPASS: "",
92
+ SSH_ASKPASS: "",
93
+ SSH_ASKPASS_REQUIRE: "never",
94
+ GIT_SSH_COMMAND: "ssh -oBatchMode=yes",
95
+ };
96
+ const child = spawn("git", commandArgs, {
97
+ cwd: options.cwd,
98
+ env,
99
+ stdio: ["pipe", "pipe", "pipe"],
100
+ detached: process.platform !== "win32",
101
+ windowsHide: true,
102
+ });
103
+ const stdout: Buffer[] = [];
104
+ const stderr: Buffer[] = [];
105
+ let total = 0;
106
+ let settled = false;
107
+ let terminationError: Error | undefined;
108
+ let escalationTimer: NodeJS.Timeout | undefined;
109
+ const limit = options.maxOutputBytes ?? DEFAULT_OUTPUT_LIMIT;
116
110
 
117
- const terminate = (error: Error) => {
118
- if (settled || terminationError) return;
119
- terminationError = error;
120
- if (child.pid && process.platform !== "win32") {
121
- try {
122
- process.kill(-child.pid, "SIGTERM");
123
- } catch {
124
- child.kill("SIGTERM");
125
- }
126
- } else {
127
- child.kill("SIGTERM");
128
- if (child.pid && process.platform === "win32") {
129
- const killer = spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
130
- stdio: "ignore",
131
- windowsHide: true,
132
- });
133
- killer.on("error", () => undefined);
134
- killer.unref();
135
- }
136
- }
137
- escalationTimer = setTimeout(() => {
138
- if (settled) return;
139
- if (child.pid && process.platform !== "win32") {
140
- try {
141
- process.kill(-child.pid, "SIGKILL");
142
- } catch {
143
- child.kill("SIGKILL");
144
- }
145
- } else {
146
- child.kill("SIGKILL");
147
- }
148
- }, 2_000);
149
- };
150
- const collect = (target: Buffer[]) => (chunk: Buffer) => {
151
- total += chunk.byteLength;
152
- if (total > limit) {
153
- terminate(new Error(`Git output exceeds the ${limit}-byte limit.`));
154
- return;
155
- }
156
- target.push(Buffer.from(chunk));
157
- };
158
- child.stdout.on("data", collect(stdout));
159
- child.stderr.on("data", collect(stderr));
160
- child.stdin.on("error", () => undefined);
161
- child.stdin.end(options.input);
162
- const onAbort = () =>
163
- terminate(
164
- options.signal?.reason instanceof Error
165
- ? options.signal.reason
166
- : new DOMException("The operation was aborted", "AbortError"),
167
- );
168
- options.signal?.addEventListener("abort", onAbort, { once: true });
169
- const timer = setTimeout(
170
- () =>
171
- terminate(
172
- new Error(`Git command timed out after ${options.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms.`),
173
- ),
174
- options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
175
- );
111
+ const terminate = (error: Error) => {
112
+ if (settled || terminationError) return;
113
+ terminationError = error;
114
+ if (child.pid && process.platform !== "win32") {
115
+ try {
116
+ process.kill(-child.pid, "SIGTERM");
117
+ } catch {
118
+ child.kill("SIGTERM");
119
+ }
120
+ } else {
121
+ child.kill("SIGTERM");
122
+ if (child.pid && process.platform === "win32") {
123
+ const killer = spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
124
+ stdio: "ignore",
125
+ windowsHide: true,
126
+ });
127
+ killer.on("error", () => undefined);
128
+ killer.unref();
129
+ }
130
+ }
131
+ escalationTimer = setTimeout(() => {
132
+ if (settled) return;
133
+ if (child.pid && process.platform !== "win32") {
134
+ try {
135
+ process.kill(-child.pid, "SIGKILL");
136
+ } catch {
137
+ child.kill("SIGKILL");
138
+ }
139
+ } else {
140
+ child.kill("SIGKILL");
141
+ }
142
+ }, 2_000);
143
+ };
144
+ const collect = (target: Buffer[]) => (chunk: Buffer) => {
145
+ total += chunk.byteLength;
146
+ if (total > limit) {
147
+ terminate(new Error(`Git output exceeds the ${limit}-byte limit.`));
148
+ return;
149
+ }
150
+ target.push(Buffer.from(chunk));
151
+ };
152
+ child.stdout.on("data", collect(stdout));
153
+ child.stderr.on("data", collect(stderr));
154
+ child.stdin.on("error", () => undefined);
155
+ child.stdin.end(options.input);
156
+ const onAbort = () =>
157
+ terminate(
158
+ options.signal?.reason instanceof Error
159
+ ? options.signal.reason
160
+ : new DOMException("The operation was aborted", "AbortError"),
161
+ );
162
+ options.signal?.addEventListener("abort", onAbort, { once: true });
163
+ const timer = setTimeout(
164
+ () => terminate(new Error(`Git command timed out after ${options.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms.`)),
165
+ options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
166
+ );
176
167
 
177
- let result: GitRunResult;
178
- try {
179
- result = await new Promise<GitRunResult>((resolve, reject) => {
180
- child.once("error", reject);
181
- child.once("close", (code) => {
182
- settled = true;
183
- if (escalationTimer) clearTimeout(escalationTimer);
184
- const stdoutBuffer = Buffer.concat(stdout);
185
- const stderrBuffer = Buffer.concat(stderr);
186
- if (terminationError) {
187
- reject(terminationError);
188
- return;
189
- }
190
- if (code !== 0) {
191
- const stderrText = stderrBuffer.toString("utf8").trim();
192
- reject(
193
- new GitCommandError(
194
- stderrText || `Git exited with status ${code ?? "unknown"}.`,
195
- code,
196
- stderrText,
197
- ),
198
- );
199
- return;
200
- }
201
- resolve({ stdout: stdoutBuffer, stderr: stderrBuffer });
202
- });
203
- });
204
- } finally {
205
- clearTimeout(timer);
206
- if (escalationTimer) clearTimeout(escalationTimer);
207
- options.signal?.removeEventListener("abort", onAbort);
208
- }
209
- return result;
168
+ let result: GitRunResult;
169
+ try {
170
+ result = await new Promise<GitRunResult>((resolve, reject) => {
171
+ child.once("error", reject);
172
+ child.once("close", (code) => {
173
+ settled = true;
174
+ if (escalationTimer) clearTimeout(escalationTimer);
175
+ const stdoutBuffer = Buffer.concat(stdout);
176
+ const stderrBuffer = Buffer.concat(stderr);
177
+ if (terminationError) {
178
+ reject(terminationError);
179
+ return;
180
+ }
181
+ if (code !== 0) {
182
+ const stderrText = stderrBuffer.toString("utf8").trim();
183
+ reject(new GitCommandError(stderrText || `Git exited with status ${code ?? "unknown"}.`, code, stderrText));
184
+ return;
185
+ }
186
+ resolve({ stdout: stdoutBuffer, stderr: stderrBuffer });
187
+ });
188
+ });
189
+ } finally {
190
+ clearTimeout(timer);
191
+ if (escalationTimer) clearTimeout(escalationTimer);
192
+ options.signal?.removeEventListener("abort", onAbort);
193
+ }
194
+ return result;
210
195
  }
211
196
 
212
197
  export function parseGitBlobBatch(output: Buffer, expectedCount: number, maxContentBytes: number) {
213
- if (!Number.isSafeInteger(expectedCount) || expectedCount < 0) {
214
- throw new Error("Invalid Git batch object count.");
215
- }
216
- if (!Number.isSafeInteger(maxContentBytes) || maxContentBytes < 0) {
217
- throw new Error("Invalid Git batch content limit.");
218
- }
219
- const blobs: Buffer[] = [];
220
- let offset = 0;
221
- let contentBytes = 0;
222
- for (let index = 0; index < expectedCount; index += 1) {
223
- const headerEnd = output.indexOf(0x0a, offset);
224
- if (headerEnd < 0) throw new Error("Git cat-file batch response is truncated.");
225
- const header = output.subarray(offset, headerEnd).toString("utf8");
226
- if (header.endsWith(" missing")) throw new Error("Git cat-file batch object is missing.");
227
- const match = /^(?<object>[0-9a-f]{40}) blob (?<size>0|[1-9][0-9]*)$/u.exec(header);
228
- if (!match?.groups) throw new Error("Git cat-file batch response is malformed.");
229
- const size = Number(match.groups.size);
230
- if (!Number.isSafeInteger(size)) throw new Error("Git cat-file batch size is malformed.");
231
- contentBytes += size;
232
- if (contentBytes > maxContentBytes) {
233
- throw new Error(`Git cat-file batch content exceeds the ${maxContentBytes}-byte limit.`);
234
- }
235
- const contentStart = headerEnd + 1;
236
- const contentEnd = contentStart + size;
237
- if (contentEnd >= output.length) throw new Error("Git cat-file batch response is truncated.");
238
- if (output[contentEnd] !== 0x0a) {
239
- throw new Error("Git cat-file batch response is malformed.");
240
- }
241
- blobs.push(Buffer.from(output.subarray(contentStart, contentEnd)));
242
- offset = contentEnd + 1;
243
- }
244
- if (offset !== output.length) throw new Error("Git cat-file batch response has trailing data.");
245
- return blobs;
198
+ if (!Number.isSafeInteger(expectedCount) || expectedCount < 0) {
199
+ throw new Error("Invalid Git batch object count.");
200
+ }
201
+ if (!Number.isSafeInteger(maxContentBytes) || maxContentBytes < 0) {
202
+ throw new Error("Invalid Git batch content limit.");
203
+ }
204
+ const blobs: Buffer[] = [];
205
+ let offset = 0;
206
+ let contentBytes = 0;
207
+ for (let index = 0; index < expectedCount; index += 1) {
208
+ const headerEnd = output.indexOf(0x0a, offset);
209
+ if (headerEnd < 0) throw new Error("Git cat-file batch response is truncated.");
210
+ const header = output.subarray(offset, headerEnd).toString("utf8");
211
+ if (header.endsWith(" missing")) throw new Error("Git cat-file batch object is missing.");
212
+ const match = /^(?<object>[0-9a-f]{40}) blob (?<size>0|[1-9][0-9]*)$/u.exec(header);
213
+ if (!match?.groups) throw new Error("Git cat-file batch response is malformed.");
214
+ const size = Number(match.groups.size);
215
+ if (!Number.isSafeInteger(size)) throw new Error("Git cat-file batch size is malformed.");
216
+ contentBytes += size;
217
+ if (contentBytes > maxContentBytes) {
218
+ throw new Error(`Git cat-file batch content exceeds the ${maxContentBytes}-byte limit.`);
219
+ }
220
+ const contentStart = headerEnd + 1;
221
+ const contentEnd = contentStart + size;
222
+ if (contentEnd >= output.length) throw new Error("Git cat-file batch response is truncated.");
223
+ if (output[contentEnd] !== 0x0a) {
224
+ throw new Error("Git cat-file batch response is malformed.");
225
+ }
226
+ blobs.push(Buffer.from(output.subarray(contentStart, contentEnd)));
227
+ offset = contentEnd + 1;
228
+ }
229
+ if (offset !== output.length) throw new Error("Git cat-file batch response has trailing data.");
230
+ return blobs;
246
231
  }
247
232
 
248
233
  export async function readGitBlobs(
249
- objects: string[],
250
- options: Omit<GitRunOptions, "input" | "maxOutputBytes"> & { maxOutputBytes: number },
234
+ objects: string[],
235
+ options: Omit<GitRunOptions, "input" | "maxOutputBytes"> & { maxOutputBytes: number },
251
236
  ) {
252
- if (objects.length === 0) return [];
253
- if (!Number.isSafeInteger(options.maxOutputBytes) || options.maxOutputBytes < 0) {
254
- throw new Error("Invalid Git batch output limit.");
255
- }
256
- if (objects.some((object) => !/^[0-9a-f]{40}$/u.test(object))) {
257
- throw new Error("Invalid Git blob object id.");
258
- }
259
- const protocolOverhead = objects.length * 96;
260
- if (!Number.isSafeInteger(protocolOverhead + options.maxOutputBytes)) {
261
- throw new Error("Invalid Git batch output limit.");
262
- }
263
- const result = await runGit(["cat-file", "--batch"], {
264
- ...options,
265
- input: `${objects.join("\n")}\n`,
266
- maxOutputBytes: options.maxOutputBytes + protocolOverhead,
267
- });
268
- return parseGitBlobBatch(result.stdout, objects.length, options.maxOutputBytes);
237
+ if (objects.length === 0) return [];
238
+ if (!Number.isSafeInteger(options.maxOutputBytes) || options.maxOutputBytes < 0) {
239
+ throw new Error("Invalid Git batch output limit.");
240
+ }
241
+ if (objects.some((object) => !/^[0-9a-f]{40}$/u.test(object))) {
242
+ throw new Error("Invalid Git blob object id.");
243
+ }
244
+ const protocolOverhead = objects.length * 96;
245
+ if (!Number.isSafeInteger(protocolOverhead + options.maxOutputBytes)) {
246
+ throw new Error("Invalid Git batch output limit.");
247
+ }
248
+ const result = await runGit(["cat-file", "--batch"], {
249
+ ...options,
250
+ input: `${objects.join("\n")}\n`,
251
+ maxOutputBytes: options.maxOutputBytes + protocolOverhead,
252
+ });
253
+ return parseGitBlobBatch(result.stdout, objects.length, options.maxOutputBytes);
269
254
  }
270
255
 
271
256
  function throwIfAborted(signal?: AbortSignal) {
272
- if (!signal?.aborted) return;
273
- throw signal.reason instanceof Error
274
- ? signal.reason
275
- : new DOMException("The operation was aborted", "AbortError");
257
+ if (!signal?.aborted) return;
258
+ throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
276
259
  }