gutterpress 0.11.4 → 0.11.6-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api/index.js CHANGED
@@ -124,7 +124,7 @@ import {
124
124
  syncProject,
125
125
  testRemoteAccess,
126
126
  validateProjectExtensions
127
- } from "../index-xefff326.js";
127
+ } from "../index-cwhgq7tq.js";
128
128
  import {
129
129
  BuildError,
130
130
  DEFAULT_PRINT_OPTS,
@@ -141,7 +141,7 @@ import {
141
141
  resolveLogger,
142
142
  restoreVersionWithBackup,
143
143
  switchBranch
144
- } from "../index-y01v2fe0.js";
144
+ } from "../index-c5ev4r1y.js";
145
145
  import {
146
146
  capabilitiesFor,
147
147
  detectProjectSource,
@@ -1,20 +1,20 @@
1
1
  import {
2
2
  executeAndReport
3
- } from "./cli-58np1xae.js";
3
+ } from "./cli-q696mp75.js";
4
4
  import {
5
5
  log
6
- } from "./cli-r2hsreyr.js";
6
+ } from "./cli-jp97snns.js";
7
7
  import {
8
8
  UsageError,
9
9
  rejectExtraPositionals,
10
10
  rejectUnknownFlags
11
- } from "./cli-en9tew8h.js";
11
+ } from "./cli-cghcd60p.js";
12
12
  import"./cli-sekhwex4.js";
13
13
  import"./cli-c41yr7he.js";
14
14
  import {
15
15
  EXIT_CODES
16
16
  } from "./cli-46ycxe6r.js";
17
- import"./cli-4mtf9ajw.js";
17
+ import"./cli-c9h2bk2k.js";
18
18
  import"./cli-v5mp7a6q.js";
19
19
  import"./cli-37x76zdn.js";
20
20
 
@@ -1,23 +1,23 @@
1
1
  import {
2
2
  runBuild,
3
3
  splitOutPath
4
- } from "./cli-58np1xae.js";
4
+ } from "./cli-q696mp75.js";
5
5
  import {
6
6
  log
7
- } from "./cli-r2hsreyr.js";
7
+ } from "./cli-jp97snns.js";
8
8
  import {
9
9
  UsageError,
10
10
  parseFormat,
11
11
  parsePdfxFlavor,
12
12
  rejectExtraPositionals,
13
13
  rejectUnknownFlags
14
- } from "./cli-en9tew8h.js";
14
+ } from "./cli-cghcd60p.js";
15
15
  import"./cli-sekhwex4.js";
16
16
  import"./cli-c41yr7he.js";
17
17
  import {
18
18
  BuildError
19
19
  } from "./cli-46ycxe6r.js";
20
- import"./cli-4mtf9ajw.js";
20
+ import"./cli-c9h2bk2k.js";
21
21
  import"./cli-v5mp7a6q.js";
22
22
  import"./cli-37x76zdn.js";
23
23
 
@@ -63,6 +63,17 @@ import git from "isomorphic-git";
63
63
  // src/lib/remote-auth/operation-log.ts
64
64
  import { appendFileSync, mkdirSync } from "node:fs";
65
65
  import path from "node:path";
66
+ function sanitizeLogText(value, secrets = []) {
67
+ let text = value;
68
+ for (const secret of secrets) {
69
+ if (!secret)
70
+ continue;
71
+ for (const form of [secret, encodeURIComponent(secret)]) {
72
+ text = text.split(form).join("[redacted]");
73
+ }
74
+ }
75
+ return text.replace(/\/\/[^/\s]*@/g, "//").replace(/([?&](?:access_token|token|password|secret|key|code)=)[^&#\s]*/gi, "$1[redacted]").replace(/\b(Bearer|Basic)\s+[A-Za-z0-9+/_.=~-]+/gi, "$1 [redacted]").replace(/\b(?:gh[pousr]_[A-Za-z0-9_]+|github_pat_[A-Za-z0-9_]+)/g, "[redacted]").replace(/[\s\x00-\x1f\x7f]+/g, " ").trim().slice(0, 6000);
76
+ }
66
77
  var noop = {
67
78
  debug: () => {},
68
79
  info: () => {},
@@ -70,7 +81,7 @@ var noop = {
70
81
  error: () => {}
71
82
  };
72
83
  var LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
73
- function createFileLogger(logFile, operation, minLevel = "debug") {
84
+ function createFileLogger(logFile, operation, minLevel = "debug", options = {}) {
74
85
  try {
75
86
  mkdirSync(path.dirname(logFile), { recursive: true });
76
87
  } catch {
@@ -80,8 +91,9 @@ function createFileLogger(logFile, operation, minLevel = "debug") {
80
91
  if (LEVEL_ORDER[level] < LEVEL_ORDER[minLevel])
81
92
  return;
82
93
  const ts = new Date().toISOString();
83
- const fields = formatData(data);
84
- const line = `[${ts}] ${level.toUpperCase().padEnd(5)} ${operation}: step=${step}${fields} | ${message}
94
+ const clean = (value) => sanitizeLogText(value, options.secrets);
95
+ const fields = formatData({ ...options.context, ...data }, clean);
96
+ const line = `[${ts}] ${level.toUpperCase().padEnd(5)} ${clean(operation)}: step=${clean(step)}${fields} | ${clean(message)}
85
97
  `;
86
98
  try {
87
99
  appendFileSync(logFile, line, "utf8");
@@ -94,7 +106,7 @@ function createFileLogger(logFile, operation, minLevel = "debug") {
94
106
  error: (step, message, data) => write("error", step, message, data)
95
107
  };
96
108
  }
97
- function formatData(data) {
109
+ function formatData(data, clean) {
98
110
  if (!data)
99
111
  return "";
100
112
  const parts = [];
@@ -102,9 +114,9 @@ function formatData(data) {
102
114
  if (value === undefined)
103
115
  continue;
104
116
  if (Array.isArray(value)) {
105
- parts.push(`${key}=${value.join(",")}`);
117
+ parts.push(`${clean(key)}=${clean(value.join(","))}`);
106
118
  } else {
107
- parts.push(`${key}=${String(value)}`);
119
+ parts.push(`${clean(key)}=${clean(String(value))}`);
108
120
  }
109
121
  }
110
122
  return parts.length > 0 ? " " + parts.join(" ") : "";
@@ -4,7 +4,7 @@ import {
4
4
  // package.json
5
5
  var package_default = {
6
6
  name: "gutterpress",
7
- version: "0.11.4",
7
+ version: "0.11.6-alpha.1",
8
8
  description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
9
9
  author: "itlackey",
10
10
  license: "MPL-2.0",
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  package_default
3
- } from "./cli-en9tew8h.js";
3
+ } from "./cli-cghcd60p.js";
4
4
  import {
5
5
  execCapture,
6
6
  findTool,
@@ -13,12 +13,12 @@ import {
13
13
  stampCreator,
14
14
  stripAnnotations,
15
15
  warn
16
- } from "./cli-r2hsreyr.js";
16
+ } from "./cli-jp97snns.js";
17
17
  import {
18
18
  DEBOUNCE,
19
19
  UsageError,
20
20
  resolvePort
21
- } from "./cli-en9tew8h.js";
21
+ } from "./cli-cghcd60p.js";
22
22
  import {
23
23
  MARGIN_BOX_IGNORED_PROPERTIES,
24
24
  RENDER_TIMEOUT_MS,
@@ -43,7 +43,7 @@ import {
43
43
  import {
44
44
  hasUncommittedChanges,
45
45
  listWorkdirChanges
46
- } from "./cli-4mtf9ajw.js";
46
+ } from "./cli-c9h2bk2k.js";
47
47
  import {
48
48
  detectProjectSource,
49
49
  repoSubPath
@@ -10418,7 +10418,7 @@ async function scaffoldProject(options) {
10418
10418
  let versionHistoryError;
10419
10419
  if (requested === "local-git") {
10420
10420
  try {
10421
- const { providerFor } = await import("./source-provider-51g6mt6y.js");
10421
+ const { providerFor } = await import("./source-provider-54a52bpd.js");
10422
10422
  const provider = providerFor({ type: "local-folder", path: projectDir });
10423
10423
  await provider.initVersionHistory({
10424
10424
  projectDir,
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  UsageError,
7
7
  package_default,
8
8
  rejectUnknownFlags
9
- } from "./cli-en9tew8h.js";
9
+ } from "./cli-cghcd60p.js";
10
10
  import {
11
11
  EXIT_CODES
12
12
  } from "./cli-46ycxe6r.js";
@@ -18,16 +18,16 @@ import {
18
18
  import { defineCommand, parseArgs, runMain } from "citty";
19
19
  import { statSync } from "node:fs";
20
20
  var SUBCOMMANDS = {
21
- new: () => import("./new-et0fffvj.js").then((m) => m.default),
22
- preview: () => import("./preview-8ptv04xf.js").then((m) => m.default),
23
- build: () => import("./build-5apre79d.js").then((m) => m.default),
24
- publish: () => import("./publish-sqp59a20.js").then((m) => m.default),
25
- lint: () => import("./lint-zeyzqkz2.js").then((m) => m.default),
26
- validate: () => import("./validate-tfk44ey7.js").then((m) => m.default),
27
- audit: () => import("./audit-hnqcf3w9.js").then((m) => m.default),
28
- preflight: () => import("./preflight-qya0kg4p.js").then((m) => m.default),
29
- doctor: () => import("./doctor-tj6k990h.js").then((m) => m.default),
30
- ext: () => import("./ext-jgwqgkrn.js").then((m) => m.default)
21
+ new: () => import("./new-s256mmkt.js").then((m) => m.default),
22
+ preview: () => import("./preview-j2rvdhg1.js").then((m) => m.default),
23
+ build: () => import("./build-sqdahz2j.js").then((m) => m.default),
24
+ publish: () => import("./publish-6px5snad.js").then((m) => m.default),
25
+ lint: () => import("./lint-t0tj3psc.js").then((m) => m.default),
26
+ validate: () => import("./validate-f0m0d2zs.js").then((m) => m.default),
27
+ audit: () => import("./audit-bhxh0bbf.js").then((m) => m.default),
28
+ preflight: () => import("./preflight-5y10233n.js").then((m) => m.default),
29
+ doctor: () => import("./doctor-gj8gtgec.js").then((m) => m.default),
30
+ ext: () => import("./ext-xhtta4bh.js").then((m) => m.default)
31
31
  };
32
32
  var VERSION = package_default.version;
33
33
  var main = defineCommand({
@@ -52,7 +52,7 @@ async function preflightRequiredInvocations(rawArgs) {
52
52
  const [command, ...commandArgs] = rawArgs;
53
53
  try {
54
54
  if (command === "new") {
55
- const { newArgs } = await import("./new-et0fffvj.js");
55
+ const { newArgs } = await import("./new-s256mmkt.js");
56
56
  rejectUnknownFlags(commandArgs, newArgs, "new");
57
57
  const parsed = parseArgs(commandArgs, {
58
58
  ...newArgs,
@@ -64,7 +64,7 @@ async function preflightRequiredInvocations(rawArgs) {
64
64
  return;
65
65
  }
66
66
  if (command === "preflight") {
67
- const { preflightArgs } = await import("./preflight-qya0kg4p.js");
67
+ const { preflightArgs } = await import("./preflight-5y10233n.js");
68
68
  rejectUnknownFlags(commandArgs, preflightArgs, "preflight");
69
69
  const parsed = parseArgs(commandArgs, {
70
70
  ...preflightArgs,
@@ -84,7 +84,7 @@ async function preflightRequiredInvocations(rawArgs) {
84
84
  }
85
85
  if (subcommand.startsWith("-"))
86
86
  return;
87
- const { EXT_SUBCOMMANDS, extAddArgs, extRemoveArgs, extEnableArgs, extDisableArgs } = await import("./ext-jgwqgkrn.js");
87
+ const { EXT_SUBCOMMANDS, extAddArgs, extRemoveArgs, extEnableArgs, extDisableArgs } = await import("./ext-xhtta4bh.js");
88
88
  if (!EXT_SUBCOMMANDS.includes(subcommand)) {
89
89
  throw new UsageError(`gutterpress ext: unknown command "${subcommand}"`);
90
90
  }
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  getSystemDiagnostics,
3
3
  log
4
- } from "./cli-r2hsreyr.js";
4
+ } from "./cli-jp97snns.js";
5
5
  import {
6
6
  UsageError,
7
7
  rejectExtraPositionals,
8
8
  rejectUnknownFlags
9
- } from "./cli-en9tew8h.js";
9
+ } from "./cli-cghcd60p.js";
10
10
  import"./cli-c41yr7he.js";
11
11
  import"./cli-46ycxe6r.js";
12
12
  import"./cli-37x76zdn.js";
@@ -10,21 +10,21 @@ import {
10
10
  removeExtension,
11
11
  searchNpmExtensions,
12
12
  setExtensionEnabled
13
- } from "./cli-58np1xae.js";
14
- import"./cli-r2hsreyr.js";
13
+ } from "./cli-q696mp75.js";
14
+ import"./cli-jp97snns.js";
15
15
  import {
16
16
  UsageError,
17
17
  exitForUsage,
18
18
  rejectExtraPositionals,
19
19
  rejectUnknownFlags,
20
20
  resolveProjectDir
21
- } from "./cli-en9tew8h.js";
21
+ } from "./cli-cghcd60p.js";
22
22
  import"./cli-sekhwex4.js";
23
23
  import"./cli-c41yr7he.js";
24
24
  import {
25
25
  EXIT_CODES
26
26
  } from "./cli-46ycxe6r.js";
27
- import"./cli-4mtf9ajw.js";
27
+ import"./cli-c9h2bk2k.js";
28
28
  import"./cli-v5mp7a6q.js";
29
29
  import"./cli-37x76zdn.js";
30
30
 
@@ -63,6 +63,63 @@ import git from "isomorphic-git";
63
63
  // src/lib/remote-auth/operation-log.ts
64
64
  import { appendFileSync, mkdirSync } from "node:fs";
65
65
  import path from "node:path";
66
+ function sanitizeLogText(value, secrets = []) {
67
+ let text = value;
68
+ for (const secret of secrets) {
69
+ if (!secret)
70
+ continue;
71
+ for (const form of [secret, encodeURIComponent(secret)]) {
72
+ text = text.split(form).join("[redacted]");
73
+ }
74
+ }
75
+ return text.replace(/\/\/[^/\s]*@/g, "//").replace(/([?&](?:access_token|token|password|secret|key|code)=)[^&#\s]*/gi, "$1[redacted]").replace(/\b(Bearer|Basic)\s+[A-Za-z0-9+/_.=~-]+/gi, "$1 [redacted]").replace(/\b(?:gh[pousr]_[A-Za-z0-9_]+|github_pat_[A-Za-z0-9_]+)/g, "[redacted]").replace(/[\s\x00-\x1f\x7f]+/g, " ").trim().slice(0, 6000);
76
+ }
77
+ function errorLogData(error) {
78
+ const out = { error: error == null ? String(error) : undefined };
79
+ let current = error;
80
+ const seen = new Set;
81
+ for (let depth = 0;depth < 3 && current != null && !seen.has(current); depth++) {
82
+ seen.add(current);
83
+ const prefix = depth === 0 ? "" : `cause${depth}_`;
84
+ if (typeof current !== "object") {
85
+ out[`${prefix}error`] = String(current);
86
+ break;
87
+ }
88
+ const err = current;
89
+ out[`${prefix}error`] = typeof err.message === "string" ? err.message : "Unknown error";
90
+ for (const key of ["code", "name", "caller", "stack", "syscall", "path"]) {
91
+ if (typeof err[key] === "string")
92
+ out[`${prefix}${key}`] = err[key];
93
+ }
94
+ const data = err.data;
95
+ if (data && typeof data === "object") {
96
+ for (const key of [
97
+ "statusCode",
98
+ "reason",
99
+ "prettyDetails",
100
+ "oid",
101
+ "ref",
102
+ "what",
103
+ "filepath",
104
+ "filepaths",
105
+ "bothModified",
106
+ "deleteByUs",
107
+ "deleteByTheirs"
108
+ ]) {
109
+ const value = data[key];
110
+ if (typeof value === "string" || typeof value === "number")
111
+ out[`${prefix}${key}`] = value;
112
+ else if (Array.isArray(value)) {
113
+ out[`${prefix}${key}`] = value.filter((v) => typeof v === "string").slice(0, 50);
114
+ if (value.length > 50)
115
+ out[`${prefix}${key}Count`] = value.length;
116
+ }
117
+ }
118
+ }
119
+ current = err.cause;
120
+ }
121
+ return out;
122
+ }
66
123
  var noop = {
67
124
  debug: () => {},
68
125
  info: () => {},
@@ -70,7 +127,7 @@ var noop = {
70
127
  error: () => {}
71
128
  };
72
129
  var LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
73
- function createFileLogger(logFile, operation, minLevel = "debug") {
130
+ function createFileLogger(logFile, operation, minLevel = "debug", options = {}) {
74
131
  try {
75
132
  mkdirSync(path.dirname(logFile), { recursive: true });
76
133
  } catch {
@@ -80,8 +137,9 @@ function createFileLogger(logFile, operation, minLevel = "debug") {
80
137
  if (LEVEL_ORDER[level] < LEVEL_ORDER[minLevel])
81
138
  return;
82
139
  const ts = new Date().toISOString();
83
- const fields = formatData(data);
84
- const line = `[${ts}] ${level.toUpperCase().padEnd(5)} ${operation}: step=${step}${fields} | ${message}
140
+ const clean = (value) => sanitizeLogText(value, options.secrets);
141
+ const fields = formatData({ ...options.context, ...data }, clean);
142
+ const line = `[${ts}] ${level.toUpperCase().padEnd(5)} ${clean(operation)}: step=${clean(step)}${fields} | ${clean(message)}
85
143
  `;
86
144
  try {
87
145
  appendFileSync(logFile, line, "utf8");
@@ -94,12 +152,12 @@ function createFileLogger(logFile, operation, minLevel = "debug") {
94
152
  error: (step, message, data) => write("error", step, message, data)
95
153
  };
96
154
  }
97
- function resolveLogger(logFile, operation) {
155
+ function resolveLogger(logFile, operation, options = {}) {
98
156
  if (!logFile)
99
157
  return noop;
100
- return createFileLogger(logFile, operation);
158
+ return createFileLogger(logFile, operation, "debug", options);
101
159
  }
102
- function formatData(data) {
160
+ function formatData(data, clean) {
103
161
  if (!data)
104
162
  return "";
105
163
  const parts = [];
@@ -107,9 +165,9 @@ function formatData(data) {
107
165
  if (value === undefined)
108
166
  continue;
109
167
  if (Array.isArray(value)) {
110
- parts.push(`${key}=${value.join(",")}`);
168
+ parts.push(`${clean(key)}=${clean(value.join(","))}`);
111
169
  } else {
112
- parts.push(`${key}=${String(value)}`);
170
+ parts.push(`${clean(key)}=${clean(String(value))}`);
113
171
  }
114
172
  }
115
173
  return parts.length > 0 ? " " + parts.join(" ") : "";
@@ -635,4 +693,4 @@ async function switchBranch(options) {
635
693
  });
636
694
  }
637
695
 
638
- export { gitFs, resolveLogger, gitScopeFor, HISTORY_PAGE_LIMIT, withRepoLock, __repoLockQueueSizeForTests, resolveGitAuthor, listWorkdirChanges, stageChanges, hasPendingChanges, hasUncommittedChanges, snapshotWorkingTreeUnlocked, isNoChangesError, providerFor, gitDirFor, snapshotStagingMarkerPath, RESTORE_BACKUP_MESSAGE, AUTO_SNAPSHOT_MESSAGE, restoreVersionWithBackup, SWITCH_BRANCH_SNAPSHOT_MESSAGE, listLocalBranches, switchBranch };
696
+ export { gitFs, errorLogData, resolveLogger, gitScopeFor, HISTORY_PAGE_LIMIT, withRepoLock, __repoLockQueueSizeForTests, resolveGitAuthor, listWorkdirChanges, stageChanges, hasPendingChanges, hasUncommittedChanges, snapshotWorkingTreeUnlocked, isNoChangesError, providerFor, gitDirFor, snapshotStagingMarkerPath, RESTORE_BACKUP_MESSAGE, AUTO_SNAPSHOT_MESSAGE, restoreVersionWithBackup, SWITCH_BRANCH_SNAPSHOT_MESSAGE, listLocalBranches, switchBranch };
@@ -20,6 +20,7 @@ import {
20
20
  spawnCapture
21
21
  } from "./index-6n15e1ze.js";
22
22
  import {
23
+ errorLogData,
23
24
  gitFs,
24
25
  gitScopeFor,
25
26
  hasPendingChanges,
@@ -30,7 +31,7 @@ import {
30
31
  snapshotStagingMarkerPath,
31
32
  snapshotWorkingTreeUnlocked,
32
33
  withRepoLock
33
- } from "./index-y01v2fe0.js";
34
+ } from "./index-c5ev4r1y.js";
34
35
  import {
35
36
  detectProjectSource,
36
37
  repoSubPath,
@@ -4874,7 +4875,7 @@ import git from "isomorphic-git";
4874
4875
  // package.json
4875
4876
  var package_default = {
4876
4877
  name: "gutterpress",
4877
- version: "0.11.4",
4878
+ version: "0.11.6-alpha.1",
4878
4879
  description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
4879
4880
  author: "itlackey",
4880
4881
  license: "MPL-2.0",
@@ -11159,7 +11160,7 @@ async function scaffoldProject(options) {
11159
11160
  let versionHistoryError;
11160
11161
  if (requested === "local-git") {
11161
11162
  try {
11162
- const { providerFor } = await import("./source-provider-jkppq9fc.js");
11163
+ const { providerFor } = await import("./source-provider-a458mmec.js");
11163
11164
  const provider = providerFor({ type: "local-folder", path: projectDir });
11164
11165
  await provider.initVersionHistory({
11165
11166
  projectDir,
@@ -11251,7 +11252,7 @@ styles:
11251
11252
  let versionHistoryError;
11252
11253
  if (requested === "local-git") {
11253
11254
  try {
11254
- const { providerFor } = await import("./source-provider-jkppq9fc.js");
11255
+ const { providerFor } = await import("./source-provider-a458mmec.js");
11255
11256
  const provider = providerFor({ type: "local-folder", path: dir });
11256
11257
  await provider.initVersionHistory({
11257
11258
  projectDir: dir,
@@ -14428,7 +14429,7 @@ async function fetchRemoteTip(dir, branch, transport, http3, cache2) {
14428
14429
  if (code === "NoRefspecError")
14429
14430
  return null;
14430
14431
  if (code === "NotFoundError" || e?.data?.statusCode === 404) {
14431
- const err = new Error("The online repository couldn't be accessed with the saved connection. Reconnect and try again.");
14432
+ const err = new Error("The online repository couldn't be accessed with the saved connection. Reconnect and try again.", { cause: e });
14432
14433
  err.code = "HttpError";
14433
14434
  err.data = { statusCode: 401 };
14434
14435
  throw err;
@@ -14787,6 +14788,7 @@ async function diagnoseProjectRemote(projectDir, options = {}) {
14787
14788
  }
14788
14789
  // src/lib/remote-auth/sync.ts
14789
14790
  import git6 from "isomorphic-git";
14791
+ import { randomUUID as randomUUID4 } from "node:crypto";
14790
14792
 
14791
14793
  // src/lib/remote-auth/converge-merge.ts
14792
14794
  import { mkdir as mkdir15, unlink as unlink3, writeFile as writeFile13 } from "node:fs/promises";
@@ -14828,13 +14830,8 @@ function mergeWithMarkers(baseContent, ourContent, theirContent) {
14828
14830
  }
14829
14831
  return merged;
14830
14832
  }
14831
- async function readBlobOrNull(dir, cache2, oid, filepath) {
14832
- try {
14833
- const r = await git5.readBlob({ fs: gitFs, dir, cache: cache2, oid, filepath });
14834
- return { oid: r.oid, blob: r.blob };
14835
- } catch {
14836
- return null;
14837
- }
14833
+ async function readBlob(dir, cache2, oid, filepath) {
14834
+ return git5.readBlob({ fs: gitFs, dir, cache: cache2, oid, filepath });
14838
14835
  }
14839
14836
  async function writeTreeFile(dir, filepath, content) {
14840
14837
  const abs = path24.join(dir, filepath);
@@ -14875,6 +14872,8 @@ async function bothAddedPaths(dir, cache2, ourTip, theirTip, allowUnrelated) {
14875
14872
  }
14876
14873
  async function convergeMerge(params) {
14877
14874
  const { dir, cache: cache2, branch, theirs, author } = params;
14875
+ const step = params.onStep ?? (() => {});
14876
+ const logger = params.logger ?? resolveLogger(undefined, "sync");
14878
14877
  const snapshot = (message) => snapshotWorkingTreeUnlocked({
14879
14878
  projectDir: dir,
14880
14879
  repoRoot: dir,
@@ -14889,6 +14888,7 @@ async function convergeMerge(params) {
14889
14888
  let tipBeforeMerge = ourTip;
14890
14889
  const attempt = async () => {
14891
14890
  tipBeforeMerge = await git5.resolveRef({ fs: gitFs, dir, ref: branch });
14891
+ step("merge", { local: tipBeforeMerge, fetched: theirs });
14892
14892
  return git5.merge({
14893
14893
  fs: gitFs,
14894
14894
  dir,
@@ -14913,13 +14913,14 @@ async function convergeMerge(params) {
14913
14913
  };
14914
14914
  const postRestore = [];
14915
14915
  const equalize = async (paths) => {
14916
+ step("equalize", { binary: paths.binary, deleteByUs: paths.deleteByUs, deleteByTheirs: paths.deleteByTheirs });
14916
14917
  for (const p of paths.binary) {
14918
+ step("read-conflict-blobs", { filepath: p });
14917
14919
  const [ourBlob, theirBlob] = await Promise.all([
14918
- readBlobOrNull(dir, cache2, ourTip, p),
14919
- readBlobOrNull(dir, cache2, theirs, p)
14920
+ readBlob(dir, cache2, ourTip, p),
14921
+ readBlob(dir, cache2, theirs, p)
14920
14922
  ]);
14921
- if (!ourBlob || !theirBlob)
14922
- continue;
14923
+ step("write-conflict-file", { filepath: p });
14923
14924
  await writeTreeFile(dir, p, theirBlob.blob);
14924
14925
  const onlinePath = onlineSiblingPath(p);
14925
14926
  postRestore.push({ path: p, bytes: ourBlob.blob });
@@ -14927,19 +14928,22 @@ async function convergeMerge(params) {
14927
14928
  keptBothFiles.push({ path: p, onlinePath });
14928
14929
  }
14929
14930
  for (const p of paths.deleteByUs) {
14930
- const theirBlob = await readBlobOrNull(dir, cache2, theirs, p);
14931
- if (theirBlob)
14932
- await writeTreeFile(dir, p, theirBlob.blob);
14931
+ step("read-conflict-blobs", { filepath: p });
14932
+ const theirBlob = await readBlob(dir, cache2, theirs, p);
14933
+ step("write-conflict-file", { filepath: p });
14934
+ await writeTreeFile(dir, p, theirBlob.blob);
14933
14935
  }
14934
14936
  for (const p of paths.deleteByTheirs) {
14935
- const ourBlob = await readBlobOrNull(dir, cache2, ourTip, p);
14937
+ step("read-conflict-blobs", { filepath: p });
14938
+ const ourBlob = await readBlob(dir, cache2, ourTip, p);
14939
+ step("delete-conflict-file", { filepath: p });
14936
14940
  await unlink3(path24.join(dir, p)).catch((e) => {
14937
14941
  if (e?.code !== "ENOENT")
14938
14942
  throw e;
14939
14943
  });
14940
- if (ourBlob)
14941
- postRestore.push({ path: p, bytes: ourBlob.blob });
14944
+ postRestore.push({ path: p, bytes: ourBlob.blob });
14942
14945
  }
14946
+ step("snapshot-equalized");
14943
14947
  if (await hasPendingChanges(dir))
14944
14948
  await snapshot(CONVERGE_PREPARE_MESSAGE);
14945
14949
  };
@@ -14947,6 +14951,7 @@ async function convergeMerge(params) {
14947
14951
  await attempt();
14948
14952
  } catch (e) {
14949
14953
  if (isMergeConflictError(e)) {
14954
+ logger.info("merge", "resolving conflicts", errorLogData(e));
14950
14955
  await equalize({
14951
14956
  binary: e.data.bothModified ?? [],
14952
14957
  deleteByUs: e.data.deleteByUs ?? [],
@@ -14954,8 +14959,14 @@ async function convergeMerge(params) {
14954
14959
  });
14955
14960
  await attempt();
14956
14961
  } else if (e?.code === "MergeNotSupportedError") {
14962
+ step("merge-base");
14957
14963
  const added = await bothAddedPaths(dir, cache2, ourTip, theirs, false);
14958
- if (added === null || added.length === 0)
14964
+ if (added === null) {
14965
+ throw Object.assign(new Error("No common commits between local and online histories", { cause: e }), {
14966
+ code: "UnrelatedHistoriesError"
14967
+ });
14968
+ }
14969
+ if (added.length === 0)
14959
14970
  throw e;
14960
14971
  await equalize({ binary: added, deleteByUs: [], deleteByTheirs: [] });
14961
14972
  await attempt();
@@ -14963,11 +14974,14 @@ async function convergeMerge(params) {
14963
14974
  throw e;
14964
14975
  }
14965
14976
  }
14977
+ step("checkout");
14966
14978
  try {
14967
14979
  await git5.checkout({ fs: gitFs, dir, cache: cache2, ref: branch, force: false });
14968
14980
  } catch (e) {
14969
14981
  if (!isCheckoutConflict(e))
14970
14982
  throw e;
14983
+ logger.warn("checkout", "checkout refused; restoring branch", errorLogData(e));
14984
+ step("rollback", { local: tipBeforeMerge });
14971
14985
  await git5.writeRef({
14972
14986
  fs: gitFs,
14973
14987
  dir,
@@ -14975,14 +14989,19 @@ async function convergeMerge(params) {
14975
14989
  value: tipBeforeMerge,
14976
14990
  force: true
14977
14991
  });
14992
+ step("checkout");
14978
14993
  throw e;
14979
14994
  }
14980
14995
  if (postRestore.length > 0) {
14981
- for (const r of postRestore)
14996
+ for (const r of postRestore) {
14997
+ step("restore-conflict-file", { filepath: r.path });
14982
14998
  await writeTreeFile(dir, r.path, r.bytes);
14999
+ }
15000
+ step("snapshot-restored");
14983
15001
  if (await hasPendingChanges(dir))
14984
15002
  await snapshot(CONVERGE_RESTORE_MESSAGE);
14985
15003
  }
15004
+ step("merged-ref");
14986
15005
  return {
14987
15006
  oid: await git5.resolveRef({ fs: gitFs, dir, ref: branch }),
14988
15007
  combinedFiles: [...combined].sort(),
@@ -15005,7 +15024,7 @@ function isPushRejected(e) {
15005
15024
  function isUnrelatedHistories(e) {
15006
15025
  const code = e?.code;
15007
15026
  const msg = e?.message ?? String(e);
15008
- return code === "MergeNotSupportedError" || /unrelated histories|no common commits|refusing to merge unrelated/i.test(msg);
15027
+ return code === "UnrelatedHistoriesError" || /unrelated histories|no common commits|refusing to merge unrelated/i.test(msg);
15009
15028
  }
15010
15029
  async function historyUnreadable(dir) {
15011
15030
  try {
@@ -15027,157 +15046,210 @@ function defaultSleep2(ms) {
15027
15046
  async function syncProject(options) {
15028
15047
  const http3 = options.httpClient ?? defaultGitHttp;
15029
15048
  const push = options.push ?? true;
15030
- const attempts = Math.max(1, options.retry?.attempts ?? DEFAULT_SYNC_RETRY.attempts);
15031
- const backoffMs = Math.max(0, options.retry?.backoffMs ?? DEFAULT_SYNC_RETRY.backoffMs);
15049
+ const requestedAttempts = options.retry?.attempts;
15050
+ const attempts = Number.isFinite(requestedAttempts) ? Math.min(10, Math.max(1, Math.floor(requestedAttempts))) : DEFAULT_SYNC_RETRY.attempts;
15051
+ const requestedBackoff = options.retry?.backoffMs;
15052
+ const backoffMs = Number.isFinite(requestedBackoff) ? Math.min(30000, Math.max(0, requestedBackoff)) : DEFAULT_SYNC_RETRY.backoffMs;
15032
15053
  const sleep = options.retry?.sleep ?? defaultSleep2;
15033
- const dir = await repoDirFor(options.projectDir);
15034
- return withRepoLock(dir, async () => {
15035
- const logger = resolveLogger(options.logFile, "sync");
15036
- const cache2 = {};
15037
- let snapshotId;
15038
- let pulled = false;
15039
- let filesChanged = false;
15040
- const combinedFiles = new Set;
15041
- const keptBothFiles = [];
15042
- const base = () => ({
15043
- ...filesChanged ? { filesChanged: true } : {},
15044
- ...snapshotId ? { snapshotId } : {}
15045
- });
15046
- const convergeExtras = () => ({
15047
- ...combinedFiles.size > 0 ? { combinedFiles: [...combinedFiles].sort() } : {},
15048
- ...keptBothFiles.length > 0 ? { keptBothFiles } : {}
15054
+ const secrets = options.credential ? [options.credential.token] : [];
15055
+ const logger = resolveLogger(options.logFile, "sync", {
15056
+ context: { run: randomUUID4(), version: package_default.version, platform: process.platform, node: process.version },
15057
+ secrets
15058
+ });
15059
+ const started = performance.now();
15060
+ let stage = "repo";
15061
+ const step = (next, data) => {
15062
+ stage = next;
15063
+ logger.info(stage, "starting", data);
15064
+ };
15065
+ const finish = (outcome) => {
15066
+ logger.info("result", "sync complete", {
15067
+ status: outcome.status,
15068
+ stage,
15069
+ push,
15070
+ elapsedMs: Math.round(performance.now() - started),
15071
+ snapshot: outcome.snapshotId ?? "none",
15072
+ filesChanged: outcome.filesChanged ?? false
15049
15073
  });
15050
- try {
15051
- const branch = await currentBranchOrThrow(dir);
15052
- const transport = await resolveTransport(dir, options);
15053
- if (push) {
15054
- snapshotId = await snapshotBeforeAction({
15055
- projectDir: options.projectDir,
15056
- dir,
15057
- message: options.message,
15058
- authorName: options.authorName,
15059
- authorEmail: options.authorEmail,
15060
- cache: cache2
15074
+ return outcome;
15075
+ };
15076
+ logger.info("sync", "starting sync", { push, attempts });
15077
+ const run2 = async () => {
15078
+ step("repo");
15079
+ const dir = await repoDirFor(options.projectDir);
15080
+ step("lock");
15081
+ return withRepoLock(dir, async () => {
15082
+ const cache2 = {};
15083
+ let snapshotId;
15084
+ let pulled = false;
15085
+ let filesChanged = false;
15086
+ const combinedFiles = new Set;
15087
+ const keptBothFiles = [];
15088
+ const base = () => ({
15089
+ ...filesChanged ? { filesChanged: true } : {},
15090
+ ...snapshotId ? { snapshotId } : {}
15091
+ });
15092
+ const convergeExtras = () => ({
15093
+ ...combinedFiles.size > 0 ? { combinedFiles: [...combinedFiles].sort() } : {},
15094
+ ...keptBothFiles.length > 0 ? { keptBothFiles } : {}
15095
+ });
15096
+ try {
15097
+ step("branch");
15098
+ const branch = await currentBranchOrThrow(dir);
15099
+ step("transport");
15100
+ const transport = await resolveTransport(dir, options);
15101
+ if (transport.credential) {
15102
+ secrets.push(transport.credential.token);
15103
+ const username = transport.credential.kind === "github-oauth" ? "x-access-token" : transport.credential.username || transport.credential.token;
15104
+ secrets.push(Buffer.from(`${username}:${transport.credential.token}`).toString("base64"));
15105
+ }
15106
+ logger.info("transport", "resolved", {
15107
+ branch,
15108
+ remote: transport.remote,
15109
+ host: transport.host,
15110
+ credential: Boolean(transport.credential)
15061
15111
  });
15062
- }
15063
- for (let attempt = 0;attempt < attempts; attempt++) {
15064
- logger.info("sync", `sync pass ${attempt + 1}/${attempts}`);
15065
- const remoteTip = await fetchRemoteTip(dir, branch, transport, http3, cache2);
15066
- let localTip = await git6.resolveRef({ fs: gitFs, dir, ref: branch });
15067
- if (push || remoteTip !== null && remoteTip !== localTip) {
15068
- const lateSnapshot = await snapshotBeforeAction({
15112
+ if (push) {
15113
+ step("snapshot");
15114
+ snapshotId = await snapshotBeforeAction({
15069
15115
  projectDir: options.projectDir,
15070
15116
  dir,
15071
- message: SYNC_LATE_EDIT_MESSAGE,
15117
+ message: options.message,
15072
15118
  authorName: options.authorName,
15073
15119
  authorEmail: options.authorEmail,
15074
15120
  cache: cache2
15075
15121
  });
15076
- if (lateSnapshot) {
15077
- snapshotId = lateSnapshot;
15078
- localTip = await git6.resolveRef({ fs: gitFs, dir, ref: branch });
15079
- }
15080
15122
  }
15081
- logger.info("sync", `branch=${branch} local=${short(localTip)} fetched=${short(remoteTip)} snapshot=${snapshotId ? short(snapshotId) : "none"}`);
15082
- let tip = localTip;
15083
- if (remoteTip && remoteTip !== localTip) {
15084
- let converge;
15085
- try {
15086
- converge = await convergeMerge({
15123
+ for (let attempt = 0;attempt < attempts; attempt++) {
15124
+ logger.info("sync", `sync pass ${attempt + 1}/${attempts}`);
15125
+ step("fetch", { attempt: attempt + 1, branch, remote: transport.remote });
15126
+ const remoteTip = await fetchRemoteTip(dir, branch, transport, http3, cache2);
15127
+ step("local-ref", { fetched: remoteTip ?? "none" });
15128
+ let localTip = await git6.resolveRef({ fs: gitFs, dir, ref: branch });
15129
+ if (push || remoteTip !== null && remoteTip !== localTip) {
15130
+ step("snapshot-after-fetch");
15131
+ const lateSnapshot = await snapshotBeforeAction({
15132
+ projectDir: options.projectDir,
15087
15133
  dir,
15088
- cache: cache2,
15089
- branch,
15090
- theirs: remoteTip,
15091
- author: await resolveGitAuthor(dir, options.authorName, options.authorEmail),
15134
+ message: SYNC_LATE_EDIT_MESSAGE,
15092
15135
  authorName: options.authorName,
15093
- authorEmail: options.authorEmail
15136
+ authorEmail: options.authorEmail,
15137
+ cache: cache2
15094
15138
  });
15095
- } catch (e) {
15096
- if (isUnrelatedHistories(e)) {
15097
- logger.warn("sync", "unrelated histories — refusing to combine");
15098
- return { status: "error", message: MSG_UNRELATED, ...base() };
15139
+ if (lateSnapshot) {
15140
+ snapshotId = lateSnapshot;
15141
+ localTip = await git6.resolveRef({ fs: gitFs, dir, ref: branch });
15099
15142
  }
15100
- throw e;
15101
15143
  }
15102
- tip = converge.oid;
15103
- if (tip !== localTip) {
15104
- pulled = true;
15105
- const [before, after] = await Promise.all([
15106
- git6.readCommit({ fs: gitFs, dir, cache: cache2, oid: localTip }),
15107
- git6.readCommit({ fs: gitFs, dir, cache: cache2, oid: tip })
15108
- ]);
15109
- filesChanged = filesChanged || before.commit.tree !== after.commit.tree;
15110
- for (const f of converge.combinedFiles)
15111
- combinedFiles.add(f);
15112
- keptBothFiles.push(...converge.keptBothFiles);
15113
- if (converge.combinedFiles.length > 0) {
15114
- logger.info("sync", `combined with markers`, { files: converge.combinedFiles });
15144
+ logger.info("sync", `branch=${branch} local=${short(localTip)} fetched=${short(remoteTip)} snapshot=${snapshotId ? short(snapshotId) : "none"}`);
15145
+ let tip = localTip;
15146
+ if (remoteTip && remoteTip !== localTip) {
15147
+ let converge;
15148
+ step("author");
15149
+ const author = await resolveGitAuthor(dir, options.authorName, options.authorEmail);
15150
+ try {
15151
+ step("merge", { local: localTip, fetched: remoteTip });
15152
+ converge = await convergeMerge({
15153
+ dir,
15154
+ cache: cache2,
15155
+ branch,
15156
+ theirs: remoteTip,
15157
+ author,
15158
+ onStep: step,
15159
+ logger,
15160
+ authorName: options.authorName,
15161
+ authorEmail: options.authorEmail
15162
+ });
15163
+ } catch (e) {
15164
+ if (isUnrelatedHistories(e)) {
15165
+ logger.warn(stage, "unrelated histories — refusing to combine", errorLogData(e));
15166
+ return { status: "error", message: MSG_UNRELATED, ...base() };
15167
+ }
15168
+ throw e;
15169
+ }
15170
+ tip = converge.oid;
15171
+ if (tip !== localTip) {
15172
+ pulled = true;
15173
+ step("compare-trees");
15174
+ const [before, after] = await Promise.all([
15175
+ git6.readCommit({ fs: gitFs, dir, cache: cache2, oid: localTip }),
15176
+ git6.readCommit({ fs: gitFs, dir, cache: cache2, oid: tip })
15177
+ ]);
15178
+ filesChanged = filesChanged || before.commit.tree !== after.commit.tree;
15179
+ for (const f of converge.combinedFiles)
15180
+ combinedFiles.add(f);
15181
+ keptBothFiles.push(...converge.keptBothFiles);
15182
+ if (converge.combinedFiles.length > 0) {
15183
+ logger.info("sync", `combined with markers`, { files: converge.combinedFiles });
15184
+ }
15115
15185
  }
15116
15186
  }
15117
- }
15118
- if (tip === remoteTip) {
15119
- logger.info("sync", `up-to-date`, { pulled });
15120
- return {
15121
- status: "up-to-date",
15122
- message: pulled ? MSG_UP_TO_DATE_PULLED : MSG_UP_TO_DATE,
15123
- ...base()
15124
- };
15125
- }
15126
- if (!push) {
15127
- logger.info("sync", `pull-only pass complete — push deferred`, { pulled });
15187
+ if (tip === remoteTip) {
15188
+ logger.info("sync", `up-to-date`, { pulled });
15189
+ return {
15190
+ status: "up-to-date",
15191
+ message: pulled ? MSG_UP_TO_DATE_PULLED : MSG_UP_TO_DATE,
15192
+ ...base()
15193
+ };
15194
+ }
15195
+ if (!push) {
15196
+ logger.info("sync", `pull-only pass complete — push deferred`, { pulled });
15197
+ return {
15198
+ status: "up-to-date",
15199
+ message: pulled ? MSG_UP_TO_DATE_PULLED : MSG_UP_TO_DATE,
15200
+ ...convergeExtras(),
15201
+ ...base()
15202
+ };
15203
+ }
15204
+ step("push", { branch, local: tip, fetched: remoteTip ?? "none" });
15205
+ try {
15206
+ await git6.push({
15207
+ fs: gitFs,
15208
+ http: http3,
15209
+ dir,
15210
+ cache: cache2,
15211
+ remote: transport.remote,
15212
+ ref: branch,
15213
+ ...onAuthFor(transport.credential)
15214
+ });
15215
+ } catch (e) {
15216
+ if (!isPushRejected(e))
15217
+ throw e;
15218
+ logger.info("push", `push rejected (non-fast-forward) — retrying`, errorLogData(e));
15219
+ if (attempt < attempts - 1 && backoffMs > 0)
15220
+ await sleep(backoffMs);
15221
+ continue;
15222
+ }
15223
+ logger.info("sync", `synced`, { pulled });
15128
15224
  return {
15129
- status: "up-to-date",
15130
- message: pulled ? MSG_UP_TO_DATE_PULLED : MSG_UP_TO_DATE,
15225
+ status: "synced",
15226
+ message: pulled ? MSG_SYNCED_MERGED : MSG_SYNCED,
15227
+ mergedRemoteChanges: pulled,
15131
15228
  ...convergeExtras(),
15132
15229
  ...base()
15133
15230
  };
15134
15231
  }
15135
- try {
15136
- await git6.push({
15137
- fs: gitFs,
15138
- http: http3,
15139
- dir,
15140
- cache: cache2,
15141
- remote: transport.remote,
15142
- ref: branch,
15143
- ...onAuthFor(transport.credential)
15144
- });
15145
- } catch (e) {
15146
- if (!isPushRejected(e)) {
15147
- const err = e;
15148
- logger.warn("sync", "push failed", {
15149
- code: err?.code ?? err?.name,
15150
- error: String(err?.message ?? e).replace(/\s+/g, " ").trim().replace(/\/\/[^/\s]*@/g, "//")
15151
- });
15152
- throw e;
15153
- }
15154
- logger.info("sync", `push rejected (non-fast-forward) — retrying`);
15155
- if (attempt < attempts - 1 && backoffMs > 0)
15156
- await sleep(backoffMs);
15157
- continue;
15232
+ logger.error("sync", `exhausted ${attempts} retry attempts (race)`);
15233
+ return { status: "error", message: MSG_BUSY, ...convergeExtras(), ...base() };
15234
+ } catch (e) {
15235
+ logger.warn(stage, `${stage} failed`, errorLogData(e));
15236
+ const setupMsg = setupErrorMessage(e);
15237
+ if (setupMsg)
15238
+ return { status: "error", message: setupMsg, ...base() };
15239
+ if (await historyUnreadable(dir)) {
15240
+ logger.error("sync", "the project's history could not be read");
15241
+ return { status: "error", message: MSG_HISTORY_UNREADABLE, ...base() };
15158
15242
  }
15159
- logger.info("sync", `synced`, { pulled });
15160
- return {
15161
- status: "synced",
15162
- message: pulled ? MSG_SYNCED_MERGED : MSG_SYNCED,
15163
- mergedRemoteChanges: pulled,
15164
- ...convergeExtras(),
15165
- ...base()
15166
- };
15167
- }
15168
- logger.error("sync", `exhausted ${attempts} retry attempts (race)`);
15169
- return { status: "error", message: MSG_BUSY, ...convergeExtras(), ...base() };
15170
- } catch (e) {
15171
- const setupMsg = setupErrorMessage(e);
15172
- if (setupMsg)
15173
- return { status: "error", message: setupMsg, ...base() };
15174
- if (await historyUnreadable(dir)) {
15175
- logger.error("sync", "the project's history could not be read");
15176
- return { status: "error", message: MSG_HISTORY_UNREADABLE, ...base() };
15243
+ return { ...failureOutcome(e, snapshotId), ...filesChanged ? { filesChanged: true } : {} };
15177
15244
  }
15178
- return { ...failureOutcome(e, snapshotId), ...filesChanged ? { filesChanged: true } : {} };
15179
- }
15180
- });
15245
+ });
15246
+ };
15247
+ try {
15248
+ return finish(await run2());
15249
+ } catch (e) {
15250
+ logger.warn(stage, `${stage} failed`, errorLogData(e));
15251
+ return finish(failureOutcome(e));
15252
+ }
15181
15253
  }
15182
15254
  async function refreshRemoteCopies(options) {
15183
15255
  const http3 = options.httpClient ?? defaultGitHttp;
package/dist/index.js CHANGED
@@ -143,7 +143,7 @@ import {
143
143
  syncProject,
144
144
  testRemoteAccess,
145
145
  validateProjectExtensions
146
- } from "./index-xefff326.js";
146
+ } from "./index-cwhgq7tq.js";
147
147
  import {
148
148
  BUILD_DIAGNOSTIC_CODES
149
149
  } from "./index-12pvhbyz.js";
@@ -163,7 +163,7 @@ import {
163
163
  resolveLogger,
164
164
  restoreVersionWithBackup,
165
165
  switchBranch
166
- } from "./index-y01v2fe0.js";
166
+ } from "./index-c5ev4r1y.js";
167
167
  import {
168
168
  capabilitiesFor,
169
169
  detectProjectSource,
@@ -1,4 +1,5 @@
1
1
  import type { GitCache, KeptBothFile } from "./sync-types.ts";
2
+ import { type LogData, type OperationLogger } from "./operation-log.ts";
2
3
  export type { KeptBothFile };
3
4
  /** Type guard exposing MergeConflictError's per-file payload. */
4
5
  export declare function isMergeConflictError(e: unknown): e is {
@@ -60,4 +61,6 @@ export declare function convergeMerge(params: {
60
61
  };
61
62
  authorName?: string;
62
63
  authorEmail?: string;
64
+ onStep?: (stage: string, data?: LogData) => void;
65
+ logger?: OperationLogger;
63
66
  }): Promise<ConvergeResult>;
@@ -9,8 +9,8 @@
9
9
  * it survives even if the process crashes mid-operation.
10
10
  *
11
11
  * SECURITY INVARIANT: this logger NEVER writes secrets, tokens, credentials,
12
- * or full remote URLs with embedded auth. Callers must pass only sanitized
13
- * data (repo slug, branch name, short OIDs, error codes, outcome status).
12
+ * or full remote URLs with embedded auth. All fields are sanitized at the
13
+ * write boundary; callers must still avoid supplying arbitrary request data.
14
14
  *
15
15
  * The logger is injectable: callers pass a `logFile` path and get a file
16
16
  * logger; omit it and a no-op logger is used (backward compatible — existing
@@ -27,8 +27,7 @@
27
27
  *
28
28
  * Cross-platform: uses `node:fs` appendFileSync + `node:path`. The caller
29
29
  * is responsible for providing a valid directory (the logger creates the
30
- * file but NOT the parent directory — that's the caller's job, e.g. the
31
- * desktop ensures `userData/logs/` exists before passing the path).
30
+ * file and its parent directory when needed).
32
31
  *
33
32
  * Compatible with `bun build --compile`: no runtime package.json reads, no
34
33
  * computed-path dynamic imports, no native bindings — just `fs.appendFileSync`.
@@ -42,6 +41,15 @@ export interface OperationLogger {
42
41
  }
43
42
  /** Structured key-value fields appended after the step. No secrets. */
44
43
  export type LogData = Record<string, string | number | boolean | string[] | undefined>;
44
+ interface LogOptions {
45
+ context?: LogData;
46
+ /** Mutable per-operation list; credentials may be resolved after logging starts. */
47
+ secrets?: readonly string[];
48
+ }
49
+ /** One line, bounded, with credentials removed BEFORE truncation. */
50
+ export declare function sanitizeLogText(value: string, secrets?: readonly string[]): string;
51
+ /** Allowlist Git/OS diagnostic fields; never serialize request/config/credential objects. */
52
+ export declare function errorLogData(error: unknown): LogData;
45
53
  /**
46
54
  * Create a file-backed operation logger. Each call appends one line to
47
55
  * `logFile` (created if it doesn't exist; the parent directory is created
@@ -52,10 +60,11 @@ export type LogData = Record<string, string | number | boolean | string[] | unde
52
60
  * @param minLevel Minimum level to write (default: "debug" — write everything).
53
61
  * @param operation Operation name shown in each line (e.g. "sync", "recovery").
54
62
  */
55
- export declare function createFileLogger(logFile: string, operation: string, minLevel?: LogLevel): OperationLogger;
63
+ export declare function createFileLogger(logFile: string, operation: string, minLevel?: LogLevel, options?: LogOptions): OperationLogger;
56
64
  /**
57
65
  * Resolve a logger from an optional `logFile` path. Returns a no-op logger
58
66
  * when `logFile` is undefined or empty (backward compatible — callers that
59
67
  * don't pass `logFile` see zero behavior change and zero file I/O).
60
68
  */
61
- export declare function resolveLogger(logFile: string | undefined, operation: string): OperationLogger;
69
+ export declare function resolveLogger(logFile: string | undefined, operation: string, options?: LogOptions): OperationLogger;
70
+ export {};
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Type/interface declarations for snapshot-first sync + conflict resolution
3
- * (#15, ADR 0006 D5). Extracted from sync.ts so the orchestrator, transport and
4
- * conflict-resolution modules share ONE definition of each result/option shape.
2
+ * Type/interface declarations for snapshot-first sync
3
+ * (#15, ADR 0006 D5). Shared by the orchestrator, transport and
4
+ * convergence modules share ONE definition of each result/option shape.
5
5
  * Pure type surface — no runtime code.
6
6
  */
7
7
  import type httpNode from "isomorphic-git/http/node";
@@ -102,16 +102,16 @@ export interface SyncProjectOptions {
102
102
  retry?: SyncRetryOptions;
103
103
  /**
104
104
  * Optional path to a log file for debugging sync/recovery operations.
105
- * When set, each step (snapshot, fetch, merge, push, conflict) is appended
106
- * as a timestamped line. Never logs secrets.
105
+ * When set, each stage and terminal outcome is appended with a run ID,
106
+ * runtime version, timing, and sanitized error details. Never logs credentials.
107
107
  */
108
108
  logFile?: string;
109
109
  }
110
110
  /** Bounded retry policy for the sync race loop (BUG 6). */
111
111
  export interface SyncRetryOptions {
112
- /** Max pull→push passes before giving up. Clamped to ≥ 1. Default 3. */
112
+ /** Max pull→push passes before giving up. Finite integer clamped to 1–10; non-finite values use default 3. */
113
113
  attempts?: number;
114
- /** Delay between passes, in ms. Clamped to ≥ 0. Default 150. */
114
+ /** Delay between passes, in ms. Clamped to 0–30,000; non-finite values use default 150. */
115
115
  backoffMs?: number;
116
116
  /** Injectable delay (tests only); defaults to a real timer. */
117
117
  sleep?: (ms: number) => Promise<void>;
@@ -1,21 +1,21 @@
1
1
  import {
2
2
  MANIFEST_FILENAMES,
3
3
  runLint
4
- } from "./cli-58np1xae.js";
4
+ } from "./cli-q696mp75.js";
5
5
  import {
6
6
  log
7
- } from "./cli-r2hsreyr.js";
7
+ } from "./cli-jp97snns.js";
8
8
  import {
9
9
  UsageError,
10
10
  rejectExtraPositionals,
11
11
  rejectUnknownFlags
12
- } from "./cli-en9tew8h.js";
12
+ } from "./cli-cghcd60p.js";
13
13
  import"./cli-sekhwex4.js";
14
14
  import"./cli-c41yr7he.js";
15
15
  import {
16
16
  EXIT_CODES
17
17
  } from "./cli-46ycxe6r.js";
18
- import"./cli-4mtf9ajw.js";
18
+ import"./cli-c9h2bk2k.js";
19
19
  import"./cli-v5mp7a6q.js";
20
20
  import"./cli-37x76zdn.js";
21
21
 
@@ -7,15 +7,15 @@ import {
7
7
  TARGET_IDS,
8
8
  scaffoldExtension,
9
9
  scaffoldProject
10
- } from "./cli-58np1xae.js";
10
+ } from "./cli-q696mp75.js";
11
11
  import {
12
12
  resolveGhostscript
13
- } from "./cli-r2hsreyr.js";
13
+ } from "./cli-jp97snns.js";
14
14
  import {
15
15
  UsageError,
16
16
  rejectExtraPositionals,
17
17
  rejectUnknownFlags
18
- } from "./cli-en9tew8h.js";
18
+ } from "./cli-cghcd60p.js";
19
19
  import"./cli-sekhwex4.js";
20
20
  import {
21
21
  isToolAvailable
@@ -23,7 +23,7 @@ import {
23
23
  import {
24
24
  EXIT_CODES
25
25
  } from "./cli-46ycxe6r.js";
26
- import"./cli-4mtf9ajw.js";
26
+ import"./cli-c9h2bk2k.js";
27
27
  import"./cli-v5mp7a6q.js";
28
28
  import"./cli-37x76zdn.js";
29
29
 
@@ -2,21 +2,21 @@ import {
2
2
  executeValidation,
3
3
  publishTargetFor,
4
4
  reportMissingTools
5
- } from "./cli-58np1xae.js";
5
+ } from "./cli-q696mp75.js";
6
6
  import {
7
7
  log
8
- } from "./cli-r2hsreyr.js";
8
+ } from "./cli-jp97snns.js";
9
9
  import {
10
10
  UsageError,
11
11
  rejectExtraPositionals,
12
12
  rejectUnknownFlags
13
- } from "./cli-en9tew8h.js";
13
+ } from "./cli-cghcd60p.js";
14
14
  import"./cli-sekhwex4.js";
15
15
  import"./cli-c41yr7he.js";
16
16
  import {
17
17
  EXIT_CODES
18
18
  } from "./cli-46ycxe6r.js";
19
- import"./cli-4mtf9ajw.js";
19
+ import"./cli-c9h2bk2k.js";
20
20
  import"./cli-v5mp7a6q.js";
21
21
  import"./cli-37x76zdn.js";
22
22
 
@@ -6,10 +6,10 @@ import {
6
6
  runBuild,
7
7
  splitOutPath,
8
8
  startPreviewServer
9
- } from "./cli-58np1xae.js";
9
+ } from "./cli-q696mp75.js";
10
10
  import {
11
11
  log
12
- } from "./cli-r2hsreyr.js";
12
+ } from "./cli-jp97snns.js";
13
13
  import {
14
14
  UsageError,
15
15
  parseFormat,
@@ -17,13 +17,13 @@ import {
17
17
  rejectExtraPositionals,
18
18
  rejectUnknownFlags,
19
19
  resolvePort
20
- } from "./cli-en9tew8h.js";
20
+ } from "./cli-cghcd60p.js";
21
21
  import"./cli-sekhwex4.js";
22
22
  import"./cli-c41yr7he.js";
23
23
  import {
24
24
  BuildError
25
25
  } from "./cli-46ycxe6r.js";
26
- import"./cli-4mtf9ajw.js";
26
+ import"./cli-c9h2bk2k.js";
27
27
  import"./cli-v5mp7a6q.js";
28
28
  import"./cli-37x76zdn.js";
29
29
 
@@ -11,22 +11,22 @@ import {
11
11
  publishProviderFor,
12
12
  resolvePublishFormat,
13
13
  runPublish
14
- } from "./cli-58np1xae.js";
14
+ } from "./cli-q696mp75.js";
15
15
  import {
16
16
  FileTokenStore,
17
17
  log
18
- } from "./cli-r2hsreyr.js";
18
+ } from "./cli-jp97snns.js";
19
19
  import {
20
20
  UsageError,
21
21
  rejectExtraPositionals,
22
22
  rejectUnknownFlags
23
- } from "./cli-en9tew8h.js";
23
+ } from "./cli-cghcd60p.js";
24
24
  import"./cli-sekhwex4.js";
25
25
  import"./cli-c41yr7he.js";
26
26
  import {
27
27
  EXIT_CODES
28
28
  } from "./cli-46ycxe6r.js";
29
- import"./cli-4mtf9ajw.js";
29
+ import"./cli-c9h2bk2k.js";
30
30
  import"./cli-v5mp7a6q.js";
31
31
  import"./cli-37x76zdn.js";
32
32
 
@@ -19,7 +19,7 @@ import {
19
19
  stageChanges,
20
20
  switchBranch,
21
21
  withRepoLock
22
- } from "./cli-4mtf9ajw.js";
22
+ } from "./cli-c9h2bk2k.js";
23
23
  import"./cli-v5mp7a6q.js";
24
24
  import"./cli-37x76zdn.js";
25
25
  export {
@@ -19,7 +19,7 @@ import {
19
19
  stageChanges,
20
20
  switchBranch,
21
21
  withRepoLock
22
- } from "./index-y01v2fe0.js";
22
+ } from "./index-c5ev4r1y.js";
23
23
  import"./index-v5mp7a6q.js";
24
24
  import"./index-37x76zdn.js";
25
25
  export {
@@ -1,20 +1,20 @@
1
1
  import {
2
2
  executeAndReport
3
- } from "./cli-58np1xae.js";
3
+ } from "./cli-q696mp75.js";
4
4
  import {
5
5
  log
6
- } from "./cli-r2hsreyr.js";
6
+ } from "./cli-jp97snns.js";
7
7
  import {
8
8
  UsageError,
9
9
  rejectExtraPositionals,
10
10
  rejectUnknownFlags
11
- } from "./cli-en9tew8h.js";
11
+ } from "./cli-cghcd60p.js";
12
12
  import"./cli-sekhwex4.js";
13
13
  import"./cli-c41yr7he.js";
14
14
  import {
15
15
  EXIT_CODES
16
16
  } from "./cli-46ycxe6r.js";
17
- import"./cli-4mtf9ajw.js";
17
+ import"./cli-c9h2bk2k.js";
18
18
  import"./cli-v5mp7a6q.js";
19
19
  import"./cli-37x76zdn.js";
20
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gutterpress",
3
- "version": "0.11.4",
3
+ "version": "0.11.6-alpha.1",
4
4
  "description": "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
5
5
  "author": "itlackey",
6
6
  "license": "MPL-2.0",