ghagga 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/dist/commands/login.test.js +0 -2
  2. package/dist/commands/login.test.js.map +1 -1
  3. package/dist/commands/memory/backfill.d.ts +13 -0
  4. package/dist/commands/memory/backfill.d.ts.map +1 -0
  5. package/dist/commands/memory/backfill.js +47 -0
  6. package/dist/commands/memory/backfill.js.map +1 -0
  7. package/dist/commands/memory/backfill.test.d.ts +11 -0
  8. package/dist/commands/memory/backfill.test.d.ts.map +1 -0
  9. package/dist/commands/memory/backfill.test.js +127 -0
  10. package/dist/commands/memory/backfill.test.js.map +1 -0
  11. package/dist/commands/memory/clear.test.js +9 -0
  12. package/dist/commands/memory/clear.test.js.map +1 -1
  13. package/dist/commands/memory/delete.test.js +9 -0
  14. package/dist/commands/memory/delete.test.js.map +1 -1
  15. package/dist/commands/memory/index.d.ts +3 -3
  16. package/dist/commands/memory/index.d.ts.map +1 -1
  17. package/dist/commands/memory/index.js +5 -3
  18. package/dist/commands/memory/index.js.map +1 -1
  19. package/dist/commands/memory/list.test.js +9 -0
  20. package/dist/commands/memory/list.test.js.map +1 -1
  21. package/dist/commands/memory/search.test.js +9 -0
  22. package/dist/commands/memory/search.test.js.map +1 -1
  23. package/dist/commands/memory/show.test.js +9 -0
  24. package/dist/commands/memory/show.test.js.map +1 -1
  25. package/dist/commands/memory/stats.test.js +9 -0
  26. package/dist/commands/memory/stats.test.js.map +1 -1
  27. package/dist/commands/memory/utils.d.ts.map +1 -1
  28. package/dist/commands/memory/utils.js +14 -1
  29. package/dist/commands/memory/utils.js.map +1 -1
  30. package/dist/commands/review-output.test.js +37 -0
  31. package/dist/commands/review-output.test.js.map +1 -1
  32. package/dist/commands/review.d.ts +12 -0
  33. package/dist/commands/review.d.ts.map +1 -1
  34. package/dist/commands/review.js +239 -5
  35. package/dist/commands/review.js.map +1 -1
  36. package/dist/commands/review.mr-exit.test.d.ts +13 -0
  37. package/dist/commands/review.mr-exit.test.d.ts.map +1 -0
  38. package/dist/commands/review.mr-exit.test.js +190 -0
  39. package/dist/commands/review.mr-exit.test.js.map +1 -0
  40. package/dist/commands/review.postback-disclosure.test.d.ts +22 -0
  41. package/dist/commands/review.postback-disclosure.test.d.ts.map +1 -0
  42. package/dist/commands/review.postback-disclosure.test.js +192 -0
  43. package/dist/commands/review.postback-disclosure.test.js.map +1 -0
  44. package/dist/commands/review.pr-exit.test.d.ts +13 -0
  45. package/dist/commands/review.pr-exit.test.d.ts.map +1 -0
  46. package/dist/commands/review.pr-exit.test.js +162 -0
  47. package/dist/commands/review.pr-exit.test.js.map +1 -0
  48. package/dist/commands/review.pr-jenkins.test.d.ts +16 -0
  49. package/dist/commands/review.pr-jenkins.test.d.ts.map +1 -0
  50. package/dist/commands/review.pr-jenkins.test.js +106 -0
  51. package/dist/commands/review.pr-jenkins.test.js.map +1 -0
  52. package/dist/commands/review.pr-nativeid-fallback.test.d.ts +15 -0
  53. package/dist/commands/review.pr-nativeid-fallback.test.d.ts.map +1 -0
  54. package/dist/commands/review.pr-nativeid-fallback.test.js +151 -0
  55. package/dist/commands/review.pr-nativeid-fallback.test.js.map +1 -0
  56. package/dist/commands/review.test.js +13 -0
  57. package/dist/commands/review.test.js.map +1 -1
  58. package/dist/index.js +39 -2
  59. package/dist/index.js.map +1 -1
  60. package/dist/lib/cli-github-client-port.d.ts +52 -0
  61. package/dist/lib/cli-github-client-port.d.ts.map +1 -0
  62. package/dist/lib/cli-github-client-port.js +199 -0
  63. package/dist/lib/cli-github-client-port.js.map +1 -0
  64. package/dist/lib/cli-github-client-port.test.d.ts +10 -0
  65. package/dist/lib/cli-github-client-port.test.d.ts.map +1 -0
  66. package/dist/lib/cli-github-client-port.test.js +47 -0
  67. package/dist/lib/cli-github-client-port.test.js.map +1 -0
  68. package/dist/lib/cli-gitlab-client-port.d.ts +62 -0
  69. package/dist/lib/cli-gitlab-client-port.d.ts.map +1 -0
  70. package/dist/lib/cli-gitlab-client-port.js +209 -0
  71. package/dist/lib/cli-gitlab-client-port.js.map +1 -0
  72. package/dist/lib/cli-gitlab-client-port.test.d.ts +9 -0
  73. package/dist/lib/cli-gitlab-client-port.test.d.ts.map +1 -0
  74. package/dist/lib/cli-gitlab-client-port.test.js +292 -0
  75. package/dist/lib/cli-gitlab-client-port.test.js.map +1 -0
  76. package/dist/lib/config.d.ts +23 -0
  77. package/dist/lib/config.d.ts.map +1 -1
  78. package/dist/lib/config.js +9 -0
  79. package/dist/lib/config.js.map +1 -1
  80. package/dist/lib/embedding.d.ts +23 -0
  81. package/dist/lib/embedding.d.ts.map +1 -0
  82. package/dist/lib/embedding.js +39 -0
  83. package/dist/lib/embedding.js.map +1 -0
  84. package/dist/lib/embedding.test.d.ts +8 -0
  85. package/dist/lib/embedding.test.d.ts.map +1 -0
  86. package/dist/lib/embedding.test.js +101 -0
  87. package/dist/lib/embedding.test.js.map +1 -0
  88. package/dist/lib/forge-postback.d.ts +72 -0
  89. package/dist/lib/forge-postback.d.ts.map +1 -0
  90. package/dist/lib/forge-postback.js +39 -0
  91. package/dist/lib/forge-postback.js.map +1 -0
  92. package/dist/lib/gitlab-api.d.ts +50 -0
  93. package/dist/lib/gitlab-api.d.ts.map +1 -0
  94. package/dist/lib/gitlab-api.js +75 -0
  95. package/dist/lib/gitlab-api.js.map +1 -0
  96. package/dist/lib/gitlab-api.test.d.ts +2 -0
  97. package/dist/lib/gitlab-api.test.d.ts.map +1 -0
  98. package/dist/lib/gitlab-api.test.js +79 -0
  99. package/dist/lib/gitlab-api.test.js.map +1 -0
  100. package/dist/lib/gitlab-e2e.manual.test.d.ts +57 -0
  101. package/dist/lib/gitlab-e2e.manual.test.d.ts.map +1 -0
  102. package/dist/lib/gitlab-e2e.manual.test.js +135 -0
  103. package/dist/lib/gitlab-e2e.manual.test.js.map +1 -0
  104. package/dist/lib/gitlab-token.d.ts +17 -0
  105. package/dist/lib/gitlab-token.d.ts.map +1 -0
  106. package/dist/lib/gitlab-token.js +24 -0
  107. package/dist/lib/gitlab-token.js.map +1 -0
  108. package/dist/lib/gitlab-token.test.d.ts +2 -0
  109. package/dist/lib/gitlab-token.test.d.ts.map +1 -0
  110. package/dist/lib/gitlab-token.test.js +38 -0
  111. package/dist/lib/gitlab-token.test.js.map +1 -0
  112. package/dist/lib/oauth.test.js +0 -1
  113. package/dist/lib/oauth.test.js.map +1 -1
  114. package/dist/lib/pr-postback.d.ts +42 -0
  115. package/dist/lib/pr-postback.d.ts.map +1 -0
  116. package/dist/lib/pr-postback.js +44 -0
  117. package/dist/lib/pr-postback.js.map +1 -0
  118. package/dist/lib/pr-postback.test.d.ts +9 -0
  119. package/dist/lib/pr-postback.test.d.ts.map +1 -0
  120. package/dist/lib/pr-postback.test.js +212 -0
  121. package/dist/lib/pr-postback.test.js.map +1 -0
  122. package/dist/lib/pr-token.d.ts +15 -0
  123. package/dist/lib/pr-token.d.ts.map +1 -0
  124. package/dist/lib/pr-token.js +22 -0
  125. package/dist/lib/pr-token.js.map +1 -0
  126. package/dist/lib/pr-token.test.d.ts +2 -0
  127. package/dist/lib/pr-token.test.d.ts.map +1 -0
  128. package/dist/lib/pr-token.test.js +25 -0
  129. package/dist/lib/pr-token.test.js.map +1 -0
  130. package/package.json +9 -8
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Shared forge-NEUTRAL composition helper for the CLI change-request post-back
3
+ * (`ghagga review --pr N` GitHub / `--mr N` GitLab) — resolves
4
+ * BL-CLI-FORGE-COMPOSITION.
5
+ *
6
+ * The P3 `--pr` path and the P4 `--mr` path are the SAME pipeline:
7
+ * resolve-token → parse-remote → build-adapter → make-ref → postSummaryComment
8
+ * The ONLY differences are forge-specific: which token env vars to read, how to
9
+ * parse the remote into a `RepoRef`, and which adapter to construct. This module
10
+ * captures the SHARED pipeline ONCE — the forge-specific steps are injected via a
11
+ * {@link ForgeCompositionBuilder} so BOTH commands route through one path instead
12
+ * of each hand-rolling the glue. Adding a third forge (Gitea) is a new builder,
13
+ * not a new branch.
14
+ *
15
+ * The actual idempotent upsert (find stale → delete → repost) lives inside the
16
+ * adapter + the forge-neutral {@link postSummaryComment}; this helper only
17
+ * orchestrates token-resolution → composition-build → post.
18
+ */
19
+ import type { ChangeRequestRef, CommentMarker, ForgeAdapterBase } from 'ghagga-forge';
20
+ import { type PostBackResult } from './pr-postback.js';
21
+ /** The forge-specific pieces a post-back composition resolves to. */
22
+ export interface ForgeComposition {
23
+ /** The forge adapter (built over its CLI client port + the resolved token). */
24
+ adapter: ForgeAdapterBase;
25
+ /** The canonical change-request ref (PR/MR) with forge-native identity. */
26
+ ref: ChangeRequestRef;
27
+ }
28
+ /**
29
+ * Builds the forge-specific composition (adapter + ref) from a resolved token +
30
+ * the change-request number. ASYNC because some forges need a pre-flight REST
31
+ * call (GitLab resolves the numeric project id from the remote path).
32
+ */
33
+ export type ForgeCompositionBuilder = (token: string, changeRequestNumber: number) => Promise<ForgeComposition>;
34
+ /**
35
+ * Why a forge post-back could not run, distinct from a thrown post error.
36
+ *
37
+ * `missing-token` is surfaced separately so the caller can print the
38
+ * forge-appropriate "set X / run ghagga login" guidance and apply the
39
+ * blocking/soft-fail exit policy uniformly.
40
+ */
41
+ export type ForgePostbackOutcome = {
42
+ kind: 'posted';
43
+ result: PostBackResult;
44
+ } | {
45
+ kind: 'missing-token';
46
+ };
47
+ /** Inputs for {@link composeForgePostback}. */
48
+ export interface ForgePostbackInput {
49
+ /** The change-request number (PR/MR). */
50
+ changeRequestNumber: number;
51
+ /** Resolve the forge token (env-first, stored fallback). null ⇒ missing. */
52
+ resolveToken: () => string | null;
53
+ /** Build the forge-specific adapter + ref from the resolved token. */
54
+ buildComposition: ForgeCompositionBuilder;
55
+ /** The pre-rendered comment body (parity with the server post-back). */
56
+ body: string;
57
+ /** The comment marker identifying GHAGGA-owned comments. */
58
+ marker: CommentMarker;
59
+ }
60
+ /**
61
+ * Run the SHARED post-back pipeline for one forge.
62
+ *
63
+ * 1. resolve the token (forge-specific resolver). Missing ⇒ `missing-token`.
64
+ * 2. build the forge composition (adapter + ref) — async (GitLab project-id).
65
+ * 3. upsert the summary comment via the forge-neutral {@link postSummaryComment}.
66
+ *
67
+ * Throws on a post/build failure (the caller owns the blocking vs soft-fail exit
68
+ * policy). A missing token is returned as data, NOT thrown, so the caller prints
69
+ * forge-appropriate guidance.
70
+ */
71
+ export declare function composeForgePostback(input: ForgePostbackInput): Promise<ForgePostbackOutcome>;
72
+ //# sourceMappingURL=forge-postback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forge-postback.d.ts","sourceRoot":"","sources":["../../src/lib/forge-postback.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACtF,OAAO,EAAE,KAAK,cAAc,EAAsB,MAAM,kBAAkB,CAAC;AAE3E,qEAAqE;AACrE,MAAM,WAAW,gBAAgB;IAC/B,+EAA+E;IAC/E,OAAO,EAAE,gBAAgB,CAAC;IAC1B,2EAA2E;IAC3E,GAAG,EAAE,gBAAgB,CAAC;CACvB;AAED;;;;GAIG;AACH,MAAM,MAAM,uBAAuB,GAAG,CACpC,KAAK,EAAE,MAAM,EACb,mBAAmB,EAAE,MAAM,KACxB,OAAO,CAAC,gBAAgB,CAAC,CAAC;AAE/B;;;;;;GAMG;AACH,MAAM,MAAM,oBAAoB,GAC5B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,cAAc,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,eAAe,CAAA;CAAE,CAAC;AAE9B,+CAA+C;AAC/C,MAAM,WAAW,kBAAkB;IACjC,yCAAyC;IACzC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,4EAA4E;IAC5E,YAAY,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAClC,sEAAsE;IACtE,gBAAgB,EAAE,uBAAuB,CAAC;IAC1C,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,MAAM,EAAE,aAAa,CAAC;CACvB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,kBAAkB,GACxB,OAAO,CAAC,oBAAoB,CAAC,CAY/B"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Shared forge-NEUTRAL composition helper for the CLI change-request post-back
3
+ * (`ghagga review --pr N` GitHub / `--mr N` GitLab) — resolves
4
+ * BL-CLI-FORGE-COMPOSITION.
5
+ *
6
+ * The P3 `--pr` path and the P4 `--mr` path are the SAME pipeline:
7
+ * resolve-token → parse-remote → build-adapter → make-ref → postSummaryComment
8
+ * The ONLY differences are forge-specific: which token env vars to read, how to
9
+ * parse the remote into a `RepoRef`, and which adapter to construct. This module
10
+ * captures the SHARED pipeline ONCE — the forge-specific steps are injected via a
11
+ * {@link ForgeCompositionBuilder} so BOTH commands route through one path instead
12
+ * of each hand-rolling the glue. Adding a third forge (Gitea) is a new builder,
13
+ * not a new branch.
14
+ *
15
+ * The actual idempotent upsert (find stale → delete → repost) lives inside the
16
+ * adapter + the forge-neutral {@link postSummaryComment}; this helper only
17
+ * orchestrates token-resolution → composition-build → post.
18
+ */
19
+ import { postSummaryComment } from './pr-postback.js';
20
+ /**
21
+ * Run the SHARED post-back pipeline for one forge.
22
+ *
23
+ * 1. resolve the token (forge-specific resolver). Missing ⇒ `missing-token`.
24
+ * 2. build the forge composition (adapter + ref) — async (GitLab project-id).
25
+ * 3. upsert the summary comment via the forge-neutral {@link postSummaryComment}.
26
+ *
27
+ * Throws on a post/build failure (the caller owns the blocking vs soft-fail exit
28
+ * policy). A missing token is returned as data, NOT thrown, so the caller prints
29
+ * forge-appropriate guidance.
30
+ */
31
+ export async function composeForgePostback(input) {
32
+ const token = input.resolveToken();
33
+ if (!token)
34
+ return { kind: 'missing-token' };
35
+ const composition = await input.buildComposition(token, input.changeRequestNumber);
36
+ const result = await postSummaryComment(composition.adapter, composition.ref, input.body, input.marker);
37
+ return { kind: 'posted', result };
38
+ }
39
+ //# sourceMappingURL=forge-postback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forge-postback.js","sourceRoot":"","sources":["../../src/lib/forge-postback.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,EAAuB,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AA6C3E;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,KAAyB;IAEzB,MAAM,KAAK,GAAG,KAAK,CAAC,YAAY,EAAE,CAAC;IACnC,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC;IAE7C,MAAM,WAAW,GAAG,MAAM,KAAK,CAAC,gBAAgB,CAAC,KAAK,EAAE,KAAK,CAAC,mBAAmB,CAAC,CAAC;IACnF,MAAM,MAAM,GAAG,MAAM,kBAAkB,CACrC,WAAW,CAAC,OAAO,EACnB,WAAW,CAAC,GAAG,EACf,KAAK,CAAC,IAAI,EACV,KAAK,CAAC,MAAM,CACb,CAAC;IACF,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;AACpC,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Minimal GitLab REST helpers for the CLI MR summary post-back (`ghagga review
3
+ * --mr N`).
4
+ *
5
+ * Scope (P4): parse the GitLab remote → group/project path, and the GitLab API
6
+ * error type. The fetch-backed note client lives in `cli-gitlab-client-port.ts`;
7
+ * this module owns the remote-URL parsing + the shared error class, mirroring
8
+ * `github-api.ts`'s `parseGitHubRemote` / `GitHubApiError`.
9
+ */
10
+ /** Thrown by the CLI GitLab fetch calls; carries the HTTP status + raw body. */
11
+ export declare class GitLabApiError extends Error {
12
+ readonly status: number;
13
+ readonly body: string;
14
+ constructor(message: string, status: number, body: string);
15
+ }
16
+ /** A parsed GitLab remote: the host (for the API base) + the project path. */
17
+ export interface ParsedGitLabRemote {
18
+ /**
19
+ * The GitLab HOST (e.g. `gitlab.com` or `gitlab.example.com`). Drives the API
20
+ * base (`https://<host>/api/v4`). For SSH remotes that carry a non-default port
21
+ * (`ssh://git@host:2222/...`) the port is STRIPPED — the API is HTTPS on the
22
+ * web host, not the SSH port.
23
+ */
24
+ host: string;
25
+ /**
26
+ * The MUTABLE group/project path (e.g. `acme/widgets` or
27
+ * `group/subgroup/project`). Used to RESOLVE the canonical numeric project id
28
+ * (via `GET /projects/:url-encoded-path`), never as the identity key itself
29
+ * (R-GITLAB).
30
+ */
31
+ projectPath: string;
32
+ }
33
+ /**
34
+ * Parse the host + group/project path from a GitLab git remote URL.
35
+ *
36
+ * Supports HTTPS, SSH (git@), and ssh:// protocol formats against ANY GitLab
37
+ * host (gitlab.com OR a self-managed instance like `gitlab.example.com`). GitLab
38
+ * supports NESTED groups (e.g. `group/subgroup/project`), so the path segment is
39
+ * captured greedily (everything after the host, minus a trailing `.git`).
40
+ *
41
+ * DISAMBIGUATION: a generic git host cannot be told apart from GitHub by URL
42
+ * shape alone. The `--mr` flag is the EXPLICIT signal that the remote is GitLab;
43
+ * this parser is only reached on that path, so it accepts any host. It still
44
+ * rejects the well-known `github.com` host to catch an obvious `--mr` misuse on a
45
+ * GitHub remote early with a clear error.
46
+ *
47
+ * @throws if the remote is not a recognizable git remote URL, or is github.com.
48
+ */
49
+ export declare function parseGitLabRemote(remoteUrl: string): ParsedGitLabRemote;
50
+ //# sourceMappingURL=gitlab-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitlab-api.d.ts","sourceRoot":"","sources":["../../src/lib/gitlab-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,gFAAgF;AAChF,qBAAa,cAAe,SAAQ,KAAK;aAGrB,MAAM,EAAE,MAAM;aACd,IAAI,EAAE,MAAM;gBAF5B,OAAO,EAAE,MAAM,EACC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM;CAK/B;AAID,8EAA8E;AAC9E,MAAM,WAAW,kBAAkB;IACjC;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,kBAAkB,CAuBvE"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Minimal GitLab REST helpers for the CLI MR summary post-back (`ghagga review
3
+ * --mr N`).
4
+ *
5
+ * Scope (P4): parse the GitLab remote → group/project path, and the GitLab API
6
+ * error type. The fetch-backed note client lives in `cli-gitlab-client-port.ts`;
7
+ * this module owns the remote-URL parsing + the shared error class, mirroring
8
+ * `github-api.ts`'s `parseGitHubRemote` / `GitHubApiError`.
9
+ */
10
+ // ─── Error ──────────────────────────────────────────────────────
11
+ /** Thrown by the CLI GitLab fetch calls; carries the HTTP status + raw body. */
12
+ export class GitLabApiError extends Error {
13
+ status;
14
+ body;
15
+ constructor(message, status, body) {
16
+ super(message);
17
+ this.status = status;
18
+ this.body = body;
19
+ this.name = 'GitLabApiError';
20
+ }
21
+ }
22
+ /**
23
+ * Parse the host + group/project path from a GitLab git remote URL.
24
+ *
25
+ * Supports HTTPS, SSH (git@), and ssh:// protocol formats against ANY GitLab
26
+ * host (gitlab.com OR a self-managed instance like `gitlab.example.com`). GitLab
27
+ * supports NESTED groups (e.g. `group/subgroup/project`), so the path segment is
28
+ * captured greedily (everything after the host, minus a trailing `.git`).
29
+ *
30
+ * DISAMBIGUATION: a generic git host cannot be told apart from GitHub by URL
31
+ * shape alone. The `--mr` flag is the EXPLICIT signal that the remote is GitLab;
32
+ * this parser is only reached on that path, so it accepts any host. It still
33
+ * rejects the well-known `github.com` host to catch an obvious `--mr` misuse on a
34
+ * GitHub remote early with a clear error.
35
+ *
36
+ * @throws if the remote is not a recognizable git remote URL, or is github.com.
37
+ */
38
+ export function parseGitLabRemote(remoteUrl) {
39
+ const trimmed = remoteUrl.trim();
40
+ // HTTPS: https://<host>/group/subgroup/repo.git
41
+ const httpsMatch = trimmed.match(/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/);
42
+ if (httpsMatch?.[1] && httpsMatch[2]) {
43
+ return finalize(stripUserInfo(httpsMatch[1]), httpsMatch[2], trimmed);
44
+ }
45
+ // SSH protocol: ssh://git@<host>[:port]/group/subgroup/repo.git
46
+ // (checked BEFORE the scp form so the `:port` is not mis-parsed as the path).
47
+ const sshProtoMatch = trimmed.match(/^ssh:\/\/[^@\s]+@([^/]+)\/(.+?)(?:\.git)?$/);
48
+ if (sshProtoMatch?.[1] && sshProtoMatch[2]) {
49
+ return finalize(stripPort(sshProtoMatch[1]), sshProtoMatch[2], trimmed);
50
+ }
51
+ // SSH (scp-style): git@<host>:group/subgroup/repo.git — no `://` scheme.
52
+ const sshMatch = trimmed.match(/^[^@\s:]+@([^:/]+):(.+?)(?:\.git)?$/);
53
+ if (sshMatch?.[1] && sshMatch[2]) {
54
+ return finalize(sshMatch[1], sshMatch[2], trimmed);
55
+ }
56
+ throw new Error(`Not a GitLab remote URL: "${trimmed}"`);
57
+ }
58
+ /** Drop a `user:pass@` / `user@` prefix that may ride on an HTTPS host. */
59
+ function stripUserInfo(host) {
60
+ const at = host.lastIndexOf('@');
61
+ return at === -1 ? host : host.slice(at + 1);
62
+ }
63
+ /** Drop a trailing `:port` from a host (the API rides on HTTPS, not the SSH port). */
64
+ function stripPort(host) {
65
+ return host.replace(/:\d+$/, '');
66
+ }
67
+ /** Reject github.com (obvious --mr misuse) and assemble the parsed remote. */
68
+ function finalize(host, projectPath, original) {
69
+ const cleanHost = stripPort(host);
70
+ if (cleanHost.toLowerCase() === 'github.com') {
71
+ throw new Error(`Not a GitLab remote URL (looks like GitHub): "${original}"`);
72
+ }
73
+ return { host: cleanHost, projectPath };
74
+ }
75
+ //# sourceMappingURL=gitlab-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitlab-api.js","sourceRoot":"","sources":["../../src/lib/gitlab-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,mEAAmE;AAEnE,gFAAgF;AAChF,MAAM,OAAO,cAAe,SAAQ,KAAK;IAGrB;IACA;IAHlB,YACE,OAAe,EACC,MAAc,EACd,IAAY;QAE5B,KAAK,CAAC,OAAO,CAAC,CAAC;QAHC,WAAM,GAAN,MAAM,CAAQ;QACd,SAAI,GAAJ,IAAI,CAAQ;QAG5B,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AAsBD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAAiB;IACjD,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC;IAEjC,gDAAgD;IAChD,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC1E,IAAI,UAAU,EAAE,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;QACrC,OAAO,QAAQ,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IACxE,CAAC;IAED,gEAAgE;IAChE,8EAA8E;IAC9E,MAAM,aAAa,GAAG,OAAO,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;IAClF,IAAI,aAAa,EAAE,CAAC,CAAC,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3C,OAAO,QAAQ,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IAC1E,CAAC;IAED,yEAAyE;IACzE,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,qCAAqC,CAAC,CAAC;IACtE,IAAI,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;QACjC,OAAO,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IACrD,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,6BAA6B,OAAO,GAAG,CAAC,CAAC;AAC3D,CAAC;AAED,2EAA2E;AAC3E,SAAS,aAAa,CAAC,IAAY;IACjC,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACjC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,sFAAsF;AACtF,SAAS,SAAS,CAAC,IAAY;IAC7B,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AACnC,CAAC;AAED,8EAA8E;AAC9E,SAAS,QAAQ,CAAC,IAAY,EAAE,WAAmB,EAAE,QAAgB;IACnE,MAAM,SAAS,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAClC,IAAI,SAAS,CAAC,WAAW,EAAE,KAAK,YAAY,EAAE,CAAC;QAC7C,MAAM,IAAI,KAAK,CAAC,iDAAiD,QAAQ,GAAG,CAAC,CAAC;IAChF,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC;AAC1C,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=gitlab-api.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitlab-api.test.d.ts","sourceRoot":"","sources":["../../src/lib/gitlab-api.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { GitLabApiError, parseGitLabRemote } from './gitlab-api.js';
3
+ describe('parseGitLabRemote (host + projectPath, any GitLab host)', () => {
4
+ it('parses an HTTPS gitlab.com remote', () => {
5
+ expect(parseGitLabRemote('https://gitlab.com/acme/widgets.git')).toEqual({
6
+ host: 'gitlab.com',
7
+ projectPath: 'acme/widgets',
8
+ });
9
+ });
10
+ it('parses an HTTPS remote without .git suffix', () => {
11
+ expect(parseGitLabRemote('https://gitlab.com/acme/widgets')).toEqual({
12
+ host: 'gitlab.com',
13
+ projectPath: 'acme/widgets',
14
+ });
15
+ });
16
+ it('parses an SSH (git@) remote', () => {
17
+ expect(parseGitLabRemote('git@gitlab.com:acme/widgets.git')).toEqual({
18
+ host: 'gitlab.com',
19
+ projectPath: 'acme/widgets',
20
+ });
21
+ });
22
+ it('parses an ssh:// protocol remote', () => {
23
+ expect(parseGitLabRemote('ssh://git@gitlab.com/acme/widgets.git')).toEqual({
24
+ host: 'gitlab.com',
25
+ projectPath: 'acme/widgets',
26
+ });
27
+ });
28
+ it('supports NESTED groups (group/subgroup/project)', () => {
29
+ expect(parseGitLabRemote('https://gitlab.com/group/subgroup/project.git')).toEqual({
30
+ host: 'gitlab.com',
31
+ projectPath: 'group/subgroup/project',
32
+ });
33
+ expect(parseGitLabRemote('git@gitlab.com:group/subgroup/project.git')).toEqual({
34
+ host: 'gitlab.com',
35
+ projectPath: 'group/subgroup/project',
36
+ });
37
+ });
38
+ // ── Self-hosted GitLab (FIX A) ──────────────────────────────────
39
+ it('parses a self-hosted HTTPS remote and returns its host', () => {
40
+ expect(parseGitLabRemote('https://gitlab.example.com/team/repo.git')).toEqual({
41
+ host: 'gitlab.example.com',
42
+ projectPath: 'team/repo',
43
+ });
44
+ });
45
+ it('parses a self-hosted SSH (git@) remote', () => {
46
+ expect(parseGitLabRemote('git@gitlab.example.com:team/repo.git')).toEqual({
47
+ host: 'gitlab.example.com',
48
+ projectPath: 'team/repo',
49
+ });
50
+ });
51
+ it('strips a port from an ssh:// self-hosted remote (API rides HTTPS)', () => {
52
+ expect(parseGitLabRemote('ssh://git@gitlab.example.com:2222/team/repo.git')).toEqual({
53
+ host: 'gitlab.example.com',
54
+ projectPath: 'team/repo',
55
+ });
56
+ });
57
+ it('strips userinfo from an HTTPS remote', () => {
58
+ expect(parseGitLabRemote('https://user:tok@gitlab.example.com/team/repo.git')).toEqual({
59
+ host: 'gitlab.example.com',
60
+ projectPath: 'team/repo',
61
+ });
62
+ });
63
+ it('throws on a github.com remote (obvious --mr misuse)', () => {
64
+ expect(() => parseGitLabRemote('https://github.com/acme/widgets.git')).toThrow(/Not a GitLab remote URL/);
65
+ expect(() => parseGitLabRemote('git@github.com:acme/widgets.git')).toThrow(/Not a GitLab remote URL/);
66
+ });
67
+ it('throws on an unrecognizable remote', () => {
68
+ expect(() => parseGitLabRemote('not-a-url')).toThrow(/Not a GitLab remote URL/);
69
+ });
70
+ });
71
+ describe('GitLabApiError', () => {
72
+ it('carries status + body', () => {
73
+ const err = new GitLabApiError('boom', 404, 'not found');
74
+ expect(err.status).toBe(404);
75
+ expect(err.body).toBe('not found');
76
+ expect(err.name).toBe('GitLabApiError');
77
+ });
78
+ });
79
+ //# sourceMappingURL=gitlab-api.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitlab-api.test.js","sourceRoot":"","sources":["../../src/lib/gitlab-api.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * P4 task 4.3 — MANUAL / E2E acceptance gate for the GitLab MR summary post-back
3
+ * (R-GITLAB). This is the ONLY test in the suite that talks to a LIVE GitLab
4
+ * instance. It is `skipIf(!process.env.GITLAB_PAT)` so CI ALWAYS skips it — CI
5
+ * coverage of the GitLab REST surface lives in `cli-gitlab-client-port.test.ts`
6
+ * (fetch-mocked). This file is the real-instance sign-off the spec mandates.
7
+ *
8
+ * ENVIRONMENT VARIABLES
9
+ * ---------------------
10
+ * REQUIRED:
11
+ * GITLAB_PAT a Personal Access Token with the `api` scope.
12
+ * GITLAB_E2E_PROJECT the `group/project` path (nested groups OK,
13
+ * e.g. `group/subgroup/project`).
14
+ * GITLAB_E2E_MR the iid of an OPEN, throwaway MR in that project. The
15
+ * test posts and sweeps notes on it, so it MUST be a
16
+ * disposable MR you control — never a real review.
17
+ * OPTIONAL (self-hosted GitLab — gitlab.com is the default):
18
+ * GITLAB_HOST a host-only override (e.g. `gitlab.example.com`) when the
19
+ * API host differs from gitlab.com.
20
+ * GITLAB_API_BASE a FULL `.../api/v4` base override, required for a
21
+ * self-managed GitLab served under a subpath
22
+ * (e.g. `https://example.com/gitlab/api/v4`). Wins over
23
+ * GITLAB_HOST. Both are resolved through the SAME
24
+ * production helper (`resolveGitLabApiBase`) the `--mr`
25
+ * command path uses, so this test exercises the real
26
+ * self-hosted resolution — not a test-only shortcut.
27
+ *
28
+ * HOW TO RUN IT MANUALLY
29
+ * ----------------------
30
+ * Against a throwaway GitLab project + OPEN, disposable MR you control:
31
+ *
32
+ * export GITLAB_PAT=glpat-xxxxxxxxxxxxxxxxxxxx # PAT with `api` scope
33
+ * export GITLAB_E2E_PROJECT=group/your-project # group/project path
34
+ * export GITLAB_E2E_MR=1 # an OPEN, throwaway MR iid
35
+ * # optional self-hosted: export GITLAB_HOST=gitlab.example.com
36
+ * pnpm --filter ghagga exec vitest run src/lib/gitlab-e2e.manual.test.ts
37
+ *
38
+ * It will: resolve the numeric project id from the path, post (idempotently
39
+ * upsert) a marker-tagged summary note to the MR, assert the returned id is a
40
+ * positive number AND that the SPECIFIC note production just created
41
+ * (first.createdNativeId) is live on the MR carrying MARKER.html, then run a
42
+ * SECOND upsert and assert (a) the first note's id is in deletedNativeIds and
43
+ * (b) against the real MR state exactly ONE marker note remains and it is the
44
+ * second note — proving the find→delete→repost idempotency FOLD against the
45
+ * live API. An `afterAll` ATTEMPTS to leave zero leftover notes (best-effort; a
46
+ * transient GitLab failure may leave one, which the next run's sweep removes).
47
+ *
48
+ * MARKER OWNERSHIP NOTE: the marker is OWNED BY THE CALLER, not the adapter —
49
+ * the test renders it into `body` (see where `body` is built below). Production
50
+ * never injects it; `upsertSummaryComment` only USES `marker.html` to FILTER
51
+ * which notes to sweep (gitlab-forge-adapter.ts:182). So the anchored assertion
52
+ * below simply verifies that the note production created persists with the
53
+ * marker on live GitLab; the "exactly one marker note after the repost" check
54
+ * is what actually proves the idempotency fold against real MR state.
55
+ */
56
+ export {};
57
+ //# sourceMappingURL=gitlab-e2e.manual.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitlab-e2e.manual.test.d.ts","sourceRoot":"","sources":["../../src/lib/gitlab-e2e.manual.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG"}
@@ -0,0 +1,135 @@
1
+ /**
2
+ * P4 task 4.3 — MANUAL / E2E acceptance gate for the GitLab MR summary post-back
3
+ * (R-GITLAB). This is the ONLY test in the suite that talks to a LIVE GitLab
4
+ * instance. It is `skipIf(!process.env.GITLAB_PAT)` so CI ALWAYS skips it — CI
5
+ * coverage of the GitLab REST surface lives in `cli-gitlab-client-port.test.ts`
6
+ * (fetch-mocked). This file is the real-instance sign-off the spec mandates.
7
+ *
8
+ * ENVIRONMENT VARIABLES
9
+ * ---------------------
10
+ * REQUIRED:
11
+ * GITLAB_PAT a Personal Access Token with the `api` scope.
12
+ * GITLAB_E2E_PROJECT the `group/project` path (nested groups OK,
13
+ * e.g. `group/subgroup/project`).
14
+ * GITLAB_E2E_MR the iid of an OPEN, throwaway MR in that project. The
15
+ * test posts and sweeps notes on it, so it MUST be a
16
+ * disposable MR you control — never a real review.
17
+ * OPTIONAL (self-hosted GitLab — gitlab.com is the default):
18
+ * GITLAB_HOST a host-only override (e.g. `gitlab.example.com`) when the
19
+ * API host differs from gitlab.com.
20
+ * GITLAB_API_BASE a FULL `.../api/v4` base override, required for a
21
+ * self-managed GitLab served under a subpath
22
+ * (e.g. `https://example.com/gitlab/api/v4`). Wins over
23
+ * GITLAB_HOST. Both are resolved through the SAME
24
+ * production helper (`resolveGitLabApiBase`) the `--mr`
25
+ * command path uses, so this test exercises the real
26
+ * self-hosted resolution — not a test-only shortcut.
27
+ *
28
+ * HOW TO RUN IT MANUALLY
29
+ * ----------------------
30
+ * Against a throwaway GitLab project + OPEN, disposable MR you control:
31
+ *
32
+ * export GITLAB_PAT=glpat-xxxxxxxxxxxxxxxxxxxx # PAT with `api` scope
33
+ * export GITLAB_E2E_PROJECT=group/your-project # group/project path
34
+ * export GITLAB_E2E_MR=1 # an OPEN, throwaway MR iid
35
+ * # optional self-hosted: export GITLAB_HOST=gitlab.example.com
36
+ * pnpm --filter ghagga exec vitest run src/lib/gitlab-e2e.manual.test.ts
37
+ *
38
+ * It will: resolve the numeric project id from the path, post (idempotently
39
+ * upsert) a marker-tagged summary note to the MR, assert the returned id is a
40
+ * positive number AND that the SPECIFIC note production just created
41
+ * (first.createdNativeId) is live on the MR carrying MARKER.html, then run a
42
+ * SECOND upsert and assert (a) the first note's id is in deletedNativeIds and
43
+ * (b) against the real MR state exactly ONE marker note remains and it is the
44
+ * second note — proving the find→delete→repost idempotency FOLD against the
45
+ * live API. An `afterAll` ATTEMPTS to leave zero leftover notes (best-effort; a
46
+ * transient GitLab failure may leave one, which the next run's sweep removes).
47
+ *
48
+ * MARKER OWNERSHIP NOTE: the marker is OWNED BY THE CALLER, not the adapter —
49
+ * the test renders it into `body` (see where `body` is built below). Production
50
+ * never injects it; `upsertSummaryComment` only USES `marker.html` to FILTER
51
+ * which notes to sweep (gitlab-forge-adapter.ts:182). So the anchored assertion
52
+ * below simply verifies that the note production created persists with the
53
+ * marker on live GitLab; the "exactly one marker note after the repost" check
54
+ * is what actually proves the idempotency fold against real MR state.
55
+ */
56
+ import { GitLabForgeAdapter } from 'ghagga-forge';
57
+ import { afterAll, describe, expect, it } from 'vitest';
58
+ import { createCliGitLabClientPort, resolveGitLabApiBase, resolveGitLabProjectId, } from './cli-gitlab-client-port.js';
59
+ import { postSummaryComment } from './pr-postback.js';
60
+ const PAT = process.env.GITLAB_PAT;
61
+ const PROJECT_PATH = process.env.GITLAB_E2E_PROJECT ?? '';
62
+ const MR_IID = Number.parseInt(process.env.GITLAB_E2E_MR ?? '', 10);
63
+ const MARKER = { html: '<!-- ghagga-review -->' };
64
+ /**
65
+ * Resolve the API base the SAME way the production `--mr` command path does:
66
+ * feed gitlab.com as the remote-derived host and let GITLAB_HOST / GITLAB_API_BASE
67
+ * override it (resolveGitLabApiBase precedence). With no override this is exactly
68
+ * the gitlab.com default the back-compat callers use.
69
+ */
70
+ const API_BASE = resolveGitLabApiBase('gitlab.com');
71
+ describe('GitLab MR summary post-back — LIVE E2E (manual gate, R-GITLAB)', () => {
72
+ // Note ids created during the test, swept best-effort in afterAll so the
73
+ // throwaway MR is left clean. Only populated when the test actually runs
74
+ // (it is empty when skipped without a PAT, so afterAll is a no-op).
75
+ const createdNoteIds = [];
76
+ let client;
77
+ let projectId;
78
+ const token = PAT;
79
+ afterAll(async () => {
80
+ // Best-effort cleanup: tolerate a per-note failure (404 / already gone /
81
+ // transient) so a delete error never fails the suite. Skipped entirely when
82
+ // the test did not run (no client / projectId resolved → nothing created).
83
+ if (!PAT || client == null || projectId == null)
84
+ return;
85
+ for (const noteId of createdNoteIds) {
86
+ try {
87
+ await client.deleteMrNote(projectId, MR_IID, noteId, token);
88
+ }
89
+ catch {
90
+ // Tolerate: leaving a stray note is better than failing the suite on
91
+ // cleanup. The next manual run's sweep removes it anyway.
92
+ }
93
+ }
94
+ });
95
+ it.skipIf(!PAT)('resolves numeric project id + posts a marker-tagged note + sweeps the stale one on re-run', async () => {
96
+ expect(PROJECT_PATH, 'set GITLAB_E2E_PROJECT=group/project').not.toBe('');
97
+ expect(Number.isInteger(MR_IID) && MR_IID > 0, 'set GITLAB_E2E_MR=<open MR iid>').toBe(true);
98
+ projectId = await resolveGitLabProjectId(PROJECT_PATH, token, API_BASE);
99
+ expect(projectId).toMatch(/^\d+$/); // numeric project id (R-GITLAB nativeId)
100
+ client = createCliGitLabClientPort(API_BASE);
101
+ const adapter = new GitLabForgeAdapter({ client, token, projectId });
102
+ const ref = {
103
+ repo: { kind: 'gitlab', nativeId: projectId, path: PROJECT_PATH },
104
+ iid: MR_IID,
105
+ };
106
+ const body = `${MARKER.html}\n## GHAGGA E2E\nposted at ${new Date().toISOString()}`;
107
+ // First upsert: posts a fresh note.
108
+ const first = await postSummaryComment(adapter, ref, body, MARKER);
109
+ expect(first.createdNativeId).toBeGreaterThan(0);
110
+ createdNoteIds.push(first.createdNativeId);
111
+ // Marker persistence: anchor the assertion to the note PRODUCTION created
112
+ // (first.createdNativeId === GitLabNote.id), not to "any note that happens
113
+ // to carry the marker". This proves the specific note the upsert posted is
114
+ // live on the MR AND still carries the marker the adapter's sweep filters
115
+ // on (gitlab-forge-adapter.ts:182).
116
+ const afterFirst = await client.listMrNotes(projectId, MR_IID, token);
117
+ const createdNote = afterFirst.find((n) => n.id === first.createdNativeId);
118
+ expect(createdNote, 'the note created by upsert must appear in the live listing').toBeDefined();
119
+ expect(createdNote?.body.includes(MARKER.html), 'the created note must carry the marker the sweep filters on').toBe(true);
120
+ // Second upsert: must SWEEP the first (idempotency) and repost.
121
+ const second = await postSummaryComment(adapter, ref, body, MARKER);
122
+ expect(second.createdNativeId).toBeGreaterThan(0);
123
+ createdNoteIds.push(second.createdNativeId);
124
+ expect(second.deletedNativeIds).toContain(first.createdNativeId);
125
+ // Idempotency FOLD against the REAL MR state (not just the return value):
126
+ // after the repost exactly ONE marker note must remain, and it must be the
127
+ // note the SECOND upsert created. This is the invariant the sweep exists to
128
+ // hold — complements the `deletedNativeIds toContain first` return-value check.
129
+ const afterSecond = await client.listMrNotes(projectId, MR_IID, token);
130
+ const markerNotes = afterSecond.filter((n) => n.body.includes(MARKER.html));
131
+ expect(markerNotes.length, 'the sweep must leave exactly one note carrying the marker').toBe(1);
132
+ expect(markerNotes[0]?.id).toBe(second.createdNativeId);
133
+ }, 30_000);
134
+ });
135
+ //# sourceMappingURL=gitlab-e2e.manual.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitlab-e2e.manual.test.js","sourceRoot":"","sources":["../../src/lib/gitlab-e2e.manual.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Token resolution for the CLI GitLab MR post-back (`ghagga review --mr N`).
3
+ *
4
+ * Resolution order (CI-first, interactive fallback):
5
+ * 1. env GITLAB_TOKEN — the GitLab CI / standard convention.
6
+ * 2. env GL_TOKEN — the `glab` CLI / alternate convention.
7
+ * 3. a GitLab-SPECIFIC stored token (config `gitlabToken`), if one exists.
8
+ *
9
+ * CRITICAL: this does NOT fall back to the GitHub stored login token
10
+ * ({@link getStoredToken}). A GitHub token against the GitLab API yields a 401 /
11
+ * nonsense — a GitHub credential is NOT a GitLab credential. When no GitLab token
12
+ * resolves, return `null`; the caller turns that into an actionable error ONLY
13
+ * when `--mr` was actually requested (non-zero exit, P3/P4 exit semantics).
14
+ */
15
+ /** Resolve a GitLab token: env GITLAB_TOKEN > GL_TOKEN > GitLab-specific stored. */
16
+ export declare function resolveMrToken(env?: NodeJS.ProcessEnv, storedTokenFn?: () => string | null): string | null;
17
+ //# sourceMappingURL=gitlab-token.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitlab-token.d.ts","sourceRoot":"","sources":["../../src/lib/gitlab-token.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,oFAAoF;AACpF,wBAAgB,cAAc,CAC5B,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,aAAa,GAAE,MAAM,MAAM,GAAG,IAA2B,GACxD,MAAM,GAAG,IAAI,CAKf"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Token resolution for the CLI GitLab MR post-back (`ghagga review --mr N`).
3
+ *
4
+ * Resolution order (CI-first, interactive fallback):
5
+ * 1. env GITLAB_TOKEN — the GitLab CI / standard convention.
6
+ * 2. env GL_TOKEN — the `glab` CLI / alternate convention.
7
+ * 3. a GitLab-SPECIFIC stored token (config `gitlabToken`), if one exists.
8
+ *
9
+ * CRITICAL: this does NOT fall back to the GitHub stored login token
10
+ * ({@link getStoredToken}). A GitHub token against the GitLab API yields a 401 /
11
+ * nonsense — a GitHub credential is NOT a GitLab credential. When no GitLab token
12
+ * resolves, return `null`; the caller turns that into an actionable error ONLY
13
+ * when `--mr` was actually requested (non-zero exit, P3/P4 exit semantics).
14
+ */
15
+ import { getStoredGitLabToken } from './config.js';
16
+ /** Resolve a GitLab token: env GITLAB_TOKEN > GL_TOKEN > GitLab-specific stored. */
17
+ export function resolveMrToken(env = process.env, storedTokenFn = getStoredGitLabToken) {
18
+ const fromEnv = env.GITLAB_TOKEN?.trim() || env.GL_TOKEN?.trim();
19
+ if (fromEnv)
20
+ return fromEnv;
21
+ const stored = storedTokenFn();
22
+ return stored?.trim() ? stored : null;
23
+ }
24
+ //# sourceMappingURL=gitlab-token.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitlab-token.js","sourceRoot":"","sources":["../../src/lib/gitlab-token.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnD,oFAAoF;AACpF,MAAM,UAAU,cAAc,CAC5B,MAAyB,OAAO,CAAC,GAAG,EACpC,gBAAqC,oBAAoB;IAEzD,MAAM,OAAO,GAAG,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,GAAG,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC;IACjE,IAAI,OAAO;QAAE,OAAO,OAAO,CAAC;IAC5B,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;IAC/B,OAAO,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;AACxC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=gitlab-token.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitlab-token.test.d.ts","sourceRoot":"","sources":["../../src/lib/gitlab-token.test.ts"],"names":[],"mappings":""}