dsh-git-branch-manage 1.1.0 → 1.2.0
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/CHANGELOG.md +17 -0
- package/lib/client.js +82 -16
- package/lib/credential-service.d.ts +32 -38
- package/lib/credential-service.js +56 -27
- package/lib/credentials.d.ts +145 -40
- package/lib/credentials.js +398 -70
- package/lib/index.js +24 -18
- package/lib/local-import.d.ts +68 -0
- package/lib/local-import.js +151 -0
- package/lib/network-tool.d.ts +5 -8
- package/lib/network-tool.js +14 -18
- package/lib/routes.d.ts +19 -2
- package/lib/routes.js +62 -19
- package/lib/shell-bridge.d.ts +57 -115
- package/lib/shell-bridge.js +96 -170
- package/lib/tool.js +2 -2
- package/lib/wire.d.ts +29 -0
- package/lib/wire.js +3 -0
- package/package.json +1 -1
- package/lib/helpers/git-credential-bridge.d.ts +0 -37
- package/lib/helpers/git-credential-bridge.js +0 -119
package/lib/shell-bridge.d.ts
CHANGED
|
@@ -1,143 +1,85 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shell credential
|
|
3
|
-
* credential panel stores one token per remote host, but 0029 could only
|
|
4
|
-
* inject it into the panel's own routes and the `git_network` tool; a plain
|
|
5
|
-
* `git push` run in a shell authenticated as whatever the machine's
|
|
6
|
-
* credential helper held — often a different account — or hung on an
|
|
7
|
-
* interactive prompt nobody could answer.
|
|
2
|
+
* Shell credential setup (host half) — decision 0037.
|
|
8
3
|
*
|
|
9
|
-
*
|
|
10
|
-
* `
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* in the shell, not only the networked ones (0034).
|
|
4
|
+
* Before 0037 the panel's token only reached commands the plugin itself ran;
|
|
5
|
+
* a plain `git push` in a shell authenticated as whatever the machine's
|
|
6
|
+
* helper held (often a different account) or hung on a prompt nobody could
|
|
7
|
+
* answer. 0032/0034 tried to bridge that through the process environment
|
|
8
|
+
* (`GIT_CONFIG_COUNT`, then a generated `GIT_CONFIG_GLOBAL` session file) —
|
|
9
|
+
* both channels existed only in the host process's own environment, so a
|
|
10
|
+
* shell that received a different environment snapshot saw nothing.
|
|
17
11
|
*
|
|
18
|
-
*
|
|
19
|
-
* git at it with `GIT_CONFIG_GLOBAL` — a name the scrubber keeps. The file:
|
|
12
|
+
* 0037 stops fighting the environment and uses git's own mechanisms:
|
|
20
13
|
*
|
|
21
|
-
* 1.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* helper (`helpers/git-credential-bridge.js`) as a `!`-shell command.
|
|
14
|
+
* 1. `<dsh-home>/git-credentials.include.gitconfig` is generated with one
|
|
15
|
+
* URL-scoped section per managed host;
|
|
16
|
+
* 2. `~/.gitconfig` gains exactly one `[include] path = <that file>` line
|
|
17
|
+
* (idempotent; see `ensureGitconfigInclude` in `credentials.ts`);
|
|
18
|
+
* 3. every git process — the plugin's own routes, `git_network`, and a
|
|
19
|
+
* plain shell — reads that include and therefore resolves the host
|
|
20
|
+
* through `credential-store`, i.e. `~/.git-credentials`.
|
|
29
21
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
22
|
+
* Nothing rides the process environment any more, which is why the module is
|
|
23
|
+
* now only a gitconfig-install + fail-fast switch manager. `GIT_TERMINAL_PROMPT=0`
|
|
24
|
+
* and `GCM_INTERACTIVE=never` stay: a host without a stored credential should
|
|
25
|
+
* fail at once instead of parking an agent shell on a GUI dialog.
|
|
34
26
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* The file is re-derived whenever the credential store changes
|
|
42
|
-
* (`src/routes.ts` calls the bridge after every save / delete / sign-in).
|
|
27
|
+
* `apply`/`dispose` still delete the retired 0032 channel's leftovers
|
|
28
|
+
* (`GIT_CONFIG_COUNT` with a broken `GIT_CONFIG_KEY_n` / `GIT_CONFIG_VALUE_n`
|
|
29
|
+
* set). The DSH subprocess scrubber strips the KEY names while the count and
|
|
30
|
+
* values survive, so a half-injected channel — left behind by an upgrade from
|
|
31
|
+
* 1.0.10 — makes git abort every command; see `legacyConfigNames` (0036).
|
|
43
32
|
*/
|
|
44
33
|
import { type CredentialStore } from './credentials.js';
|
|
45
|
-
/**
|
|
46
|
-
export declare const BRIDGE_HELPER_ENV = "DGB_BRIDGE_HELPER";
|
|
47
|
-
/** Env var carrying a debug log path to the helper script (unset by default). */
|
|
48
|
-
export declare const BRIDGE_DEBUG_ENV = "DGB_BRIDGE_DEBUG";
|
|
49
|
-
/** Env var pointing git at the generated session configuration file. */
|
|
50
|
-
export declare const SESSION_GITCONFIG_ENV = "GIT_CONFIG_GLOBAL";
|
|
51
|
-
/** Host-side seams the bridge reads through (tests substitute all of them). */
|
|
34
|
+
/** Host-side seams the setup reads through (tests substitute all of them). */
|
|
52
35
|
export interface ShellBridgeDeps {
|
|
53
36
|
/** User home directory seam. */
|
|
54
37
|
readonly home: () => string;
|
|
55
38
|
/** `$DSH_HOME` seam (blank/undefined = unset). */
|
|
56
39
|
readonly envHome: () => string | undefined;
|
|
57
|
-
/** Store seam; the fs-backed store
|
|
40
|
+
/** Store seam; the fs-backed local store when absent. */
|
|
58
41
|
readonly store?: CredentialStore;
|
|
59
42
|
/** The environment object to mutate; `process.env` when absent. */
|
|
60
43
|
readonly env?: Record<string, string | undefined>;
|
|
61
|
-
/**
|
|
62
|
-
* absent. */
|
|
63
|
-
readonly helperPath?: string;
|
|
64
|
-
/** Node executable that runs the helper; `process.execPath` when absent. */
|
|
65
|
-
readonly nodePath?: string;
|
|
66
|
-
/** Directory for the session config; the OS temp dir when absent. */
|
|
67
|
-
readonly configDir?: () => string;
|
|
68
|
-
/** Exact session config path (tests); `<configDir>/dsh-git-branch-<pid>.gitconfig` when absent. */
|
|
69
|
-
readonly configPath?: string;
|
|
70
|
-
/** User global config the session file includes; `<home>/.gitconfig` when absent. */
|
|
44
|
+
/** User global config the generated file is included from; `<home>/.gitconfig` when absent. */
|
|
71
45
|
readonly userGitconfig?: string;
|
|
72
46
|
}
|
|
73
|
-
/** The
|
|
47
|
+
/** The setup's operations: re-derive the include, and undo it. */
|
|
74
48
|
export interface ShellBridge {
|
|
75
|
-
/**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
*
|
|
49
|
+
/** The generated include file (diagnostics/tests). */
|
|
50
|
+
readonly configPath: () => string;
|
|
51
|
+
/** Read the credential store and rewrite the include + `~/.gitconfig`.
|
|
52
|
+
* A store that cannot be read writes an empty include (no host is
|
|
53
|
+
* scoped), while the fail-fast switches still go out. */
|
|
79
54
|
readonly apply: () => Promise<void>;
|
|
80
|
-
/** Remove the
|
|
55
|
+
/** Remove the fail-fast switches, the include line and the generated file. */
|
|
81
56
|
readonly dispose: () => Promise<void>;
|
|
82
|
-
/** The session config path this bridge writes. */
|
|
83
|
-
readonly configPath: () => string;
|
|
84
57
|
}
|
|
85
|
-
/**
|
|
86
|
-
export
|
|
87
|
-
/**
|
|
88
|
-
* Quote one executable or path for git's `!` shell command. Backslashes are
|
|
89
|
-
* normalized to forward slashes first: git runs `!` helpers through `sh`,
|
|
90
|
-
* where `C:\\…` would be read as escape sequences.
|
|
91
|
-
* @param value - an absolute path.
|
|
92
|
-
*/
|
|
93
|
-
export declare function shellQuote(value: string): string;
|
|
94
|
-
/**
|
|
95
|
-
* The helper command git runs for one configured host: a `!`-prefixed shell
|
|
96
|
-
* command, because git does not word-split a helper value by itself (a bare
|
|
97
|
-
* `node "C:/…/bridge.js"` is looked up as the program `git-credential-node`).
|
|
98
|
-
* @param nodePath - the node executable.
|
|
99
|
-
* @param helperPath - the compiled helper script.
|
|
100
|
-
*/
|
|
101
|
-
export declare function bridgeCommand(nodePath: string, helperPath: string): string;
|
|
58
|
+
/** The retired 0032 channel's count variable. */
|
|
59
|
+
export declare const LEGACY_COUNT_ENV = "GIT_CONFIG_COUNT";
|
|
102
60
|
/**
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* @param
|
|
117
|
-
* @param options - the node/helper pair and the user config to include.
|
|
118
|
-
*/
|
|
119
|
-
export declare function sessionGitconfigBody(hosts: readonly string[], options: {
|
|
120
|
-
readonly nodePath: string;
|
|
121
|
-
readonly helperPath: string;
|
|
122
|
-
readonly userGitconfig?: string;
|
|
123
|
-
}): string;
|
|
124
|
-
/**
|
|
125
|
-
* The environment assignments pointing git at the session config: the config
|
|
126
|
-
* path itself, the credential file the helper reads, and the two fail-fast
|
|
127
|
-
* switches. `hosts` live in the file now, not in the environment.
|
|
128
|
-
* @param options - the session config path and the credential file.
|
|
61
|
+
* Environment names left over from the retired 0032 channel that would make
|
|
62
|
+
* git abort with `fatal: unable to parse command-line config`.
|
|
63
|
+
*
|
|
64
|
+
* The DSH subprocess scrubber deletes every `GIT_CONFIG_KEY_<n>` (the name
|
|
65
|
+
* contains `KEY`) while `GIT_CONFIG_COUNT` / `GIT_CONFIG_VALUE_<n>` survive, so
|
|
66
|
+
* the channel is injected half-way and git can no longer parse it. Those
|
|
67
|
+
* leftovers outlive the code that wrote them: upgrading the plugin from 1.0.10
|
|
68
|
+
* leaves them in the host process environment, where they break every `git`
|
|
69
|
+
* command in every derived shell until someone deletes them by hand.
|
|
70
|
+
*
|
|
71
|
+
* A count whose pairs are all present is valid git input (a caller may set the
|
|
72
|
+
* channel deliberately), so it is left untouched; only a broken channel — or a
|
|
73
|
+
* non-numeric count — is reported for removal.
|
|
74
|
+
* @param env - the environment to inspect.
|
|
129
75
|
*/
|
|
130
|
-
export declare function
|
|
131
|
-
readonly configPath: string;
|
|
132
|
-
readonly credentialFile: string;
|
|
133
|
-
}): readonly BridgeAssignment[];
|
|
76
|
+
export declare function legacyConfigNames(env: Record<string, string | undefined>): string[];
|
|
134
77
|
/**
|
|
135
|
-
* Build the
|
|
136
|
-
* rewrites the
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* unauthenticated at worst) while the fail-fast switches still go out.
|
|
78
|
+
* Build the setup over one host process. Every {@link ShellBridge.apply}
|
|
79
|
+
* rewrites the include for the current store, so a deleted host loses its
|
|
80
|
+
* helper scope just like a freshly added one gains it. A failing write never
|
|
81
|
+
* throws: the worst case is a stale include until the next credential save,
|
|
82
|
+
* and git keeps working with the machine's own configuration.
|
|
141
83
|
* @param deps - host seams.
|
|
142
84
|
*/
|
|
143
85
|
export declare function createShellBridge(deps: ShellBridgeDeps): ShellBridge;
|
package/lib/shell-bridge.js
CHANGED
|
@@ -1,217 +1,143 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shell credential
|
|
3
|
-
* credential panel stores one token per remote host, but 0029 could only
|
|
4
|
-
* inject it into the panel's own routes and the `git_network` tool; a plain
|
|
5
|
-
* `git push` run in a shell authenticated as whatever the machine's
|
|
6
|
-
* credential helper held — often a different account — or hung on an
|
|
7
|
-
* interactive prompt nobody could answer.
|
|
2
|
+
* Shell credential setup (host half) — decision 0037.
|
|
8
3
|
*
|
|
9
|
-
*
|
|
10
|
-
* `
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* in the shell, not only the networked ones (0034).
|
|
4
|
+
* Before 0037 the panel's token only reached commands the plugin itself ran;
|
|
5
|
+
* a plain `git push` in a shell authenticated as whatever the machine's
|
|
6
|
+
* helper held (often a different account) or hung on a prompt nobody could
|
|
7
|
+
* answer. 0032/0034 tried to bridge that through the process environment
|
|
8
|
+
* (`GIT_CONFIG_COUNT`, then a generated `GIT_CONFIG_GLOBAL` session file) —
|
|
9
|
+
* both channels existed only in the host process's own environment, so a
|
|
10
|
+
* shell that received a different environment snapshot saw nothing.
|
|
17
11
|
*
|
|
18
|
-
*
|
|
19
|
-
* git at it with `GIT_CONFIG_GLOBAL` — a name the scrubber keeps. The file:
|
|
12
|
+
* 0037 stops fighting the environment and uses git's own mechanisms:
|
|
20
13
|
*
|
|
21
|
-
* 1.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* helper (`helpers/git-credential-bridge.js`) as a `!`-shell command.
|
|
14
|
+
* 1. `<dsh-home>/git-credentials.include.gitconfig` is generated with one
|
|
15
|
+
* URL-scoped section per managed host;
|
|
16
|
+
* 2. `~/.gitconfig` gains exactly one `[include] path = <that file>` line
|
|
17
|
+
* (idempotent; see `ensureGitconfigInclude` in `credentials.ts`);
|
|
18
|
+
* 3. every git process — the plugin's own routes, `git_network`, and a
|
|
19
|
+
* plain shell — reads that include and therefore resolves the host
|
|
20
|
+
* through `credential-store`, i.e. `~/.git-credentials`.
|
|
29
21
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
22
|
+
* Nothing rides the process environment any more, which is why the module is
|
|
23
|
+
* now only a gitconfig-install + fail-fast switch manager. `GIT_TERMINAL_PROMPT=0`
|
|
24
|
+
* and `GCM_INTERACTIVE=never` stay: a host without a stored credential should
|
|
25
|
+
* fail at once instead of parking an agent shell on a GUI dialog.
|
|
34
26
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* The file is re-derived whenever the credential store changes
|
|
42
|
-
* (`src/routes.ts` calls the bridge after every save / delete / sign-in).
|
|
43
|
-
*/
|
|
44
|
-
import { chmodSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
45
|
-
import { tmpdir } from 'node:os';
|
|
46
|
-
import { dirname, join } from 'node:path';
|
|
47
|
-
import { fileURLToPath } from 'node:url';
|
|
48
|
-
import { credentialFilePath, createCredentialStore, } from './credentials.js';
|
|
49
|
-
/** Env var carrying the credential file path to the helper script. */
|
|
50
|
-
export const BRIDGE_HELPER_ENV = 'DGB_BRIDGE_HELPER';
|
|
51
|
-
/** Env var carrying a debug log path to the helper script (unset by default). */
|
|
52
|
-
export const BRIDGE_DEBUG_ENV = 'DGB_BRIDGE_DEBUG';
|
|
53
|
-
/** Env var pointing git at the generated session configuration file. */
|
|
54
|
-
export const SESSION_GITCONFIG_ENV = 'GIT_CONFIG_GLOBAL';
|
|
55
|
-
/**
|
|
56
|
-
* Quote one executable or path for git's `!` shell command. Backslashes are
|
|
57
|
-
* normalized to forward slashes first: git runs `!` helpers through `sh`,
|
|
58
|
-
* where `C:\\…` would be read as escape sequences.
|
|
59
|
-
* @param value - an absolute path.
|
|
60
|
-
*/
|
|
61
|
-
export function shellQuote(value) {
|
|
62
|
-
return `"${value.replace(/\\/g, '/')}"`;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* The helper command git runs for one configured host: a `!`-prefixed shell
|
|
66
|
-
* command, because git does not word-split a helper value by itself (a bare
|
|
67
|
-
* `node "C:/…/bridge.js"` is looked up as the program `git-credential-node`).
|
|
68
|
-
* @param nodePath - the node executable.
|
|
69
|
-
* @param helperPath - the compiled helper script.
|
|
70
|
-
*/
|
|
71
|
-
export function bridgeCommand(nodePath, helperPath) {
|
|
72
|
-
return `!${shellQuote(nodePath)} ${shellQuote(helperPath)}`;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Escape one value for a gitconfig line: a literal `"` becomes `\"`, which is
|
|
76
|
-
* how a quoted path survives git's config parser and still reaches `sh`
|
|
77
|
-
* intact. Without this the quotes are eaten and `sh` splits
|
|
78
|
-
* `C:/Program Files/…` on the space.
|
|
79
|
-
* @param value - the raw helper command.
|
|
80
|
-
*/
|
|
81
|
-
export function gitconfigEscape(value) {
|
|
82
|
-
return value.replace(/"/g, '\\"');
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* The session gitconfig body for the given hosts: the user's own config
|
|
86
|
-
* included (read-only), the whole machine helper list reset to empty, and per
|
|
87
|
-
* host a URL-scoped reset followed by the bridge command. The empty value
|
|
88
|
-
* before each command is what cuts the included `gh` helper (and any other
|
|
89
|
-
* helper the user configures for that URL) out of that scope.
|
|
90
|
-
* @param hosts - normalized hosts with a stored credential.
|
|
91
|
-
* @param options - the node/helper pair and the user config to include.
|
|
27
|
+
* `apply`/`dispose` still delete the retired 0032 channel's leftovers
|
|
28
|
+
* (`GIT_CONFIG_COUNT` with a broken `GIT_CONFIG_KEY_n` / `GIT_CONFIG_VALUE_n`
|
|
29
|
+
* set). The DSH subprocess scrubber strips the KEY names while the count and
|
|
30
|
+
* values survive, so a half-injected channel — left behind by an upgrade from
|
|
31
|
+
* 1.0.10 — makes git abort every command; see `legacyConfigNames` (0036).
|
|
92
32
|
*/
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
// (`helper-selector` → GCM), which raises a GUI prompt no agent shell can
|
|
107
|
-
// answer. With it, such a host fails at once with "terminal prompts
|
|
108
|
-
// disabled", and the URL-scoped pairs below are the only way credentials
|
|
109
|
-
// get back into a shell.
|
|
110
|
-
lines.push('[credential]', '\thelper =');
|
|
111
|
-
const command = gitconfigEscape(bridgeCommand(options.nodePath, options.helperPath));
|
|
112
|
-
for (const host of unique) {
|
|
113
|
-
lines.push(`[credential "https://${host}/"]`,
|
|
114
|
-
// Empty first: it resets whatever helper list the included user config
|
|
115
|
-
// accumulated for this URL (the local `gh` helper), or git would answer
|
|
116
|
-
// with that account before the bridge is ever consulted.
|
|
117
|
-
'\thelper =', `\thelper = ${command}`);
|
|
118
|
-
}
|
|
119
|
-
return `${lines.join('\n')}\n`;
|
|
120
|
-
}
|
|
33
|
+
import { rm } from 'node:fs/promises';
|
|
34
|
+
import { join } from 'node:path';
|
|
35
|
+
import { credentialIncludeBody, credentialIncludeFile, createLocalCredentialStore, ensureGitconfigInclude, removeGitconfigInclude, writeFileAtomic, } from './credentials.js';
|
|
36
|
+
/** The retired 0032 channel's count variable. */
|
|
37
|
+
export const LEGACY_COUNT_ENV = 'GIT_CONFIG_COUNT';
|
|
38
|
+
/** Names of the retired 0032 channel's per-entry pairs. */
|
|
39
|
+
const LEGACY_KEY_NAME = /^GIT_CONFIG_KEY_\d+$/;
|
|
40
|
+
const LEGACY_VALUE_NAME = /^GIT_CONFIG_VALUE_\d+$/;
|
|
41
|
+
/** Fail fast instead of parking a shell on a prompt or a GUI dialog. */
|
|
42
|
+
const FAIL_FAST = [
|
|
43
|
+
['GIT_TERMINAL_PROMPT', '0'],
|
|
44
|
+
['GCM_INTERACTIVE', 'never'],
|
|
45
|
+
];
|
|
121
46
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
47
|
+
* Environment names left over from the retired 0032 channel that would make
|
|
48
|
+
* git abort with `fatal: unable to parse command-line config`.
|
|
49
|
+
*
|
|
50
|
+
* The DSH subprocess scrubber deletes every `GIT_CONFIG_KEY_<n>` (the name
|
|
51
|
+
* contains `KEY`) while `GIT_CONFIG_COUNT` / `GIT_CONFIG_VALUE_<n>` survive, so
|
|
52
|
+
* the channel is injected half-way and git can no longer parse it. Those
|
|
53
|
+
* leftovers outlive the code that wrote them: upgrading the plugin from 1.0.10
|
|
54
|
+
* leaves them in the host process environment, where they break every `git`
|
|
55
|
+
* command in every derived shell until someone deletes them by hand.
|
|
56
|
+
*
|
|
57
|
+
* A count whose pairs are all present is valid git input (a caller may set the
|
|
58
|
+
* channel deliberately), so it is left untouched; only a broken channel — or a
|
|
59
|
+
* non-numeric count — is reported for removal.
|
|
60
|
+
* @param env - the environment to inspect.
|
|
126
61
|
*/
|
|
127
|
-
export function
|
|
128
|
-
|
|
129
|
-
[
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
62
|
+
export function legacyConfigNames(env) {
|
|
63
|
+
if (env[LEGACY_COUNT_ENV] === undefined)
|
|
64
|
+
return [];
|
|
65
|
+
const count = Number(env[LEGACY_COUNT_ENV]);
|
|
66
|
+
const paired = Number.isInteger(count) && count >= 0
|
|
67
|
+
&& Array.from({ length: count }, (_, index) => `GIT_CONFIG_KEY_${index}`)
|
|
68
|
+
.every(name => env[name] !== undefined);
|
|
69
|
+
if (paired)
|
|
70
|
+
return [];
|
|
71
|
+
return Object.keys(env).filter(name => name === LEGACY_COUNT_ENV || LEGACY_KEY_NAME.test(name) || LEGACY_VALUE_NAME.test(name));
|
|
135
72
|
}
|
|
136
73
|
/**
|
|
137
|
-
* Build the
|
|
138
|
-
* rewrites the
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* unauthenticated at worst) while the fail-fast switches still go out.
|
|
74
|
+
* Build the setup over one host process. Every {@link ShellBridge.apply}
|
|
75
|
+
* rewrites the include for the current store, so a deleted host loses its
|
|
76
|
+
* helper scope just like a freshly added one gains it. A failing write never
|
|
77
|
+
* throws: the worst case is a stale include until the next credential save,
|
|
78
|
+
* and git keeps working with the machine's own configuration.
|
|
143
79
|
* @param deps - host seams.
|
|
144
80
|
*/
|
|
145
81
|
export function createShellBridge(deps) {
|
|
146
82
|
const env = deps.env ?? process.env;
|
|
147
|
-
const
|
|
148
|
-
|
|
83
|
+
const includePath = credentialIncludeFile(deps.home(), deps.envHome());
|
|
84
|
+
const gitconfig = () => deps.userGitconfig ?? join(deps.home(), '.gitconfig');
|
|
85
|
+
const store = () => deps.store ?? createLocalCredentialStore(deps.home(), deps.envHome());
|
|
149
86
|
let applied = [];
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
writeFileSync(partial, body, { encoding: 'utf8', mode: 0o600 });
|
|
156
|
-
renameSync(partial, configPath);
|
|
157
|
-
try {
|
|
158
|
-
chmodSync(configPath, 0o600);
|
|
159
|
-
}
|
|
160
|
-
catch {
|
|
161
|
-
// Best effort: Windows ignores the mode, the file holds no secret.
|
|
87
|
+
/** Delete the retired 0032 channel's leftovers before git can see them. */
|
|
88
|
+
const dropLegacy = () => {
|
|
89
|
+
for (const key of legacyConfigNames(env)) {
|
|
90
|
+
delete env[key];
|
|
91
|
+
applied.push(key);
|
|
162
92
|
}
|
|
163
93
|
};
|
|
164
94
|
return {
|
|
165
|
-
configPath: () =>
|
|
95
|
+
configPath: () => includePath,
|
|
166
96
|
async apply() {
|
|
167
97
|
let hosts = [];
|
|
168
98
|
try {
|
|
169
|
-
hosts = (await
|
|
99
|
+
hosts = (await store().load()).map(entry => entry.host);
|
|
170
100
|
}
|
|
171
101
|
catch {
|
|
172
|
-
// An unreadable store means no host can be
|
|
173
|
-
//
|
|
174
|
-
//
|
|
102
|
+
// An unreadable store means no host can be scoped. The include file
|
|
103
|
+
// is still rewritten empty, so a stale host does not keep a helper
|
|
104
|
+
// pointing at a file that no longer holds its credential.
|
|
175
105
|
hosts = [];
|
|
176
106
|
}
|
|
177
|
-
const helperPath = deps.helperPath
|
|
178
|
-
?? fileURLToPath(new URL('./helpers/git-credential-bridge.js', import.meta.url));
|
|
179
|
-
const body = sessionGitconfigBody(hosts, {
|
|
180
|
-
nodePath: deps.nodePath ?? process.execPath,
|
|
181
|
-
helperPath,
|
|
182
|
-
userGitconfig: deps.userGitconfig ?? join(deps.home(), '.gitconfig'),
|
|
183
|
-
});
|
|
184
|
-
let assignments = [
|
|
185
|
-
['GIT_TERMINAL_PROMPT', '0'],
|
|
186
|
-
['GCM_INTERACTIVE', 'never'],
|
|
187
|
-
];
|
|
188
107
|
try {
|
|
189
|
-
|
|
190
|
-
|
|
108
|
+
// Write the include first: if `~/.gitconfig` cannot be updated, the
|
|
109
|
+
// file it already points at is at least current.
|
|
110
|
+
await writeFileAtomic(includePath, credentialIncludeBody(hosts), 0o600);
|
|
111
|
+
await ensureGitconfigInclude(gitconfig(), includePath);
|
|
191
112
|
}
|
|
192
113
|
catch {
|
|
193
|
-
//
|
|
194
|
-
// GIT_CONFIG_GLOBAL at a missing file (that would drop the reset and
|
|
195
|
-
// let the machine chain pop a dialog); only the fail-fast switches
|
|
196
|
-
// remain, so a shell git runs unauthenticated but never hangs.
|
|
114
|
+
// Deliberately swallowed; see the contract above.
|
|
197
115
|
}
|
|
116
|
+
dropLegacy();
|
|
198
117
|
for (const key of applied)
|
|
199
118
|
delete env[key];
|
|
200
119
|
applied = [];
|
|
201
|
-
for (const [key, value] of
|
|
120
|
+
for (const [key, value] of FAIL_FAST) {
|
|
202
121
|
env[key] = value;
|
|
203
122
|
applied.push(key);
|
|
204
123
|
}
|
|
205
124
|
},
|
|
206
125
|
async dispose() {
|
|
126
|
+
dropLegacy();
|
|
207
127
|
for (const key of applied)
|
|
208
128
|
delete env[key];
|
|
209
129
|
applied = [];
|
|
210
130
|
try {
|
|
211
|
-
|
|
131
|
+
await removeGitconfigInclude(gitconfig(), includePath);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Best effort: a leftover include still points at the file below.
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
await rm(includePath, { force: true });
|
|
212
138
|
}
|
|
213
139
|
catch {
|
|
214
|
-
// Best effort:
|
|
140
|
+
// Best effort: the file holds no secret and `apply` rewrites it.
|
|
215
141
|
}
|
|
216
142
|
},
|
|
217
143
|
};
|
package/lib/tool.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* @module dsh-git-branch/tool
|
|
16
16
|
*/
|
|
17
|
-
import { basicUsername,
|
|
17
|
+
import { basicUsername, createLocalCredentialStore, maskSecret, } from './credentials.js';
|
|
18
18
|
/** Model-facing tool name. */
|
|
19
19
|
export const GIT_CREDENTIALS_TOOL_NAME = 'git_credentials';
|
|
20
20
|
/** Project one stored entry onto its model-facing row. */
|
|
@@ -48,7 +48,7 @@ export function renderGitCredentials(value) {
|
|
|
48
48
|
* @returns a definition ready for `ctx.tools.register`.
|
|
49
49
|
*/
|
|
50
50
|
export function gitCredentialsTool(deps) {
|
|
51
|
-
const storeOf = () => deps.credentialsService?.store ?? deps.store ??
|
|
51
|
+
const storeOf = () => deps.credentialsService?.store ?? deps.store ?? createLocalCredentialStore(deps.home(), deps.envHome());
|
|
52
52
|
/** Read the store, folding a read failure into the result value. */
|
|
53
53
|
const read = async (args) => {
|
|
54
54
|
const store = storeOf();
|
package/lib/wire.d.ts
CHANGED
|
@@ -70,6 +70,9 @@ export declare const ROUTE_CREDENTIALS_OAUTH_START = "/plugin/git-worktree/crede
|
|
|
70
70
|
/** POST ROUTE_PREFIX/credentials-oauth-poll — poll a started browser sign-in;
|
|
71
71
|
* stores the token and answers the fresh masked list once the user approves. */
|
|
72
72
|
export declare const ROUTE_CREDENTIALS_OAUTH_POLL = "/plugin/git-worktree/credentials-oauth-poll";
|
|
73
|
+
/** POST ROUTE_PREFIX/credentials-import — copy the credentials the machine's
|
|
74
|
+
* own helpers already answer for into the managed store (decision 0038). */
|
|
75
|
+
export declare const ROUTE_CREDENTIALS_IMPORT = "/plugin/git-worktree/credentials-import";
|
|
73
76
|
/** POST ROUTE_PREFIX/remotes — the repository's configured remotes. */
|
|
74
77
|
export declare const ROUTE_REMOTES = "/plugin/git-worktree/remotes";
|
|
75
78
|
/** POST ROUTE_PREFIX/remote — add, re-point or remove ONE remote. */
|
|
@@ -626,6 +629,32 @@ export interface CredentialDeleteBody {
|
|
|
626
629
|
}
|
|
627
630
|
/** POST credentials-delete response body: the updated masked list. */
|
|
628
631
|
export type CredentialDeleteResult = CredentialsResult;
|
|
632
|
+
/** POST credentials-import request body — absent hosts means "whatever the
|
|
633
|
+
* machine's gitconfig points git at". */
|
|
634
|
+
export interface CredentialImportBody {
|
|
635
|
+
/** Explicit hosts to probe; omitted, the gitconfig's URL-scoped hosts are
|
|
636
|
+
* the candidates. */
|
|
637
|
+
hosts?: string[];
|
|
638
|
+
}
|
|
639
|
+
/** One host the import copied into the store (secret never included). */
|
|
640
|
+
export interface CredentialImportEntry {
|
|
641
|
+
host: string;
|
|
642
|
+
/** `oauth2` for a token credential, the helper's user name otherwise. */
|
|
643
|
+
username: string;
|
|
644
|
+
method: CredentialMethod;
|
|
645
|
+
}
|
|
646
|
+
/** One host whose machine helper had nothing to give; the store is untouched. */
|
|
647
|
+
export interface CredentialImportFailure {
|
|
648
|
+
host: string;
|
|
649
|
+
error: string;
|
|
650
|
+
}
|
|
651
|
+
/** POST credentials-import response body: the fresh masked list plus what the
|
|
652
|
+
* import did per host. */
|
|
653
|
+
export interface CredentialImportResult extends CredentialsResult {
|
|
654
|
+
imported: CredentialImportEntry[];
|
|
655
|
+
skipped: string[];
|
|
656
|
+
failed: CredentialImportFailure[];
|
|
657
|
+
}
|
|
629
658
|
/** POST credentials-oauth-start request body. */
|
|
630
659
|
export interface CredentialOAuthStartBody {
|
|
631
660
|
/** Remote host whose browser sign-in to start (must support device flow). */
|
package/lib/wire.js
CHANGED
|
@@ -70,6 +70,9 @@ export const ROUTE_CREDENTIALS_OAUTH_START = `${ROUTE_PREFIX}/credentials-oauth-
|
|
|
70
70
|
/** POST ROUTE_PREFIX/credentials-oauth-poll — poll a started browser sign-in;
|
|
71
71
|
* stores the token and answers the fresh masked list once the user approves. */
|
|
72
72
|
export const ROUTE_CREDENTIALS_OAUTH_POLL = `${ROUTE_PREFIX}/credentials-oauth-poll`;
|
|
73
|
+
/** POST ROUTE_PREFIX/credentials-import — copy the credentials the machine's
|
|
74
|
+
* own helpers already answer for into the managed store (decision 0038). */
|
|
75
|
+
export const ROUTE_CREDENTIALS_IMPORT = `${ROUTE_PREFIX}/credentials-import`;
|
|
73
76
|
/** POST ROUTE_PREFIX/remotes — the repository's configured remotes. */
|
|
74
77
|
export const ROUTE_REMOTES = `${ROUTE_PREFIX}/remotes`;
|
|
75
78
|
/** POST ROUTE_PREFIX/remote — add, re-point or remove ONE remote. */
|
package/package.json
CHANGED