dsh-git-branch-manage 1.0.10 → 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 +41 -0
- package/lib/client.js +82 -16
- package/lib/credential-service.d.ts +68 -0
- package/lib/credential-service.js +85 -0
- package/lib/credentials.d.ts +145 -40
- package/lib/credentials.js +398 -70
- package/lib/index.js +33 -21
- package/lib/local-import.d.ts +68 -0
- package/lib/local-import.js +151 -0
- package/lib/network-tool.d.ts +9 -8
- package/lib/network-tool.js +14 -18
- package/lib/routes.d.ts +23 -2
- package/lib/routes.js +65 -18
- package/lib/shell-bridge.d.ts +61 -69
- package/lib/shell-bridge.js +113 -90
- package/lib/tool.d.ts +4 -0
- package/lib/tool.js +2 -2
- package/lib/wire.d.ts +29 -0
- package/lib/wire.js +3 -0
- package/package.json +2 -1
- package/lib/helpers/git-credential-bridge.d.ts +0 -32
- package/lib/helpers/git-credential-bridge.js +0 -114
package/lib/shell-bridge.d.ts
CHANGED
|
@@ -1,93 +1,85 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shell credential
|
|
3
|
-
* per remote host, but 0029 could only inject it into the panel's own routes
|
|
4
|
-
* and the `git_network` tool; a plain `git push` run in a shell authenticated
|
|
5
|
-
* as whatever the machine's credential helper held — often a different
|
|
6
|
-
* account — or hung on an interactive prompt nobody could answer.
|
|
2
|
+
* Shell credential setup (host half) — decision 0037.
|
|
7
3
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* there, so the machine chain (`helper-selector` → GCM) can neither answer
|
|
16
|
-
* with a different account nor open a blocking dialog for those hosts.
|
|
17
|
-
* The list is reset globally as well: a host with no stored credential then
|
|
18
|
-
* fails at once ("terminal prompts disabled") instead of reaching the machine
|
|
19
|
-
* chain, whose interactive GUI dialog nobody in an agent shell can answer.
|
|
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.
|
|
20
11
|
*
|
|
21
|
-
*
|
|
22
|
-
* with no usable credential fails fast instead of parking a shell on a prompt
|
|
23
|
-
* (the failure mode that started this: a 3m38s silent hang).
|
|
12
|
+
* 0037 stops fighting the environment and uses git's own mechanisms:
|
|
24
13
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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`.
|
|
21
|
+
*
|
|
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.
|
|
26
|
+
*
|
|
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).
|
|
27
32
|
*/
|
|
28
33
|
import { type CredentialStore } from './credentials.js';
|
|
29
|
-
/**
|
|
30
|
-
export declare const BRIDGE_HELPER_ENV = "DSH_GIT_BRIDGE_HELPER";
|
|
31
|
-
/** Env var carrying a debug log path to the helper script (unset by default). */
|
|
32
|
-
export declare const BRIDGE_DEBUG_ENV = "DSH_GIT_BRIDGE_DEBUG";
|
|
33
|
-
/** 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). */
|
|
34
35
|
export interface ShellBridgeDeps {
|
|
35
36
|
/** User home directory seam. */
|
|
36
37
|
readonly home: () => string;
|
|
37
38
|
/** `$DSH_HOME` seam (blank/undefined = unset). */
|
|
38
39
|
readonly envHome: () => string | undefined;
|
|
39
|
-
/** Store seam; the fs-backed store
|
|
40
|
+
/** Store seam; the fs-backed local store when absent. */
|
|
40
41
|
readonly store?: CredentialStore;
|
|
41
42
|
/** The environment object to mutate; `process.env` when absent. */
|
|
42
43
|
readonly env?: Record<string, string | undefined>;
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
readonly helperPath?: string;
|
|
46
|
-
/** Node executable that runs the helper; `process.execPath` when absent. */
|
|
47
|
-
readonly nodePath?: string;
|
|
44
|
+
/** User global config the generated file is included from; `<home>/.gitconfig` when absent. */
|
|
45
|
+
readonly userGitconfig?: string;
|
|
48
46
|
}
|
|
49
|
-
/** The
|
|
47
|
+
/** The setup's operations: re-derive the include, and undo it. */
|
|
50
48
|
export interface ShellBridge {
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
*
|
|
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. */
|
|
55
54
|
readonly apply: () => Promise<void>;
|
|
55
|
+
/** Remove the fail-fast switches, the include line and the generated file. */
|
|
56
|
+
readonly dispose: () => Promise<void>;
|
|
56
57
|
}
|
|
57
|
-
/**
|
|
58
|
-
export
|
|
59
|
-
/**
|
|
60
|
-
* Quote one executable or path for git's `!` shell command. Backslashes are
|
|
61
|
-
* normalized to forward slashes first: git runs `!` helpers through `sh`,
|
|
62
|
-
* where `C:\…` would be read as escape sequences.
|
|
63
|
-
* @param value - an absolute path.
|
|
64
|
-
*/
|
|
65
|
-
export declare function shellQuote(value: string): string;
|
|
58
|
+
/** The retired 0032 channel's count variable. */
|
|
59
|
+
export declare const LEGACY_COUNT_ENV = "GIT_CONFIG_COUNT";
|
|
66
60
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* @param
|
|
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.
|
|
81
75
|
*/
|
|
82
|
-
export declare function
|
|
83
|
-
readonly nodePath: string;
|
|
84
|
-
readonly helperPath: string;
|
|
85
|
-
readonly credentialFile: string;
|
|
86
|
-
}): readonly BridgeAssignment[];
|
|
76
|
+
export declare function legacyConfigNames(env: Record<string, string | undefined>): string[];
|
|
87
77
|
/**
|
|
88
|
-
* Build the
|
|
89
|
-
*
|
|
90
|
-
*
|
|
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.
|
|
91
83
|
* @param deps - host seams.
|
|
92
84
|
*/
|
|
93
85
|
export declare function createShellBridge(deps: ShellBridgeDeps): ShellBridge;
|
package/lib/shell-bridge.js
CHANGED
|
@@ -1,121 +1,144 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shell credential
|
|
3
|
-
* per remote host, but 0029 could only inject it into the panel's own routes
|
|
4
|
-
* and the `git_network` tool; a plain `git push` run in a shell authenticated
|
|
5
|
-
* as whatever the machine's credential helper held — often a different
|
|
6
|
-
* account — or hung on an interactive prompt nobody could answer.
|
|
2
|
+
* Shell credential setup (host half) — decision 0037.
|
|
7
3
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* there, so the machine chain (`helper-selector` → GCM) can neither answer
|
|
16
|
-
* with a different account nor open a blocking dialog for those hosts.
|
|
17
|
-
* The list is reset globally as well: a host with no stored credential then
|
|
18
|
-
* fails at once ("terminal prompts disabled") instead of reaching the machine
|
|
19
|
-
* chain, whose interactive GUI dialog nobody in an agent shell can answer.
|
|
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.
|
|
20
11
|
*
|
|
21
|
-
*
|
|
22
|
-
* with no usable credential fails fast instead of parking a shell on a prompt
|
|
23
|
-
* (the failure mode that started this: a 3m38s silent hang).
|
|
12
|
+
* 0037 stops fighting the environment and uses git's own mechanisms:
|
|
24
13
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* The helper command git runs for one configured host: a `!`-prefixed shell
|
|
45
|
-
* command, because git does not word-split a helper value by itself (a bare
|
|
46
|
-
* `node "C:/…/bridge.js"` is looked up as the program `git-credential-node`).
|
|
47
|
-
* @param nodePath - the node executable.
|
|
48
|
-
* @param helperPath - the compiled helper script.
|
|
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`.
|
|
21
|
+
*
|
|
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.
|
|
26
|
+
*
|
|
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).
|
|
49
32
|
*/
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
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
|
+
];
|
|
53
46
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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.
|
|
60
61
|
*/
|
|
61
|
-
export function
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
assignments.push(['GIT_CONFIG_KEY_0', 'credential.helper'], ['GIT_CONFIG_VALUE_0', '']);
|
|
72
|
-
unique.forEach((host, index) => {
|
|
73
|
-
const key = `credential.https://${host}/.helper`;
|
|
74
|
-
const empty = String(index * 2 + 1);
|
|
75
|
-
const command = String(index * 2 + 2);
|
|
76
|
-
assignments.push([`GIT_CONFIG_KEY_${empty}`, key], [`GIT_CONFIG_VALUE_${empty}`, '']);
|
|
77
|
-
assignments.push([`GIT_CONFIG_KEY_${command}`, key], [`GIT_CONFIG_VALUE_${command}`, bridgeCommand(options.nodePath, options.helperPath)]);
|
|
78
|
-
});
|
|
79
|
-
assignments.push([BRIDGE_HELPER_ENV, options.credentialFile]);
|
|
80
|
-
// Fail fast instead of parking a shell on a prompt or a GUI dialog.
|
|
81
|
-
assignments.push(['GIT_TERMINAL_PROMPT', '0'], ['GCM_INTERACTIVE', 'never']);
|
|
82
|
-
return assignments;
|
|
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));
|
|
83
72
|
}
|
|
84
73
|
/**
|
|
85
|
-
* Build the
|
|
86
|
-
*
|
|
87
|
-
*
|
|
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.
|
|
88
79
|
* @param deps - host seams.
|
|
89
80
|
*/
|
|
90
81
|
export function createShellBridge(deps) {
|
|
91
82
|
const env = deps.env ?? process.env;
|
|
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());
|
|
92
86
|
let applied = [];
|
|
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);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
93
94
|
return {
|
|
95
|
+
configPath: () => includePath,
|
|
94
96
|
async apply() {
|
|
95
97
|
let hosts = [];
|
|
96
98
|
try {
|
|
97
|
-
hosts = (await
|
|
99
|
+
hosts = (await store().load()).map(entry => entry.host);
|
|
98
100
|
}
|
|
99
101
|
catch {
|
|
100
|
-
// An unreadable store means no host can be
|
|
101
|
-
//
|
|
102
|
-
//
|
|
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.
|
|
103
105
|
hosts = [];
|
|
104
106
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
try {
|
|
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);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Deliberately swallowed; see the contract above.
|
|
115
|
+
}
|
|
116
|
+
dropLegacy();
|
|
112
117
|
for (const key of applied)
|
|
113
118
|
delete env[key];
|
|
114
119
|
applied = [];
|
|
115
|
-
for (const [key, value] of
|
|
120
|
+
for (const [key, value] of FAIL_FAST) {
|
|
116
121
|
env[key] = value;
|
|
117
122
|
applied.push(key);
|
|
118
123
|
}
|
|
119
124
|
},
|
|
125
|
+
async dispose() {
|
|
126
|
+
dropLegacy();
|
|
127
|
+
for (const key of applied)
|
|
128
|
+
delete env[key];
|
|
129
|
+
applied = [];
|
|
130
|
+
try {
|
|
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 });
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Best effort: the file holds no secret and `apply` rewrites it.
|
|
141
|
+
}
|
|
142
|
+
},
|
|
120
143
|
};
|
|
121
144
|
}
|
package/lib/tool.d.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* @module dsh-git-branch/tool
|
|
16
16
|
*/
|
|
17
17
|
import { type CredentialStore } from './credentials.js';
|
|
18
|
+
import type { CredentialService } from './credential-service.js';
|
|
18
19
|
/** Model-facing tool name. */
|
|
19
20
|
export declare const GIT_CREDENTIALS_TOOL_NAME = "git_credentials";
|
|
20
21
|
/** One text block in a tool result. */
|
|
@@ -58,6 +59,9 @@ export interface GitCredentialsToolDeps {
|
|
|
58
59
|
readonly envHome: () => string | undefined;
|
|
59
60
|
/** Store seam; the fs-backed store at the resolved path when absent. */
|
|
60
61
|
readonly store?: CredentialStore;
|
|
62
|
+
/** The single credential entry point (decision 0034); when wired its store
|
|
63
|
+
* is shared with the routes and the shell bridge. */
|
|
64
|
+
readonly credentialsService?: CredentialService;
|
|
61
65
|
}
|
|
62
66
|
/** A tool definition as `ctx.tools.register` accepts it. */
|
|
63
67
|
export interface GitCredentialsTool {
|
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.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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-git-branch-manage",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "提供 git 分支与 worktree 管理:输入框工具栏显示当前分支、可原地切换;支持 IDEA 风格分支选择器、远程分支检出、右键操作菜单(新建/重命名/删除/复制)、推送与更新当前分支,以及把分支隔离到独立 worktree 工作区,并管理其创建与删除。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"!lib/**/*.map",
|
|
46
46
|
"cordis.patch.yml",
|
|
47
47
|
"README.md",
|
|
48
|
+
"CHANGELOG.md",
|
|
48
49
|
"locale/*.json",
|
|
49
50
|
"icon.svg"
|
|
50
51
|
],
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Standalone git credential helper shipped with the dsh-git-branch plugin.
|
|
3
|
-
*
|
|
4
|
-
* The panel's credential file (`~/.dsh/git-credentials.json`) normally rides
|
|
5
|
-
* `-c http.<url>.extraHeader` config on the routes and the `git_network` tool.
|
|
6
|
-
* This helper extends that coverage to a plain shell `git push`: the plugin's
|
|
7
|
-
* host process exports `GIT_CONFIG_COUNT/KEY/VALUE` entries that install this
|
|
8
|
-
* script as a URL-scoped credential helper for every stored host, so commands
|
|
9
|
-
* the model runs in the shell authenticate as the configured account too.
|
|
10
|
-
*
|
|
11
|
-
* Design constraints (see docs/decisions/0029 and 0032):
|
|
12
|
-
* - The user's global git config and credential helpers are never written;
|
|
13
|
-
* the injection lives in the process environment only.
|
|
14
|
-
* - The token never enters the conversation: this script reads the
|
|
15
|
-
* credential file directly on the host and answers git on stdout.
|
|
16
|
-
* - URL scope (`credential.https://<host>/.helper`) confines the helper to
|
|
17
|
-
* hosts with a stored credential. The host half also clears the machine
|
|
18
|
-
* helper list, so a host this script cannot answer fails fast instead of
|
|
19
|
-
* opening an interactive credential dialog.
|
|
20
|
-
* - `get` is silent on a miss (git falls through to the next helper);
|
|
21
|
-
* `store`/`erase` are pass-through by decision — writing back would race
|
|
22
|
-
* the panel's atomic save and leak the file shape.
|
|
23
|
-
*
|
|
24
|
-
* Environment contract (set by src/shell-bridge.ts):
|
|
25
|
-
* - `DSH_GIT_BRIDGE_HELPER` — credential file path (required).
|
|
26
|
-
* - `DSH_GIT_BRIDGE_HOME` — user home directory (fallback resolution).
|
|
27
|
-
* - `DSH_GIT_BRIDGE_DSH_HOME` — `$DSH_HOME` override (optional).
|
|
28
|
-
* - `DSH_GIT_BRIDGE_DEBUG` — when non-blank, a debug log path (optional).
|
|
29
|
-
*
|
|
30
|
-
* @module dsh-git-branch/git-credential-bridge
|
|
31
|
-
*/
|
|
32
|
-
export {};
|