@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
package/dist/utils/git.js CHANGED
@@ -11,24 +11,67 @@ export async function getGitRemoteOriginUrl(cwd = process.cwd()) {
11
11
  return undefined;
12
12
  }
13
13
  }
14
+ export async function isInsideGitRepo(cwd = process.cwd()) {
15
+ try {
16
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], { cwd });
17
+ return String(stdout ?? "").trim() === "true";
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ /**
24
+ * Infer a "owner/repo" slug from a git remote URL.
25
+ *
26
+ * Handles:
27
+ * GitHub / GitLab:
28
+ * git@github.com:owner/repo.git
29
+ * https://github.com/owner/repo(.git)
30
+ * ssh://git@github.com/owner/repo(.git)
31
+ *
32
+ * Azure DevOps:
33
+ * https://dev.azure.com/org/project/_git/repo
34
+ * https://org.visualstudio.com/project/_git/repo
35
+ * git@ssh.dev.azure.com:v3/org/project/repo (new SSH format)
36
+ * org@vs-ssh.visualstudio.com:v3/org/project/repo (legacy SSH format)
37
+ *
38
+ * Returns:
39
+ * GitHub/GitLab: "owner/repo"
40
+ * Azure: "org/project/repo"
41
+ */
14
42
  export function inferRepoSlugFromRemote(url) {
15
- // Supports common GitHub/GitLab URL formats:
16
- // - git@github.com:owner/repo.git
17
- // - https://github.com/owner/repo(.git)
18
- // - ssh://git@github.com/owner/repo(.git)
19
43
  const cleaned = url.trim();
20
- const patterns = [
44
+ // ── GitHub / GitLab ──────────────────────────────────────────────────────
45
+ const ghGlPatterns = [
21
46
  /^(?:git@)(?:github\.com|gitlab\.com):(?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?$/i,
22
47
  /^(?:https?:\/\/)(?:github\.com|gitlab\.com)\/(?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?(?:\/)?$/i,
23
- /^(?:ssh:\/\/)(?:git@)(?:github\.com|gitlab\.com)\/(?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?$/i
48
+ /^(?:ssh:\/\/)(?:git@)?(?:github\.com|gitlab\.com)\/(?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?$/i,
24
49
  ];
25
- for (const re of patterns) {
50
+ for (const re of ghGlPatterns) {
26
51
  const match = re.exec(cleaned);
27
52
  const owner = match?.groups?.["owner"];
28
53
  const repo = match?.groups?.["repo"];
29
54
  if (owner && repo)
30
55
  return `${owner}/${repo}`;
31
56
  }
57
+ // ── Azure DevOps HTTPS ────────────────────────────────────────────────────
58
+ // https://dev.azure.com/{org}/{project}/_git/{repo}
59
+ const azHttps = /https?:\/\/dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/]+?)(?:\.git)?(?:\/)?$/i.exec(cleaned);
60
+ if (azHttps)
61
+ return `${azHttps[1]}/${azHttps[2]}/${azHttps[3]}`;
62
+ // https://{org}.visualstudio.com/{project}/_git/{repo}
63
+ const azVs = /https?:\/\/([^.]+)\.visualstudio\.com\/([^/]+)\/_git\/([^/]+?)(?:\.git)?(?:\/)?$/i.exec(cleaned);
64
+ if (azVs)
65
+ return `${azVs[1]}/${azVs[2]}/${azVs[3]}`;
66
+ // ── Azure DevOps SSH ──────────────────────────────────────────────────────
67
+ // New format: git@ssh.dev.azure.com:v3/{org}/{project}/{repo}
68
+ const azSshNew = /^[^@]+@ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?$/i.exec(cleaned);
69
+ if (azSshNew)
70
+ return `${azSshNew[1]}/${azSshNew[2]}/${azSshNew[3]}`;
71
+ // Legacy format: {org}@vs-ssh.visualstudio.com:v3/{org}/{project}/{repo}
72
+ const azSshLegacy = /^[^@]+@vs-ssh\.visualstudio\.com:v3\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?$/i.exec(cleaned);
73
+ if (azSshLegacy)
74
+ return `${azSshLegacy[1]}/${azSshLegacy[2]}/${azSshLegacy[3]}`;
32
75
  return undefined;
33
76
  }
34
77
  export async function resolveRepoSlugFromCwd(cwd = process.cwd()) {
@@ -37,4 +80,17 @@ export async function resolveRepoSlugFromCwd(cwd = process.cwd()) {
37
80
  return undefined;
38
81
  return inferRepoSlugFromRemote(originUrl);
39
82
  }
83
+ export function detectProviderFromRemote(url) {
84
+ const cleaned = url.trim();
85
+ if (cleaned.includes("github.com"))
86
+ return "github";
87
+ if (cleaned.includes("gitlab.com"))
88
+ return "gitlab";
89
+ if (cleaned.includes("dev.azure.com") || // HTTPS + new SSH (ssh.dev.azure.com)
90
+ cleaned.includes("visualstudio.com") // legacy HTTPS + legacy SSH (vs-ssh.visualstudio.com)
91
+ ) {
92
+ return "azure";
93
+ }
94
+ return undefined;
95
+ }
40
96
  //# sourceMappingURL=git.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/utils/git.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAE1C,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE;IAC7D,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,mBAAmB,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;QACjG,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACxC,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;IAC1C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,GAAW;IACjD,6CAA6C;IAC7C,kCAAkC;IAClC,wCAAwC;IACxC,0CAA0C;IAC1C,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAE3B,MAAM,QAAQ,GAAa;QACzB,mFAAmF;QACnF,kGAAkG;QAClG,gGAAgG;KACjG,CAAC;IAEF,KAAK,MAAM,EAAE,IAAI,QAAQ,EAAE,CAAC;QAC1B,MAAM,KAAK,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC/B,MAAM,KAAK,GAAG,KAAK,EAAE,MAAM,EAAE,CAAC,OAAO,CAAC,CAAC;QACvC,MAAM,IAAI,GAAG,KAAK,EAAE,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC;QACrC,IAAI,KAAK,IAAI,IAAI;YAAE,OAAO,GAAG,KAAK,IAAI,IAAI,EAAE,CAAC;IAC/C,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE;IAC9D,MAAM,SAAS,GAAG,MAAM,qBAAqB,CAAC,GAAG,CAAC,CAAC;IACnD,IAAI,CAAC,SAAS;QAAE,OAAO,SAAS,CAAC;IACjC,OAAO,uBAAuB,CAAC,SAAS,CAAC,CAAC;AAC5C,CAAC","sourcesContent":["import { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst execFileAsync = promisify(execFile);\n\nexport async function getGitRemoteOriginUrl(cwd = process.cwd()): Promise<string | undefined> {\n try {\n const { stdout } = await execFileAsync(\"git\", [\"config\", \"--get\", \"remote.origin.url\"], { cwd });\n const url = String(stdout ?? \"\").trim();\n return url.length > 0 ? url : undefined;\n } catch {\n return undefined;\n }\n}\n\nexport function inferRepoSlugFromRemote(url: string): string | undefined {\n // Supports common GitHub/GitLab URL formats:\n // - git@github.com:owner/repo.git\n // - https://github.com/owner/repo(.git)\n // - ssh://git@github.com/owner/repo(.git)\n const cleaned = url.trim();\n\n const patterns: RegExp[] = [\n /^(?:git@)(?:github\\.com|gitlab\\.com):(?<owner>[^/]+)\\/(?<repo>[^/]+?)(?:\\.git)?$/i,\n /^(?:https?:\\/\\/)(?:github\\.com|gitlab\\.com)\\/(?<owner>[^/]+)\\/(?<repo>[^/]+?)(?:\\.git)?(?:\\/)?$/i,\n /^(?:ssh:\\/\\/)(?:git@)(?:github\\.com|gitlab\\.com)\\/(?<owner>[^/]+)\\/(?<repo>[^/]+?)(?:\\.git)?$/i\n ];\n\n for (const re of patterns) {\n const match = re.exec(cleaned);\n const owner = match?.groups?.[\"owner\"];\n const repo = match?.groups?.[\"repo\"];\n if (owner && repo) return `${owner}/${repo}`;\n }\n\n return undefined;\n}\n\nexport async function resolveRepoSlugFromCwd(cwd = process.cwd()): Promise<string | undefined> {\n const originUrl = await getGitRemoteOriginUrl(cwd);\n if (!originUrl) return undefined;\n return inferRepoSlugFromRemote(originUrl);\n}\n"]}
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/utils/git.ts"],"names":[],"mappings":"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","sourcesContent":["import { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport type { ProviderKind } from \"../types/provider.js\";\n\nconst execFileAsync = promisify(execFile);\n\nexport async function getGitRemoteOriginUrl(cwd = process.cwd()): Promise<string | undefined> {\n try {\n const { stdout } = await execFileAsync(\"git\", [\"config\", \"--get\", \"remote.origin.url\"], { cwd });\n const url = String(stdout ?? \"\").trim();\n return url.length > 0 ? url : undefined;\n } catch {\n return undefined;\n }\n}\n\nexport async function isInsideGitRepo(cwd = process.cwd()): Promise<boolean> {\n try {\n const { stdout } = await execFileAsync(\"git\", [\"rev-parse\", \"--is-inside-work-tree\"], { cwd });\n return String(stdout ?? \"\").trim() === \"true\";\n } catch {\n return false;\n }\n}\n\n/**\n * Infer a \"owner/repo\" slug from a git remote URL.\n *\n * Handles:\n * GitHub / GitLab:\n * git@github.com:owner/repo.git\n * https://github.com/owner/repo(.git)\n * ssh://git@github.com/owner/repo(.git)\n *\n * Azure DevOps:\n * https://dev.azure.com/org/project/_git/repo\n * https://org.visualstudio.com/project/_git/repo\n * git@ssh.dev.azure.com:v3/org/project/repo (new SSH format)\n * org@vs-ssh.visualstudio.com:v3/org/project/repo (legacy SSH format)\n *\n * Returns:\n * GitHub/GitLab: \"owner/repo\"\n * Azure: \"org/project/repo\"\n */\nexport function inferRepoSlugFromRemote(url: string): string | undefined {\n const cleaned = url.trim();\n\n // ── GitHub / GitLab ──────────────────────────────────────────────────────\n const ghGlPatterns: RegExp[] = [\n /^(?:git@)(?:github\\.com|gitlab\\.com):(?<owner>[^/]+)\\/(?<repo>[^/]+?)(?:\\.git)?$/i,\n /^(?:https?:\\/\\/)(?:github\\.com|gitlab\\.com)\\/(?<owner>[^/]+)\\/(?<repo>[^/]+?)(?:\\.git)?(?:\\/)?$/i,\n /^(?:ssh:\\/\\/)(?:git@)?(?:github\\.com|gitlab\\.com)\\/(?<owner>[^/]+)\\/(?<repo>[^/]+?)(?:\\.git)?$/i,\n ];\n\n for (const re of ghGlPatterns) {\n const match = re.exec(cleaned);\n const owner = match?.groups?.[\"owner\"];\n const repo = match?.groups?.[\"repo\"];\n if (owner && repo) return `${owner}/${repo}`;\n }\n\n // ── Azure DevOps HTTPS ────────────────────────────────────────────────────\n // https://dev.azure.com/{org}/{project}/_git/{repo}\n const azHttps = /https?:\\/\\/dev\\.azure\\.com\\/([^/]+)\\/([^/]+)\\/_git\\/([^/]+?)(?:\\.git)?(?:\\/)?$/i.exec(cleaned);\n if (azHttps) return `${azHttps[1]}/${azHttps[2]}/${azHttps[3]}`;\n\n // https://{org}.visualstudio.com/{project}/_git/{repo}\n const azVs = /https?:\\/\\/([^.]+)\\.visualstudio\\.com\\/([^/]+)\\/_git\\/([^/]+?)(?:\\.git)?(?:\\/)?$/i.exec(cleaned);\n if (azVs) return `${azVs[1]}/${azVs[2]}/${azVs[3]}`;\n\n // ── Azure DevOps SSH ──────────────────────────────────────────────────────\n // New format: git@ssh.dev.azure.com:v3/{org}/{project}/{repo}\n const azSshNew = /^[^@]+@ssh\\.dev\\.azure\\.com:v3\\/([^/]+)\\/([^/]+)\\/([^/]+?)(?:\\.git)?$/i.exec(cleaned);\n if (azSshNew) return `${azSshNew[1]}/${azSshNew[2]}/${azSshNew[3]}`;\n\n // Legacy format: {org}@vs-ssh.visualstudio.com:v3/{org}/{project}/{repo}\n const azSshLegacy = /^[^@]+@vs-ssh\\.visualstudio\\.com:v3\\/([^/]+)\\/([^/]+)\\/([^/]+?)(?:\\.git)?$/i.exec(cleaned);\n if (azSshLegacy) return `${azSshLegacy[1]}/${azSshLegacy[2]}/${azSshLegacy[3]}`;\n\n return undefined;\n}\n\nexport async function resolveRepoSlugFromCwd(cwd = process.cwd()): Promise<string | undefined> {\n const originUrl = await getGitRemoteOriginUrl(cwd);\n if (!originUrl) return undefined;\n return inferRepoSlugFromRemote(originUrl);\n}\n\nexport function detectProviderFromRemote(url: string): ProviderKind | undefined {\n const cleaned = url.trim();\n if (cleaned.includes(\"github.com\")) return \"github\";\n if (cleaned.includes(\"gitlab.com\")) return \"gitlab\";\n if (\n cleaned.includes(\"dev.azure.com\") || // HTTPS + new SSH (ssh.dev.azure.com)\n cleaned.includes(\"visualstudio.com\") // legacy HTTPS + legacy SSH (vs-ssh.visualstudio.com)\n ) {\n return \"azure\";\n }\n return undefined;\n}\n"]}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Extended git operations for the implement workflow.
3
+ * All functions execute native git commands via child_process.
4
+ */
5
+ export declare function getCurrentBranch(cwd?: string): Promise<string>;
6
+ /**
7
+ * Resolve the default branch (main/master/develop) by inspecting the remote.
8
+ * Falls back to "main" if nothing can be determined.
9
+ */
10
+ export declare function getDefaultBranchFromGit(cwd?: string, configuredDefault?: string): Promise<string>;
11
+ /**
12
+ * Create and checkout a new branch.
13
+ * If the branch already exists, just check it out.
14
+ */
15
+ export declare function createAndCheckoutBranch(branchName: string, cwd?: string): Promise<void>;
16
+ /** Sanitise a free-form task string into a valid branch name */
17
+ export declare function slugifyBranchName(task: string, prefix?: string): string;
18
+ /**
19
+ * Write content to a file inside the repo, creating parent directories as needed.
20
+ * Paths should be relative to `cwd`.
21
+ */
22
+ export declare function writeRepoFile(relativePath: string, content: string, cwd?: string): Promise<void>;
23
+ /**
24
+ * Apply a unified diff string using `git apply`.
25
+ * Returns `true` if applied cleanly, `false` if it failed (caller decides how
26
+ * to handle partial failures).
27
+ */
28
+ export declare function applyUnifiedDiff(unifiedDiff: string, cwd?: string): Promise<{
29
+ ok: boolean;
30
+ error?: string;
31
+ }>;
32
+ export declare function stageAll(cwd?: string): Promise<void>;
33
+ export declare function hasStagedChanges(cwd?: string): Promise<boolean>;
34
+ export declare function commitChanges(message: string, cwd?: string): Promise<string>;
35
+ export declare function pushBranch(branchName: string, cwd?: string): Promise<void>;
36
+ /**
37
+ * List all tracked files in the repo (respects .gitignore).
38
+ * Returns paths relative to `cwd`.
39
+ */
40
+ export declare function listTrackedFiles(cwd?: string): Promise<string[]>;
41
+ /**
42
+ * Get the unified diff of only staged changes (`git diff --cached`).
43
+ * Use this when the caller wants to commit only what's already in the index.
44
+ */
45
+ export declare function getStagedDiff(cwd?: string): Promise<string>;
46
+ /**
47
+ * Get a compact stat summary of only staged changes (`git diff --cached --stat`).
48
+ */
49
+ export declare function getStagedDiffStat(cwd?: string): Promise<string>;
50
+ /**
51
+ * Get the unified diff of all uncommitted changes (staged + unstaged).
52
+ */
53
+ export declare function getWorkingDiff(cwd?: string): Promise<string>;
54
+ /**
55
+ * Get a compact summary of staged changes (--stat format):
56
+ * lists every changed file with insertion/deletion counts.
57
+ * Always small regardless of diff size — used to give the AI the
58
+ * complete picture of what changed even when the full diff is truncated.
59
+ */
60
+ export declare function getWorkingDiffStat(cwd?: string): Promise<string>;
61
+ /**
62
+ * Auto-detect the most likely base branch for the current feature branch.
63
+ *
64
+ * Strategy:
65
+ * 1. Check if the current branch has a configured upstream tracking branch.
66
+ * 2. Otherwise, try common default branch names (main, master, develop, dev).
67
+ * 3. For each candidate, count commits on HEAD that are NOT in that branch.
68
+ * The candidate with the fewest such commits is the likely origin.
69
+ *
70
+ * Falls back to "main" if nothing can be determined.
71
+ */
72
+ export declare function detectBaseBranch(cwd?: string): Promise<string>;
73
+ /**
74
+ * Get the one-line commit log for commits on HEAD that are not in baseBranch.
75
+ * Used to give AI context about what this branch adds.
76
+ */
77
+ export declare function getBranchCommits(cwd?: string, baseBranch?: string): Promise<string[]>;
78
+ /**
79
+ * Get the unified diff of all changes between baseBranch and HEAD.
80
+ * This is what the PR reviewer would see — all additions across all commits.
81
+ */
82
+ export declare function getBranchDiff(cwd?: string, baseBranch?: string): Promise<string>;
83
+ /**
84
+ * Get the --stat summary of all changes between baseBranch and HEAD.
85
+ * Lists every file changed with insertion/deletion counts.
86
+ * Used alongside a truncated diff so the AI sees the full file list even
87
+ * when the detailed patch is cut off.
88
+ */
89
+ export declare function getBranchStat(cwd?: string, baseBranch?: string): Promise<string>;
90
+ /**
91
+ * Read file content as a string. Returns empty string if file doesn't exist.
92
+ */
93
+ export declare function readRepoFile(relativePath: string, cwd?: string): Promise<string | undefined>;
94
+ /**
95
+ * Check whether a branch exists on the remote (origin).
96
+ * Uses git ls-remote which does not require a full fetch.
97
+ */
98
+ export declare function branchExistsOnRemote(branchName: string, cwd?: string): Promise<boolean>;
99
+ /**
100
+ * Returns true if the working tree has uncommitted changes (staged or unstaged).
101
+ */
102
+ export declare function isWorkingTreeDirty(cwd?: string): Promise<boolean>;
103
+ /**
104
+ * Returns `git status --short` output for use in AI context.
105
+ * Returns an empty string when the working tree is clean or git fails.
106
+ */
107
+ export declare function getGitStatus(cwd?: string): Promise<string>;
108
+ /**
109
+ * Returns the last `count` commits as one-line summaries (hash + subject).
110
+ * Defaults to the last 10 commits.
111
+ */
112
+ export declare function getRecentCommits(cwd?: string, count?: number): Promise<string[]>;
113
+ /**
114
+ * Returns the list of git stashes as `stash@{0}: ...` strings.
115
+ * Returns an empty array when there are no stashes or git fails.
116
+ */
117
+ export declare function getStashList(cwd?: string): Promise<string[]>;
118
+ //# sourceMappingURL=gitOps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitOps.d.ts","sourceRoot":"","sources":["../../src/utils/gitOps.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,380 @@
1
+ /**
2
+ * Extended git operations for the implement workflow.
3
+ * All functions execute native git commands via child_process.
4
+ */
5
+ import { execFile, exec } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+ import { writeFile, mkdir } from "node:fs/promises";
8
+ import { dirname, resolve, join } from "node:path";
9
+ import { GitxError } from "./errors.js";
10
+ const execFileAsync = promisify(execFile);
11
+ const execAsync = promisify(exec);
12
+ // ─── Internal helper ──────────────────────────────────────────────────────────
13
+ async function git(args, cwd) {
14
+ try {
15
+ const { stdout } = await execFileAsync("git", args, { cwd });
16
+ return String(stdout ?? "").trim();
17
+ }
18
+ catch (err) {
19
+ const stderr = err.stderr ??
20
+ err.message ??
21
+ String(err);
22
+ throw new GitxError(`git ${args[0]} failed: ${stderr.trim()}`, {
23
+ exitCode: 1,
24
+ cause: err,
25
+ });
26
+ }
27
+ }
28
+ // ─── Branch operations ────────────────────────────────────────────────────────
29
+ export async function getCurrentBranch(cwd = process.cwd()) {
30
+ return git(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
31
+ }
32
+ /**
33
+ * Resolve the default branch (main/master/develop) by inspecting the remote.
34
+ * Falls back to "main" if nothing can be determined.
35
+ */
36
+ export async function getDefaultBranchFromGit(cwd = process.cwd(), configuredDefault) {
37
+ if (configuredDefault)
38
+ return configuredDefault;
39
+ try {
40
+ // Try to read from remote HEAD reference
41
+ const out = await git(["rev-parse", "--abbrev-ref", "origin/HEAD"], cwd);
42
+ // "origin/main" → "main"
43
+ return out.replace(/^origin\//, "") || "main";
44
+ }
45
+ catch {
46
+ // Fall back to checking common branch names
47
+ try {
48
+ const branches = await git(["branch", "-r", "--format=%(refname:short)"], cwd);
49
+ const candidates = ["origin/main", "origin/master", "origin/develop"];
50
+ for (const candidate of candidates) {
51
+ if (branches.split("\n").some((b) => b.trim() === candidate)) {
52
+ return candidate.replace("origin/", "");
53
+ }
54
+ }
55
+ }
56
+ catch {
57
+ /* ignore */
58
+ }
59
+ return "main";
60
+ }
61
+ }
62
+ /**
63
+ * Create and checkout a new branch.
64
+ * If the branch already exists, just check it out.
65
+ */
66
+ export async function createAndCheckoutBranch(branchName, cwd = process.cwd()) {
67
+ try {
68
+ await git(["checkout", "-b", branchName], cwd);
69
+ }
70
+ catch {
71
+ // Branch might already exist – try checking it out
72
+ await git(["checkout", branchName], cwd);
73
+ }
74
+ }
75
+ /** Sanitise a free-form task string into a valid branch name */
76
+ export function slugifyBranchName(task, prefix = "gitx") {
77
+ const slug = task
78
+ .toLowerCase()
79
+ .replace(/[^a-z0-9]+/g, "-")
80
+ .replace(/^-+|-+$/g, "")
81
+ .slice(0, 50);
82
+ const ts = Date.now().toString(36);
83
+ return `${prefix}/${slug}-${ts}`;
84
+ }
85
+ // ─── File operations ──────────────────────────────────────────────────────────
86
+ /**
87
+ * Write content to a file inside the repo, creating parent directories as needed.
88
+ * Paths should be relative to `cwd`.
89
+ */
90
+ export async function writeRepoFile(relativePath, content, cwd = process.cwd()) {
91
+ const abs = resolve(join(cwd, relativePath));
92
+ await mkdir(dirname(abs), { recursive: true });
93
+ await writeFile(abs, content, "utf-8");
94
+ }
95
+ // ─── Diff application ─────────────────────────────────────────────────────────
96
+ /**
97
+ * Apply a unified diff string using `git apply`.
98
+ * Returns `true` if applied cleanly, `false` if it failed (caller decides how
99
+ * to handle partial failures).
100
+ */
101
+ export async function applyUnifiedDiff(unifiedDiff, cwd = process.cwd()) {
102
+ try {
103
+ // Use --3way to handle minor conflicts gracefully
104
+ const { stdout, stderr } = await execAsync(`echo ${JSON.stringify(unifiedDiff)} | git apply --3way --whitespace=fix -`, { cwd });
105
+ return { ok: true, error: stderr || stdout || undefined };
106
+ }
107
+ catch (err) {
108
+ const stderr = err.stderr ??
109
+ err.message ??
110
+ String(err);
111
+ return { ok: false, error: stderr.trim() };
112
+ }
113
+ }
114
+ // ─── Staging & committing ─────────────────────────────────────────────────────
115
+ export async function stageAll(cwd = process.cwd()) {
116
+ await git(["add", "-A"], cwd);
117
+ }
118
+ export async function hasStagedChanges(cwd = process.cwd()) {
119
+ try {
120
+ const out = await git(["diff", "--cached", "--name-only"], cwd);
121
+ return out.trim().length > 0;
122
+ }
123
+ catch {
124
+ return false;
125
+ }
126
+ }
127
+ export async function commitChanges(message, cwd = process.cwd()) {
128
+ await git(["commit", "-m", message], cwd);
129
+ return git(["rev-parse", "HEAD"], cwd);
130
+ }
131
+ // ─── Push ─────────────────────────────────────────────────────────────────────
132
+ export async function pushBranch(branchName, cwd = process.cwd()) {
133
+ await git(["push", "--set-upstream", "origin", branchName], cwd);
134
+ }
135
+ // ─── Repo inspection ──────────────────────────────────────────────────────────
136
+ /**
137
+ * List all tracked files in the repo (respects .gitignore).
138
+ * Returns paths relative to `cwd`.
139
+ */
140
+ export async function listTrackedFiles(cwd = process.cwd()) {
141
+ try {
142
+ const out = await git(["ls-files"], cwd);
143
+ return out
144
+ .split("\n")
145
+ .map((l) => l.trim())
146
+ .filter(Boolean);
147
+ }
148
+ catch {
149
+ return [];
150
+ }
151
+ }
152
+ /**
153
+ * Get the unified diff of only staged changes (`git diff --cached`).
154
+ * Use this when the caller wants to commit only what's already in the index.
155
+ */
156
+ export async function getStagedDiff(cwd = process.cwd()) {
157
+ try {
158
+ return await git(["diff", "--cached"], cwd);
159
+ }
160
+ catch {
161
+ return "";
162
+ }
163
+ }
164
+ /**
165
+ * Get a compact stat summary of only staged changes (`git diff --cached --stat`).
166
+ */
167
+ export async function getStagedDiffStat(cwd = process.cwd()) {
168
+ try {
169
+ return await git(["diff", "--cached", "--stat"], cwd);
170
+ }
171
+ catch {
172
+ return "";
173
+ }
174
+ }
175
+ /**
176
+ * Get the unified diff of all uncommitted changes (staged + unstaged).
177
+ */
178
+ export async function getWorkingDiff(cwd = process.cwd()) {
179
+ try {
180
+ const staged = await git(["diff", "--cached"], cwd);
181
+ const unstaged = await git(["diff"], cwd);
182
+ return [staged, unstaged].filter(Boolean).join("\n");
183
+ }
184
+ catch {
185
+ return "";
186
+ }
187
+ }
188
+ /**
189
+ * Get a compact summary of staged changes (--stat format):
190
+ * lists every changed file with insertion/deletion counts.
191
+ * Always small regardless of diff size — used to give the AI the
192
+ * complete picture of what changed even when the full diff is truncated.
193
+ */
194
+ export async function getWorkingDiffStat(cwd = process.cwd()) {
195
+ try {
196
+ const staged = await git(["diff", "--cached", "--stat"], cwd);
197
+ const unstaged = await git(["diff", "--stat"], cwd);
198
+ return [staged, unstaged].filter(Boolean).join("\n");
199
+ }
200
+ catch {
201
+ return "";
202
+ }
203
+ }
204
+ /**
205
+ * Auto-detect the most likely base branch for the current feature branch.
206
+ *
207
+ * Strategy:
208
+ * 1. Check if the current branch has a configured upstream tracking branch.
209
+ * 2. Otherwise, try common default branch names (main, master, develop, dev).
210
+ * 3. For each candidate, count commits on HEAD that are NOT in that branch.
211
+ * The candidate with the fewest such commits is the likely origin.
212
+ *
213
+ * Falls back to "main" if nothing can be determined.
214
+ */
215
+ export async function detectBaseBranch(cwd = process.cwd()) {
216
+ // Get current branch name upfront — used for all checks below
217
+ const current = await git(["rev-parse", "--abbrev-ref", "HEAD"], cwd).catch(() => "");
218
+ // 1. Try upstream tracking branch — only useful if it points to a DIFFERENT
219
+ // branch (e.g. origin/main), not the branch's own remote tracking ref.
220
+ // e.g. "origin/gitx/test" → "gitx/test" == current → skip
221
+ // "origin/main" → "main" != current → use it
222
+ try {
223
+ const upstream = await git(["rev-parse", "--abbrev-ref", "@{upstream}"], cwd);
224
+ const branch = upstream.replace(/^[^/]+\//, "").trim();
225
+ if (branch && branch !== current)
226
+ return branch;
227
+ }
228
+ catch {
229
+ // No upstream configured — fall through
230
+ }
231
+ // 2. Check remote HEAD (origin's default branch)
232
+ try {
233
+ const remoteHead = await git(["rev-parse", "--abbrev-ref", "origin/HEAD"], cwd);
234
+ const branch = remoteHead.replace(/^origin\//, "").trim();
235
+ if (branch && branch !== current)
236
+ return branch;
237
+ }
238
+ catch {
239
+ // Not available — fall through
240
+ }
241
+ // 3. Count commits ahead of each common default branch name
242
+ const candidates = ["main", "master", "develop", "dev", "staging"];
243
+ const counts = [];
244
+ for (const candidate of candidates) {
245
+ if (candidate === current)
246
+ continue;
247
+ try {
248
+ // Count commits on HEAD not in candidate
249
+ const out = await git(["rev-list", "--count", `${candidate}..HEAD`], cwd);
250
+ counts.push({ branch: candidate, ahead: parseInt(out.trim(), 10) || 0 });
251
+ }
252
+ catch {
253
+ // Branch doesn't exist locally — skip
254
+ }
255
+ }
256
+ if (counts.length > 0) {
257
+ // Pick the branch with the fewest commits ahead (closest ancestor)
258
+ counts.sort((a, b) => a.ahead - b.ahead);
259
+ return counts[0].branch;
260
+ }
261
+ return "main";
262
+ }
263
+ /**
264
+ * Get the one-line commit log for commits on HEAD that are not in baseBranch.
265
+ * Used to give AI context about what this branch adds.
266
+ */
267
+ export async function getBranchCommits(cwd = process.cwd(), baseBranch = "main") {
268
+ try {
269
+ const out = await git(["log", "--oneline", "--no-decorate", `${baseBranch}..HEAD`], cwd);
270
+ return out.split("\n").map((l) => l.trim()).filter(Boolean);
271
+ }
272
+ catch {
273
+ return [];
274
+ }
275
+ }
276
+ /**
277
+ * Get the unified diff of all changes between baseBranch and HEAD.
278
+ * This is what the PR reviewer would see — all additions across all commits.
279
+ */
280
+ export async function getBranchDiff(cwd = process.cwd(), baseBranch = "main") {
281
+ try {
282
+ // Three-dot diff: all changes introduced by this branch vs. the merge base
283
+ return await git(["diff", `${baseBranch}...HEAD`], cwd);
284
+ }
285
+ catch {
286
+ return "";
287
+ }
288
+ }
289
+ /**
290
+ * Get the --stat summary of all changes between baseBranch and HEAD.
291
+ * Lists every file changed with insertion/deletion counts.
292
+ * Used alongside a truncated diff so the AI sees the full file list even
293
+ * when the detailed patch is cut off.
294
+ */
295
+ export async function getBranchStat(cwd = process.cwd(), baseBranch = "main") {
296
+ try {
297
+ return await git(["diff", "--stat", `${baseBranch}...HEAD`], cwd);
298
+ }
299
+ catch {
300
+ return "";
301
+ }
302
+ }
303
+ /**
304
+ * Read file content as a string. Returns empty string if file doesn't exist.
305
+ */
306
+ export async function readRepoFile(relativePath, cwd = process.cwd()) {
307
+ try {
308
+ const abs = resolve(join(cwd, relativePath));
309
+ const { readFile } = await import("node:fs/promises");
310
+ return await readFile(abs, "utf-8");
311
+ }
312
+ catch {
313
+ return undefined;
314
+ }
315
+ }
316
+ /**
317
+ * Check whether a branch exists on the remote (origin).
318
+ * Uses git ls-remote which does not require a full fetch.
319
+ */
320
+ export async function branchExistsOnRemote(branchName, cwd = process.cwd()) {
321
+ try {
322
+ const out = await git(["ls-remote", "--heads", "origin", `refs/heads/${branchName}`], cwd);
323
+ return out.trim().length > 0;
324
+ }
325
+ catch {
326
+ return false;
327
+ }
328
+ }
329
+ /**
330
+ * Returns true if the working tree has uncommitted changes (staged or unstaged).
331
+ */
332
+ export async function isWorkingTreeDirty(cwd = process.cwd()) {
333
+ try {
334
+ const out = await git(["status", "--porcelain"], cwd);
335
+ return out.trim().length > 0;
336
+ }
337
+ catch {
338
+ return false;
339
+ }
340
+ }
341
+ // ─── Context helpers for `gitx ask` ──────────────────────────────────────────
342
+ /**
343
+ * Returns `git status --short` output for use in AI context.
344
+ * Returns an empty string when the working tree is clean or git fails.
345
+ */
346
+ export async function getGitStatus(cwd = process.cwd()) {
347
+ try {
348
+ return await git(["status", "--short"], cwd);
349
+ }
350
+ catch {
351
+ return "";
352
+ }
353
+ }
354
+ /**
355
+ * Returns the last `count` commits as one-line summaries (hash + subject).
356
+ * Defaults to the last 10 commits.
357
+ */
358
+ export async function getRecentCommits(cwd = process.cwd(), count = 10) {
359
+ try {
360
+ const out = await git(["log", "--oneline", "--no-decorate", `-${count}`], cwd);
361
+ return out.split("\n").map((l) => l.trim()).filter(Boolean);
362
+ }
363
+ catch {
364
+ return [];
365
+ }
366
+ }
367
+ /**
368
+ * Returns the list of git stashes as `stash@{0}: ...` strings.
369
+ * Returns an empty array when there are no stashes or git fails.
370
+ */
371
+ export async function getStashList(cwd = process.cwd()) {
372
+ try {
373
+ const out = await git(["stash", "list"], cwd);
374
+ return out.split("\n").map((l) => l.trim()).filter(Boolean);
375
+ }
376
+ catch {
377
+ return [];
378
+ }
379
+ }
380
+ //# sourceMappingURL=gitOps.js.map