@karmaniverous/jeeves-watcher 0.19.0-0 → 0.19.0-2
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
|
|
4
|
-
export {
|
|
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
|
-
|
|
1369
|
-
await execFileAsync('git', ['push', '--force', pushUrl, 'HEAD'], {
|
|
1568
|
+
await gitPushNonInteractive({
|
|
1370
1569
|
cwd: this.rootPath,
|
|
1371
|
-
|
|
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
|
-
|
|
1531
|
-
await execFileAsync('git', ['push', pushUrl, 'HEAD'], {
|
|
1731
|
+
await gitPushNonInteractive({
|
|
1532
1732
|
cwd: rootPath,
|
|
1533
|
-
|
|
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-
|
|
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,
|
|
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
|
-
|
|
4760
|
-
await execFileAsync('git', ['push', '--force', pushUrl, 'HEAD'], {
|
|
4946
|
+
await gitPushNonInteractive({
|
|
4761
4947
|
cwd: this.rootPath,
|
|
4762
|
-
|
|
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
|
-
|
|
5188
|
-
await execFileAsync('git', ['push', pushUrl, 'HEAD'], {
|
|
5375
|
+
await gitPushNonInteractive({
|
|
5189
5376
|
cwd: rootPath,
|
|
5190
|
-
|
|
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-
|
|
12
|
-
"@karmaniverous/jeeves-watcher-core": "^0.2.
|
|
11
|
+
"@karmaniverous/jeeves": "0.6.0-8",
|
|
12
|
+
"@karmaniverous/jeeves-watcher-core": "^0.2.6-1",
|
|
13
13
|
"@karmaniverous/jsonmap": "^2.1.1",
|
|
14
14
|
"@langchain/textsplitters": "*",
|
|
15
15
|
"@qdrant/js-client-rest": "^1.19.0",
|
|
@@ -107,6 +107,10 @@
|
|
|
107
107
|
"release": true
|
|
108
108
|
},
|
|
109
109
|
"hooks": {
|
|
110
|
+
"after:bump": [
|
|
111
|
+
"npm install --package-lock-only --ignore-scripts",
|
|
112
|
+
"git add ../../package-lock.json"
|
|
113
|
+
],
|
|
110
114
|
"after:init": [
|
|
111
115
|
"npm run lint",
|
|
112
116
|
"npm run typecheck",
|
|
@@ -133,7 +137,7 @@
|
|
|
133
137
|
"url": "git+https://github.com/karmaniverous/jeeves-watcher.git"
|
|
134
138
|
},
|
|
135
139
|
"scripts": {
|
|
136
|
-
"build": "npm run generate:schema && rimraf dist && cross-env NO_COLOR=1 rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript",
|
|
140
|
+
"build": "npm run generate:schema && rimraf dist && cross-env NO_COLOR=1 rollup --config rollup.config.ts --configPlugin \"@rollup/plugin-typescript={paths:{}}\"",
|
|
137
141
|
"changelog": "git-cliff -o CHANGELOG.md",
|
|
138
142
|
"diagrams": "cd diagrams && plantuml -tpng -o ../assets -r .",
|
|
139
143
|
"generate:schema": "tsx src/config/generate-schema.ts",
|
|
@@ -146,5 +150,5 @@
|
|
|
146
150
|
},
|
|
147
151
|
"type": "module",
|
|
148
152
|
"types": "dist/index.d.ts",
|
|
149
|
-
"version": "0.19.0-
|
|
153
|
+
"version": "0.19.0-2"
|
|
150
154
|
}
|