@g-abhishek/gitx 0.1.1 → 0.1.4

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.
Files changed (161) hide show
  1. package/README.md +374 -3
  2. package/dist/ai/claudeAi.d.ts +35 -0
  3. package/dist/ai/claudeAi.d.ts.map +1 -0
  4. package/dist/ai/claudeAi.js +396 -0
  5. package/dist/ai/claudeAi.js.map +1 -0
  6. package/dist/ai/claudeCliAi.d.ts +27 -0
  7. package/dist/ai/claudeCliAi.d.ts.map +1 -0
  8. package/dist/ai/claudeCliAi.js +312 -0
  9. package/dist/ai/claudeCliAi.js.map +1 -0
  10. package/dist/ai/localClaudeAi.d.ts +2 -0
  11. package/dist/ai/localClaudeAi.d.ts.map +1 -0
  12. package/dist/ai/localClaudeAi.js +4 -0
  13. package/dist/ai/localClaudeAi.js.map +1 -0
  14. package/dist/ai/mockAi.d.ts +8 -1
  15. package/dist/ai/mockAi.d.ts.map +1 -1
  16. package/dist/ai/mockAi.js +57 -0
  17. package/dist/ai/mockAi.js.map +1 -1
  18. package/dist/ai/openAiAi.d.ts +33 -0
  19. package/dist/ai/openAiAi.d.ts.map +1 -0
  20. package/dist/ai/openAiAi.js +388 -0
  21. package/dist/ai/openAiAi.js.map +1 -0
  22. package/dist/ai/reviewHelpers.d.ts +66 -0
  23. package/dist/ai/reviewHelpers.d.ts.map +1 -0
  24. package/dist/ai/reviewHelpers.js +559 -0
  25. package/dist/ai/reviewHelpers.js.map +1 -0
  26. package/dist/ai/types.d.ts +247 -0
  27. package/dist/ai/types.d.ts.map +1 -1
  28. package/dist/ai/types.js.map +1 -1
  29. package/dist/cli/commands/ask.d.ts +27 -0
  30. package/dist/cli/commands/ask.d.ts.map +1 -0
  31. package/dist/cli/commands/ask.js +230 -0
  32. package/dist/cli/commands/ask.js.map +1 -0
  33. package/dist/cli/commands/commit.d.ts +16 -0
  34. package/dist/cli/commands/commit.d.ts.map +1 -0
  35. package/dist/cli/commands/commit.js +163 -0
  36. package/dist/cli/commands/commit.js.map +1 -0
  37. package/dist/cli/commands/config.d.ts +4 -0
  38. package/dist/cli/commands/config.d.ts.map +1 -0
  39. package/dist/cli/commands/config.js +666 -0
  40. package/dist/cli/commands/config.js.map +1 -0
  41. package/dist/cli/commands/implement.d.ts.map +1 -1
  42. package/dist/cli/commands/implement.js +149 -28
  43. package/dist/cli/commands/implement.js.map +1 -1
  44. package/dist/cli/commands/init.d.ts +4 -0
  45. package/dist/cli/commands/init.d.ts.map +1 -1
  46. package/dist/cli/commands/init.js +7 -54
  47. package/dist/cli/commands/init.js.map +1 -1
  48. package/dist/cli/commands/port.d.ts +32 -0
  49. package/dist/cli/commands/port.d.ts.map +1 -0
  50. package/dist/cli/commands/port.js +554 -0
  51. package/dist/cli/commands/port.js.map +1 -0
  52. package/dist/cli/commands/pr/close.d.ts +15 -0
  53. package/dist/cli/commands/pr/close.d.ts.map +1 -0
  54. package/dist/cli/commands/pr/close.js +71 -0
  55. package/dist/cli/commands/pr/close.js.map +1 -0
  56. package/dist/cli/commands/pr/create.d.ts +17 -0
  57. package/dist/cli/commands/pr/create.d.ts.map +1 -1
  58. package/dist/cli/commands/pr/create.js +209 -5
  59. package/dist/cli/commands/pr/create.js.map +1 -1
  60. package/dist/cli/commands/pr/fixComments.d.ts.map +1 -1
  61. package/dist/cli/commands/pr/fixComments.js +77 -5
  62. package/dist/cli/commands/pr/fixComments.js.map +1 -1
  63. package/dist/cli/commands/pr/index.d.ts.map +1 -1
  64. package/dist/cli/commands/pr/index.js +4 -0
  65. package/dist/cli/commands/pr/index.js.map +1 -1
  66. package/dist/cli/commands/pr/list.d.ts.map +1 -1
  67. package/dist/cli/commands/pr/list.js +26 -3
  68. package/dist/cli/commands/pr/list.js.map +1 -1
  69. package/dist/cli/commands/pr/merge.d.ts +23 -0
  70. package/dist/cli/commands/pr/merge.d.ts.map +1 -0
  71. package/dist/cli/commands/pr/merge.js +191 -0
  72. package/dist/cli/commands/pr/merge.js.map +1 -0
  73. package/dist/cli/commands/pr/review.d.ts.map +1 -1
  74. package/dist/cli/commands/pr/review.js +123 -5
  75. package/dist/cli/commands/pr/review.js.map +1 -1
  76. package/dist/cli/commands/push.d.ts +16 -0
  77. package/dist/cli/commands/push.d.ts.map +1 -0
  78. package/dist/cli/commands/push.js +166 -0
  79. package/dist/cli/commands/push.js.map +1 -0
  80. package/dist/cli/commands/sync.d.ts +24 -0
  81. package/dist/cli/commands/sync.d.ts.map +1 -0
  82. package/dist/cli/commands/sync.js +414 -0
  83. package/dist/cli/commands/sync.js.map +1 -0
  84. package/dist/cli/index.d.ts.map +1 -1
  85. package/dist/cli/index.js +34 -6
  86. package/dist/cli/index.js.map +1 -1
  87. package/dist/config/config.d.ts +20 -3
  88. package/dist/config/config.d.ts.map +1 -1
  89. package/dist/config/config.js +103 -24
  90. package/dist/config/config.js.map +1 -1
  91. package/dist/config/schema.d.ts.map +1 -1
  92. package/dist/config/schema.js +70 -9
  93. package/dist/config/schema.js.map +1 -1
  94. package/dist/core/context.d.ts +13 -0
  95. package/dist/core/context.d.ts.map +1 -0
  96. package/dist/core/context.js +2 -0
  97. package/dist/core/context.js.map +1 -0
  98. package/dist/core/gitx.d.ts +47 -0
  99. package/dist/core/gitx.d.ts.map +1 -1
  100. package/dist/core/gitx.js +204 -9
  101. package/dist/core/gitx.js.map +1 -1
  102. package/dist/index.d.ts +1 -5
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.js +4 -1
  105. package/dist/index.js.map +1 -1
  106. package/dist/providers/azure.d.ts +26 -0
  107. package/dist/providers/azure.d.ts.map +1 -0
  108. package/dist/providers/azure.js +256 -0
  109. package/dist/providers/azure.js.map +1 -0
  110. package/dist/providers/base.d.ts +104 -0
  111. package/dist/providers/base.d.ts.map +1 -0
  112. package/dist/providers/base.js +5 -0
  113. package/dist/providers/base.js.map +1 -0
  114. package/dist/providers/factory.d.ts +8 -0
  115. package/dist/providers/factory.d.ts.map +1 -0
  116. package/dist/providers/factory.js +25 -0
  117. package/dist/providers/factory.js.map +1 -0
  118. package/dist/providers/github.d.ts +19 -0
  119. package/dist/providers/github.d.ts.map +1 -0
  120. package/dist/providers/github.js +291 -0
  121. package/dist/providers/github.js.map +1 -0
  122. package/dist/providers/gitlab.d.ts +19 -0
  123. package/dist/providers/gitlab.d.ts.map +1 -0
  124. package/dist/providers/gitlab.js +186 -0
  125. package/dist/providers/gitlab.js.map +1 -0
  126. package/dist/types/config.d.ts +53 -9
  127. package/dist/types/config.d.ts.map +1 -1
  128. package/dist/types/config.js.map +1 -1
  129. package/dist/utils/azureAuth.d.ts +51 -0
  130. package/dist/utils/azureAuth.d.ts.map +1 -0
  131. package/dist/utils/azureAuth.js +172 -0
  132. package/dist/utils/azureAuth.js.map +1 -0
  133. package/dist/utils/git.d.ts +22 -0
  134. package/dist/utils/git.d.ts.map +1 -1
  135. package/dist/utils/git.js +63 -7
  136. package/dist/utils/git.js.map +1 -1
  137. package/dist/utils/gitOps.d.ts +118 -0
  138. package/dist/utils/gitOps.d.ts.map +1 -0
  139. package/dist/utils/gitOps.js +380 -0
  140. package/dist/utils/gitOps.js.map +1 -0
  141. package/dist/utils/lockFile.d.ts +13 -0
  142. package/dist/utils/lockFile.d.ts.map +1 -0
  143. package/dist/utils/lockFile.js +54 -0
  144. package/dist/utils/lockFile.js.map +1 -0
  145. package/dist/utils/retry.d.ts +10 -0
  146. package/dist/utils/retry.d.ts.map +1 -0
  147. package/dist/utils/retry.js +31 -0
  148. package/dist/utils/retry.js.map +1 -0
  149. package/dist/workflows/implement.d.ts +41 -0
  150. package/dist/workflows/implement.d.ts.map +1 -0
  151. package/dist/workflows/implement.js +219 -0
  152. package/dist/workflows/implement.js.map +1 -0
  153. package/dist/workflows/pr.d.ts +41 -0
  154. package/dist/workflows/pr.d.ts.map +1 -0
  155. package/dist/workflows/pr.js +285 -0
  156. package/dist/workflows/pr.js.map +1 -0
  157. package/dist/workflows/prAddress.d.ts +55 -0
  158. package/dist/workflows/prAddress.d.ts.map +1 -0
  159. package/dist/workflows/prAddress.js +349 -0
  160. package/dist/workflows/prAddress.js.map +1 -0
  161. package/package.json +1 -1
@@ -0,0 +1,186 @@
1
+ import axios, { isAxiosError } from "axios";
2
+ import { withRetry } from "../utils/retry.js";
3
+ import { GitxError } from "../utils/errors.js";
4
+ // ─── GitLab Provider ──────────────────────────────────────────────────────────
5
+ export class GitLabProvider {
6
+ http;
7
+ constructor(token) {
8
+ this.http = axios.create({
9
+ baseURL: "https://gitlab.com/api/v4",
10
+ headers: {
11
+ "PRIVATE-TOKEN": token,
12
+ "Content-Type": "application/json",
13
+ },
14
+ timeout: 20_000,
15
+ });
16
+ }
17
+ /** GitLab requires URL-encoded namespace/project slugs */
18
+ enc(slug) {
19
+ return encodeURIComponent(slug);
20
+ }
21
+ async listPRs(repoSlug) {
22
+ try {
23
+ const { data } = await withRetry(() => this.http.get(`/projects/${this.enc(repoSlug)}/merge_requests`, { params: { state: "opened", per_page: 50 } }));
24
+ return data.map(mapGlMr);
25
+ }
26
+ catch (err) {
27
+ throw wrapGlError(err, "list MRs");
28
+ }
29
+ }
30
+ async getPR(repoSlug, prNumber) {
31
+ try {
32
+ const { data } = await this.http.get(`/projects/${this.enc(repoSlug)}/merge_requests/${prNumber}`);
33
+ return mapGlMr(data);
34
+ }
35
+ catch (err) {
36
+ throw wrapGlError(err, `get MR !${prNumber}`);
37
+ }
38
+ }
39
+ async createPR(repoSlug, opts) {
40
+ try {
41
+ const { data } = await this.http.post(`/projects/${this.enc(repoSlug)}/merge_requests`, {
42
+ title: opts.title,
43
+ description: opts.body,
44
+ source_branch: opts.head,
45
+ target_branch: opts.base,
46
+ draft: opts.draft ?? false,
47
+ });
48
+ return mapGlMr(data);
49
+ }
50
+ catch (err) {
51
+ throw wrapGlError(err, "create MR");
52
+ }
53
+ }
54
+ async getPRComments(repoSlug, prNumber) {
55
+ try {
56
+ const { data } = await this.http.get(`/projects/${this.enc(repoSlug)}/merge_requests/${prNumber}/notes`, { params: { per_page: 100 } });
57
+ return data
58
+ .filter((n) => !n.system)
59
+ .map((n) => ({
60
+ id: n.id,
61
+ body: n.body,
62
+ author: n.author.username,
63
+ createdAt: n.created_at,
64
+ }));
65
+ }
66
+ catch (err) {
67
+ throw wrapGlError(err, `get MR !${prNumber} notes`);
68
+ }
69
+ }
70
+ async addPRComment(repoSlug, prNumber, body) {
71
+ try {
72
+ await this.http.post(`/projects/${this.enc(repoSlug)}/merge_requests/${prNumber}/notes`, { body });
73
+ }
74
+ catch (err) {
75
+ throw wrapGlError(err, `comment on MR !${prNumber}`);
76
+ }
77
+ }
78
+ async getPRDiff(repoSlug, prNumber) {
79
+ try {
80
+ const { data } = await this.http.get(`/projects/${this.enc(repoSlug)}/merge_requests/${prNumber}/diffs`);
81
+ return data.map((d) => `--- a/${d.old_path}\n+++ b/${d.new_path}\n${d.diff}`).join("\n\n");
82
+ }
83
+ catch {
84
+ return "";
85
+ }
86
+ }
87
+ async mergePR(repoSlug, prNumber, opts) {
88
+ try {
89
+ await this.http.put(`/projects/${this.enc(repoSlug)}/merge_requests/${prNumber}/merge`, {
90
+ squash: opts.method === "squash",
91
+ merge_commit_message: opts.commitTitle ?? undefined,
92
+ should_remove_source_branch: opts.deleteSourceBranch ?? false,
93
+ });
94
+ }
95
+ catch (err) {
96
+ throw wrapGlError(err, `merge MR !${prNumber}`);
97
+ }
98
+ }
99
+ async closePR(repoSlug, prNumber) {
100
+ try {
101
+ await this.http.put(`/projects/${this.enc(repoSlug)}/merge_requests/${prNumber}`, { state_event: "close" });
102
+ }
103
+ catch (err) {
104
+ throw wrapGlError(err, `close MR !${prNumber}`);
105
+ }
106
+ }
107
+ async submitPRReview(repoSlug, prNumber, opts) {
108
+ // Build the overall review note with verdict prefix
109
+ const verdictPrefix = opts.event === "approve" ? "✅ **APPROVED**" :
110
+ opts.event === "request_changes" ? "🔴 **CHANGES REQUESTED**" : "💬 **REVIEW COMMENT**";
111
+ const overallBody = `${verdictPrefix}\n\n${opts.body}`;
112
+ try {
113
+ // Post the summary as a plain MR note
114
+ await this.http.post(`/projects/${this.enc(repoSlug)}/merge_requests/${prNumber}/notes`, { body: overallBody });
115
+ // Post inline comments as MR notes with file+line context embedded in the body
116
+ // (GitLab discussion positions require SHAs; use enriched body as fallback)
117
+ for (const c of opts.comments ?? []) {
118
+ const inlineBody = `**\`${c.path}:${c.line}\`**\n\n${c.body}`;
119
+ await this.http.post(`/projects/${this.enc(repoSlug)}/merge_requests/${prNumber}/notes`, { body: inlineBody }).catch(() => { });
120
+ }
121
+ }
122
+ catch (err) {
123
+ throw wrapGlError(err, `submit review on MR !${prNumber}`);
124
+ }
125
+ }
126
+ async replyToComment(repoSlug, prNumber, commentId, body) {
127
+ try {
128
+ // GitLab: add a note to the MR (thread replies require discussion IDs which we don't store)
129
+ const replyBody = `*(in reply to comment #${commentId})*\n\n${body}`;
130
+ await this.http.post(`/projects/${this.enc(repoSlug)}/merge_requests/${prNumber}/notes`, { body: replyBody });
131
+ }
132
+ catch (err) {
133
+ throw wrapGlError(err, `reply to comment #${commentId}`);
134
+ }
135
+ }
136
+ async getDefaultBranch(repoSlug) {
137
+ try {
138
+ const { data } = await withRetry(() => this.http.get(`/projects/${this.enc(repoSlug)}`));
139
+ return data.default_branch ?? "main";
140
+ }
141
+ catch (err) {
142
+ throw wrapGlError(err, "get default branch");
143
+ }
144
+ }
145
+ }
146
+ // ─── Mappers ──────────────────────────────────────────────────────────────────
147
+ function mapGlMr(d) {
148
+ let state;
149
+ if (d.state === "merged")
150
+ state = "merged";
151
+ else if (d.state === "opened")
152
+ state = "open";
153
+ else
154
+ state = "closed";
155
+ return {
156
+ id: d.id,
157
+ number: d.iid,
158
+ title: d.title,
159
+ body: d.description ?? "",
160
+ state,
161
+ head: d.source_branch,
162
+ base: d.target_branch,
163
+ url: d.web_url,
164
+ author: d.author?.username ?? "unknown",
165
+ createdAt: d.created_at,
166
+ updatedAt: d.updated_at,
167
+ };
168
+ }
169
+ function wrapGlError(err, action) {
170
+ if (isAxiosError(err)) {
171
+ const status = err.response?.status;
172
+ const msg = err.response?.data?.message ?? err.message;
173
+ if (status === 401) {
174
+ return new GitxError(`GitLab authentication failed while trying to ${action}. Check your token with \`gitx config set-provider gitlab\`.`, { exitCode: 1, cause: err });
175
+ }
176
+ if (status === 404) {
177
+ return new GitxError(`GitLab resource not found while trying to ${action}. Verify the repo slug and token scopes.`, { exitCode: 1, cause: err });
178
+ }
179
+ return new GitxError(`GitLab API error (${status ?? "network"}) while trying to ${action}: ${String(msg)}`, { exitCode: 1, cause: err });
180
+ }
181
+ return new GitxError(`Unexpected error while trying to ${action}: ${String(err)}`, {
182
+ exitCode: 1,
183
+ cause: err,
184
+ });
185
+ }
186
+ //# sourceMappingURL=gitlab.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitlab.js","sourceRoot":"","sources":["../../src/providers/gitlab.ts"],"names":[],"mappings":"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","sourcesContent":["import axios, { type AxiosInstance, isAxiosError } from \"axios\";\nimport { withRetry } from \"../utils/retry.js\";\nimport { GitxError } from \"../utils/errors.js\";\nimport type {\n CreatePrOptions,\n GitProvider,\n MergePrOptions,\n PullRequest,\n PullRequestComment,\n SubmitReviewOptions,\n} from \"./base.js\";\n\n// ─── Raw GitLab API shapes ────────────────────────────────────────────────────\ninterface GlMr {\n id: number;\n iid: number;\n title: string;\n description: string | null;\n state: string; // \"opened\" | \"closed\" | \"merged\" | \"locked\"\n source_branch: string;\n target_branch: string;\n web_url: string;\n author: { username: string } | null;\n created_at: string;\n updated_at: string;\n}\n\ninterface GlNote {\n id: number;\n body: string;\n author: { username: string };\n created_at: string;\n system: boolean;\n}\n\ninterface GlProject {\n default_branch: string;\n}\n\n// ─── GitLab Provider ──────────────────────────────────────────────────────────\nexport class GitLabProvider implements GitProvider {\n private readonly http: AxiosInstance;\n\n constructor(token: string) {\n this.http = axios.create({\n baseURL: \"https://gitlab.com/api/v4\",\n headers: {\n \"PRIVATE-TOKEN\": token,\n \"Content-Type\": \"application/json\",\n },\n timeout: 20_000,\n });\n }\n\n /** GitLab requires URL-encoded namespace/project slugs */\n private enc(slug: string): string {\n return encodeURIComponent(slug);\n }\n\n async listPRs(repoSlug: string): Promise<PullRequest[]> {\n try {\n const { data } = await withRetry(() => this.http.get<GlMr[]>(\n `/projects/${this.enc(repoSlug)}/merge_requests`,\n { params: { state: \"opened\", per_page: 50 } }\n ));\n return data.map(mapGlMr);\n } catch (err) {\n throw wrapGlError(err, \"list MRs\");\n }\n }\n\n async getPR(repoSlug: string, prNumber: number): Promise<PullRequest> {\n try {\n const { data } = await this.http.get<GlMr>(\n `/projects/${this.enc(repoSlug)}/merge_requests/${prNumber}`\n );\n return mapGlMr(data);\n } catch (err) {\n throw wrapGlError(err, `get MR !${prNumber}`);\n }\n }\n\n async createPR(repoSlug: string, opts: CreatePrOptions): Promise<PullRequest> {\n try {\n const { data } = await this.http.post<GlMr>(\n `/projects/${this.enc(repoSlug)}/merge_requests`,\n {\n title: opts.title,\n description: opts.body,\n source_branch: opts.head,\n target_branch: opts.base,\n draft: opts.draft ?? false,\n }\n );\n return mapGlMr(data);\n } catch (err) {\n throw wrapGlError(err, \"create MR\");\n }\n }\n\n async getPRComments(repoSlug: string, prNumber: number): Promise<PullRequestComment[]> {\n try {\n const { data } = await this.http.get<GlNote[]>(\n `/projects/${this.enc(repoSlug)}/merge_requests/${prNumber}/notes`,\n { params: { per_page: 100 } }\n );\n return data\n .filter((n) => !n.system)\n .map((n) => ({\n id: n.id,\n body: n.body,\n author: n.author.username,\n createdAt: n.created_at,\n }));\n } catch (err) {\n throw wrapGlError(err, `get MR !${prNumber} notes`);\n }\n }\n\n async addPRComment(repoSlug: string, prNumber: number, body: string): Promise<void> {\n try {\n await this.http.post(\n `/projects/${this.enc(repoSlug)}/merge_requests/${prNumber}/notes`,\n { body }\n );\n } catch (err) {\n throw wrapGlError(err, `comment on MR !${prNumber}`);\n }\n }\n\n\n async getPRDiff(repoSlug: string, prNumber: number): Promise<string> {\n try {\n const { data } = await this.http.get<Array<{ diff: string; new_path: string; old_path: string }>>(\n `/projects/${this.enc(repoSlug)}/merge_requests/${prNumber}/diffs`\n );\n return data.map((d) => `--- a/${d.old_path}\\n+++ b/${d.new_path}\\n${d.diff}`).join(\"\\n\\n\");\n } catch {\n return \"\";\n }\n }\n\n async mergePR(repoSlug: string, prNumber: number, opts: MergePrOptions): Promise<void> {\n try {\n await this.http.put(\n `/projects/${this.enc(repoSlug)}/merge_requests/${prNumber}/merge`,\n {\n squash: opts.method === \"squash\",\n merge_commit_message: opts.commitTitle ?? undefined,\n should_remove_source_branch: opts.deleteSourceBranch ?? false,\n }\n );\n } catch (err) {\n throw wrapGlError(err, `merge MR !${prNumber}`);\n }\n }\n\n async closePR(repoSlug: string, prNumber: number): Promise<void> {\n try {\n await this.http.put(\n `/projects/${this.enc(repoSlug)}/merge_requests/${prNumber}`,\n { state_event: \"close\" }\n );\n } catch (err) {\n throw wrapGlError(err, `close MR !${prNumber}`);\n }\n }\n\n async submitPRReview(repoSlug: string, prNumber: number, opts: SubmitReviewOptions): Promise<void> {\n // Build the overall review note with verdict prefix\n const verdictPrefix =\n opts.event === \"approve\" ? \"✅ **APPROVED**\" :\n opts.event === \"request_changes\" ? \"🔴 **CHANGES REQUESTED**\" : \"💬 **REVIEW COMMENT**\";\n\n const overallBody = `${verdictPrefix}\\n\\n${opts.body}`;\n\n try {\n // Post the summary as a plain MR note\n await this.http.post(\n `/projects/${this.enc(repoSlug)}/merge_requests/${prNumber}/notes`,\n { body: overallBody }\n );\n\n // Post inline comments as MR notes with file+line context embedded in the body\n // (GitLab discussion positions require SHAs; use enriched body as fallback)\n for (const c of opts.comments ?? []) {\n const inlineBody = `**\\`${c.path}:${c.line}\\`**\\n\\n${c.body}`;\n await this.http.post(\n `/projects/${this.enc(repoSlug)}/merge_requests/${prNumber}/notes`,\n { body: inlineBody }\n ).catch(() => {});\n }\n } catch (err) {\n throw wrapGlError(err, `submit review on MR !${prNumber}`);\n }\n }\n\n async replyToComment(repoSlug: string, prNumber: number, commentId: number, body: string): Promise<void> {\n try {\n // GitLab: add a note to the MR (thread replies require discussion IDs which we don't store)\n const replyBody = `*(in reply to comment #${commentId})*\\n\\n${body}`;\n await this.http.post(\n `/projects/${this.enc(repoSlug)}/merge_requests/${prNumber}/notes`,\n { body: replyBody }\n );\n } catch (err) {\n throw wrapGlError(err, `reply to comment #${commentId}`);\n }\n }\n\n async getDefaultBranch(repoSlug: string): Promise<string> {\n try {\n const { data } = await withRetry(() =>\n this.http.get<GlProject>(`/projects/${this.enc(repoSlug)}`)\n );\n return data.default_branch ?? \"main\";\n } catch (err) {\n throw wrapGlError(err, \"get default branch\");\n }\n }\n}\n\n// ─── Mappers ──────────────────────────────────────────────────────────────────\nfunction mapGlMr(d: GlMr): PullRequest {\n let state: \"open\" | \"closed\" | \"merged\";\n if (d.state === \"merged\") state = \"merged\";\n else if (d.state === \"opened\") state = \"open\";\n else state = \"closed\";\n\n return {\n id: d.id,\n number: d.iid,\n title: d.title,\n body: d.description ?? \"\",\n state,\n head: d.source_branch,\n base: d.target_branch,\n url: d.web_url,\n author: d.author?.username ?? \"unknown\",\n createdAt: d.created_at,\n updatedAt: d.updated_at,\n };\n}\n\nfunction wrapGlError(err: unknown, action: string): GitxError {\n if (isAxiosError(err)) {\n const status = err.response?.status;\n const msg = (err.response?.data as Record<string, unknown> | undefined)?.message ?? err.message;\n if (status === 401) {\n return new GitxError(\n `GitLab authentication failed while trying to ${action}. Check your token with \\`gitx config set-provider gitlab\\`.`,\n { exitCode: 1, cause: err }\n );\n }\n if (status === 404) {\n return new GitxError(\n `GitLab resource not found while trying to ${action}. Verify the repo slug and token scopes.`,\n { exitCode: 1, cause: err }\n );\n }\n return new GitxError(\n `GitLab API error (${status ?? \"network\"}) while trying to ${action}: ${String(msg)}`,\n { exitCode: 1, cause: err }\n );\n }\n return new GitxError(`Unexpected error while trying to ${action}: ${String(err)}`, {\n exitCode: 1,\n cause: err,\n });\n}\n"]}
@@ -1,14 +1,58 @@
1
- import type { ProviderKind } from "./provider.js";
1
+ export type AiProviderKind = "claude" | "openai" | "claude-cli";
2
+ export interface AiProviderEntry {
3
+ /** Not needed for "claude-cli" */
4
+ apiKey?: string;
5
+ /** Optional model override */
6
+ model?: string;
7
+ }
8
+ /**
9
+ * Credential entry for a git hosting provider.
10
+ *
11
+ * - PAT (default): supply a `token` string.
12
+ * - GCM (Azure only): set `authMethod: "gcm"` and omit `token`.
13
+ * gitx will call `git credential fill` via Git Credential Manager at
14
+ * runtime to obtain a short-lived OAuth Bearer token.
15
+ */
16
+ export interface ProviderEntry {
17
+ /** Personal Access Token. Required when authMethod is "pat" (default). */
18
+ token?: string;
19
+ /**
20
+ * Authentication method.
21
+ * - "pat" (default) — PAT stored in the gitx config.
22
+ * - "gcm" — OAuth via Git Credential Manager (Azure DevOps only).
23
+ */
24
+ authMethod?: "pat" | "gcm";
25
+ }
2
26
  export interface GitxConfig {
3
- provider: ProviderKind;
4
- token: string;
5
27
  /**
6
- * Optional: when omitted, `gitx` will infer it from the current working directory's
7
- * `remote.origin.url` (where possible).
8
- *
9
- * Format: owner/name (or provider-specific slug).
28
+ * Git hosting provider credentials.
29
+ * gitx auto-detects which one to use from the repo's remote.origin.url.
30
+ */
31
+ providers: Partial<Record<"github" | "gitlab" | "azure", ProviderEntry>>;
32
+ /**
33
+ * All configured AI providers.
34
+ * - "claude" → Anthropic API key required
35
+ * - "openai" → OpenAI API key required
36
+ * - "claude-cli"→ uses locally installed `claude` CLI (no key needed)
37
+ */
38
+ aiProviders?: Partial<Record<AiProviderKind, AiProviderEntry>>;
39
+ /**
40
+ * Which AI provider to use by default.
41
+ * If omitted, gitx checks env vars then auto-detects claude-cli.
42
+ */
43
+ defaultAiProvider?: AiProviderKind;
44
+ /**
45
+ * Default base branch when it can't be inferred from the remote.
46
+ */
47
+ defaultBranch?: string;
48
+ /**
49
+ * @deprecated Use aiProviders + defaultAiProvider.
50
+ * Kept for backward-compat; migrated automatically on first load.
10
51
  */
11
- repo?: string;
12
- defaultBranch: string;
52
+ ai?: {
53
+ provider: AiProviderKind;
54
+ apiKey?: string;
55
+ model?: string;
56
+ };
13
57
  }
14
58
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,YAAY,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;CACvB"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,GAAG,YAAY,CAAC;AAEhE,MAAM,WAAW,eAAe;IAC9B,kCAAkC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8BAA8B;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,UAAU,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;CAC5B;AAED,MAAM,WAAW,UAAU;IACzB;;;OAGG;IACH,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,GAAG,QAAQ,GAAG,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC;IAEzE;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,cAAc,EAAE,eAAe,CAAC,CAAC,CAAC;IAE/D;;;OAGG;IACH,iBAAiB,CAAC,EAAE,cAAc,CAAC;IAEnC;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,EAAE,CAAC,EAAE;QACH,QAAQ,EAAE,cAAc,CAAC;QACzB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH"}
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"","sourcesContent":["import type { ProviderKind } from \"./provider.js\";\n\nexport interface GitxConfig {\n provider: ProviderKind;\n token: string;\n /**\n * Optional: when omitted, `gitx` will infer it from the current working directory's\n * `remote.origin.url` (where possible).\n *\n * Format: owner/name (or provider-specific slug).\n */\n repo?: string;\n defaultBranch: string;\n}\n"]}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"","sourcesContent":["export type AiProviderKind = \"claude\" | \"openai\" | \"claude-cli\";\n\nexport interface AiProviderEntry {\n /** Not needed for \"claude-cli\" */\n apiKey?: string;\n /** Optional model override */\n model?: string;\n}\n\n/**\n * Credential entry for a git hosting provider.\n *\n * - PAT (default): supply a `token` string.\n * - GCM (Azure only): set `authMethod: \"gcm\"` and omit `token`.\n * gitx will call `git credential fill` via Git Credential Manager at\n * runtime to obtain a short-lived OAuth Bearer token.\n */\nexport interface ProviderEntry {\n /** Personal Access Token. Required when authMethod is \"pat\" (default). */\n token?: string;\n /**\n * Authentication method.\n * - \"pat\" (default) — PAT stored in the gitx config.\n * - \"gcm\" — OAuth via Git Credential Manager (Azure DevOps only).\n */\n authMethod?: \"pat\" | \"gcm\";\n}\n\nexport interface GitxConfig {\n /**\n * Git hosting provider credentials.\n * gitx auto-detects which one to use from the repo's remote.origin.url.\n */\n providers: Partial<Record<\"github\" | \"gitlab\" | \"azure\", ProviderEntry>>;\n\n /**\n * All configured AI providers.\n * - \"claude\" → Anthropic API key required\n * - \"openai\" → OpenAI API key required\n * - \"claude-cli\"→ uses locally installed `claude` CLI (no key needed)\n */\n aiProviders?: Partial<Record<AiProviderKind, AiProviderEntry>>;\n\n /**\n * Which AI provider to use by default.\n * If omitted, gitx checks env vars then auto-detects claude-cli.\n */\n defaultAiProvider?: AiProviderKind;\n\n /**\n * Default base branch when it can't be inferred from the remote.\n */\n defaultBranch?: string;\n\n /**\n * @deprecated Use aiProviders + defaultAiProvider.\n * Kept for backward-compat; migrated automatically on first load.\n */\n ai?: {\n provider: AiProviderKind;\n apiKey?: string;\n model?: string;\n };\n}\n"]}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Azure DevOps GCM (Git Credential Manager) authentication helpers.
3
+ *
4
+ * Instead of a PAT token, this module uses `git credential fill` to obtain a
5
+ * short-lived OAuth Bearer token managed by GCM. No token is ever stored in
6
+ * the gitx config file — GCM is the secure, OS-level credential store.
7
+ *
8
+ * Prerequisites (run once):
9
+ * git config --global credential.azreposCredentialType oauth
10
+ * git config --global credential.https://dev.azure.com.useHttpPath true
11
+ */
12
+ export interface GcmVerifyResult {
13
+ ok: boolean;
14
+ issues: string[];
15
+ fixes: string[];
16
+ }
17
+ /**
18
+ * Fetch an OAuth Bearer token for the given Azure DevOps org via GCM.
19
+ *
20
+ * Uses `git credential fill` which reads from the OS credential store
21
+ * (Windows Credential Manager / macOS Keychain / GNOME Keyring).
22
+ * Caches the result in-memory so repeated calls within the same process are
23
+ * instant (< 1 ms vs ~5 ms for the subprocess round-trip).
24
+ *
25
+ * @param org Azure DevOps org name, e.g. "GoFynd" or "mycompany"
26
+ */
27
+ export declare function getTokenViaGcm(org: string): Promise<string>;
28
+ /**
29
+ * Clear the in-memory token cache for the given org (or all orgs).
30
+ * Useful after a 401 response — forces a fresh fetch on the next call.
31
+ */
32
+ export declare function invalidateGcmCache(org?: string): void;
33
+ /**
34
+ * Decode a JWT token (without verifying its signature) and return the
35
+ * expiry Unix timestamp from the `exp` claim, or `null` if not present.
36
+ *
37
+ * Azure DevOps OAuth access tokens typically expire in ~1 hour.
38
+ * GCM handles refresh silently before the token expires.
39
+ */
40
+ export declare function decodeJwtExpiry(token: string): number | null;
41
+ /**
42
+ * Verify that GCM is correctly configured for Azure DevOps and that a token
43
+ * can actually be fetched for the given org.
44
+ *
45
+ * Returns `{ ok: true }` when everything is fine, or `{ ok: false }` with a
46
+ * list of `issues` (human-readable) and `fixes` (shell commands to run).
47
+ *
48
+ * @param org Azure DevOps org name, e.g. "GoFynd"
49
+ */
50
+ export declare function verifyGcmSetup(org: string): Promise<GcmVerifyResult>;
51
+ //# sourceMappingURL=azureAuth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"azureAuth.d.ts","sourceRoot":"","sources":["../../src/utils/azureAuth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAsCH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AASD;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAgCjE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAMrD;AAID;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAW5D;AAID;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CA6C1E"}
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Azure DevOps GCM (Git Credential Manager) authentication helpers.
3
+ *
4
+ * Instead of a PAT token, this module uses `git credential fill` to obtain a
5
+ * short-lived OAuth Bearer token managed by GCM. No token is ever stored in
6
+ * the gitx config file — GCM is the secure, OS-level credential store.
7
+ *
8
+ * Prerequisites (run once):
9
+ * git config --global credential.azreposCredentialType oauth
10
+ * git config --global credential.https://dev.azure.com.useHttpPath true
11
+ */
12
+ import { execFile, spawn } from "node:child_process";
13
+ import { promisify } from "node:util";
14
+ const execFileAsync = promisify(execFile);
15
+ /**
16
+ * Run `git <args>` and return stdout as a string.
17
+ * Uses spawn so we can write to stdin (needed for `git credential fill`).
18
+ */
19
+ function spawnGit(args, stdinData) {
20
+ return new Promise((resolve, reject) => {
21
+ const child = spawn("git", args, { stdio: ["pipe", "pipe", "pipe"] });
22
+ let stdout = "";
23
+ let stderr = "";
24
+ child.stdout.on("data", (chunk) => { stdout += chunk.toString(); });
25
+ child.stderr.on("data", (chunk) => { stderr += chunk.toString(); });
26
+ child.on("error", (err) => reject(err));
27
+ child.on("close", (code) => {
28
+ if (code !== 0) {
29
+ reject(new Error(`git ${args[0] ?? ""} exited ${code}: ${stderr.trim() || stdout.trim()}`));
30
+ }
31
+ else {
32
+ resolve(stdout);
33
+ }
34
+ });
35
+ if (stdinData !== undefined) {
36
+ child.stdin.write(stdinData);
37
+ }
38
+ child.stdin.end();
39
+ });
40
+ }
41
+ // ─── In-memory token cache ────────────────────────────────────────────────────
42
+ // Tokens are cached for the lifetime of the current process. GCM handles
43
+ // background refresh; gitx just re-reads on the next process invocation.
44
+ const tokenCache = new Map();
45
+ // ─── Token fetch ──────────────────────────────────────────────────────────────
46
+ /**
47
+ * Fetch an OAuth Bearer token for the given Azure DevOps org via GCM.
48
+ *
49
+ * Uses `git credential fill` which reads from the OS credential store
50
+ * (Windows Credential Manager / macOS Keychain / GNOME Keyring).
51
+ * Caches the result in-memory so repeated calls within the same process are
52
+ * instant (< 1 ms vs ~5 ms for the subprocess round-trip).
53
+ *
54
+ * @param org Azure DevOps org name, e.g. "GoFynd" or "mycompany"
55
+ */
56
+ export async function getTokenViaGcm(org) {
57
+ const cached = tokenCache.get(org);
58
+ if (cached)
59
+ return cached;
60
+ const input = `protocol=https\nhost=dev.azure.com\npath=${org}\n\n`;
61
+ let stdout;
62
+ try {
63
+ stdout = await spawnGit(["credential", "fill"], input);
64
+ }
65
+ catch (err) {
66
+ const msg = err instanceof Error ? err.message : String(err);
67
+ throw new Error(`GCM token fetch failed for org "${org}": ${msg}\n` +
68
+ `Make sure Git Credential Manager is installed and the following git config is set:\n` +
69
+ ` git config --global credential.azreposCredentialType oauth\n` +
70
+ ` git config --global credential.https://dev.azure.com.useHttpPath true\n` +
71
+ `Then trigger a login by running any authenticated git command against the repo.`);
72
+ }
73
+ // Parse "password=<token>" line from git credential output
74
+ const match = stdout.match(/^password=(.+)$/m);
75
+ if (!match?.[1]) {
76
+ throw new Error(`GCM returned no token for org "${org}".\n` +
77
+ `Try running: git pull (to trigger a fresh Azure DevOps login via browser)`);
78
+ }
79
+ const token = match[1].trim();
80
+ tokenCache.set(org, token);
81
+ return token;
82
+ }
83
+ /**
84
+ * Clear the in-memory token cache for the given org (or all orgs).
85
+ * Useful after a 401 response — forces a fresh fetch on the next call.
86
+ */
87
+ export function invalidateGcmCache(org) {
88
+ if (org) {
89
+ tokenCache.delete(org);
90
+ }
91
+ else {
92
+ tokenCache.clear();
93
+ }
94
+ }
95
+ // ─── JWT helpers ──────────────────────────────────────────────────────────────
96
+ /**
97
+ * Decode a JWT token (without verifying its signature) and return the
98
+ * expiry Unix timestamp from the `exp` claim, or `null` if not present.
99
+ *
100
+ * Azure DevOps OAuth access tokens typically expire in ~1 hour.
101
+ * GCM handles refresh silently before the token expires.
102
+ */
103
+ export function decodeJwtExpiry(token) {
104
+ const parts = token.split(".");
105
+ if (parts.length !== 3 || !parts[1])
106
+ return null;
107
+ try {
108
+ const padded = parts[1].padEnd(parts[1].length + ((4 - (parts[1].length % 4)) % 4), "=");
109
+ const payload = Buffer.from(padded, "base64").toString("utf8");
110
+ const json = JSON.parse(payload);
111
+ return typeof json.exp === "number" ? json.exp : null;
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ }
117
+ // ─── GCM setup verification ───────────────────────────────────────────────────
118
+ /**
119
+ * Verify that GCM is correctly configured for Azure DevOps and that a token
120
+ * can actually be fetched for the given org.
121
+ *
122
+ * Returns `{ ok: true }` when everything is fine, or `{ ok: false }` with a
123
+ * list of `issues` (human-readable) and `fixes` (shell commands to run).
124
+ *
125
+ * @param org Azure DevOps org name, e.g. "GoFynd"
126
+ */
127
+ export async function verifyGcmSetup(org) {
128
+ const issues = [];
129
+ const fixes = [];
130
+ // 1. Check: credential.https://dev.azure.com.useHttpPath = true
131
+ try {
132
+ const { stdout } = await execFileAsync("git", [
133
+ "config", "--global", "credential.https://dev.azure.com.useHttpPath",
134
+ ]);
135
+ if (stdout.trim() !== "true") {
136
+ issues.push("`credential.https://dev.azure.com.useHttpPath` is not set to `true`");
137
+ fixes.push("git config --global credential.https://dev.azure.com.useHttpPath true");
138
+ }
139
+ }
140
+ catch {
141
+ issues.push("`credential.https://dev.azure.com.useHttpPath` is not configured");
142
+ fixes.push("git config --global credential.https://dev.azure.com.useHttpPath true");
143
+ }
144
+ // 2. Check: credential.azreposCredentialType = oauth
145
+ try {
146
+ const { stdout } = await execFileAsync("git", [
147
+ "config", "--global", "credential.azreposCredentialType",
148
+ ]);
149
+ if (stdout.trim() !== "oauth") {
150
+ issues.push("`credential.azreposCredentialType` is not set to `oauth`");
151
+ fixes.push("git config --global credential.azreposCredentialType oauth");
152
+ }
153
+ }
154
+ catch {
155
+ issues.push("`credential.azreposCredentialType` is not configured");
156
+ fixes.push("git config --global credential.azreposCredentialType oauth");
157
+ }
158
+ // 3. If git config looks good, do a live token fetch
159
+ if (issues.length === 0) {
160
+ try {
161
+ invalidateGcmCache(org); // always do a fresh fetch during verification
162
+ await getTokenViaGcm(org);
163
+ }
164
+ catch (err) {
165
+ const msg = err instanceof Error ? err.message.split("\n")[0] : String(err);
166
+ issues.push(`Token fetch failed: ${msg}`);
167
+ fixes.push("git pull (to trigger a fresh Azure DevOps login via browser)");
168
+ }
169
+ }
170
+ return { ok: issues.length === 0, issues, fixes };
171
+ }
172
+ //# sourceMappingURL=azureAuth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"azureAuth.js","sourceRoot":"","sources":["../../src/utils/azureAuth.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Azure DevOps GCM (Git Credential Manager) authentication helpers.\n *\n * Instead of a PAT token, this module uses `git credential fill` to obtain a\n * short-lived OAuth Bearer token managed by GCM. No token is ever stored in\n * the gitx config file — GCM is the secure, OS-level credential store.\n *\n * Prerequisites (run once):\n * git config --global credential.azreposCredentialType oauth\n * git config --global credential.https://dev.azure.com.useHttpPath true\n */\n\nimport { execFile, spawn } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst execFileAsync = promisify(execFile);\n\n/**\n * Run `git <args>` and return stdout as a string.\n * Uses spawn so we can write to stdin (needed for `git credential fill`).\n */\nfunction spawnGit(args: string[], stdinData?: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const child = spawn(\"git\", args, { stdio: [\"pipe\", \"pipe\", \"pipe\"] });\n let stdout = \"\";\n let stderr = \"\";\n\n child.stdout.on(\"data\", (chunk: Buffer) => { stdout += chunk.toString(); });\n child.stderr.on(\"data\", (chunk: Buffer) => { stderr += chunk.toString(); });\n\n child.on(\"error\", (err) => reject(err));\n child.on(\"close\", (code) => {\n if (code !== 0) {\n reject(new Error(`git ${args[0] ?? \"\"} exited ${code}: ${stderr.trim() || stdout.trim()}`));\n } else {\n resolve(stdout);\n }\n });\n\n if (stdinData !== undefined) {\n child.stdin.write(stdinData);\n }\n child.stdin.end();\n });\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface GcmVerifyResult {\n ok: boolean;\n issues: string[];\n fixes: string[];\n}\n\n// ─── In-memory token cache ────────────────────────────────────────────────────\n// Tokens are cached for the lifetime of the current process. GCM handles\n// background refresh; gitx just re-reads on the next process invocation.\nconst tokenCache = new Map<string, string>();\n\n// ─── Token fetch ──────────────────────────────────────────────────────────────\n\n/**\n * Fetch an OAuth Bearer token for the given Azure DevOps org via GCM.\n *\n * Uses `git credential fill` which reads from the OS credential store\n * (Windows Credential Manager / macOS Keychain / GNOME Keyring).\n * Caches the result in-memory so repeated calls within the same process are\n * instant (< 1 ms vs ~5 ms for the subprocess round-trip).\n *\n * @param org Azure DevOps org name, e.g. \"GoFynd\" or \"mycompany\"\n */\nexport async function getTokenViaGcm(org: string): Promise<string> {\n const cached = tokenCache.get(org);\n if (cached) return cached;\n\n const input = `protocol=https\\nhost=dev.azure.com\\npath=${org}\\n\\n`;\n\n let stdout: string;\n try {\n stdout = await spawnGit([\"credential\", \"fill\"], input);\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(\n `GCM token fetch failed for org \"${org}\": ${msg}\\n` +\n `Make sure Git Credential Manager is installed and the following git config is set:\\n` +\n ` git config --global credential.azreposCredentialType oauth\\n` +\n ` git config --global credential.https://dev.azure.com.useHttpPath true\\n` +\n `Then trigger a login by running any authenticated git command against the repo.`\n );\n }\n\n // Parse \"password=<token>\" line from git credential output\n const match = stdout.match(/^password=(.+)$/m);\n if (!match?.[1]) {\n throw new Error(\n `GCM returned no token for org \"${org}\".\\n` +\n `Try running: git pull (to trigger a fresh Azure DevOps login via browser)`\n );\n }\n\n const token = match[1].trim();\n tokenCache.set(org, token);\n return token;\n}\n\n/**\n * Clear the in-memory token cache for the given org (or all orgs).\n * Useful after a 401 response — forces a fresh fetch on the next call.\n */\nexport function invalidateGcmCache(org?: string): void {\n if (org) {\n tokenCache.delete(org);\n } else {\n tokenCache.clear();\n }\n}\n\n// ─── JWT helpers ──────────────────────────────────────────────────────────────\n\n/**\n * Decode a JWT token (without verifying its signature) and return the\n * expiry Unix timestamp from the `exp` claim, or `null` if not present.\n *\n * Azure DevOps OAuth access tokens typically expire in ~1 hour.\n * GCM handles refresh silently before the token expires.\n */\nexport function decodeJwtExpiry(token: string): number | null {\n const parts = token.split(\".\");\n if (parts.length !== 3 || !parts[1]) return null;\n try {\n const padded = parts[1].padEnd(parts[1].length + ((4 - (parts[1].length % 4)) % 4), \"=\");\n const payload = Buffer.from(padded, \"base64\").toString(\"utf8\");\n const json = JSON.parse(payload) as { exp?: number };\n return typeof json.exp === \"number\" ? json.exp : null;\n } catch {\n return null;\n }\n}\n\n// ─── GCM setup verification ───────────────────────────────────────────────────\n\n/**\n * Verify that GCM is correctly configured for Azure DevOps and that a token\n * can actually be fetched for the given org.\n *\n * Returns `{ ok: true }` when everything is fine, or `{ ok: false }` with a\n * list of `issues` (human-readable) and `fixes` (shell commands to run).\n *\n * @param org Azure DevOps org name, e.g. \"GoFynd\"\n */\nexport async function verifyGcmSetup(org: string): Promise<GcmVerifyResult> {\n const issues: string[] = [];\n const fixes: string[] = [];\n\n // 1. Check: credential.https://dev.azure.com.useHttpPath = true\n try {\n const { stdout } = await execFileAsync(\"git\", [\n \"config\", \"--global\", \"credential.https://dev.azure.com.useHttpPath\",\n ]);\n if (stdout.trim() !== \"true\") {\n issues.push(\"`credential.https://dev.azure.com.useHttpPath` is not set to `true`\");\n fixes.push(\"git config --global credential.https://dev.azure.com.useHttpPath true\");\n }\n } catch {\n issues.push(\"`credential.https://dev.azure.com.useHttpPath` is not configured\");\n fixes.push(\"git config --global credential.https://dev.azure.com.useHttpPath true\");\n }\n\n // 2. Check: credential.azreposCredentialType = oauth\n try {\n const { stdout } = await execFileAsync(\"git\", [\n \"config\", \"--global\", \"credential.azreposCredentialType\",\n ]);\n if (stdout.trim() !== \"oauth\") {\n issues.push(\"`credential.azreposCredentialType` is not set to `oauth`\");\n fixes.push(\"git config --global credential.azreposCredentialType oauth\");\n }\n } catch {\n issues.push(\"`credential.azreposCredentialType` is not configured\");\n fixes.push(\"git config --global credential.azreposCredentialType oauth\");\n }\n\n // 3. If git config looks good, do a live token fetch\n if (issues.length === 0) {\n try {\n invalidateGcmCache(org); // always do a fresh fetch during verification\n await getTokenViaGcm(org);\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message.split(\"\\n\")[0] : String(err);\n issues.push(`Token fetch failed: ${msg}`);\n fixes.push(\"git pull (to trigger a fresh Azure DevOps login via browser)\");\n }\n }\n\n return { ok: issues.length === 0, issues, fixes };\n}\n"]}
@@ -1,4 +1,26 @@
1
+ import type { ProviderKind } from "../types/provider.js";
1
2
  export declare function getGitRemoteOriginUrl(cwd?: string): Promise<string | undefined>;
3
+ export declare function isInsideGitRepo(cwd?: string): Promise<boolean>;
4
+ /**
5
+ * Infer a "owner/repo" slug from a git remote URL.
6
+ *
7
+ * Handles:
8
+ * GitHub / GitLab:
9
+ * git@github.com:owner/repo.git
10
+ * https://github.com/owner/repo(.git)
11
+ * ssh://git@github.com/owner/repo(.git)
12
+ *
13
+ * Azure DevOps:
14
+ * https://dev.azure.com/org/project/_git/repo
15
+ * https://org.visualstudio.com/project/_git/repo
16
+ * git@ssh.dev.azure.com:v3/org/project/repo (new SSH format)
17
+ * org@vs-ssh.visualstudio.com:v3/org/project/repo (legacy SSH format)
18
+ *
19
+ * Returns:
20
+ * GitHub/GitLab: "owner/repo"
21
+ * Azure: "org/project/repo"
22
+ */
2
23
  export declare function inferRepoSlugFromRemote(url: string): string | undefined;
3
24
  export declare function resolveRepoSlugFromCwd(cwd?: string): Promise<string | undefined>;
25
+ export declare function detectProviderFromRemote(url: string): ProviderKind | undefined;
4
26
  //# sourceMappingURL=git.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../src/utils/git.ts"],"names":[],"mappings":"AAKA,wBAAsB,qBAAqB,CAAC,GAAG,SAAgB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQ5F;AAED,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAqBvE;AAED,wBAAsB,sBAAsB,CAAC,GAAG,SAAgB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAI7F"}
1
+ {"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../src/utils/git.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAIzD,wBAAsB,qBAAqB,CAAC,GAAG,SAAgB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQ5F;AAED,wBAAsB,eAAe,CAAC,GAAG,SAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CAO3E;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAoCvE;AAED,wBAAsB,sBAAsB,CAAC,GAAG,SAAgB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAI7F;AAED,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAW9E"}