@ferrule-io/ok-fine 0.3.6 → 0.3.7

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 (63) hide show
  1. package/README.md +4 -3
  2. package/dist/auth/discovery.js +11 -0
  3. package/dist/auth/verifier.js +44 -1
  4. package/dist/config.js +130 -6
  5. package/dist/http/rest.js +1 -1
  6. package/dist/http/ui.js +118 -0
  7. package/dist/mcp/server.js +3 -2
  8. package/dist/okf/lint.js +20 -0
  9. package/dist/server.js +113 -4
  10. package/dist/store/fs-util.js +59 -4
  11. package/dist/store/git-backend.js +208 -59
  12. package/dist/store/git.js +92 -19
  13. package/dist/ui/assets/bash-BiGZg9Om.js +1 -0
  14. package/dist/ui/assets/css-BGIxQoYs.js +1 -0
  15. package/dist/ui/assets/css-DUjzNfgv.js +1 -0
  16. package/dist/ui/assets/diff-woXpYk--.js +1 -0
  17. package/dist/ui/assets/dockerfile-IyjqRm3v.js +1 -0
  18. package/dist/ui/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  19. package/dist/ui/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  20. package/dist/ui/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  21. package/dist/ui/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  22. package/dist/ui/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  23. package/dist/ui/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  24. package/dist/ui/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  25. package/dist/ui/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  26. package/dist/ui/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  27. package/dist/ui/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  28. package/dist/ui/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  29. package/dist/ui/assets/github-dark-default-DXG-b-1a.js +1 -0
  30. package/dist/ui/assets/github-light-default-BXViO-2h.js +1 -0
  31. package/dist/ui/assets/go-rLFTqkRN.js +1 -0
  32. package/dist/ui/assets/highlight-CW19vWvw.js +153 -0
  33. package/dist/ui/assets/html-BMn1ogTm.js +1 -0
  34. package/dist/ui/assets/html-BlpXnbzK.js +1 -0
  35. package/dist/ui/assets/index-0b2NNRC5.js +127 -0
  36. package/dist/ui/assets/index-CFuTAZQM.css +1 -0
  37. package/dist/ui/assets/java-BtIQNnN0.js +1 -0
  38. package/dist/ui/assets/java-DxlWrXwe.js +1 -0
  39. package/dist/ui/assets/javascript-BzbQS41l.js +1 -0
  40. package/dist/ui/assets/javascript-Yelw-5ZN.js +1 -0
  41. package/dist/ui/assets/json-qhed-kSA.js +1 -0
  42. package/dist/ui/assets/jsx-D3KKoC5L.js +1 -0
  43. package/dist/ui/assets/jsx-hEbJ82HF.js +1 -0
  44. package/dist/ui/assets/markdown-BYOwaDjH.js +1 -0
  45. package/dist/ui/assets/python-gzcpVVnB.js +1 -0
  46. package/dist/ui/assets/ruby-Di5fYh6S.js +1 -0
  47. package/dist/ui/assets/rust-DpiIf5JO.js +1 -0
  48. package/dist/ui/assets/shellscript-CqHdAlCp.js +1 -0
  49. package/dist/ui/assets/sql-BOsskAmu.js +1 -0
  50. package/dist/ui/assets/sql-CNu3Or2W.js +1 -0
  51. package/dist/ui/assets/toml-CcmNWLt0.js +1 -0
  52. package/dist/ui/assets/tsx-5f3ulCja.js +1 -0
  53. package/dist/ui/assets/tsx-BOvsFOI6.js +1 -0
  54. package/dist/ui/assets/typescript-Utq2Cl8c.js +1 -0
  55. package/dist/ui/assets/typescript-j_1H8WHN.js +1 -0
  56. package/dist/ui/assets/yaml-CxbubiML.js +1 -0
  57. package/dist/ui/assets/yaml-DijMHRDA.js +1 -0
  58. package/dist/ui/favicon.svg +11 -0
  59. package/dist/ui/index.html +16 -0
  60. package/dist/ui/theme-init.js +10 -0
  61. package/dist/version.js +1 -1
  62. package/package.json +25 -4
  63. package/dist/store/repo.js +0 -471
@@ -1,13 +1,15 @@
1
- import { mkdir, readdir, readFile, rm, rmdir, writeFile } from "node:fs/promises";
1
+ import { lstat, mkdir, readdir, readFile, rm, rmdir } from "node:fs/promises";
2
2
  import { join, posix } from "node:path";
3
3
  import { OkfError } from "../errors.js";
4
4
  import { nowIso } from "../okf/frontmatter.js";
5
5
  import { PROJECT_RE } from "../okf/paths.js";
6
6
  import { parseActor } from "../okf/semantics.js";
7
- import { atomicWrite, isDirectory, listTreeFiles, pathExists } from "./fs-util.js";
7
+ import { assertContainedPath, atomicWrite, listTreeFiles, pathExists } from "./fs-util.js";
8
8
  import { Git, isPushRejection, redactRemote } from "./git.js";
9
9
  import { Mutex } from "./mutex.js";
10
10
  import { PathIndex } from "./path-index.js";
11
+ const REMOTE_BASE_REF = "refs/ok-fine/remote-base";
12
+ const REMOTE_REWRITTEN_MSG = "remote history was rewritten; syncing halted. Stop ok-fine, remove DATA_DIR/repo, and restart to re-clone";
11
13
  function extractChangedProjects(diffOutput) {
12
14
  const lines = diffOutput
13
15
  .split(/\r?\n/)
@@ -32,11 +34,13 @@ class GitTx {
32
34
  this.index = index;
33
35
  }
34
36
  async writeFile(project, path, content) {
37
+ await assertContainedPath(this.repoDir, project, path);
35
38
  await atomicWrite(join(this.repoDir, project, path), content);
36
39
  this.index.add(project, path);
37
40
  this.touched = true;
38
41
  }
39
42
  async deleteFile(project, path) {
43
+ await assertContainedPath(this.repoDir, project, path);
40
44
  const projectDir = join(this.repoDir, project);
41
45
  await rm(join(projectDir, path), { force: true });
42
46
  let dir = posix.dirname(path);
@@ -57,15 +61,43 @@ class GitTx {
57
61
  this.touched = true;
58
62
  }
59
63
  async deleteProject(project) {
60
- await rm(join(this.repoDir, project), { recursive: true, force: true });
64
+ const projectDir = join(this.repoDir, project);
65
+ try {
66
+ const s = await lstat(projectDir);
67
+ if (s.isSymbolicLink()) {
68
+ await rm(projectDir, { force: true });
69
+ }
70
+ else {
71
+ await rm(projectDir, { recursive: true, force: true });
72
+ }
73
+ }
74
+ catch (err) {
75
+ if (err.code !== "ENOENT") {
76
+ throw err;
77
+ }
78
+ }
61
79
  this.index.setProject(project, []);
62
80
  this.touched = true;
63
81
  }
64
82
  async replaceProject(project, source) {
65
83
  const projectDir = join(this.repoDir, project);
66
- await rm(projectDir, { recursive: true, force: true });
84
+ try {
85
+ const s = await lstat(projectDir);
86
+ if (s.isSymbolicLink()) {
87
+ await rm(projectDir, { force: true });
88
+ }
89
+ else {
90
+ await rm(projectDir, { recursive: true, force: true });
91
+ }
92
+ }
93
+ catch (err) {
94
+ if (err.code !== "ENOENT") {
95
+ throw err;
96
+ }
97
+ }
67
98
  const written = [];
68
99
  for (const path of source.paths) {
100
+ await assertContainedPath(this.repoDir, project, path);
69
101
  const content = await source.read(path);
70
102
  if (content !== null) {
71
103
  await atomicWrite(join(projectDir, path), content);
@@ -102,6 +134,7 @@ export class GitBackend {
102
134
  }
103
135
  async close() {
104
136
  await this.mutex.idle();
137
+ await this.git.close();
105
138
  }
106
139
  async projects() {
107
140
  return this.index.projects();
@@ -114,18 +147,30 @@ export class GitBackend {
114
147
  return null;
115
148
  }
116
149
  try {
117
- return await readFile(join(this.repoDir, project, path));
118
- }
119
- catch (err) {
120
- if (err.code === "ENOENT") {
150
+ await assertContainedPath(this.repoDir, project, path);
151
+ const fullPath = join(this.repoDir, project, path);
152
+ const s = await lstat(fullPath);
153
+ if (s.isSymbolicLink() || !s.isFile()) {
121
154
  return null;
122
155
  }
123
- throw err;
156
+ return await readFile(fullPath);
157
+ }
158
+ catch {
159
+ return null;
124
160
  }
125
161
  }
126
162
  async reindexProject(project) {
127
163
  const dir = join(this.repoDir, project);
128
- const paths = (await isDirectory(dir)) ? await listTreeFiles(dir) : [];
164
+ let paths = [];
165
+ try {
166
+ const s = await lstat(dir);
167
+ if (!s.isSymbolicLink() && s.isDirectory()) {
168
+ paths = await listTreeFiles(dir);
169
+ }
170
+ }
171
+ catch {
172
+ paths = [];
173
+ }
129
174
  this.index.setProject(project, paths);
130
175
  }
131
176
  async reindexAll() {
@@ -200,6 +245,7 @@ export class GitBackend {
200
245
  if (remoteBranchExists) {
201
246
  await repo.git.run(["checkout", "-B", config.gitBranch, `origin/${config.gitBranch}`]);
202
247
  headHasCommits = true;
248
+ await repo.setRememberedRemoteTip(remoteBranchRev.stdout.trim());
203
249
  }
204
250
  else {
205
251
  // Initialize empty bundle root files
@@ -214,6 +260,8 @@ export class GitBackend {
214
260
  allowFail: true,
215
261
  });
216
262
  headHasCommits = true;
263
+ const initialHead = (await repo.git.run(["rev-parse", "HEAD"])).stdout.trim();
264
+ await repo.setRememberedRemoteTip(initialHead);
217
265
  }
218
266
  }
219
267
  else {
@@ -245,24 +293,67 @@ export class GitBackend {
245
293
  "okf: configure merge and ignore attributes",
246
294
  ]);
247
295
  if (repo.hasRemote) {
248
- await repo.git.run(["push", "origin", `HEAD:refs/heads/${config.gitBranch}`], {
296
+ const pushRes = await repo.git.run(["push", "origin", `HEAD:refs/heads/${config.gitBranch}`], {
249
297
  allowFail: true,
250
298
  });
299
+ if (pushRes.code === 0) {
300
+ const headSha = (await repo.git.run(["rev-parse", "HEAD"])).stdout.trim();
301
+ await repo.setRememberedRemoteTip(headSha);
302
+ }
251
303
  }
252
304
  }
253
305
  }
254
306
  await repo.reindexAll();
255
307
  return repo;
256
308
  }
309
+ async getRememberedRemoteTip() {
310
+ const res = await this.git.run(["rev-parse", "--verify", REMOTE_BASE_REF], {
311
+ allowFail: true,
312
+ });
313
+ if (res.code === 0 && res.stdout.trim().length > 0) {
314
+ return res.stdout.trim();
315
+ }
316
+ return null;
317
+ }
318
+ async setRememberedRemoteTip(sha) {
319
+ await this.git.run(["update-ref", REMOTE_BASE_REF, sha], { allowFail: true });
320
+ }
321
+ async isRemoteAncestorHalted(remoteTip) {
322
+ const rememberedTip = await this.getRememberedRemoteTip();
323
+ if (!rememberedTip) {
324
+ await this.setRememberedRemoteTip(remoteTip);
325
+ return false;
326
+ }
327
+ if (rememberedTip === remoteTip) {
328
+ return false;
329
+ }
330
+ const ancestorCheck = await this.git.run(["merge-base", "--is-ancestor", rememberedTip, remoteTip], {
331
+ allowFail: true,
332
+ });
333
+ if (ancestorCheck.code !== 0) {
334
+ this.lastError = REMOTE_REWRITTEN_MSG;
335
+ this.log.error({ rememberedTip, remoteTip }, REMOTE_REWRITTEN_MSG);
336
+ return true;
337
+ }
338
+ return false;
339
+ }
257
340
  async ensureAttributesAndIgnore() {
258
341
  let changed = false;
259
342
  const gitattributesPath = join(this.repoDir, ".gitattributes");
260
343
  let attrContent = "";
261
344
  try {
262
- attrContent = await readFile(gitattributesPath, "utf8");
345
+ const s = await lstat(gitattributesPath);
346
+ if (s.isSymbolicLink()) {
347
+ await rm(gitattributesPath, { force: true });
348
+ }
349
+ else {
350
+ attrContent = await readFile(gitattributesPath, "utf8");
351
+ }
263
352
  }
264
- catch {
265
- // not exists
353
+ catch (err) {
354
+ if (err.code !== "ENOENT") {
355
+ throw err;
356
+ }
266
357
  }
267
358
  const requiredAttrLines = ["**/index.md merge=union", "**/log.md merge=union"];
268
359
  let newAttrContent = attrContent;
@@ -275,17 +366,25 @@ export class GitBackend {
275
366
  }
276
367
  }
277
368
  if (newAttrContent !== attrContent) {
278
- await writeFile(gitattributesPath, newAttrContent, "utf8");
369
+ await atomicWrite(gitattributesPath, newAttrContent);
279
370
  await this.git.run(["add", ".gitattributes"]);
280
371
  changed = true;
281
372
  }
282
373
  const gitignorePath = join(this.repoDir, ".gitignore");
283
374
  let ignoreContent = "";
284
375
  try {
285
- ignoreContent = await readFile(gitignorePath, "utf8");
376
+ const s = await lstat(gitignorePath);
377
+ if (s.isSymbolicLink()) {
378
+ await rm(gitignorePath, { force: true });
379
+ }
380
+ else {
381
+ ignoreContent = await readFile(gitignorePath, "utf8");
382
+ }
286
383
  }
287
- catch {
288
- // not exists
384
+ catch (err) {
385
+ if (err.code !== "ENOENT") {
386
+ throw err;
387
+ }
289
388
  }
290
389
  const requiredIgnoreLine = ".*.tmp";
291
390
  let newIgnoreContent = ignoreContent;
@@ -296,7 +395,7 @@ export class GitBackend {
296
395
  newIgnoreContent += `${requiredIgnoreLine}\n`;
297
396
  }
298
397
  if (newIgnoreContent !== ignoreContent) {
299
- await writeFile(gitignorePath, newIgnoreContent, "utf8");
398
+ await atomicWrite(gitignorePath, newIgnoreContent);
300
399
  await this.git.run(["add", ".gitignore"]);
301
400
  changed = true;
302
401
  }
@@ -348,6 +447,7 @@ export class GitBackend {
348
447
  async transaction(spec, work) {
349
448
  return this.mutex.run(async () => {
350
449
  const warnings = [];
450
+ let syncHalted = false;
351
451
  // Step 1: Remote sync check before work
352
452
  if (this.hasRemote) {
353
453
  const fetchRes = await this.git.run(["fetch", "origin", this.config.gitBranch], { allowFail: true });
@@ -356,20 +456,32 @@ export class GitBackend {
356
456
  warnings.push(`remote unreachable: ${firstLine}`);
357
457
  }
358
458
  else {
359
- const behindRes = await this.git.run(["rev-list", "--count", `HEAD..origin/${this.config.gitBranch}`], {
459
+ const remoteRevRes = await this.git.run(["rev-parse", "--verify", `origin/${this.config.gitBranch}`], {
360
460
  allowFail: true,
361
461
  });
362
- const behind = Number.parseInt(behindRes.stdout.trim(), 10) || 0;
363
- if (behind > 0) {
364
- const before = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
365
- const rebaseRes = await this.git.run(["rebase", `origin/${this.config.gitBranch}`], { allowFail: true });
366
- if (rebaseRes.code !== 0) {
367
- warnings.push(await this.preserveConflict());
462
+ if (remoteRevRes.code === 0) {
463
+ const remoteTip = remoteRevRes.stdout.trim();
464
+ if (await this.isRemoteAncestorHalted(remoteTip)) {
465
+ syncHalted = true;
466
+ warnings.push(this.lastError ?? REMOTE_REWRITTEN_MSG);
368
467
  }
369
- const after = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
370
- if (before !== after) {
371
- const diffRes = await this.git.run(["diff", "--name-only", before, after]);
372
- await this.resync(extractChangedProjects(diffRes.stdout));
468
+ }
469
+ if (!syncHalted) {
470
+ const behindRes = await this.git.run(["rev-list", "--count", `HEAD..origin/${this.config.gitBranch}`], {
471
+ allowFail: true,
472
+ });
473
+ const behind = Number.parseInt(behindRes.stdout.trim(), 10) || 0;
474
+ if (behind > 0) {
475
+ const before = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
476
+ const rebaseRes = await this.git.run(["rebase", `origin/${this.config.gitBranch}`], { allowFail: true });
477
+ if (rebaseRes.code !== 0) {
478
+ warnings.push(await this.preserveConflict());
479
+ }
480
+ const after = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
481
+ if (before !== after) {
482
+ const diffRes = await this.git.run(["diff", "--name-only", before, after]);
483
+ await this.resync(extractChangedProjects(diffRes.stdout));
484
+ }
373
485
  }
374
486
  }
375
487
  }
@@ -399,42 +511,61 @@ export class GitBackend {
399
511
  // Step 4: Push commit if made
400
512
  let pushed = null;
401
513
  if (commitSha !== null && this.hasRemote) {
402
- let pushSuccess = false;
403
- for (let attempt = 0; attempt < 3; attempt++) {
404
- const pushRes = await this.git.run(["push", "origin", `HEAD:refs/heads/${this.config.gitBranch}`], {
405
- allowFail: true,
406
- });
407
- if (pushRes.code === 0) {
408
- pushSuccess = true;
409
- pushed = true;
410
- break;
411
- }
412
- if (isPushRejection(pushRes.stderr)) {
413
- const pre = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
414
- await this.git.run(["fetch", "origin", this.config.gitBranch], { allowFail: true });
415
- const rebaseRes = await this.git.run(["rebase", `origin/${this.config.gitBranch}`], { allowFail: true });
416
- if (rebaseRes.code === 0) {
417
- const post = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
418
- const diffRes = await this.git.run(["diff", "--name-only", pre, post]);
419
- await this.resync(extractChangedProjects(diffRes.stdout));
514
+ if (syncHalted) {
515
+ pushed = false;
516
+ }
517
+ else {
518
+ let pushSuccess = false;
519
+ for (let attempt = 0; attempt < 3; attempt++) {
520
+ const pushRes = await this.git.run(["push", "origin", `HEAD:refs/heads/${this.config.gitBranch}`], {
521
+ allowFail: true,
522
+ });
523
+ if (pushRes.code === 0) {
524
+ pushSuccess = true;
525
+ pushed = true;
526
+ await this.setRememberedRemoteTip(commitSha);
527
+ break;
528
+ }
529
+ if (isPushRejection(pushRes.stderr)) {
530
+ const pre = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
531
+ await this.git.run(["fetch", "origin", this.config.gitBranch], { allowFail: true });
532
+ const remoteRevRes = await this.git.run(["rev-parse", "--verify", `origin/${this.config.gitBranch}`], {
533
+ allowFail: true,
534
+ });
535
+ if (remoteRevRes.code === 0) {
536
+ const remoteTip = remoteRevRes.stdout.trim();
537
+ if (await this.isRemoteAncestorHalted(remoteTip)) {
538
+ pushed = false;
539
+ warnings.push(this.lastError ?? REMOTE_REWRITTEN_MSG);
540
+ break;
541
+ }
542
+ }
543
+ const rebaseRes = await this.git.run(["rebase", `origin/${this.config.gitBranch}`], {
544
+ allowFail: true,
545
+ });
546
+ if (rebaseRes.code === 0) {
547
+ const post = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
548
+ const diffRes = await this.git.run(["diff", "--name-only", pre, post]);
549
+ await this.resync(extractChangedProjects(diffRes.stdout));
550
+ }
551
+ else {
552
+ await this.git.run(["rebase", "--abort"], { allowFail: true });
553
+ await this.git.run(["reset", "--hard", `origin/${this.config.gitBranch}`]);
554
+ await this.resync(spec.projects);
555
+ throw new OkfError("upstream_conflict", 409, "change conflicts with a concurrent upstream edit; re-read and retry");
556
+ }
420
557
  }
421
558
  else {
422
- await this.git.run(["rebase", "--abort"], { allowFail: true });
423
- await this.git.run(["reset", "--hard", `origin/${this.config.gitBranch}`]);
424
- await this.resync(spec.projects);
425
- throw new OkfError("upstream_conflict", 409, "change conflicts with a concurrent upstream edit; re-read and retry");
559
+ pushed = false;
560
+ const firstLine = pushRes.stderr.split(/\r?\n/)[0] ?? "unknown error";
561
+ warnings.push(`push failed; will retry on next sync: ${firstLine}`);
562
+ break;
426
563
  }
427
564
  }
428
- else {
565
+ if (!pushSuccess && pushed === null) {
429
566
  pushed = false;
430
- const firstLine = pushRes.stderr.split(/\r?\n/)[0] ?? "unknown error";
431
- warnings.push(`push failed; will retry on next sync: ${firstLine}`);
432
- break;
433
567
  }
434
568
  }
435
- if (!pushSuccess && pushed === null) {
436
- pushed = false;
437
- }
438
569
  }
439
570
  return {
440
571
  value: workRes.value,
@@ -463,6 +594,17 @@ export class GitBackend {
463
594
  this.lastError = fetchRes.stderr.split(/\r?\n/)[0] ?? "fetch failed";
464
595
  return this.syncStatus();
465
596
  }
597
+ const remoteRevRes = await this.git.run(["rev-parse", "--verify", `origin/${this.config.gitBranch}`], {
598
+ allowFail: true,
599
+ });
600
+ if (remoteRevRes.code !== 0) {
601
+ this.lastError = "remote ref not found";
602
+ return this.syncStatus();
603
+ }
604
+ const remoteTip = remoteRevRes.stdout.trim();
605
+ if (await this.isRemoteAncestorHalted(remoteTip)) {
606
+ return this.syncStatus();
607
+ }
466
608
  const behindRes = await this.git.run(["rev-list", "--count", `HEAD..origin/${this.config.gitBranch}`], {
467
609
  allowFail: true,
468
610
  });
@@ -491,6 +633,13 @@ export class GitBackend {
491
633
  const pushError = `push failed: ${pushRes.stderr.split(/\r?\n/)[0] ?? "unknown error"}`;
492
634
  runError = runError ? `${runError}; ${pushError}` : pushError;
493
635
  }
636
+ else {
637
+ const headSha = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
638
+ await this.setRememberedRemoteTip(headSha);
639
+ }
640
+ }
641
+ else {
642
+ await this.setRememberedRemoteTip(remoteTip);
494
643
  }
495
644
  // lastError describes the most recent sync run only.
496
645
  this.lastError = runError;
package/dist/store/git.js CHANGED
@@ -1,21 +1,70 @@
1
1
  import { spawn } from "node:child_process";
2
- import { chmod, copyFile, mkdir } from "node:fs/promises";
2
+ import { chmod, copyFile, mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
3
4
  import { join } from "node:path";
4
5
  import { pathExists } from "./fs-util.js";
5
6
  export class GitError extends Error {
6
7
  args;
7
8
  code;
8
9
  stderr;
9
- constructor(args, code, stderr) {
10
- super(`git ${args.join(" ")} failed with exit code ${code}:\n${stderr}`);
10
+ constructor(args, code, stderr, configuredRemote) {
11
+ const redactedArgs = redactArgs(args, configuredRemote);
12
+ const redactedStderr = redactStderr(stderr);
13
+ super(`git ${redactedArgs.join(" ")} failed with exit code ${code}:\n${redactedStderr}`);
11
14
  this.name = "GitError";
12
- this.args = args;
15
+ this.args = redactedArgs;
13
16
  this.code = code;
14
- this.stderr = stderr;
17
+ this.stderr = redactedStderr;
15
18
  }
16
19
  }
17
20
  export function redactRemote(url) {
18
- return url.replace(/(https?:\/\/)[^/@\s]+@/g, "$1");
21
+ return url.replace(/([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)[^/@\s]+@/g, "$1");
22
+ }
23
+ export function redactArgs(args, configuredRemote) {
24
+ const result = [];
25
+ for (let i = 0; i < args.length; i++) {
26
+ const arg = args[i];
27
+ if (arg === undefined) {
28
+ continue;
29
+ }
30
+ if (arg === "-m") {
31
+ result.push("-m");
32
+ if (i + 1 < args.length) {
33
+ result.push("<message>");
34
+ i++;
35
+ }
36
+ continue;
37
+ }
38
+ if (arg.startsWith("-m") && arg.length > 2) {
39
+ result.push("-m<message>");
40
+ continue;
41
+ }
42
+ if (arg === "--message") {
43
+ result.push("--message");
44
+ if (i + 1 < args.length) {
45
+ result.push("<message>");
46
+ i++;
47
+ }
48
+ continue;
49
+ }
50
+ if (arg.startsWith("--message=")) {
51
+ result.push("--message=<message>");
52
+ continue;
53
+ }
54
+ if ((configuredRemote && arg === configuredRemote) ||
55
+ arg.includes("://") ||
56
+ arg.startsWith("git@") ||
57
+ /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(arg) ||
58
+ /(https?:\/\/)[^/@\s]+@/.test(arg)) {
59
+ result.push(redactRemote(arg));
60
+ continue;
61
+ }
62
+ result.push(arg);
63
+ }
64
+ return result;
65
+ }
66
+ export function redactStderr(stderr) {
67
+ return redactRemote(stderr);
19
68
  }
20
69
  export function isPushRejection(stderr) {
21
70
  return stderr.includes("[rejected]") || stderr.includes("non-fast-forward") || stderr.includes("fetch first");
@@ -50,6 +99,7 @@ export class Git {
50
99
  homeDir;
51
100
  config;
52
101
  env;
102
+ sshTempDir;
53
103
  baseArgs;
54
104
  constructor(repoDir, homeDir, config) {
55
105
  this.repoDir = repoDir;
@@ -99,31 +149,54 @@ export class Git {
99
149
  "-c",
100
150
  "core.autocrlf=false",
101
151
  "-c",
152
+ "core.fsmonitor=false",
153
+ "-c",
154
+ "protocol.ext.allow=never",
155
+ "-c",
102
156
  "log.showSignature=false",
103
157
  ];
158
+ if (!config.gitHostEnv) {
159
+ this.baseArgs.push("-c", "core.sshCommand=ssh");
160
+ }
104
161
  if (config.gitHttpUsername && config.gitHttpPassword) {
105
162
  this.baseArgs.push("-c", "credential.helper=", "-c", "credential.helper=!f() { echo username=$GIT_HTTP_USERNAME; echo password=$GIT_HTTP_PASSWORD; }; f");
106
163
  }
107
164
  }
165
+ get sshKeyPath() {
166
+ return this.sshTempDir ? join(this.sshTempDir, "id_okf") : undefined;
167
+ }
168
+ async close() {
169
+ if (this.sshTempDir) {
170
+ try {
171
+ await rm(this.sshTempDir, { recursive: true, force: true });
172
+ }
173
+ finally {
174
+ this.sshTempDir = undefined;
175
+ }
176
+ }
177
+ }
108
178
  async prepareSsh() {
109
179
  if (!this.config.gitSshKeyPath) {
110
180
  return;
111
181
  }
112
- const sshDir = join(this.homeDir, ".ssh");
113
- await mkdir(sshDir, { recursive: true, mode: 0o700 });
114
- const targetKeyPath = join(sshDir, "id_okf");
182
+ try {
183
+ await rm(join(this.homeDir, ".ssh", "id_okf"), { force: true });
184
+ }
185
+ catch {
186
+ // Ignore if homeDir or .ssh directory does not exist
187
+ }
188
+ if (!this.sshTempDir) {
189
+ this.sshTempDir = await mkdtemp(join(tmpdir(), "okf-ssh-"));
190
+ await chmod(this.sshTempDir, 0o700);
191
+ }
192
+ const targetKeyPath = join(this.sshTempDir, "id_okf");
115
193
  await copyFile(this.config.gitSshKeyPath, targetKeyPath);
116
194
  await chmod(targetKeyPath, 0o600);
117
- let sshCmd = `ssh -i "${targetKeyPath}" -o IdentitiesOnly=yes -o BatchMode=yes`;
118
- // The chart always sets the path; the Secret key is optional, so strict mode requires the file to exist.
119
195
  const knownHosts = this.config.gitSshKnownHostsPath;
120
- if (knownHosts && (await pathExists(knownHosts))) {
121
- sshCmd += ` -o UserKnownHostsFile="${knownHosts}" -o StrictHostKeyChecking=yes`;
122
- }
123
- else {
124
- sshCmd += ` -o UserKnownHostsFile="${join(sshDir, "known_hosts")}" -o StrictHostKeyChecking=accept-new`;
196
+ if (!knownHosts || !(await pathExists(knownHosts))) {
197
+ throw new Error(`Configured known_hosts file does not exist: ${knownHosts}`);
125
198
  }
126
- this.env.GIT_SSH_COMMAND = sshCmd;
199
+ this.env.GIT_SSH_COMMAND = `ssh -i "${targetKeyPath}" -o IdentitiesOnly=yes -o BatchMode=yes -o UserKnownHostsFile="${knownHosts}" -o StrictHostKeyChecking=yes`;
127
200
  }
128
201
  run(args, opts) {
129
202
  const { promise, resolve, reject } = Promise.withResolvers();
@@ -159,7 +232,7 @@ export class Git {
159
232
  const stdout = Buffer.concat(stdoutChunks).toString("utf8");
160
233
  const stderr = Buffer.concat(stderrChunks).toString("utf8") + extraStderr;
161
234
  if (failed && !opts?.allowFail) {
162
- reject(new GitError(args, code, stderr));
235
+ reject(new GitError(args, code, stderr, this.config.gitRemoteUrl));
163
236
  }
164
237
  else {
165
238
  resolve({ code, stdout, stderr });
@@ -192,7 +265,7 @@ export class Git {
192
265
  });
193
266
  child.on("close", (code) => {
194
267
  if (code !== 0 && code !== null) {
195
- child.stdout.destroy(new GitError(args, code, stderrBuffer));
268
+ child.stdout.destroy(new GitError(args, code, stderrBuffer, this.config.gitRemoteUrl));
196
269
  }
197
270
  });
198
271
  return child.stdout;
@@ -0,0 +1 @@
1
+ import{t as e}from"./shellscript-CqHdAlCp.js";export{e as default};