@amonstack/gitea-mcp 0.2.2 → 0.3.1
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/README.md +78 -4
- package/README.zh-CN.md +69 -5
- package/dist/assets/instructions.md +19 -7
- package/dist/assets/skills/gitea-configure/SKILL.md +60 -0
- package/dist/assets/skills/gitea-resolve-repo/SKILL.md +17 -6
- package/dist/cli.js +17 -8
- package/dist/cli.js.map +1 -1
- package/dist/credentials.d.ts +145 -0
- package/dist/credentials.js +141 -0
- package/dist/credentials.js.map +1 -0
- package/dist/git-config.d.ts +97 -0
- package/dist/git-config.js +295 -0
- package/dist/git-config.js.map +1 -0
- package/dist/gitea-client.d.ts +55 -2
- package/dist/gitea-client.js +110 -8
- package/dist/gitea-client.js.map +1 -1
- package/dist/server.d.ts +3 -2
- package/dist/server.js +36 -25
- package/dist/server.js.map +1 -1
- package/dist/tools.d.ts +1 -0
- package/dist/tools.js +1 -0
- package/dist/tools.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential candidate model and state machine for fault-tolerant Gitea auth.
|
|
3
|
+
*
|
|
4
|
+
* A discovered credential is treated as an opaque candidate: the `secret`
|
|
5
|
+
* field might be a Gitea Personal Access Token, an account login password,
|
|
6
|
+
* or an OAuth token — the type cannot be determined statically from any
|
|
7
|
+
* source (`~/.git-credentials` stores whatever the user typed into git's
|
|
8
|
+
* password prompt, and `[gitea] token` / `GITEA_TOKEN` are by convention but
|
|
9
|
+
* not guarantee a PAT). The runtime therefore tries each candidate under
|
|
10
|
+
* one or more HTTP auth schemes, advancing on 401/403 until something works
|
|
11
|
+
* or every candidate × scheme is exhausted.
|
|
12
|
+
*
|
|
13
|
+
* This module is pure (no I/O, no fetch) so the state machine is unit-tested
|
|
14
|
+
* directly. Mutation is contained to per-candidate state fields; callers
|
|
15
|
+
* never replace the candidates array.
|
|
16
|
+
*
|
|
17
|
+
* SECURITY: `secret` is never logged, never interpolated into error
|
|
18
|
+
* messages, and never surfaced by `summarizeCandidates`. Diagnostic output
|
|
19
|
+
* only ever carries `secretPresent: boolean` and a masked `username`.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Decide scheme ordering for a credential-store entry based on username
|
|
23
|
+
* heuristic. Config/env candidates always use `["token"]` (per project
|
|
24
|
+
* decision to preserve their "simple token" semantics).
|
|
25
|
+
*
|
|
26
|
+
* - Username `oauth2`, `x-oauth-basic`, or empty → `["token", "basic"]`:
|
|
27
|
+
* these are git OAuth conventions; basic auth with such a username fails
|
|
28
|
+
* for real passwords, so try `token` first.
|
|
29
|
+
* - Any other (real-looking) username → `["basic", "token"]`: basic auth
|
|
30
|
+
* with the correct username works for both PATs and passwords, so it has
|
|
31
|
+
* the widest coverage and goes first.
|
|
32
|
+
*/
|
|
33
|
+
export function orderSchemesForCredentialStore(username) {
|
|
34
|
+
if (username === undefined)
|
|
35
|
+
return ["token", "basic"];
|
|
36
|
+
const u = username.toLowerCase();
|
|
37
|
+
if (u === "oauth2" || u === "x-oauth-basic") {
|
|
38
|
+
return ["token", "basic"];
|
|
39
|
+
}
|
|
40
|
+
return ["basic", "token"];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build the `Authorization` header value for one attempt. Mutates nothing.
|
|
44
|
+
*
|
|
45
|
+
* For `basic`, the username falls back to `oauth2` when absent (a missing
|
|
46
|
+
* username only happens for malformed credential-store entries; Gitea
|
|
47
|
+
* rejects basic auth without a username, so the attempt will fail and the
|
|
48
|
+
* state machine will advance).
|
|
49
|
+
*/
|
|
50
|
+
export function buildAuthHeader(candidate, scheme) {
|
|
51
|
+
if (scheme === "basic") {
|
|
52
|
+
const user = candidate.username ?? "oauth2";
|
|
53
|
+
const encoded = Buffer.from(`${user}:${candidate.secret}`).toString("base64");
|
|
54
|
+
return `Basic ${encoded}`;
|
|
55
|
+
}
|
|
56
|
+
return `token ${candidate.secret}`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Pick the next (candidate, scheme) to try. Skips exhausted candidates and
|
|
60
|
+
* candidates whose scheme list is fully tried. Does NOT mutate state — the
|
|
61
|
+
* caller records the attempt via `markAttemptFailed` / `markAttemptSucceeded`.
|
|
62
|
+
*
|
|
63
|
+
* Returns null when every candidate × scheme has been tried.
|
|
64
|
+
*/
|
|
65
|
+
export function pickNextAttempt(candidates) {
|
|
66
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
67
|
+
const c = candidates[i];
|
|
68
|
+
if (c.status === "exhausted")
|
|
69
|
+
continue;
|
|
70
|
+
// Active candidates are already locked — the caller short-circuits
|
|
71
|
+
// before entering this iteration loop (GiteaClient.request checks
|
|
72
|
+
// findActiveCandidateIndex first). Here we skip them defensively.
|
|
73
|
+
if (c.status === "active")
|
|
74
|
+
continue;
|
|
75
|
+
if (c.nextSchemeIndex >= c.schemes.length)
|
|
76
|
+
continue;
|
|
77
|
+
return { candidateIndex: i, scheme: c.schemes[c.nextSchemeIndex] };
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Record that an attempt failed with `error` (a short reason like "401").
|
|
83
|
+
* Advances the candidate's scheme index; when no schemes remain, marks the
|
|
84
|
+
* candidate as exhausted.
|
|
85
|
+
*/
|
|
86
|
+
export function markAttemptFailed(candidates, candidateIndex, error) {
|
|
87
|
+
const c = candidates[candidateIndex];
|
|
88
|
+
c.lastError = error;
|
|
89
|
+
c.lastTriedScheme = c.schemes[c.nextSchemeIndex];
|
|
90
|
+
c.nextSchemeIndex += 1;
|
|
91
|
+
if (c.nextSchemeIndex >= c.schemes.length || c.status === "active") {
|
|
92
|
+
c.status = "exhausted";
|
|
93
|
+
c.activeScheme = undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Record that an attempt succeeded. Marks the candidate as active, locks in
|
|
98
|
+
* `activeScheme`, and marks all PRIOR candidates as exhausted (they were
|
|
99
|
+
* tried and failed before this one succeeded).
|
|
100
|
+
*/
|
|
101
|
+
export function markAttemptSucceeded(candidates, candidateIndex, scheme) {
|
|
102
|
+
for (let i = 0; i < candidateIndex; i++) {
|
|
103
|
+
if (candidates[i].status !== "active")
|
|
104
|
+
candidates[i].status = "exhausted";
|
|
105
|
+
}
|
|
106
|
+
const c = candidates[candidateIndex];
|
|
107
|
+
c.status = "active";
|
|
108
|
+
c.activeScheme = scheme;
|
|
109
|
+
c.lastError = undefined;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Mask a username for diagnostic output: first character plus `***`. Returns
|
|
113
|
+
* null when the candidate has no username (config/env sources, or malformed
|
|
114
|
+
* credential-store entries).
|
|
115
|
+
*/
|
|
116
|
+
export function maskUsername(username) {
|
|
117
|
+
if (!username)
|
|
118
|
+
return null;
|
|
119
|
+
return `${username.charAt(0)}***`;
|
|
120
|
+
}
|
|
121
|
+
export function summarizeCandidates(candidates) {
|
|
122
|
+
return candidates.map((c) => ({
|
|
123
|
+
source: c.source,
|
|
124
|
+
schemes: c.schemes,
|
|
125
|
+
username: maskUsername(c.username),
|
|
126
|
+
secretPresent: c.secret.length > 0,
|
|
127
|
+
status: c.status,
|
|
128
|
+
lastTriedScheme: c.lastTriedScheme ?? null,
|
|
129
|
+
activeScheme: c.activeScheme ?? null,
|
|
130
|
+
lastError: c.lastError ?? null,
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
/** Find the index of the active candidate, or null when none is active. */
|
|
134
|
+
export function findActiveCandidateIndex(candidates) {
|
|
135
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
136
|
+
if (candidates[i].status === "active")
|
|
137
|
+
return i;
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.js","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAiEH;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,8BAA8B,CAAC,QAAiB;IAC9D,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACtD,MAAM,CAAC,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;IACjC,IAAI,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,eAAe,EAAE,CAAC;QAC5C,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC5B,CAAC;IACD,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;AAC5B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,SAA8B,EAAE,MAAkB;IAChF,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;QACvB,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,IAAI,QAAQ,CAAC;QAC5C,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAC9E,OAAO,SAAS,OAAO,EAAE,CAAC;IAC5B,CAAC;IACD,OAAO,SAAS,SAAS,CAAC,MAAM,EAAE,CAAC;AACrC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,UAAiC;IAC/D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3C,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;QACxB,IAAI,CAAC,CAAC,MAAM,KAAK,WAAW;YAAE,SAAS;QACvC,mEAAmE;QACnE,kEAAkE;QAClE,kEAAkE;QAClE,IAAI,CAAC,CAAC,MAAM,KAAK,QAAQ;YAAE,SAAS;QACpC,IAAI,CAAC,CAAC,eAAe,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM;YAAE,SAAS;QACpD,OAAO,EAAE,cAAc,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,EAAE,CAAC;IACrE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAC/B,UAAiC,EACjC,cAAsB,EACtB,KAAa;IAEb,MAAM,CAAC,GAAG,UAAU,CAAC,cAAc,CAAC,CAAC;IACrC,CAAC,CAAC,SAAS,GAAG,KAAK,CAAC;IACpB,CAAC,CAAC,eAAe,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;IACjD,CAAC,CAAC,eAAe,IAAI,CAAC,CAAC;IACvB,IAAI,CAAC,CAAC,eAAe,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QACnE,CAAC,CAAC,MAAM,GAAG,WAAW,CAAC;QACvB,CAAC,CAAC,YAAY,GAAG,SAAS,CAAC;IAC7B,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAClC,UAAiC,EACjC,cAAsB,EACtB,MAAkB;IAElB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,cAAc,EAAE,CAAC,EAAE,EAAE,CAAC;QACxC,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,QAAQ;YAAE,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,WAAW,CAAC;IAC5E,CAAC;IACD,MAAM,CAAC,GAAG,UAAU,CAAC,cAAc,CAAC,CAAC;IACrC,CAAC,CAAC,MAAM,GAAG,QAAQ,CAAC;IACpB,CAAC,CAAC,YAAY,GAAG,MAAM,CAAC;IACxB,CAAC,CAAC,SAAS,GAAG,SAAS,CAAC;AAC1B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,QAAiB;IAC5C,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3B,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;AACpC,CAAC;AAmBD,MAAM,UAAU,mBAAmB,CACjC,UAAiC;IAEjC,OAAO,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5B,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC;QAClC,aAAa,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;QAClC,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,eAAe,EAAE,CAAC,CAAC,eAAe,IAAI,IAAI;QAC1C,YAAY,EAAE,CAAC,CAAC,YAAY,IAAI,IAAI;QACpC,SAAS,EAAE,CAAC,CAAC,SAAS,IAAI,IAAI;KAC/B,CAAC,CAAC,CAAC;AACN,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,wBAAwB,CAAC,UAAiC;IACxE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3C,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,QAAQ;YAAE,OAAO,CAAC,CAAC;IAClD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { type CredentialDiscoveryResult } from "./credentials.js";
|
|
2
|
+
/** A single parsed git remote. `remote` is the remote name (`origin`, `upstream`, ...). */
|
|
3
|
+
export interface ParsedRemote {
|
|
4
|
+
remote: string;
|
|
5
|
+
url: string;
|
|
6
|
+
host: string;
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
owner: string;
|
|
9
|
+
repo: string;
|
|
10
|
+
}
|
|
11
|
+
/** A raw `[remote "<name>"]` url entry extracted from a git config file. */
|
|
12
|
+
export interface RawRemote {
|
|
13
|
+
name: string;
|
|
14
|
+
url: string;
|
|
15
|
+
}
|
|
16
|
+
export interface DiscoverOptions {
|
|
17
|
+
cwd?: string;
|
|
18
|
+
env?: NodeJS.ProcessEnv;
|
|
19
|
+
/** Override credential-store paths (defaults to XDG then `~/.git-credentials`). */
|
|
20
|
+
credentialsPaths?: string[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A parsed `~/.git-credentials` line. Both `username` and `password` are
|
|
24
|
+
* URL-decoded. Either may be absent:
|
|
25
|
+
* - `https://user:pass@host` → both present
|
|
26
|
+
* - `https://:pass@host` → only password
|
|
27
|
+
* - `https://tok@host` → only username (git stores a token here when the
|
|
28
|
+
* host accepts one as the "username"; the caller treats it as the secret)
|
|
29
|
+
*
|
|
30
|
+
* `path` is the URL pathname with leading/trailing slashes and any `.git`
|
|
31
|
+
* suffix stripped, used to narrow multiple host matches toward the target repo.
|
|
32
|
+
*/
|
|
33
|
+
export interface ParsedCredentialEntry {
|
|
34
|
+
username?: string;
|
|
35
|
+
password?: string;
|
|
36
|
+
host: string;
|
|
37
|
+
path: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Parse a git remote URL into host/baseUrl/owner/repo. Accepts `ssh://`, the
|
|
41
|
+
* scp-like `user@host:owner/repo` form, and `http(s)://`. SSH URLs derive an
|
|
42
|
+
* `https://` baseUrl because the Gitea API is served over HTTP(S); a non-standard
|
|
43
|
+
* web port cannot be inferred from an SSH URL — use an HTTPS remote or GITEA_BASE_URL.
|
|
44
|
+
*/
|
|
45
|
+
export declare function parseGitRemoteUrl(url: string, remote?: string): ParsedRemote | null;
|
|
46
|
+
/** Extract every `[remote "<name>"]` url entry from a git config file's contents. */
|
|
47
|
+
export declare function readGitRemotes(content: string): RawRemote[];
|
|
48
|
+
/** Parse all remotes in a git config file's contents, dropping unparseable urls. */
|
|
49
|
+
export declare function parseRemotes(content: string): ParsedRemote[];
|
|
50
|
+
/** Pick the remote to derive values from: `upstream` first, then `origin`, then the first. */
|
|
51
|
+
export declare function selectRemote(remotes: ParsedRemote[]): ParsedRemote | null;
|
|
52
|
+
/**
|
|
53
|
+
* Read a Gitea token from a git config. A scoped `[gitea "<baseUrl>"] token = ...`
|
|
54
|
+
* section wins; a bare `[gitea] token = ...` is the fallback. Returns undefined
|
|
55
|
+
* when neither matches.
|
|
56
|
+
*/
|
|
57
|
+
export declare function readTokenFromGitConfig(content: string, baseUrl: string): string | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Parse every credential-store line whose host matches. Each line is a URL of
|
|
60
|
+
* the form `protocol://[user[:pass]]@host[:port][/path]`; malformed and
|
|
61
|
+
* non-matching lines are skipped. Returns entries in file order; callers
|
|
62
|
+
* narrow and re-sort by repo path specificity.
|
|
63
|
+
*
|
|
64
|
+
* The `password` field — when present — holds whatever the user typed into
|
|
65
|
+
* git's password prompt: a real account password, a Personal Access Token,
|
|
66
|
+
* or an OAuth token. Git itself does not distinguish, and neither does this
|
|
67
|
+
* parser; the GiteaClient runtime tries each entry under multiple auth
|
|
68
|
+
* schemes to discover what works.
|
|
69
|
+
*/
|
|
70
|
+
export declare function parseGitCredentials(content: string, host: string): ParsedCredentialEntry[];
|
|
71
|
+
/** Default credential-store paths: `$XDG_CONFIG_HOME/git/credentials` then `~/.git-credentials`. */
|
|
72
|
+
export declare function defaultCredentialsPaths(): string[];
|
|
73
|
+
/**
|
|
74
|
+
* Discover the Gitea connection config from env + the local git context.
|
|
75
|
+
*
|
|
76
|
+
* baseUrl: `GITEA_BASE_URL` (env) wins; otherwise derived from the selected
|
|
77
|
+
* remote (`upstream` → `origin` → first). Returns null only when neither is
|
|
78
|
+
* available — callers should treat that as "do not start the server".
|
|
79
|
+
*
|
|
80
|
+
* candidates (in priority order):
|
|
81
|
+
* 1. `[gitea "<baseUrl>"] token` / bare `[gitea] token` from `.git/config`
|
|
82
|
+
* (explicit user configuration; `token` scheme only).
|
|
83
|
+
* 2. `GITEA_TOKEN` env var (explicit env; `token` scheme only — preserves
|
|
84
|
+
* the simple-token semantics).
|
|
85
|
+
* 3. Every host-matching entry in a git credential store, narrowed by repo
|
|
86
|
+
* path specificity (most specific first). Each entry may be a PAT,
|
|
87
|
+
* password, or OAuth token; the client tries each under `basic` and/or
|
|
88
|
+
* `token` schemes per the username heuristic.
|
|
89
|
+
*
|
|
90
|
+
* owner/repo: `GITEA_DEFAULT_OWNER`/`GITEA_DEFAULT_REPO` (env) win; otherwise
|
|
91
|
+
* taken from the selected remote.
|
|
92
|
+
*
|
|
93
|
+
* The result may have an empty `candidates` array (anonymous mode); the server
|
|
94
|
+
* still starts and a Skill guides the user to provide one. Write tools will
|
|
95
|
+
* fail with 401/403 until a working credential is added.
|
|
96
|
+
*/
|
|
97
|
+
export declare function discoverConfig(options?: DiscoverOptions): Promise<CredentialDiscoveryResult | null>;
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { orderSchemesForCredentialStore, } from "./credentials.js";
|
|
5
|
+
/**
|
|
6
|
+
* Parse a git remote URL into host/baseUrl/owner/repo. Accepts `ssh://`, the
|
|
7
|
+
* scp-like `user@host:owner/repo` form, and `http(s)://`. SSH URLs derive an
|
|
8
|
+
* `https://` baseUrl because the Gitea API is served over HTTP(S); a non-standard
|
|
9
|
+
* web port cannot be inferred from an SSH URL — use an HTTPS remote or GITEA_BASE_URL.
|
|
10
|
+
*/
|
|
11
|
+
export function parseGitRemoteUrl(url, remote = "origin") {
|
|
12
|
+
const u = url.trim();
|
|
13
|
+
let m = u.match(/^ssh:\/\/(?:[^@/\s]+@)?([^:/\s]+)(?::\d+)?\/([^/\s]+)\/([^/\s]+?)(?:\.git)?$/);
|
|
14
|
+
if (m) {
|
|
15
|
+
const [, host, owner, repo] = m;
|
|
16
|
+
return { remote, url: u, host, baseUrl: `https://${host}`, owner, repo };
|
|
17
|
+
}
|
|
18
|
+
m = u.match(/^(https?:)\/\/(?:[^@/\s]+@)?([^:/\s]+)(?::(\d+))?\/([^/\s]+)\/([^/\s]+?)(?:\.git)?$/);
|
|
19
|
+
if (m) {
|
|
20
|
+
const [, scheme, host, port, owner, repo] = m;
|
|
21
|
+
const baseUrl = port ? `${scheme}//${host}:${port}` : `${scheme}//${host}`;
|
|
22
|
+
return { remote, url: u, host: port ? `${host}:${port}` : host, baseUrl, owner, repo };
|
|
23
|
+
}
|
|
24
|
+
m = u.match(/^(?:[^@/\s]+@)?([^@:/\s]+):([^/\s]+)\/([^/\s]+?)(?:\.git)?$/);
|
|
25
|
+
if (m) {
|
|
26
|
+
const [, host, owner, repo] = m;
|
|
27
|
+
return { remote, url: u, host, baseUrl: `https://${host}`, owner, repo };
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
/** Extract every `[remote "<name>"]` url entry from a git config file's contents. */
|
|
32
|
+
export function readGitRemotes(content) {
|
|
33
|
+
const remotes = [];
|
|
34
|
+
let currentName = null;
|
|
35
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
36
|
+
const section = rawLine.match(/^\s*\[remote\s+"([^"]+)"\]/);
|
|
37
|
+
if (section) {
|
|
38
|
+
currentName = section[1];
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (/^\s*\[[^\]]+\]/.test(rawLine)) {
|
|
42
|
+
currentName = null;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (currentName !== null) {
|
|
46
|
+
const urlMatch = rawLine.match(/^\s*url\s*=\s*(.+?)\s*$/);
|
|
47
|
+
if (urlMatch) {
|
|
48
|
+
remotes.push({ name: currentName, url: urlMatch[1] });
|
|
49
|
+
currentName = null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return remotes;
|
|
54
|
+
}
|
|
55
|
+
/** Parse all remotes in a git config file's contents, dropping unparseable urls. */
|
|
56
|
+
export function parseRemotes(content) {
|
|
57
|
+
return readGitRemotes(content)
|
|
58
|
+
.map((r) => parseGitRemoteUrl(r.url, r.name))
|
|
59
|
+
.filter((r) => r !== null);
|
|
60
|
+
}
|
|
61
|
+
/** Pick the remote to derive values from: `upstream` first, then `origin`, then the first. */
|
|
62
|
+
export function selectRemote(remotes) {
|
|
63
|
+
if (remotes.length === 0)
|
|
64
|
+
return null;
|
|
65
|
+
return (remotes.find((r) => r.remote === "upstream") ??
|
|
66
|
+
remotes.find((r) => r.remote === "origin") ??
|
|
67
|
+
remotes[0]);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Read a Gitea token from a git config. A scoped `[gitea "<baseUrl>"] token = ...`
|
|
71
|
+
* section wins; a bare `[gitea] token = ...` is the fallback. Returns undefined
|
|
72
|
+
* when neither matches.
|
|
73
|
+
*/
|
|
74
|
+
export function readTokenFromGitConfig(content, baseUrl) {
|
|
75
|
+
const escaped = baseUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
76
|
+
const scoped = content.match(new RegExp(`\\[gitea\\s+"${escaped}"\\]([\\s\\S]*?)(?=\\n\\s*\\[|$)`));
|
|
77
|
+
if (scoped) {
|
|
78
|
+
const t = scoped[1].match(/^\s*token\s*=\s*(.+?)\s*$/m);
|
|
79
|
+
if (t)
|
|
80
|
+
return t[1];
|
|
81
|
+
}
|
|
82
|
+
const globalSection = content.match(/(?:^|\n)\[gitea\][\t ]*([\s\S]*?)(?=\n\s*\[|$)/);
|
|
83
|
+
if (globalSection) {
|
|
84
|
+
const t = globalSection[1].match(/^\s*token\s*=\s*(.+?)\s*$/m);
|
|
85
|
+
if (t)
|
|
86
|
+
return t[1];
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Parse every credential-store line whose host matches. Each line is a URL of
|
|
92
|
+
* the form `protocol://[user[:pass]]@host[:port][/path]`; malformed and
|
|
93
|
+
* non-matching lines are skipped. Returns entries in file order; callers
|
|
94
|
+
* narrow and re-sort by repo path specificity.
|
|
95
|
+
*
|
|
96
|
+
* The `password` field — when present — holds whatever the user typed into
|
|
97
|
+
* git's password prompt: a real account password, a Personal Access Token,
|
|
98
|
+
* or an OAuth token. Git itself does not distinguish, and neither does this
|
|
99
|
+
* parser; the GiteaClient runtime tries each entry under multiple auth
|
|
100
|
+
* schemes to discover what works.
|
|
101
|
+
*/
|
|
102
|
+
export function parseGitCredentials(content, host) {
|
|
103
|
+
const entries = [];
|
|
104
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
105
|
+
const line = rawLine.trim();
|
|
106
|
+
if (!line || line.startsWith("#"))
|
|
107
|
+
continue;
|
|
108
|
+
try {
|
|
109
|
+
const credUrl = new URL(line);
|
|
110
|
+
if (credUrl.host !== host)
|
|
111
|
+
continue;
|
|
112
|
+
entries.push({
|
|
113
|
+
username: credUrl.username ? decodeURIComponent(credUrl.username) : undefined,
|
|
114
|
+
password: credUrl.password ? decodeURIComponent(credUrl.password) : undefined,
|
|
115
|
+
host: credUrl.host,
|
|
116
|
+
path: credUrl.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, ""),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return entries;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Score a credential entry's path specificity against the target repo path
|
|
127
|
+
* (`owner/repo`). Higher = more specific. Used to narrow multiple host
|
|
128
|
+
* matches toward the most relevant entry first.
|
|
129
|
+
*
|
|
130
|
+
* - Path is empty (host-only entry) → 0
|
|
131
|
+
* - Path is a prefix of the repo path → length of the matching path
|
|
132
|
+
* - Path does not match → -1 (deprioritized but still tried as a fallback)
|
|
133
|
+
*/
|
|
134
|
+
function scoreEntryPath(entryPath, repoPath) {
|
|
135
|
+
if (!entryPath)
|
|
136
|
+
return 0;
|
|
137
|
+
if (!repoPath)
|
|
138
|
+
return 0;
|
|
139
|
+
if (repoPath === entryPath || repoPath.startsWith(`${entryPath}/`)) {
|
|
140
|
+
return entryPath.length;
|
|
141
|
+
}
|
|
142
|
+
return -1;
|
|
143
|
+
}
|
|
144
|
+
/** Default credential-store paths: `$XDG_CONFIG_HOME/git/credentials` then `~/.git-credentials`. */
|
|
145
|
+
export function defaultCredentialsPaths() {
|
|
146
|
+
const paths = [];
|
|
147
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
148
|
+
if (xdg)
|
|
149
|
+
paths.push(join(xdg, "git", "credentials"));
|
|
150
|
+
paths.push(join(homedir(), ".git-credentials"));
|
|
151
|
+
return paths;
|
|
152
|
+
}
|
|
153
|
+
async function readOptionalFile(path) {
|
|
154
|
+
try {
|
|
155
|
+
return await readFile(path, "utf-8");
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
if (err.code !== "ENOENT")
|
|
159
|
+
throw err;
|
|
160
|
+
return "";
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Build a `CandidateCredential` from a parsed credential-store entry.
|
|
165
|
+
*
|
|
166
|
+
* - `https://:pass@host` or `https://user:pass@host` → secret = pass,
|
|
167
|
+
* username preserved (basic auth needs it).
|
|
168
|
+
* - `https://tok@host` (username but no password) → secret = tok, no
|
|
169
|
+
* username: this is git's "store the token as the username" convention.
|
|
170
|
+
* Try `token` first (most common), fall back to `basic`.
|
|
171
|
+
*/
|
|
172
|
+
function candidateFromEntry(entry) {
|
|
173
|
+
if (entry.password) {
|
|
174
|
+
return {
|
|
175
|
+
source: "credential-store",
|
|
176
|
+
username: entry.username,
|
|
177
|
+
secret: entry.password,
|
|
178
|
+
schemes: orderSchemesForCredentialStore(entry.username),
|
|
179
|
+
status: "pending",
|
|
180
|
+
nextSchemeIndex: 0,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (entry.username) {
|
|
184
|
+
return {
|
|
185
|
+
source: "credential-store",
|
|
186
|
+
secret: entry.username,
|
|
187
|
+
schemes: ["token", "basic"],
|
|
188
|
+
status: "pending",
|
|
189
|
+
nextSchemeIndex: 0,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Discover the Gitea connection config from env + the local git context.
|
|
196
|
+
*
|
|
197
|
+
* baseUrl: `GITEA_BASE_URL` (env) wins; otherwise derived from the selected
|
|
198
|
+
* remote (`upstream` → `origin` → first). Returns null only when neither is
|
|
199
|
+
* available — callers should treat that as "do not start the server".
|
|
200
|
+
*
|
|
201
|
+
* candidates (in priority order):
|
|
202
|
+
* 1. `[gitea "<baseUrl>"] token` / bare `[gitea] token` from `.git/config`
|
|
203
|
+
* (explicit user configuration; `token` scheme only).
|
|
204
|
+
* 2. `GITEA_TOKEN` env var (explicit env; `token` scheme only — preserves
|
|
205
|
+
* the simple-token semantics).
|
|
206
|
+
* 3. Every host-matching entry in a git credential store, narrowed by repo
|
|
207
|
+
* path specificity (most specific first). Each entry may be a PAT,
|
|
208
|
+
* password, or OAuth token; the client tries each under `basic` and/or
|
|
209
|
+
* `token` schemes per the username heuristic.
|
|
210
|
+
*
|
|
211
|
+
* owner/repo: `GITEA_DEFAULT_OWNER`/`GITEA_DEFAULT_REPO` (env) win; otherwise
|
|
212
|
+
* taken from the selected remote.
|
|
213
|
+
*
|
|
214
|
+
* The result may have an empty `candidates` array (anonymous mode); the server
|
|
215
|
+
* still starts and a Skill guides the user to provide one. Write tools will
|
|
216
|
+
* fail with 401/403 until a working credential is added.
|
|
217
|
+
*/
|
|
218
|
+
export async function discoverConfig(options = {}) {
|
|
219
|
+
const cwd = options.cwd ?? process.cwd();
|
|
220
|
+
const env = options.env ?? process.env;
|
|
221
|
+
const envBaseUrl = env.GITEA_BASE_URL;
|
|
222
|
+
const gitConfigContent = await readOptionalFile(join(cwd, ".git", "config"));
|
|
223
|
+
const parsedRemotes = parseRemotes(gitConfigContent);
|
|
224
|
+
const selected = selectRemote(parsedRemotes);
|
|
225
|
+
const baseUrl = envBaseUrl ?? selected?.baseUrl;
|
|
226
|
+
if (!baseUrl)
|
|
227
|
+
return null;
|
|
228
|
+
let host;
|
|
229
|
+
if (envBaseUrl) {
|
|
230
|
+
try {
|
|
231
|
+
host = new URL(envBaseUrl).host;
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
host = undefined;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
host = selected?.host;
|
|
239
|
+
}
|
|
240
|
+
const candidates = [];
|
|
241
|
+
// Source 1: .git/config [gitea "<baseUrl>"] token / bare [gitea] token.
|
|
242
|
+
if (host) {
|
|
243
|
+
const configToken = readTokenFromGitConfig(gitConfigContent, baseUrl);
|
|
244
|
+
if (configToken) {
|
|
245
|
+
candidates.push({
|
|
246
|
+
source: "gitea-config",
|
|
247
|
+
secret: configToken,
|
|
248
|
+
schemes: ["token"],
|
|
249
|
+
status: "pending",
|
|
250
|
+
nextSchemeIndex: 0,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Source 2: GITEA_TOKEN env (simple-token semantics — no scheme probing).
|
|
255
|
+
const envToken = env.GITEA_TOKEN;
|
|
256
|
+
if (envToken) {
|
|
257
|
+
candidates.push({
|
|
258
|
+
source: "env",
|
|
259
|
+
secret: envToken,
|
|
260
|
+
schemes: ["token"],
|
|
261
|
+
status: "pending",
|
|
262
|
+
nextSchemeIndex: 0,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
// Source 3..N: credential-store entries, host-matched and path-narrowed.
|
|
266
|
+
if (host) {
|
|
267
|
+
const paths = options.credentialsPaths ?? defaultCredentialsPaths();
|
|
268
|
+
const repoPath = selected ? `${selected.owner}/${selected.repo}` : "";
|
|
269
|
+
const scored = [];
|
|
270
|
+
let order = 0;
|
|
271
|
+
for (const path of paths) {
|
|
272
|
+
const cred = await readOptionalFile(path);
|
|
273
|
+
if (!cred)
|
|
274
|
+
continue;
|
|
275
|
+
for (const entry of parseGitCredentials(cred, host)) {
|
|
276
|
+
scored.push({ entry, score: scoreEntryPath(entry.path, repoPath), order: order++ });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// Sort by score desc; stable within same score (preserve file/discovery order).
|
|
280
|
+
scored.sort((a, b) => b.score - a.score || a.order - b.order);
|
|
281
|
+
for (const { entry } of scored) {
|
|
282
|
+
const candidate = candidateFromEntry(entry);
|
|
283
|
+
if (candidate)
|
|
284
|
+
candidates.push(candidate);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
baseUrl,
|
|
289
|
+
defaultOwner: env.GITEA_DEFAULT_OWNER ?? selected?.owner,
|
|
290
|
+
defaultRepo: env.GITEA_DEFAULT_REPO ?? selected?.repo,
|
|
291
|
+
remote: selected?.remote,
|
|
292
|
+
candidates,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
//# sourceMappingURL=git-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-config.js","sourceRoot":"","sources":["../src/git-config.ts"],"names":[],"mappings":"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"}
|
package/dist/gitea-client.d.ts
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
|
+
import { type CandidateCredential, type CandidateSummary } from "./credentials.js";
|
|
1
2
|
export interface GiteaConfig {
|
|
2
3
|
baseUrl: string;
|
|
3
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Legacy single-token mode. When `candidates` is omitted, this is wrapped
|
|
6
|
+
* as a one-element candidate list with the `token` scheme (preserving the
|
|
7
|
+
* pre-multi-credential behavior exactly).
|
|
8
|
+
*/
|
|
9
|
+
token?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Credential candidates in priority order. When provided, enables the
|
|
12
|
+
* fault-tolerant auth state machine: each candidate × scheme is tried in
|
|
13
|
+
* order until one succeeds, with 401/403 advancing to the next attempt.
|
|
14
|
+
*/
|
|
15
|
+
candidates?: CandidateCredential[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* HTTP error from the Gitea API. Carries `status` as a structured field so
|
|
19
|
+
* callers (the retry loop, tests) can branch on it without parsing the
|
|
20
|
+
* message string (AGENTS.md §2.3 forbids substring-based control flow).
|
|
21
|
+
*/
|
|
22
|
+
export declare class GiteaApiError extends Error {
|
|
23
|
+
readonly status: number;
|
|
24
|
+
readonly statusText: string;
|
|
25
|
+
readonly body: string;
|
|
26
|
+
constructor(status: number, statusText: string, body: string);
|
|
4
27
|
}
|
|
5
28
|
export interface Issue {
|
|
6
29
|
id: number;
|
|
@@ -140,8 +163,38 @@ export interface UpdateMilestoneParams {
|
|
|
140
163
|
}
|
|
141
164
|
export declare class GiteaClient {
|
|
142
165
|
private baseUrl;
|
|
143
|
-
private
|
|
166
|
+
private candidates;
|
|
144
167
|
constructor(config: GiteaConfig);
|
|
168
|
+
/**
|
|
169
|
+
* Snapshot of the credential state machine — for the `gitea_status` tool.
|
|
170
|
+
* Secrets are never included; only `secretPresent: boolean` and a masked
|
|
171
|
+
* `username`. See `summarizeCandidates` in `credentials.ts`.
|
|
172
|
+
*/
|
|
173
|
+
getCredentialStatus(): {
|
|
174
|
+
candidates: CandidateSummary[];
|
|
175
|
+
activeIndex: number | null;
|
|
176
|
+
totalCandidates: number;
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* Single HTTP call. Throws `GiteaApiError` on non-2xx so the retry loop can
|
|
180
|
+
* branch on `status` (never on the message string). The `authHeader` is
|
|
181
|
+
* pre-built by the caller from the active candidate + scheme.
|
|
182
|
+
*/
|
|
183
|
+
private doRequest;
|
|
184
|
+
/**
|
|
185
|
+
* Auth-aware request entry point. Three modes:
|
|
186
|
+
*
|
|
187
|
+
* 1. Active candidate exists (a prior attempt succeeded): reuse its locked
|
|
188
|
+
* scheme directly, no iteration.
|
|
189
|
+
* 2. No candidates at all: anonymous request (no Authorization header).
|
|
190
|
+
* 3. Otherwise: iterate (candidate, scheme) pairs in priority order, trying
|
|
191
|
+
* each until one succeeds. On 401/403 the current attempt is marked
|
|
192
|
+
* failed and the next is tried; non-auth errors propagate immediately
|
|
193
|
+
* (we do NOT mask 5xx / network errors as auth failures). When every
|
|
194
|
+
* candidate × scheme is exhausted, the most recent `GiteaApiError` is
|
|
195
|
+
* re-thrown so the caller sees the underlying status/body; the
|
|
196
|
+
* `gitea_status` tool surfaces the full attempt history.
|
|
197
|
+
*/
|
|
145
198
|
private request;
|
|
146
199
|
listIssues(params: ListIssuesParams): Promise<Issue[]>;
|
|
147
200
|
getIssue(owner: string, repo: string, index: number): Promise<Issue>;
|