dsh-git-branch-manage 1.0.8 → 1.0.9
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/lib/index.js +8 -0
- package/lib/network-tool.d.ts +111 -0
- package/lib/network-tool.js +246 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { childProcessExec } from './git.js';
|
|
|
11
11
|
import { ACCEL_DEFAULT_URL, accelerateUrlError } from './normalize.js';
|
|
12
12
|
import { handleCredentials, handleCredentialsDelete, handleCredentialsOAuthPoll, handleCredentialsOAuthStart, handleCredentialsSave, handleRemote, handleRemotes, handleAiCommitMessage, handleChanges, handleCommit, handleCommitFiles, handleCreateBranch, handleCreateTag, handleCreateWorktree, handleDeleteBranch, handleEnsureDirectory, handleFetch, handleGroupWorktrees, handleInspectWorktree, handleLog, handlePathExists, handlePurgeDirectory, handlePushTag, handleRemoveWorktree, handleRenameBranch, handleStage, handleStatus, handleSwitch, handleUpdate, handlePush, handleWorktreesAll, } from './routes.js';
|
|
13
13
|
import { validateRootDir } from './settings.js';
|
|
14
|
+
import { gitNetworkTool } from './network-tool.js';
|
|
14
15
|
import { gitCredentialsTool } from './tool.js';
|
|
15
16
|
import { ROUTE_AI_COMMIT_MESSAGE, ROUTE_BRANCH, ROUTE_BRANCH_DELETE, ROUTE_BRANCH_RENAME, ROUTE_CHANGES, ROUTE_COMMIT, ROUTE_COMMIT_FILES, ROUTE_CREDENTIALS, ROUTE_CREDENTIALS_DELETE, ROUTE_CREDENTIALS_OAUTH_POLL, ROUTE_CREDENTIALS_OAUTH_START, ROUTE_CREDENTIALS_SAVE, ROUTE_ENSURE_DIRECTORY, ROUTE_EXISTS, ROUTE_FETCH, ROUTE_GROUP, ROUTE_INSPECT, ROUTE_LOG, ROUTE_PUSH, ROUTE_PURGE, ROUTE_REMOTE, ROUTE_REMOTES, ROUTE_REMOVE, ROUTE_STAGE, ROUTE_STATUS, ROUTE_SWITCH, ROUTE_TAG, ROUTE_TAG_PUSH, ROUTE_UPDATE, ROUTE_WORKTREE, ROUTE_WORKTREES_ALL } from './wire.js';
|
|
16
17
|
export const name = 'dsh-git-branch';
|
|
@@ -163,6 +164,13 @@ export function apply(ctx, config = {}) {
|
|
|
163
164
|
ctx.inject(['tools'], (toolsCtx) => {
|
|
164
165
|
const tools = toolsCtx.tools;
|
|
165
166
|
toolsCtx.effect(() => tools.register(gitCredentialsTool({ home: () => homedir(), envHome: () => process.env.DSH_HOME })), 'git-worktree: git_credentials tool');
|
|
167
|
+
// The panel routes are the only other place that injects the stored
|
|
168
|
+
// credentials; a model running `git push` through the shell would go out
|
|
169
|
+
// as the machine's helper account instead. This tool runs one network
|
|
170
|
+
// subcommand with the same `-c http.<url>.extraHeader` prefix, so the
|
|
171
|
+
// session can authenticate as the configured account without the secret
|
|
172
|
+
// ever entering the conversation.
|
|
173
|
+
toolsCtx.effect(() => tools.register(gitNetworkTool({ home: () => homedir(), envHome: () => process.env.DSH_HOME })), 'git-worktree: git_network tool');
|
|
166
174
|
});
|
|
167
175
|
ctx.inject(['webServer'], (webCtx) => {
|
|
168
176
|
/** Send one outcome as JSON; no-store because repo facts are point-in-time. */
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model-facing `git_network` tool (host half).
|
|
3
|
+
*
|
|
4
|
+
* The credentials panel stores one token per remote host, but the panel's own
|
|
5
|
+
* fetch / pull / push routes are the only places that inject it (`-c
|
|
6
|
+
* http.<url>.extraHeader=…`). A model running `git push` through the shell
|
|
7
|
+
* therefore authenticates as whatever the machine's credential helper holds —
|
|
8
|
+
* often a different account, which is exactly the complaint this tool closes:
|
|
9
|
+
* it runs ONE git network command with the same `authConfigArgs` the routes
|
|
10
|
+
* use, so the session pushes as the account the user configured, and the
|
|
11
|
+
* secret never enters the conversation.
|
|
12
|
+
*
|
|
13
|
+
* It is deliberately narrower than a shell wrapper: a fixed subcommand
|
|
14
|
+
* allow-list (fetch / pull / push / ls-remote), no force/delete refspecs, no
|
|
15
|
+
* `--upload-pack` / `--receive-pack` / `-c` options, and an absolute
|
|
16
|
+
* `repoPath` as cwd. Those exclusions are about blast radius, not about
|
|
17
|
+
* authentication: the tool exists to let a session reach the network with the
|
|
18
|
+
* configured credential, not to become a general command runner.
|
|
19
|
+
*
|
|
20
|
+
* @module dsh-git-branch/network-tool
|
|
21
|
+
*/
|
|
22
|
+
import { type CredentialStore, type GitCredential } from './credentials.js';
|
|
23
|
+
import { type Exec } from './git.js';
|
|
24
|
+
/** Model-facing tool name. */
|
|
25
|
+
export declare const GIT_NETWORK_TOOL_NAME = "git_network";
|
|
26
|
+
/** The only git subcommands this tool will run. */
|
|
27
|
+
export declare const GIT_NETWORK_SUBCOMMANDS: readonly ["fetch", "pull", "push", "ls-remote"];
|
|
28
|
+
/** Upper bound on the argument vector (the panel's own commands need far fewer). */
|
|
29
|
+
export declare const GIT_NETWORK_MAX_ARGS = 32;
|
|
30
|
+
/** The arguments this tool accepts. */
|
|
31
|
+
export interface GitNetworkArgs {
|
|
32
|
+
/** Absolute directory inside the target repository; the command's cwd. */
|
|
33
|
+
readonly repoPath?: string;
|
|
34
|
+
/** Git arguments, subcommand first (`['push', 'origin', 'main']`). */
|
|
35
|
+
readonly args?: readonly string[];
|
|
36
|
+
}
|
|
37
|
+
/** A validated request, ready to run. */
|
|
38
|
+
export interface GitNetworkRequest {
|
|
39
|
+
readonly repoPath: string;
|
|
40
|
+
readonly args: readonly string[];
|
|
41
|
+
}
|
|
42
|
+
/** The canonical value `execute` returns. */
|
|
43
|
+
export interface GitNetworkValue {
|
|
44
|
+
/** Directory the command ran in (echoed for the transcript). */
|
|
45
|
+
readonly repoPath: string;
|
|
46
|
+
/** The git subcommand that ran. */
|
|
47
|
+
readonly subcommand: string;
|
|
48
|
+
/** git's exit status; a non-zero status is a normal result, not an error. */
|
|
49
|
+
readonly exitCode: number;
|
|
50
|
+
/** Standard output, secret-redacted and length-capped. */
|
|
51
|
+
readonly stdout: string;
|
|
52
|
+
/** Standard error, secret-redacted and length-capped. */
|
|
53
|
+
readonly stderr: string;
|
|
54
|
+
/** Hosts a credential was injected for (injected in full on every call). */
|
|
55
|
+
readonly authenticatedHosts: readonly string[];
|
|
56
|
+
/** Whether either stream was cut at the length cap. */
|
|
57
|
+
readonly truncated: boolean;
|
|
58
|
+
/** Why the credential store could not be read; the command then ran bare. */
|
|
59
|
+
readonly credentialsError?: string;
|
|
60
|
+
}
|
|
61
|
+
/** Host-side seams the tool reads through. */
|
|
62
|
+
export interface GitNetworkToolDeps {
|
|
63
|
+
/** User home directory seam. */
|
|
64
|
+
readonly home: () => string;
|
|
65
|
+
/** `$DSH_HOME` seam (blank/undefined = unset). */
|
|
66
|
+
readonly envHome: () => string | undefined;
|
|
67
|
+
/** Store seam; the fs-backed store at the resolved path when absent. */
|
|
68
|
+
readonly store?: CredentialStore;
|
|
69
|
+
/** Process seam; the real `git` over node:child_process when absent. */
|
|
70
|
+
readonly exec?: Exec;
|
|
71
|
+
}
|
|
72
|
+
/** A tool definition as `ctx.tools.register` accepts it. */
|
|
73
|
+
export interface GitNetworkTool {
|
|
74
|
+
readonly name: string;
|
|
75
|
+
readonly description: string;
|
|
76
|
+
readonly parameters: Record<string, unknown>;
|
|
77
|
+
readonly output: {
|
|
78
|
+
readonly schema: Record<string, unknown>;
|
|
79
|
+
readonly render: (args: GitNetworkArgs, value: GitNetworkValue) => readonly {
|
|
80
|
+
readonly type: 'text';
|
|
81
|
+
readonly text: string;
|
|
82
|
+
}[];
|
|
83
|
+
};
|
|
84
|
+
readonly execute: (args: GitNetworkArgs, exec?: {
|
|
85
|
+
readonly signal?: AbortSignal;
|
|
86
|
+
}) => Promise<GitNetworkValue>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Why a request cannot run (undefined = fine). Pure and exhaustive so the
|
|
90
|
+
* tests can pin every rejection without spawning git.
|
|
91
|
+
* @param repoPath - the requested cwd (validated as an absolute path).
|
|
92
|
+
* @param args - the requested git argument vector (subcommand first).
|
|
93
|
+
*/
|
|
94
|
+
export declare function gitNetworkRequestIssue(repoPath: unknown, args: unknown): string | undefined;
|
|
95
|
+
/**
|
|
96
|
+
* Remove stored secrets — raw and in the Basic header's base64 form — from
|
|
97
|
+
* git's own diagnostics. git does not echo the Authorization header, but the
|
|
98
|
+
* redaction also covers a remote URL that embeds the token, and costs one
|
|
99
|
+
* pass per stored host.
|
|
100
|
+
* @param text - one captured stream.
|
|
101
|
+
* @param entries - the credentials that were injected.
|
|
102
|
+
*/
|
|
103
|
+
export declare function redactSecrets(text: string, entries: readonly GitCredential[]): string;
|
|
104
|
+
/** Compose the model-facing text of one result. */
|
|
105
|
+
export declare function renderGitNetwork(value: GitNetworkValue): string;
|
|
106
|
+
/**
|
|
107
|
+
* Build the `git_network` tool definition.
|
|
108
|
+
* @param deps - host seams (home/env, an optional store and an optional exec).
|
|
109
|
+
* @returns a definition ready for `ctx.tools.register`.
|
|
110
|
+
*/
|
|
111
|
+
export declare function gitNetworkTool(deps: GitNetworkToolDeps): GitNetworkTool;
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model-facing `git_network` tool (host half).
|
|
3
|
+
*
|
|
4
|
+
* The credentials panel stores one token per remote host, but the panel's own
|
|
5
|
+
* fetch / pull / push routes are the only places that inject it (`-c
|
|
6
|
+
* http.<url>.extraHeader=…`). A model running `git push` through the shell
|
|
7
|
+
* therefore authenticates as whatever the machine's credential helper holds —
|
|
8
|
+
* often a different account, which is exactly the complaint this tool closes:
|
|
9
|
+
* it runs ONE git network command with the same `authConfigArgs` the routes
|
|
10
|
+
* use, so the session pushes as the account the user configured, and the
|
|
11
|
+
* secret never enters the conversation.
|
|
12
|
+
*
|
|
13
|
+
* It is deliberately narrower than a shell wrapper: a fixed subcommand
|
|
14
|
+
* allow-list (fetch / pull / push / ls-remote), no force/delete refspecs, no
|
|
15
|
+
* `--upload-pack` / `--receive-pack` / `-c` options, and an absolute
|
|
16
|
+
* `repoPath` as cwd. Those exclusions are about blast radius, not about
|
|
17
|
+
* authentication: the tool exists to let a session reach the network with the
|
|
18
|
+
* configured credential, not to become a general command runner.
|
|
19
|
+
*
|
|
20
|
+
* @module dsh-git-branch/network-tool
|
|
21
|
+
*/
|
|
22
|
+
import { isAbsolute } from 'node:path';
|
|
23
|
+
import { authConfigArgs, basicUsername, createCredentialStore, credentialFilePath, } from './credentials.js';
|
|
24
|
+
import { childProcessExec, NETWORK_TIMEOUT_MS } from './git.js';
|
|
25
|
+
/** Model-facing tool name. */
|
|
26
|
+
export const GIT_NETWORK_TOOL_NAME = 'git_network';
|
|
27
|
+
/** The only git subcommands this tool will run. */
|
|
28
|
+
export const GIT_NETWORK_SUBCOMMANDS = ['fetch', 'pull', 'push', 'ls-remote'];
|
|
29
|
+
/** Upper bound on the argument vector (the panel's own commands need far fewer). */
|
|
30
|
+
export const GIT_NETWORK_MAX_ARGS = 32;
|
|
31
|
+
/** Upper bound on one argument token. */
|
|
32
|
+
const MAX_TOKEN_LENGTH = 512;
|
|
33
|
+
/** Output kept from each stream, in characters; git diagnostics are short. */
|
|
34
|
+
const MAX_OUTPUT_CHARS = 64 * 1024;
|
|
35
|
+
/**
|
|
36
|
+
* Global options that would either replace the injected credential, point
|
|
37
|
+
* git at another repository, or load another config. They belong BEFORE the
|
|
38
|
+
* subcommand in git's own grammar, so the allow-list below already rejects
|
|
39
|
+
* them; this set is defence in depth for arguments passed after it.
|
|
40
|
+
*/
|
|
41
|
+
const BLOCKED_FLAGS = new Set([
|
|
42
|
+
'-c',
|
|
43
|
+
'--config-env',
|
|
44
|
+
'--exec-path',
|
|
45
|
+
'--git-dir',
|
|
46
|
+
'--work-tree',
|
|
47
|
+
'--namespace',
|
|
48
|
+
'--bare',
|
|
49
|
+
'--no-replace-objects',
|
|
50
|
+
]);
|
|
51
|
+
/** Same defence for flags that take a value with `=`. */
|
|
52
|
+
const BLOCKED_PREFIXES = ['--config-env=', '--upload-pack', '--receive-pack', '--exec='];
|
|
53
|
+
/** `push` is the one subcommand where these change the remote irreversibly. */
|
|
54
|
+
const PUSH_BLOCKED_FLAGS = new Set([
|
|
55
|
+
'-f',
|
|
56
|
+
'--force',
|
|
57
|
+
'--force-with-lease',
|
|
58
|
+
'--force-if-includes',
|
|
59
|
+
'--mirror',
|
|
60
|
+
'--delete',
|
|
61
|
+
'-d',
|
|
62
|
+
'--prune',
|
|
63
|
+
]);
|
|
64
|
+
/**
|
|
65
|
+
* Why a request cannot run (undefined = fine). Pure and exhaustive so the
|
|
66
|
+
* tests can pin every rejection without spawning git.
|
|
67
|
+
* @param repoPath - the requested cwd (validated as an absolute path).
|
|
68
|
+
* @param args - the requested git argument vector (subcommand first).
|
|
69
|
+
*/
|
|
70
|
+
export function gitNetworkRequestIssue(repoPath, args) {
|
|
71
|
+
if (typeof repoPath !== 'string' || repoPath.trim() === '')
|
|
72
|
+
return '"repoPath" must be a non-empty string';
|
|
73
|
+
if (!isAbsolute(repoPath))
|
|
74
|
+
return '"repoPath" must be an absolute path';
|
|
75
|
+
if (!Array.isArray(args) || args.length === 0)
|
|
76
|
+
return '"args" must be a non-empty array of git arguments';
|
|
77
|
+
if (args.length > GIT_NETWORK_MAX_ARGS)
|
|
78
|
+
return `"args" may hold at most ${String(GIT_NETWORK_MAX_ARGS)} items`;
|
|
79
|
+
const tokens = [];
|
|
80
|
+
for (const item of args) {
|
|
81
|
+
if (typeof item !== 'string')
|
|
82
|
+
return '"args" must contain only strings';
|
|
83
|
+
if (item === '' || item.includes('\0'))
|
|
84
|
+
return '"args" must not contain empty or NUL tokens';
|
|
85
|
+
if (item.length > MAX_TOKEN_LENGTH)
|
|
86
|
+
return `one argument exceeds ${String(MAX_TOKEN_LENGTH)} characters`;
|
|
87
|
+
tokens.push(item);
|
|
88
|
+
}
|
|
89
|
+
const subcommand = tokens[0];
|
|
90
|
+
if (subcommand === undefined)
|
|
91
|
+
return '"args" must name a git subcommand first';
|
|
92
|
+
if (!GIT_NETWORK_SUBCOMMANDS.includes(subcommand)) {
|
|
93
|
+
return `"${subcommand}" is not permitted here; use one of ${GIT_NETWORK_SUBCOMMANDS.join(', ')}`;
|
|
94
|
+
}
|
|
95
|
+
for (const token of tokens.slice(1)) {
|
|
96
|
+
if (BLOCKED_FLAGS.has(token))
|
|
97
|
+
return `"${token}" is not permitted by this tool`;
|
|
98
|
+
const blocked = BLOCKED_PREFIXES.find(prefix => token.startsWith(prefix));
|
|
99
|
+
if (blocked !== undefined)
|
|
100
|
+
return `"${blocked}…" is not permitted by this tool`;
|
|
101
|
+
if (subcommand === 'push') {
|
|
102
|
+
if (PUSH_BLOCKED_FLAGS.has(token))
|
|
103
|
+
return `"${token}" is not permitted: this tool never forces or deletes on the remote`;
|
|
104
|
+
if (token.startsWith('+') || token.startsWith(':')) {
|
|
105
|
+
return 'force/delete refspecs ("+" or ":") are not permitted: this tool never forces or deletes on the remote';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
/** Validate one raw request or throw the reason as a plain Error. */
|
|
112
|
+
function requestOf(args) {
|
|
113
|
+
const issue = gitNetworkRequestIssue(args.repoPath, args.args);
|
|
114
|
+
if (issue !== undefined)
|
|
115
|
+
throw new Error(issue);
|
|
116
|
+
return { repoPath: args.repoPath, args: [...args.args] };
|
|
117
|
+
}
|
|
118
|
+
/** Cut one stream at the cap; `truncated` reports whether anything was lost. */
|
|
119
|
+
function cap(text) {
|
|
120
|
+
return text.length <= MAX_OUTPUT_CHARS
|
|
121
|
+
? { text, truncated: false }
|
|
122
|
+
: { text: text.slice(0, MAX_OUTPUT_CHARS), truncated: true };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Remove stored secrets — raw and in the Basic header's base64 form — from
|
|
126
|
+
* git's own diagnostics. git does not echo the Authorization header, but the
|
|
127
|
+
* redaction also covers a remote URL that embeds the token, and costs one
|
|
128
|
+
* pass per stored host.
|
|
129
|
+
* @param text - one captured stream.
|
|
130
|
+
* @param entries - the credentials that were injected.
|
|
131
|
+
*/
|
|
132
|
+
export function redactSecrets(text, entries) {
|
|
133
|
+
let out = text;
|
|
134
|
+
for (const entry of entries) {
|
|
135
|
+
if (entry.secret !== '')
|
|
136
|
+
out = out.split(entry.secret).join('****');
|
|
137
|
+
const basic = Buffer.from(`${basicUsername(entry)}:${entry.secret}`, 'utf8').toString('base64');
|
|
138
|
+
out = out.split(basic).join('****');
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
/** Compose the model-facing text of one result. */
|
|
143
|
+
export function renderGitNetwork(value) {
|
|
144
|
+
const hosts = value.authenticatedHosts.length === 0 ? 'none' : value.authenticatedHosts.join(', ');
|
|
145
|
+
const lines = [
|
|
146
|
+
`git ${value.subcommand} (exit ${String(value.exitCode)}) in ${value.repoPath}`,
|
|
147
|
+
`credentials injected for: ${hosts}`,
|
|
148
|
+
];
|
|
149
|
+
if (value.credentialsError !== undefined)
|
|
150
|
+
lines.push(`credentials unavailable: ${value.credentialsError}`);
|
|
151
|
+
if (value.stdout.trim() !== '')
|
|
152
|
+
lines.push('--- stdout ---', value.stdout.trimEnd());
|
|
153
|
+
if (value.stderr.trim() !== '')
|
|
154
|
+
lines.push('--- stderr ---', value.stderr.trimEnd());
|
|
155
|
+
if (value.truncated)
|
|
156
|
+
lines.push('(output truncated at the length cap)');
|
|
157
|
+
return lines.join('\n');
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Build the `git_network` tool definition.
|
|
161
|
+
* @param deps - host seams (home/env, an optional store and an optional exec).
|
|
162
|
+
* @returns a definition ready for `ctx.tools.register`.
|
|
163
|
+
*/
|
|
164
|
+
export function gitNetworkTool(deps) {
|
|
165
|
+
const storeOf = () => deps.store ?? createCredentialStore(credentialFilePath(deps.home(), deps.envHome()));
|
|
166
|
+
const exec = deps.exec ?? childProcessExec;
|
|
167
|
+
return {
|
|
168
|
+
name: GIT_NETWORK_TOOL_NAME,
|
|
169
|
+
description: 'Run ONE git network command (fetch, pull, push or ls-remote) inside a repository with the '
|
|
170
|
+
+ 'plugin\'s stored Git credentials injected, so the command authenticates as the account configured in the '
|
|
171
|
+
+ 'Git credential panel instead of whatever the machine\'s git credential helper happens to hold. Use it when '
|
|
172
|
+
+ 'you must push/fetch/pull yourself; a plain shell `git push` does NOT see these credentials. Pass the git '
|
|
173
|
+
+ 'arguments subcommand-first, for example ["push"], ["fetch", "--all", "--prune"] or '
|
|
174
|
+
+ '["push", "origin", "main"]. Force/delete refspecs, --force/--delete/--mirror/--prune and '
|
|
175
|
+
+ '--upload-pack/--receive-pack style options are rejected by design. The secret itself never appears in the '
|
|
176
|
+
+ 'result: only exit status, redacted stdout/stderr and the hosts that were authenticated.',
|
|
177
|
+
parameters: {
|
|
178
|
+
type: 'object',
|
|
179
|
+
properties: {
|
|
180
|
+
repoPath: {
|
|
181
|
+
type: 'string',
|
|
182
|
+
description: 'Absolute path to any directory inside the target repository; the command runs with this as cwd.',
|
|
183
|
+
},
|
|
184
|
+
args: {
|
|
185
|
+
type: 'array',
|
|
186
|
+
items: { type: 'string' },
|
|
187
|
+
description: 'Git arguments with the subcommand first. Permitted subcommands: fetch, pull, push, ls-remote.',
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
required: ['repoPath', 'args'],
|
|
191
|
+
additionalProperties: false,
|
|
192
|
+
},
|
|
193
|
+
output: {
|
|
194
|
+
schema: {
|
|
195
|
+
type: 'object',
|
|
196
|
+
properties: {
|
|
197
|
+
repoPath: { type: 'string' },
|
|
198
|
+
subcommand: { type: 'string' },
|
|
199
|
+
exitCode: { type: 'integer' },
|
|
200
|
+
stdout: { type: 'string' },
|
|
201
|
+
stderr: { type: 'string' },
|
|
202
|
+
authenticatedHosts: { type: 'array', items: { type: 'string' } },
|
|
203
|
+
truncated: { type: 'boolean' },
|
|
204
|
+
credentialsError: { type: 'string' },
|
|
205
|
+
},
|
|
206
|
+
required: ['repoPath', 'subcommand', 'exitCode', 'stdout', 'stderr', 'authenticatedHosts', 'truncated'],
|
|
207
|
+
},
|
|
208
|
+
render: (_args, value) => [
|
|
209
|
+
{ type: 'text', text: renderGitNetwork(value) },
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
async execute(raw) {
|
|
213
|
+
const request = requestOf(raw ?? {});
|
|
214
|
+
let entries = [];
|
|
215
|
+
let credentialsError;
|
|
216
|
+
try {
|
|
217
|
+
entries = await storeOf().load();
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
// A damaged credential file must not make the tool unusable: run the
|
|
221
|
+
// command bare and report why, exactly like the routes do.
|
|
222
|
+
credentialsError = error instanceof Error ? error.message : String(error);
|
|
223
|
+
}
|
|
224
|
+
const hosts = [...new Set(entries.map(entry => entry.host))].sort();
|
|
225
|
+
const result = await exec('git', [...authConfigArgs(entries), ...request.args], {
|
|
226
|
+
cwd: request.repoPath,
|
|
227
|
+
timeoutMs: NETWORK_TIMEOUT_MS,
|
|
228
|
+
});
|
|
229
|
+
const stdout = cap(redactSecrets(result.stdout, entries));
|
|
230
|
+
const stderrSource = result.stderr.trim() === '' && result.errorMessage !== undefined
|
|
231
|
+
? result.errorMessage
|
|
232
|
+
: result.stderr;
|
|
233
|
+
const stderr = cap(redactSecrets(stderrSource, entries));
|
|
234
|
+
return {
|
|
235
|
+
repoPath: request.repoPath,
|
|
236
|
+
subcommand: request.args[0] ?? '',
|
|
237
|
+
exitCode: result.code,
|
|
238
|
+
stdout: stdout.text,
|
|
239
|
+
stderr: stderr.text,
|
|
240
|
+
authenticatedHosts: hosts,
|
|
241
|
+
truncated: stdout.truncated || stderr.truncated,
|
|
242
|
+
...(credentialsError === undefined ? {} : { credentialsError }),
|
|
243
|
+
};
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
package/package.json
CHANGED