@karmaniverous/jeeves-watcher 0.19.0-1 → 0.19.0-3

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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createConfigApplyHandler, createConfigQueryHandler as createConfigQueryHandler$1, createStatusHandler, DEFAULT_PORTS, getBindAddress } from '@karmaniverous/jeeves';
3
- import { n as normalizeSlashes, a as normalizeError, e as execFileAsync, G as GIT_TIMEOUT_STANDARD, b as GIT_TIMEOUT_CHERRY_PICK, c as buildAuthenticatedPushUrl, d as GIT_TIMEOUT_PUSH, g as getExecErrorFields, r as retry, f as gitAddViaStdin, h as normalizePathCase, i as findRootForPath, j as GitignoreFilter, k as executeReindex, V as VALID_REINDEX_SCOPES, m as mergeInferenceRules, l as isPathWatched, o as getWatchRootBases, C as CONFIG_WATCH_VALID_SCOPES, s as sleep, p as loadConfig } from './index.js';
4
- export { q as startFromConfig } from './index.js';
3
+ import { n as normalizeSlashes, a as normalizeError, e as execFileAsync, G as GIT_TIMEOUT_STANDARD, b as GIT_TIMEOUT_CHERRY_PICK, c as GIT_TIMEOUT_PUSH, g as getExecErrorFields, r as retry, d as gitAddViaStdin, f as normalizePathCase, h as findRootForPath, i as GitignoreFilter, j as executeReindex, V as VALID_REINDEX_SCOPES, m as mergeInferenceRules, k as isPathWatched, l as getWatchRootBases, C as CONFIG_WATCH_VALID_SCOPES, s as sleep, o as loadConfig } from './index.js';
4
+ export { p as startFromConfig } from './index.js';
5
5
  import { mkdirSync, existsSync, readFileSync, writeFileSync, rmSync, renameSync, readdirSync, statSync } from 'node:fs';
6
6
  import { join, dirname, resolve, extname, basename, sep, isAbsolute, relative } from 'node:path';
7
7
  import { DatabaseSync } from 'node:sqlite';
@@ -1064,6 +1064,206 @@ class CommitMessageBuilder {
1064
1064
  }
1065
1065
  }
1066
1066
 
1067
+ /**
1068
+ * @module vcs/gitNetwork
1069
+ * Non-interactive, credential-safe execution of git commands that touch the
1070
+ * network.
1071
+ *
1072
+ * A background service must never wait on a human. `GIT_TERMINAL_PROMPT=0`
1073
+ * alone is not enough: credential helpers configured at system, global, or
1074
+ * local level (notably Git Credential Manager, the Git for Windows default)
1075
+ * ignore it and block waiting for an interactive sign-in when the remote
1076
+ * rejects the credentials. Every git network call in the service (currently
1077
+ * push and squash force push) therefore goes through {@link execGitNetwork}.
1078
+ *
1079
+ * An access token is never placed on the command line (Node's exec errors
1080
+ * echo the full command, and those errors reach logs and the API). It is
1081
+ * passed to git as an HTTP `Authorization` header through
1082
+ * `GIT_CONFIG_*` environment variables, for that one invocation only. As
1083
+ * defence in depth, every error is passed through {@link sanitizeGitError}
1084
+ * before it leaves this module.
1085
+ */
1086
+ /** Leading git arguments that disable the askpass prompt path. */
1087
+ const NO_ASKPASS_ARGS = ['-c', 'core.askPass='];
1088
+ /**
1089
+ * Leading git arguments that discard every configured credential helper.
1090
+ *
1091
+ * An empty `credential.helper` value resets the helper list for this one
1092
+ * invocation, discarding helpers from system, global, and local config
1093
+ * (command-line config is read last).
1094
+ */
1095
+ const NO_CREDENTIAL_HELPER_ARGS = [
1096
+ '-c',
1097
+ 'credential.helper=',
1098
+ ];
1099
+ /** HTTP Basic user name paired with a token (accepted by GitHub). */
1100
+ const TOKEN_USER = 'x-access-token';
1101
+ /** Replacement for redacted secrets. */
1102
+ const REDACTED = '***';
1103
+ /**
1104
+ * Build the leading git arguments for a network call.
1105
+ *
1106
+ * @param clearCredentialHelpers - Discard configured credential helpers. Use
1107
+ * when an injected token is the credential; otherwise keep helpers so
1108
+ * stored credentials still work.
1109
+ * @returns Arguments to place before the git subcommand.
1110
+ */
1111
+ function buildGitNetworkArgs(clearCredentialHelpers) {
1112
+ return clearCredentialHelpers
1113
+ ? [...NO_ASKPASS_ARGS, ...NO_CREDENTIAL_HELPER_ARGS]
1114
+ : [...NO_ASKPASS_ARGS];
1115
+ }
1116
+ /**
1117
+ * Build the environment for a git network call.
1118
+ *
1119
+ * Config entries are appended via `GIT_CONFIG_COUNT` /
1120
+ * `GIT_CONFIG_KEY_<n>` / `GIT_CONFIG_VALUE_<n>` (after any entries
1121
+ * already in `baseEnv`), which keeps their values off the command line.
1122
+ *
1123
+ * @param baseEnv - Environment to extend (default: `process.env`).
1124
+ * @param config - Git config entries for this invocation only.
1125
+ * @returns A copy of `baseEnv` with every interactive prompt path disabled.
1126
+ */
1127
+ function buildGitNetworkEnv(baseEnv = process.env, config = []) {
1128
+ const env = {
1129
+ ...baseEnv,
1130
+ // Never prompt on the terminal.
1131
+ GIT_TERMINAL_PROMPT: '0',
1132
+ // Git Credential Manager: fail instead of showing sign-in UI.
1133
+ GCM_INTERACTIVE: 'never',
1134
+ // Empty askpass programs are treated as unset, so no dialog is spawned.
1135
+ GIT_ASKPASS: '',
1136
+ SSH_ASKPASS: '',
1137
+ };
1138
+ if (config.length === 0)
1139
+ return env;
1140
+ const existing = Number.parseInt(baseEnv.GIT_CONFIG_COUNT ?? '', 10);
1141
+ const offset = Number.isInteger(existing) && existing > 0 ? existing : 0;
1142
+ config.forEach(([key, value], i) => {
1143
+ env[`GIT_CONFIG_KEY_${String(offset + i)}`] = key;
1144
+ env[`GIT_CONFIG_VALUE_${String(offset + i)}`] = value;
1145
+ });
1146
+ env.GIT_CONFIG_COUNT = String(offset + config.length);
1147
+ return env;
1148
+ }
1149
+ /**
1150
+ * Build the HTTP header value that authenticates a token.
1151
+ *
1152
+ * @param accessToken - The token.
1153
+ * @returns `Authorization: Basic <base64(x-access-token:token)>`.
1154
+ */
1155
+ function buildAuthHeader(accessToken) {
1156
+ const basic = Buffer.from(`${TOKEN_USER}:${accessToken}`).toString('base64');
1157
+ return `Authorization: Basic ${basic}`;
1158
+ }
1159
+ /**
1160
+ * Build the git config entry that sends a token to one remote URL.
1161
+ *
1162
+ * Scoping the header to `http.<url>.extraHeader` keeps it from being sent
1163
+ * to any other URL (e.g. a redirect target).
1164
+ *
1165
+ * @param remoteUrl - Remote URL the header applies to.
1166
+ * @param accessToken - The token.
1167
+ * @returns The config entry.
1168
+ */
1169
+ function buildAuthHeaderConfig(remoteUrl, accessToken) {
1170
+ return [`http.${remoteUrl}.extraHeader`, buildAuthHeader(accessToken)];
1171
+ }
1172
+ /**
1173
+ * Replace every known encoding of a secret in a string.
1174
+ *
1175
+ * Covers the raw value, its URL encoding, its base64 encoding, and the base64
1176
+ * Basic credential sent in the auth header.
1177
+ *
1178
+ * @param text - Text to redact.
1179
+ * @param secret - Secret to remove; no-op when empty or undefined.
1180
+ * @returns The redacted text.
1181
+ */
1182
+ function redactSecret(text, secret) {
1183
+ if (!secret)
1184
+ return text;
1185
+ const forms = [
1186
+ Buffer.from(`${TOKEN_USER}:${secret}`).toString('base64'),
1187
+ Buffer.from(secret).toString('base64'),
1188
+ encodeURIComponent(secret),
1189
+ secret,
1190
+ ].sort((a, b) => b.length - a.length);
1191
+ return forms.reduce((out, form) => out.split(form).join(REDACTED), text);
1192
+ }
1193
+ /**
1194
+ * Strip a secret from a thrown git error before it is logged or stored.
1195
+ *
1196
+ * Returns a new `Error` whose message, stack, and string properties
1197
+ * (`cmd`, `stderr`, `stdout`, ...) are redacted; non-string properties
1198
+ * other than the cause are copied as-is. The original error (and its cause) is
1199
+ * not referenced.
1200
+ *
1201
+ * @param error - The caught value.
1202
+ * @param secret - Secret to remove.
1203
+ * @returns A redacted error.
1204
+ */
1205
+ function sanitizeGitError(error, secret) {
1206
+ if (!(error instanceof Error)) {
1207
+ return new Error(redactSecret(String(error), secret));
1208
+ }
1209
+ const clean = new Error(redactSecret(error.message, secret));
1210
+ clean.name = error.name;
1211
+ clean.stack = redactSecret(error.stack ?? '', secret);
1212
+ const target = clean;
1213
+ for (const [key, value] of Object.entries(error)) {
1214
+ if (key === 'cause')
1215
+ continue;
1216
+ target[key] =
1217
+ typeof value === 'string' ? redactSecret(value, secret) : value;
1218
+ }
1219
+ return clean;
1220
+ }
1221
+ /**
1222
+ * Run a git network command non-interactively.
1223
+ *
1224
+ * @param args - Git arguments, e.g. `['push', url, 'HEAD']`.
1225
+ * @param options - Working directory, timeout, credential policy, and secrets.
1226
+ * @returns The child's stdout and stderr.
1227
+ * @throws A {@link sanitizeGitError | sanitized} error on failure.
1228
+ */
1229
+ async function execGitNetwork(args, options) {
1230
+ try {
1231
+ return await execFileAsync('git', [...buildGitNetworkArgs(options.clearCredentialHelpers), ...args], {
1232
+ cwd: options.cwd,
1233
+ env: buildGitNetworkEnv(process.env, options.config),
1234
+ timeout: options.timeout,
1235
+ });
1236
+ }
1237
+ catch (error) {
1238
+ throw sanitizeGitError(error, options.secret);
1239
+ }
1240
+ }
1241
+ /**
1242
+ * Push `HEAD` to a remote non-interactively.
1243
+ *
1244
+ * For an HTTPS remote with a token, the token is sent as an auth header
1245
+ * (never in the URL) and configured credential helpers are discarded, since
1246
+ * the token is the credential. Otherwise helpers are kept so stored
1247
+ * credentials still work.
1248
+ *
1249
+ * @param options - Push target and behaviour.
1250
+ * @throws A sanitized error (token redacted) on failure.
1251
+ */
1252
+ async function gitPushNonInteractive(options) {
1253
+ const { remoteUrl, accessToken } = options;
1254
+ const token = accessToken && /^https:\/\//i.test(remoteUrl) ? accessToken : undefined;
1255
+ const args = options.force
1256
+ ? ['push', '--force', remoteUrl, 'HEAD']
1257
+ : ['push', remoteUrl, 'HEAD'];
1258
+ await execGitNetwork(args, {
1259
+ cwd: options.cwd,
1260
+ timeout: options.timeout,
1261
+ clearCredentialHelpers: token !== undefined,
1262
+ config: token ? [buildAuthHeaderConfig(remoteUrl, token)] : [],
1263
+ secret: accessToken,
1264
+ });
1265
+ }
1266
+
1067
1267
  /**
1068
1268
  * @module vcs/SquashManager
1069
1269
  * Handles scheduled squash retention for git-backed VCS roots.
@@ -1365,10 +1565,11 @@ class SquashManager {
1365
1565
  if (!this.remoteUrl)
1366
1566
  return;
1367
1567
  try {
1368
- const pushUrl = buildAuthenticatedPushUrl(this.remoteUrl, this.accessToken);
1369
- await execFileAsync('git', ['push', '--force', pushUrl, 'HEAD'], {
1568
+ await gitPushNonInteractive({
1370
1569
  cwd: this.rootPath,
1371
- env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
1570
+ remoteUrl: this.remoteUrl,
1571
+ accessToken: this.accessToken,
1572
+ force: true,
1372
1573
  timeout: GIT_TIMEOUT_PUSH,
1373
1574
  });
1374
1575
  this.logger.info({ root: this.rootPath, remote: this.remoteUrl }, 'Squash force push succeeded');
@@ -1527,10 +1728,10 @@ async function pushToRemote(rootPath, remoteUrl, accessToken, pushErrors, logger
1527
1728
  if (!remoteUrl)
1528
1729
  return undefined;
1529
1730
  try {
1530
- const pushUrl = buildAuthenticatedPushUrl(remoteUrl, accessToken);
1531
- await execFileAsync('git', ['push', pushUrl, 'HEAD'], {
1731
+ await gitPushNonInteractive({
1532
1732
  cwd: rootPath,
1533
- env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
1733
+ remoteUrl,
1734
+ accessToken,
1534
1735
  timeout: GIT_TIMEOUT_PUSH,
1535
1736
  });
1536
1737
  const timestamp = new Date().toISOString();
@@ -265,7 +265,7 @@ async function startFromConfig(configPath, descriptor) {
265
265
  const config = await loadConfig(resolvedPath);
266
266
  // Dynamic import breaks the circular dependency between this module
267
267
  // and app/index.ts (which defines JeevesWatcher and re-exports startFromConfig).
268
- const { JeevesWatcher } = await import('./index-BVfdWEhb.js');
268
+ const { JeevesWatcher } = await import('./index-CD0v-oGg.js');
269
269
  const app = new JeevesWatcher(config, resolvedPath, descriptor);
270
270
  installShutdownHandlers(() => app.stop());
271
271
  await app.start();
@@ -680,19 +680,6 @@ function getExecErrorFields(error) {
680
680
  const stdout = typeof rec['stdout'] === 'string' ? rec['stdout'] : '';
681
681
  return { message: error.message || '', stderr, stdout };
682
682
  }
683
- /**
684
- * Build an authenticated push URL by injecting a token into an HTTPS remote.
685
- * Non-HTTPS URLs are returned as-is.
686
- *
687
- * @param remoteUrl - The remote repository URL.
688
- * @param accessToken - Optional access token to inject.
689
- * @returns The URL with the token injected if applicable.
690
- */
691
- function buildAuthenticatedPushUrl(remoteUrl, accessToken) {
692
- if (!accessToken)
693
- return remoteUrl;
694
- return remoteUrl.replace(/^https:\/\//, `https://${encodeURIComponent(accessToken)}@`);
695
- }
696
683
  /**
697
684
  * Stage files via stdin to avoid ENAMETOOLONG on Windows.
698
685
  *
@@ -1652,4 +1639,4 @@ const watcherDescriptor = {
1652
1639
  const program = createServiceCli(watcherDescriptor);
1653
1640
  program.parse();
1654
1641
 
1655
- export { CONFIG_WATCH_VALID_SCOPES as C, GIT_TIMEOUT_STANDARD as G, VALID_REINDEX_SCOPES as V, normalizeError as a, GIT_TIMEOUT_CHERRY_PICK as b, buildAuthenticatedPushUrl as c, GIT_TIMEOUT_PUSH as d, execFileAsync as e, gitAddViaStdin as f, getExecErrorFields as g, normalizePathCase as h, findRootForPath as i, GitignoreFilter as j, executeReindex as k, isPathWatched as l, mergeInferenceRules as m, normalizeSlashes as n, getWatchRootBases as o, loadConfig as p, startFromConfig as q, retry as r, sleep as s };
1642
+ export { CONFIG_WATCH_VALID_SCOPES as C, GIT_TIMEOUT_STANDARD as G, VALID_REINDEX_SCOPES as V, normalizeError as a, GIT_TIMEOUT_CHERRY_PICK as b, GIT_TIMEOUT_PUSH as c, gitAddViaStdin as d, execFileAsync as e, normalizePathCase as f, getExecErrorFields as g, findRootForPath as h, GitignoreFilter as i, executeReindex as j, isPathWatched as k, getWatchRootBases as l, mergeInferenceRules as m, normalizeSlashes as n, loadConfig as o, startFromConfig as p, retry as r, sleep as s };
package/dist/index.js CHANGED
@@ -447,19 +447,6 @@ function getExecErrorFields(error) {
447
447
  const stdout = typeof rec['stdout'] === 'string' ? rec['stdout'] : '';
448
448
  return { message: error.message || '', stderr, stdout };
449
449
  }
450
- /**
451
- * Build an authenticated push URL by injecting a token into an HTTPS remote.
452
- * Non-HTTPS URLs are returned as-is.
453
- *
454
- * @param remoteUrl - The remote repository URL.
455
- * @param accessToken - Optional access token to inject.
456
- * @returns The URL with the token injected if applicable.
457
- */
458
- function buildAuthenticatedPushUrl(remoteUrl, accessToken) {
459
- if (!accessToken)
460
- return remoteUrl;
461
- return remoteUrl.replace(/^https:\/\//, `https://${encodeURIComponent(accessToken)}@`);
462
- }
463
450
  /**
464
451
  * Stage files via stdin to avoid ENAMETOOLONG on Windows.
465
452
  *
@@ -4455,6 +4442,206 @@ class CommitMessageGenerator {
4455
4442
  }
4456
4443
  }
4457
4444
 
4445
+ /**
4446
+ * @module vcs/gitNetwork
4447
+ * Non-interactive, credential-safe execution of git commands that touch the
4448
+ * network.
4449
+ *
4450
+ * A background service must never wait on a human. `GIT_TERMINAL_PROMPT=0`
4451
+ * alone is not enough: credential helpers configured at system, global, or
4452
+ * local level (notably Git Credential Manager, the Git for Windows default)
4453
+ * ignore it and block waiting for an interactive sign-in when the remote
4454
+ * rejects the credentials. Every git network call in the service (currently
4455
+ * push and squash force push) therefore goes through {@link execGitNetwork}.
4456
+ *
4457
+ * An access token is never placed on the command line (Node's exec errors
4458
+ * echo the full command, and those errors reach logs and the API). It is
4459
+ * passed to git as an HTTP `Authorization` header through
4460
+ * `GIT_CONFIG_*` environment variables, for that one invocation only. As
4461
+ * defence in depth, every error is passed through {@link sanitizeGitError}
4462
+ * before it leaves this module.
4463
+ */
4464
+ /** Leading git arguments that disable the askpass prompt path. */
4465
+ const NO_ASKPASS_ARGS = ['-c', 'core.askPass='];
4466
+ /**
4467
+ * Leading git arguments that discard every configured credential helper.
4468
+ *
4469
+ * An empty `credential.helper` value resets the helper list for this one
4470
+ * invocation, discarding helpers from system, global, and local config
4471
+ * (command-line config is read last).
4472
+ */
4473
+ const NO_CREDENTIAL_HELPER_ARGS = [
4474
+ '-c',
4475
+ 'credential.helper=',
4476
+ ];
4477
+ /** HTTP Basic user name paired with a token (accepted by GitHub). */
4478
+ const TOKEN_USER = 'x-access-token';
4479
+ /** Replacement for redacted secrets. */
4480
+ const REDACTED = '***';
4481
+ /**
4482
+ * Build the leading git arguments for a network call.
4483
+ *
4484
+ * @param clearCredentialHelpers - Discard configured credential helpers. Use
4485
+ * when an injected token is the credential; otherwise keep helpers so
4486
+ * stored credentials still work.
4487
+ * @returns Arguments to place before the git subcommand.
4488
+ */
4489
+ function buildGitNetworkArgs(clearCredentialHelpers) {
4490
+ return clearCredentialHelpers
4491
+ ? [...NO_ASKPASS_ARGS, ...NO_CREDENTIAL_HELPER_ARGS]
4492
+ : [...NO_ASKPASS_ARGS];
4493
+ }
4494
+ /**
4495
+ * Build the environment for a git network call.
4496
+ *
4497
+ * Config entries are appended via `GIT_CONFIG_COUNT` /
4498
+ * `GIT_CONFIG_KEY_<n>` / `GIT_CONFIG_VALUE_<n>` (after any entries
4499
+ * already in `baseEnv`), which keeps their values off the command line.
4500
+ *
4501
+ * @param baseEnv - Environment to extend (default: `process.env`).
4502
+ * @param config - Git config entries for this invocation only.
4503
+ * @returns A copy of `baseEnv` with every interactive prompt path disabled.
4504
+ */
4505
+ function buildGitNetworkEnv(baseEnv = process.env, config = []) {
4506
+ const env = {
4507
+ ...baseEnv,
4508
+ // Never prompt on the terminal.
4509
+ GIT_TERMINAL_PROMPT: '0',
4510
+ // Git Credential Manager: fail instead of showing sign-in UI.
4511
+ GCM_INTERACTIVE: 'never',
4512
+ // Empty askpass programs are treated as unset, so no dialog is spawned.
4513
+ GIT_ASKPASS: '',
4514
+ SSH_ASKPASS: '',
4515
+ };
4516
+ if (config.length === 0)
4517
+ return env;
4518
+ const existing = Number.parseInt(baseEnv.GIT_CONFIG_COUNT ?? '', 10);
4519
+ const offset = Number.isInteger(existing) && existing > 0 ? existing : 0;
4520
+ config.forEach(([key, value], i) => {
4521
+ env[`GIT_CONFIG_KEY_${String(offset + i)}`] = key;
4522
+ env[`GIT_CONFIG_VALUE_${String(offset + i)}`] = value;
4523
+ });
4524
+ env.GIT_CONFIG_COUNT = String(offset + config.length);
4525
+ return env;
4526
+ }
4527
+ /**
4528
+ * Build the HTTP header value that authenticates a token.
4529
+ *
4530
+ * @param accessToken - The token.
4531
+ * @returns `Authorization: Basic <base64(x-access-token:token)>`.
4532
+ */
4533
+ function buildAuthHeader(accessToken) {
4534
+ const basic = Buffer.from(`${TOKEN_USER}:${accessToken}`).toString('base64');
4535
+ return `Authorization: Basic ${basic}`;
4536
+ }
4537
+ /**
4538
+ * Build the git config entry that sends a token to one remote URL.
4539
+ *
4540
+ * Scoping the header to `http.<url>.extraHeader` keeps it from being sent
4541
+ * to any other URL (e.g. a redirect target).
4542
+ *
4543
+ * @param remoteUrl - Remote URL the header applies to.
4544
+ * @param accessToken - The token.
4545
+ * @returns The config entry.
4546
+ */
4547
+ function buildAuthHeaderConfig(remoteUrl, accessToken) {
4548
+ return [`http.${remoteUrl}.extraHeader`, buildAuthHeader(accessToken)];
4549
+ }
4550
+ /**
4551
+ * Replace every known encoding of a secret in a string.
4552
+ *
4553
+ * Covers the raw value, its URL encoding, its base64 encoding, and the base64
4554
+ * Basic credential sent in the auth header.
4555
+ *
4556
+ * @param text - Text to redact.
4557
+ * @param secret - Secret to remove; no-op when empty or undefined.
4558
+ * @returns The redacted text.
4559
+ */
4560
+ function redactSecret(text, secret) {
4561
+ if (!secret)
4562
+ return text;
4563
+ const forms = [
4564
+ Buffer.from(`${TOKEN_USER}:${secret}`).toString('base64'),
4565
+ Buffer.from(secret).toString('base64'),
4566
+ encodeURIComponent(secret),
4567
+ secret,
4568
+ ].sort((a, b) => b.length - a.length);
4569
+ return forms.reduce((out, form) => out.split(form).join(REDACTED), text);
4570
+ }
4571
+ /**
4572
+ * Strip a secret from a thrown git error before it is logged or stored.
4573
+ *
4574
+ * Returns a new `Error` whose message, stack, and string properties
4575
+ * (`cmd`, `stderr`, `stdout`, ...) are redacted; non-string properties
4576
+ * other than the cause are copied as-is. The original error (and its cause) is
4577
+ * not referenced.
4578
+ *
4579
+ * @param error - The caught value.
4580
+ * @param secret - Secret to remove.
4581
+ * @returns A redacted error.
4582
+ */
4583
+ function sanitizeGitError(error, secret) {
4584
+ if (!(error instanceof Error)) {
4585
+ return new Error(redactSecret(String(error), secret));
4586
+ }
4587
+ const clean = new Error(redactSecret(error.message, secret));
4588
+ clean.name = error.name;
4589
+ clean.stack = redactSecret(error.stack ?? '', secret);
4590
+ const target = clean;
4591
+ for (const [key, value] of Object.entries(error)) {
4592
+ if (key === 'cause')
4593
+ continue;
4594
+ target[key] =
4595
+ typeof value === 'string' ? redactSecret(value, secret) : value;
4596
+ }
4597
+ return clean;
4598
+ }
4599
+ /**
4600
+ * Run a git network command non-interactively.
4601
+ *
4602
+ * @param args - Git arguments, e.g. `['push', url, 'HEAD']`.
4603
+ * @param options - Working directory, timeout, credential policy, and secrets.
4604
+ * @returns The child's stdout and stderr.
4605
+ * @throws A {@link sanitizeGitError | sanitized} error on failure.
4606
+ */
4607
+ async function execGitNetwork(args, options) {
4608
+ try {
4609
+ return await execFileAsync('git', [...buildGitNetworkArgs(options.clearCredentialHelpers), ...args], {
4610
+ cwd: options.cwd,
4611
+ env: buildGitNetworkEnv(process.env, options.config),
4612
+ timeout: options.timeout,
4613
+ });
4614
+ }
4615
+ catch (error) {
4616
+ throw sanitizeGitError(error, options.secret);
4617
+ }
4618
+ }
4619
+ /**
4620
+ * Push `HEAD` to a remote non-interactively.
4621
+ *
4622
+ * For an HTTPS remote with a token, the token is sent as an auth header
4623
+ * (never in the URL) and configured credential helpers are discarded, since
4624
+ * the token is the credential. Otherwise helpers are kept so stored
4625
+ * credentials still work.
4626
+ *
4627
+ * @param options - Push target and behaviour.
4628
+ * @throws A sanitized error (token redacted) on failure.
4629
+ */
4630
+ async function gitPushNonInteractive(options) {
4631
+ const { remoteUrl, accessToken } = options;
4632
+ const token = accessToken && /^https:\/\//i.test(remoteUrl) ? accessToken : undefined;
4633
+ const args = options.force
4634
+ ? ['push', '--force', remoteUrl, 'HEAD']
4635
+ : ['push', remoteUrl, 'HEAD'];
4636
+ await execGitNetwork(args, {
4637
+ cwd: options.cwd,
4638
+ timeout: options.timeout,
4639
+ clearCredentialHelpers: token !== undefined,
4640
+ config: token ? [buildAuthHeaderConfig(remoteUrl, token)] : [],
4641
+ secret: accessToken,
4642
+ });
4643
+ }
4644
+
4458
4645
  /**
4459
4646
  * @module vcs/SquashManager
4460
4647
  * Handles scheduled squash retention for git-backed VCS roots.
@@ -4756,10 +4943,11 @@ class SquashManager {
4756
4943
  if (!this.remoteUrl)
4757
4944
  return;
4758
4945
  try {
4759
- const pushUrl = buildAuthenticatedPushUrl(this.remoteUrl, this.accessToken);
4760
- await execFileAsync('git', ['push', '--force', pushUrl, 'HEAD'], {
4946
+ await gitPushNonInteractive({
4761
4947
  cwd: this.rootPath,
4762
- env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
4948
+ remoteUrl: this.remoteUrl,
4949
+ accessToken: this.accessToken,
4950
+ force: true,
4763
4951
  timeout: GIT_TIMEOUT_PUSH,
4764
4952
  });
4765
4953
  this.logger.info({ root: this.rootPath, remote: this.remoteUrl }, 'Squash force push succeeded');
@@ -5184,10 +5372,10 @@ async function pushToRemote(rootPath, remoteUrl, accessToken, pushErrors, logger
5184
5372
  if (!remoteUrl)
5185
5373
  return undefined;
5186
5374
  try {
5187
- const pushUrl = buildAuthenticatedPushUrl(remoteUrl, accessToken);
5188
- await execFileAsync('git', ['push', pushUrl, 'HEAD'], {
5375
+ await gitPushNonInteractive({
5189
5376
  cwd: rootPath,
5190
- env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
5377
+ remoteUrl,
5378
+ accessToken,
5191
5379
  timeout: GIT_TIMEOUT_PUSH,
5192
5380
  });
5193
5381
  const timestamp = new Date().toISOString();
package/package.json CHANGED
@@ -8,8 +8,8 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "@commander-js/extra-typings": "^15.0.0",
11
- "@karmaniverous/jeeves": "0.6.0-7",
12
- "@karmaniverous/jeeves-watcher-core": "^0.2.6-0",
11
+ "@karmaniverous/jeeves": "0.6.0-9",
12
+ "@karmaniverous/jeeves-watcher-core": "^0.2.6-2",
13
13
  "@karmaniverous/jsonmap": "^2.1.1",
14
14
  "@langchain/textsplitters": "*",
15
15
  "@qdrant/js-client-rest": "^1.19.0",
@@ -150,5 +150,5 @@
150
150
  },
151
151
  "type": "module",
152
152
  "types": "dist/index.d.ts",
153
- "version": "0.19.0-1"
153
+ "version": "0.19.0-3"
154
154
  }