dsh-git-branch-manage 1.0.7 → 1.0.8

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.
@@ -81,3 +81,27 @@ export declare function accelerateUrlError(value: string): AccelerateUrlIssue |
81
81
  * @returns the prefix ending in `/`.
82
82
  */
83
83
  export declare function normalizeAccelerateUrl(value: string): string;
84
+ /** Why a user-typed REMOTE name is not acceptable yet (null = fine). */
85
+ export type RemoteNameIssue = 'empty' | 'leadingDash' | 'illegal';
86
+ /**
87
+ * Pre-flight check of a remote name: non-empty; no leading `-` (git would
88
+ * parse it as a flag); letters, digits, `.`, `_` and `-` only (`/`, space and
89
+ * `~^:?*[\` are outside what a remote name can be typed as here); the lone
90
+ * `.`/`..` are rejected. Git stays the authority — a miss surfaces through
91
+ * the error envelope — this only feeds immediate form feedback.
92
+ * @param name - raw draft text.
93
+ * @returns the issue kind, or null when acceptable.
94
+ */
95
+ export declare function remoteNameIssue(name: string): RemoteNameIssue | null;
96
+ /** Why a user-typed REMOTE URL is not acceptable yet (undefined = fine). */
97
+ export type RemoteUrlIssue = 'empty' | 'whitespace' | 'invalid';
98
+ /**
99
+ * Pre-flight check of a remote URL. Accepted shapes: any `scheme://…` URL
100
+ * (`https://`, `ssh://`, `git://`, `file://`), an scp-like
101
+ * `user@host:path`, and an absolute local path (a legitimate `file` remote).
102
+ * Everything else — a bare word, a URL with stray whitespace, a value git
103
+ * would read as a flag — is rejected. Git validates the remote once added.
104
+ * @param url - raw draft text.
105
+ * @returns the issue kind, or undefined when acceptable.
106
+ */
107
+ export declare function remoteUrlIssue(url: string): RemoteUrlIssue | undefined;
package/lib/normalize.js CHANGED
@@ -126,3 +126,50 @@ export function normalizeAccelerateUrl(value) {
126
126
  const trimmed = value.trim();
127
127
  return trimmed.endsWith('/') ? trimmed : `${trimmed}/`;
128
128
  }
129
+ /**
130
+ * Pre-flight check of a remote name: non-empty; no leading `-` (git would
131
+ * parse it as a flag); letters, digits, `.`, `_` and `-` only (`/`, space and
132
+ * `~^:?*[\` are outside what a remote name can be typed as here); the lone
133
+ * `.`/`..` are rejected. Git stays the authority — a miss surfaces through
134
+ * the error envelope — this only feeds immediate form feedback.
135
+ * @param name - raw draft text.
136
+ * @returns the issue kind, or null when acceptable.
137
+ */
138
+ export function remoteNameIssue(name) {
139
+ if (name.trim() === '')
140
+ return 'empty';
141
+ if (name.startsWith('-'))
142
+ return 'leadingDash';
143
+ if (!/^[A-Za-z0-9._-]+$/.test(name))
144
+ return 'illegal';
145
+ if (name === '.' || name === '..')
146
+ return 'illegal';
147
+ return null;
148
+ }
149
+ /**
150
+ * Pre-flight check of a remote URL. Accepted shapes: any `scheme://…` URL
151
+ * (`https://`, `ssh://`, `git://`, `file://`), an scp-like
152
+ * `user@host:path`, and an absolute local path (a legitimate `file` remote).
153
+ * Everything else — a bare word, a URL with stray whitespace, a value git
154
+ * would read as a flag — is rejected. Git validates the remote once added.
155
+ * @param url - raw draft text.
156
+ * @returns the issue kind, or undefined when acceptable.
157
+ */
158
+ export function remoteUrlIssue(url) {
159
+ const trimmed = url.trim();
160
+ if (trimmed === '')
161
+ return 'empty';
162
+ if (trimmed.startsWith('-'))
163
+ return 'invalid';
164
+ // Local-path remotes may contain spaces (`C:\my repo`), so the path shape
165
+ // is classified before the whitespace rule.
166
+ if (isAbsoluteConfigPath(trimmed))
167
+ return undefined;
168
+ if (/\s/.test(trimmed))
169
+ return 'whitespace';
170
+ if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(trimmed))
171
+ return undefined;
172
+ if (/^[^\s@/]+@[^\s:/]+:\S+$/.test(trimmed))
173
+ return undefined;
174
+ return 'invalid';
175
+ }
package/lib/oauth.d.ts ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * GitHub OAuth device flow behind the credential dialog's site sign-in.
3
+ *
4
+ * A browser sign-in must never need a client SECRET (that would have to ship
5
+ * in the bundle) and must work from a machine that cannot accept inbound
6
+ * connections, so the plugin uses the OAuth 2.0 device authorization grant:
7
+ * the host asks GitHub for a one-time user code, the dialog opens the browser
8
+ * and shows that code, and the plugin polls the token endpoint until the user
9
+ * approves. The token that comes back is stored like a pasted personal access
10
+ * token (method `token`) and the user never sees a raw PAT workflow.
11
+ *
12
+ * Only hosts whose device flow this module knows are accepted; the caller
13
+ * decides which of them to offer in the UI.
14
+ *
15
+ * @module git-worktree/oauth
16
+ */
17
+ /** The hosts whose device flow this module implements. The dialog imports the
18
+ * same list, so the offered buttons cannot drift from what the host supports. */
19
+ export declare const DEVICE_FLOW_HOSTS: readonly string[];
20
+ /**
21
+ * Fallback OAuth App client id. GitHub's device flow requires a client id but
22
+ * never a client secret; the shipped value is the public GitHub CLI app,
23
+ * which lets a stock `gh` install and this plugin sign in with the same app.
24
+ * Point `DSH_GITHUB_OAUTH_CLIENT_ID` at a dedicated OAuth App to use one.
25
+ */
26
+ export declare const DEFAULT_OAUTH_CLIENT_ID = "178c6fc778ccc68e1d6a";
27
+ /** Scopes one push needs: `repo` covers private repositories and write access. */
28
+ export declare const OAUTH_SCOPE = "repo";
29
+ /** One device-code grant, reduced to what the browser needs to show/open. */
30
+ export interface DeviceCodeGrant {
31
+ /** Polling credential; never rendered. */
32
+ deviceCode: string;
33
+ /** The short code the user types on the verification page. */
34
+ userCode: string;
35
+ /** Verification page (without the code). */
36
+ verificationUri: string;
37
+ /** Verification page with the code pre-filled, when GitHub returns one. */
38
+ verificationUriComplete?: string;
39
+ /** Minimum seconds between polls. */
40
+ intervalSeconds: number;
41
+ /** Seconds until the grant expires. */
42
+ expiresInSeconds: number;
43
+ }
44
+ /** One poll answer: the finished token, or why there is no token yet. */
45
+ export type DeviceFlowPoll = {
46
+ status: 'token';
47
+ token: string;
48
+ } | {
49
+ status: 'pending';
50
+ } | {
51
+ status: 'slow_down';
52
+ }
53
+ /** The transport broke (DNS, TLS, reset keep-alive connection): the grant is
54
+ * untouched, so a poll loop should retry instead of ending the sign-in. */
55
+ | {
56
+ status: 'network';
57
+ message: string;
58
+ } | {
59
+ status: 'denied';
60
+ message: string;
61
+ } | {
62
+ status: 'expired';
63
+ message: string;
64
+ } | {
65
+ status: 'failed';
66
+ message: string;
67
+ };
68
+ /**
69
+ * The OAuth App client id to use: the environment override when set, else the
70
+ * shipped default.
71
+ * @param env - environment seam (`process.env` in production).
72
+ */
73
+ export declare function deviceFlowClientId(env?: Readonly<Record<string, string | undefined>>): string;
74
+ /** Whether browser sign-in is implemented for this (normalized) host. */
75
+ export declare function isDeviceFlowHost(host: string): boolean;
76
+ /**
77
+ * A request that never reached the host: DNS failure, TLS interference, or a
78
+ * reset pooled connection. Unlike an OAuth error answer this says nothing
79
+ * about the grant, so the poll loop may retry it.
80
+ */
81
+ export declare class DeviceFlowTransportError extends Error {
82
+ /** The endpoint that could not be reached. */
83
+ readonly url: string;
84
+ /**
85
+ * @param url - the endpoint that could not be reached.
86
+ * @param cause - the underlying fetch failure.
87
+ */
88
+ constructor(url: string, cause: unknown);
89
+ }
90
+ /**
91
+ * Ask GitHub for a device code. The caller opens `verificationUriComplete`
92
+ * (or `verificationUri` plus the shown `userCode`) and then polls.
93
+ * @param fetchImpl - fetch seam (global fetch in production, a fake in tests).
94
+ * @param host - normalized host; must be a {@link DEVICE_FLOW_HOSTS} member.
95
+ * @param clientId - OAuth App client id, see {@link deviceFlowClientId}.
96
+ * @param timeoutMs - per-request budget for the device-code request.
97
+ */
98
+ export declare function startDeviceFlow(fetchImpl: typeof fetch, host: string, clientId: string, timeoutMs?: number): Promise<DeviceCodeGrant>;
99
+ /**
100
+ * Poll the token endpoint once for one grant. GitHub answers `400` with an
101
+ * `error` code until the user acts, so a pending grant is a normal answer,
102
+ * not a thrown failure; only transport problems throw.
103
+ * @param fetchImpl - fetch seam.
104
+ * @param host - normalized host.
105
+ * @param clientId - the same client id the grant was started with.
106
+ * @param deviceCode - the grant's `deviceCode`.
107
+ * @param timeoutMs - per-request budget for this poll.
108
+ */
109
+ export declare function pollDeviceFlow(fetchImpl: typeof fetch, host: string, clientId: string, deviceCode: string, timeoutMs?: number): Promise<DeviceFlowPoll>;
package/lib/oauth.js ADDED
@@ -0,0 +1,182 @@
1
+ /**
2
+ * GitHub OAuth device flow behind the credential dialog's site sign-in.
3
+ *
4
+ * A browser sign-in must never need a client SECRET (that would have to ship
5
+ * in the bundle) and must work from a machine that cannot accept inbound
6
+ * connections, so the plugin uses the OAuth 2.0 device authorization grant:
7
+ * the host asks GitHub for a one-time user code, the dialog opens the browser
8
+ * and shows that code, and the plugin polls the token endpoint until the user
9
+ * approves. The token that comes back is stored like a pasted personal access
10
+ * token (method `token`) and the user never sees a raw PAT workflow.
11
+ *
12
+ * Only hosts whose device flow this module knows are accepted; the caller
13
+ * decides which of them to offer in the UI.
14
+ *
15
+ * @module git-worktree/oauth
16
+ */
17
+ import { BROWSER_SIGN_IN_HOSTS } from './wire.js';
18
+ /** The hosts whose device flow this module implements. The dialog imports the
19
+ * same list, so the offered buttons cannot drift from what the host supports. */
20
+ export const DEVICE_FLOW_HOSTS = BROWSER_SIGN_IN_HOSTS;
21
+ /**
22
+ * Fallback OAuth App client id. GitHub's device flow requires a client id but
23
+ * never a client secret; the shipped value is the public GitHub CLI app,
24
+ * which lets a stock `gh` install and this plugin sign in with the same app.
25
+ * Point `DSH_GITHUB_OAUTH_CLIENT_ID` at a dedicated OAuth App to use one.
26
+ */
27
+ export const DEFAULT_OAUTH_CLIENT_ID = '178c6fc778ccc68e1d6a';
28
+ /** Scopes one push needs: `repo` covers private repositories and write access. */
29
+ export const OAUTH_SCOPE = 'repo';
30
+ /**
31
+ * The OAuth App client id to use: the environment override when set, else the
32
+ * shipped default.
33
+ * @param env - environment seam (`process.env` in production).
34
+ */
35
+ export function deviceFlowClientId(env = process.env) {
36
+ const configured = env.DSH_GITHUB_OAUTH_CLIENT_ID?.trim();
37
+ return configured === undefined || configured === '' ? DEFAULT_OAUTH_CLIENT_ID : configured;
38
+ }
39
+ /** Whether browser sign-in is implemented for this (normalized) host. */
40
+ export function isDeviceFlowHost(host) {
41
+ return DEVICE_FLOW_HOSTS.includes(host);
42
+ }
43
+ /** Read one non-empty string field from a parsed JSON object. */
44
+ function readString(payload, key) {
45
+ const value = payload[key];
46
+ return typeof value === 'string' && value !== '' ? value : undefined;
47
+ }
48
+ /**
49
+ * A request that never reached the host: DNS failure, TLS interference, or a
50
+ * reset pooled connection. Unlike an OAuth error answer this says nothing
51
+ * about the grant, so the poll loop may retry it.
52
+ */
53
+ export class DeviceFlowTransportError extends Error {
54
+ /** The endpoint that could not be reached. */
55
+ url;
56
+ /**
57
+ * @param url - the endpoint that could not be reached.
58
+ * @param cause - the underlying fetch failure.
59
+ */
60
+ constructor(url, cause) {
61
+ super(`${url} could not be reached (${cause instanceof Error ? cause.message : String(cause)})`);
62
+ this.name = 'DeviceFlowTransportError';
63
+ this.url = url;
64
+ }
65
+ }
66
+ /**
67
+ * POST one form-encoded body and parse the JSON answer. Network failures and
68
+ * unexpected non-2xx answers become one thrown Error; callers map it to a
69
+ * route failure. `allowErrorStatus` keeps the body of a documented OAuth
70
+ * error answer (the token endpoint reports a pending grant as `400`), which
71
+ * is an answer, not a transport failure.
72
+ */
73
+ async function postForm(fetchImpl, url, fields, timeoutMs, allowErrorStatus = false) {
74
+ let response;
75
+ try {
76
+ response = await fetchImpl(url, {
77
+ method: 'POST',
78
+ headers: {
79
+ 'content-type': 'application/x-www-form-urlencoded',
80
+ accept: 'application/json',
81
+ },
82
+ body: new URLSearchParams(fields).toString(),
83
+ signal: AbortSignal.timeout(timeoutMs),
84
+ });
85
+ }
86
+ catch (error) {
87
+ throw new DeviceFlowTransportError(url, error);
88
+ }
89
+ let payload;
90
+ let parsed = false;
91
+ try {
92
+ payload = await response.json();
93
+ parsed = true;
94
+ }
95
+ catch {
96
+ parsed = false;
97
+ }
98
+ if (!parsed)
99
+ throw new Error(`${url} answered a non-JSON body`);
100
+ if (!response.ok && !allowErrorStatus)
101
+ throw new Error(`${url} answered HTTP ${String(response.status)}`);
102
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
103
+ throw new Error(`${url} answered an unexpected body`);
104
+ }
105
+ return payload;
106
+ }
107
+ /**
108
+ * Ask GitHub for a device code. The caller opens `verificationUriComplete`
109
+ * (or `verificationUri` plus the shown `userCode`) and then polls.
110
+ * @param fetchImpl - fetch seam (global fetch in production, a fake in tests).
111
+ * @param host - normalized host; must be a {@link DEVICE_FLOW_HOSTS} member.
112
+ * @param clientId - OAuth App client id, see {@link deviceFlowClientId}.
113
+ * @param timeoutMs - per-request budget for the device-code request.
114
+ */
115
+ export async function startDeviceFlow(fetchImpl, host, clientId, timeoutMs = 15_000) {
116
+ const payload = await postForm(fetchImpl, `https://${host}/login/device/code`, { client_id: clientId, scope: OAUTH_SCOPE }, timeoutMs);
117
+ const deviceCode = readString(payload, 'device_code');
118
+ const userCode = readString(payload, 'user_code');
119
+ const verificationUri = readString(payload, 'verification_uri');
120
+ if (deviceCode === undefined || userCode === undefined || verificationUri === undefined) {
121
+ throw new Error(`${host} did not return a device code (is device flow enabled for the OAuth app?)`);
122
+ }
123
+ const interval = typeof payload.interval === 'number' && payload.interval > 0 ? payload.interval : 5;
124
+ const expiresIn = typeof payload.expires_in === 'number' && payload.expires_in > 0 ? payload.expires_in : 900;
125
+ const verificationUriComplete = readString(payload, 'verification_uri_complete');
126
+ return {
127
+ deviceCode,
128
+ userCode,
129
+ verificationUri,
130
+ ...(verificationUriComplete === undefined ? {} : { verificationUriComplete }),
131
+ intervalSeconds: interval,
132
+ expiresInSeconds: expiresIn,
133
+ };
134
+ }
135
+ /**
136
+ * Poll the token endpoint once for one grant. GitHub answers `400` with an
137
+ * `error` code until the user acts, so a pending grant is a normal answer,
138
+ * not a thrown failure; only transport problems throw.
139
+ * @param fetchImpl - fetch seam.
140
+ * @param host - normalized host.
141
+ * @param clientId - the same client id the grant was started with.
142
+ * @param deviceCode - the grant's `deviceCode`.
143
+ * @param timeoutMs - per-request budget for this poll.
144
+ */
145
+ export async function pollDeviceFlow(fetchImpl, host, clientId, deviceCode, timeoutMs = 15_000) {
146
+ let payload;
147
+ try {
148
+ payload = await postForm(fetchImpl, `https://${host}/login/oauth/access_token`, {
149
+ client_id: clientId,
150
+ device_code: deviceCode,
151
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
152
+ }, timeoutMs,
153
+ // A pending grant is a 400 with an OAuth error body, not a failure.
154
+ true);
155
+ }
156
+ catch (error) {
157
+ // A dropped connection says nothing about the grant: report it as a
158
+ // retryable answer so one blip cannot end a sign-in the user already
159
+ // approved in the browser. Any other failure (malformed body, HTTP 5xx)
160
+ // still throws and surfaces as a route error.
161
+ if (error instanceof DeviceFlowTransportError)
162
+ return { status: 'network', message: error.message };
163
+ throw error;
164
+ }
165
+ const token = readString(payload, 'access_token');
166
+ if (token !== undefined)
167
+ return { status: 'token', token };
168
+ const code = readString(payload, 'error') ?? 'unknown_error';
169
+ const description = readString(payload, 'error_description') ?? code;
170
+ switch (code) {
171
+ case 'authorization_pending':
172
+ return { status: 'pending' };
173
+ case 'slow_down':
174
+ return { status: 'slow_down' };
175
+ case 'expired_token':
176
+ return { status: 'expired', message: description };
177
+ case 'access_denied':
178
+ return { status: 'denied', message: description };
179
+ default:
180
+ return { status: 'failed', message: description };
181
+ }
182
+ }
package/lib/routes.d.ts CHANGED
@@ -3,8 +3,9 @@
3
3
  * shell (index.ts) owns req/res mechanics; everything testable lives here.
4
4
  */
5
5
  import { type CopyFilesSeams } from './copy-files.js';
6
+ import { type CredentialStore } from './credentials.js';
6
7
  import { type DirExists, type ExcludeSeams, type Exec } from './git.js';
7
- import type { AiCommitMessageResult, ChangesResult, CommitFilesResult, CommitResult, CreateBranchResult, CreateTagResult, CreateWorktreeResult, EnsureDirectoryResult, FetchResult, GroupWorkspacesResult, InspectWorktreeResult, LogResult, PathExistsResult, PurgeDirectoryResult, PushTagResult, RemoveWorktreeResult, RepoStatus, RouteError, StageResult, SwitchResult, UpdateResult, WorktreesAllResult } from './wire.js';
8
+ import type { AiCommitMessageResult, ChangesResult, CommitFilesResult, CommitResult, CreateBranchResult, CreateTagResult, CreateWorktreeResult, CredentialOAuthPollResult, CredentialOAuthStartResult, CredentialsResult, EnsureDirectoryResult, FetchResult, GroupWorkspacesResult, InspectWorktreeResult, LogResult, PathExistsResult, PurgeDirectoryResult, PushTagResult, RemoveWorktreeResult, RemotesResult, RepoStatus, RouteError, StageResult, SwitchResult, UpdateResult, WorktreesAllResult } from './wire.js';
8
9
  /** Everything the handlers need from the host half. */
9
10
  export interface RouteDeps {
10
11
  exec: Exec;
@@ -28,6 +29,8 @@ export interface RouteDeps {
28
29
  excludeSeams?: ExcludeSeams;
29
30
  /** Post-create file copy seams (tests substitute). */
30
31
  copyFilesSeams?: CopyFilesSeams;
32
+ /** HTTP seam for the browser sign-in routes (absent = global fetch). */
33
+ fetchImpl?: typeof fetch;
31
34
  /** Directory probe seam over fs.stat (true = exists AND is a directory);
32
35
  * tests substitute. */
33
36
  statDirectory?: (path: string) => Promise<boolean>;
@@ -38,6 +41,9 @@ export interface RouteDeps {
38
41
  listDir?: (path: string) => Promise<string[]>;
39
42
  /** Recursive force rm seam (fs.rm with retries); tests substitute. */
40
43
  rmRecursive?: (path: string) => Promise<void>;
44
+ /** Global per-host credential store. Absent = the fs-backed store at the
45
+ * resolved dsh home; tests substitute an in-memory one. */
46
+ credentialsStore?: CredentialStore;
41
47
  }
42
48
  /** Real fs-backed one-level directory listing (names only). Plain FILES are
43
49
  * filtered out — the storage root can carry strays (a legacy settings.json
@@ -50,7 +56,7 @@ export declare function fsMkdirRecursive(path: string): Promise<void>;
50
56
  /** One route outcome: HTTP status plus the JSON body. */
51
57
  export interface RouteOutcome {
52
58
  status: number;
53
- body: RepoStatus | CreateWorktreeResult | SwitchResult | CreateBranchResult | FetchResult | UpdateResult | GroupWorkspacesResult | InspectWorktreeResult | RemoveWorktreeResult | PathExistsResult | EnsureDirectoryResult | WorktreesAllResult | PurgeDirectoryResult | ChangesResult | StageResult | CommitResult | LogResult | CommitFilesResult | CreateTagResult | PushTagResult | AiCommitMessageResult | RouteError;
59
+ body: RepoStatus | CreateWorktreeResult | SwitchResult | CreateBranchResult | FetchResult | UpdateResult | GroupWorkspacesResult | InspectWorktreeResult | RemoveWorktreeResult | PathExistsResult | EnsureDirectoryResult | WorktreesAllResult | PurgeDirectoryResult | ChangesResult | StageResult | CommitResult | LogResult | CommitFilesResult | CreateTagResult | PushTagResult | AiCommitMessageResult | CredentialsResult | CredentialOAuthStartResult | CredentialOAuthPollResult | RemotesResult | RouteError;
54
60
  }
55
61
  /**
56
62
  * POST /group — git belonging facts for a batch of workspace directories.
@@ -351,3 +357,62 @@ export interface LlmStreamRuntime {
351
357
  * @param body - parsed request body: `{ repoPath, provider, model, effort?, hint? }`.
352
358
  */
353
359
  export declare function handleAiCommitMessage(deps: RouteDeps, llm: LlmStreamRuntime, body: unknown): Promise<RouteOutcome>;
360
+ /**
361
+ * POST /credentials — every stored credential as a MASKED row. One row per
362
+ * host: the store is global (not per repository), so the panel edits one
363
+ * address for github.com, gitlab.com, a self-hosted gitea alike.
364
+ * @param deps - host dependencies.
365
+ * @param body - parsed request body: absent or `{}`.
366
+ */
367
+ export declare function handleCredentials(deps: RouteDeps, body: unknown): Promise<RouteOutcome>;
368
+ /**
369
+ * POST /credentials-save — create or update the credential of ONE host. A
370
+ * blank `secret` on an existing host keeps the stored secret (the browser
371
+ * only ever had the mask), while a new host requires one.
372
+ * @param deps - host dependencies.
373
+ * @param body - parsed request body: `{ host, username, method, secret? }`.
374
+ */
375
+ export declare function handleCredentialsSave(deps: RouteDeps, body: unknown): Promise<RouteOutcome>;
376
+ /**
377
+ * POST /credentials-delete — forget ONE host's credential. The other hosts
378
+ * keep theirs: deletion is per host, never a store reset.
379
+ * @param deps - host dependencies.
380
+ * @param body - parsed request body: `{ host }`.
381
+ */
382
+ export declare function handleCredentialsDelete(deps: RouteDeps, body: unknown): Promise<RouteOutcome>;
383
+ /**
384
+ * POST /credentials-oauth-start — start a browser sign-in for ONE host
385
+ * (GitHub device flow). The answer carries a one-time user code and the
386
+ * polling credential; the dialog opens the verification page and polls
387
+ * /credentials-oauth-poll until the user approves.
388
+ * @param deps - host dependencies.
389
+ * @param body - parsed request body: `{ host }`.
390
+ */
391
+ export declare function handleCredentialsOAuthStart(deps: RouteDeps, body: unknown): Promise<RouteOutcome>;
392
+ /**
393
+ * POST /credentials-oauth-poll — one poll of a started browser sign-in.
394
+ * A pending grant answers `{ status: 'pending' }` so the dialog keeps
395
+ * waiting; an approved grant is stored as a `token` credential for the host
396
+ * (the token itself never crosses the wire again) and the fresh masked list
397
+ * comes back.
398
+ * @param deps - host dependencies.
399
+ * @param body - parsed request body: `{ host, deviceCode }`.
400
+ */
401
+ export declare function handleCredentialsOAuthPoll(deps: RouteDeps, body: unknown): Promise<RouteOutcome>;
402
+ /**
403
+ * POST /remotes — the repository's configured remotes with their fetch and
404
+ * push URLs. Remotes belong to the repository, not to a worktree, so any
405
+ * directory inside it answers the full list.
406
+ * @param deps - host dependencies.
407
+ * @param body - parsed request body: `{ repoPath }`.
408
+ */
409
+ export declare function handleRemotes(deps: RouteDeps, body: unknown): Promise<RouteOutcome>;
410
+ /**
411
+ * POST /remote — add, re-point or remove ONE remote, then answer the fresh
412
+ * list. Existence is checked against `git remote -v` first, so a duplicate
413
+ * `add` and a missing `set-url`/`remove` answer a clean 400 instead of git's
414
+ * stderr; the URL is pre-flighted by {@link remoteUrlIssue}.
415
+ * @param deps - host dependencies.
416
+ * @param body - parsed request body: `{ repoPath, action, name, url? }`.
417
+ */
418
+ export declare function handleRemote(deps: RouteDeps, body: unknown): Promise<RouteOutcome>;