@gitkraken/core-gitlens 0.24.0 → 0.25.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 (141) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +23 -15
  3. package/dist/git/errors.d.ts +6 -5
  4. package/dist/git/errors.js +7 -6
  5. package/dist/git/errors.js.map +1 -1
  6. package/dist/git/features.d.ts +1 -1
  7. package/dist/git/features.js +1 -1
  8. package/dist/git/features.js.map +1 -1
  9. package/dist/git/models/remoteProvider.d.ts +4 -1
  10. package/dist/git/models/remoteProvider.js.map +1 -1
  11. package/dist/git/providers/provider.d.ts +4 -2
  12. package/dist/git/utils/branch.utils.d.ts +4 -1
  13. package/dist/git/utils/branch.utils.js +5 -2
  14. package/dist/git/utils/branch.utils.js.map +1 -1
  15. package/dist/git/utils/mergeConflicts.utils.js +2 -2
  16. package/dist/git/utils/mergeConflicts.utils.js.map +1 -1
  17. package/dist/git-cli/cliGitProvider.d.ts +14 -3
  18. package/dist/git-cli/cliGitProvider.js +71 -34
  19. package/dist/git-cli/cliGitProvider.js.map +1 -1
  20. package/dist/git-cli/exec/git.d.ts +7 -1
  21. package/dist/git-cli/exec/git.js +19 -4
  22. package/dist/git-cli/exec/git.js.map +1 -1
  23. package/dist/git-cli/providers/branchRemote.js +2 -5
  24. package/dist/git-cli/providers/branchRemote.js.map +1 -1
  25. package/dist/git-cli/providers/branches.js +4 -4
  26. package/dist/git-cli/providers/branches.js.map +1 -1
  27. package/dist/git-cli/providers/config.js +4 -1
  28. package/dist/git-cli/providers/config.js.map +1 -1
  29. package/dist/plus/ai/errors.d.ts +4 -1
  30. package/dist/plus/ai/errors.js +4 -0
  31. package/dist/plus/ai/errors.js.map +1 -1
  32. package/dist/plus/ai/providers/mistralProvider.js +1 -1
  33. package/dist/plus/ai/providers/mistralProvider.js.map +1 -1
  34. package/dist/plus/ai/providers/openAICompatibleProviderBase.js +1 -0
  35. package/dist/plus/ai/providers/openAICompatibleProviderBase.js.map +1 -1
  36. package/dist/plus/git-github/api/github.js +12 -13
  37. package/dist/plus/git-github/api/github.js.map +1 -1
  38. package/dist/plus/git-github/api/token.d.ts +2 -0
  39. package/dist/plus/integrations/authentication/cloudIntegrationService.d.ts +1 -1
  40. package/dist/plus/integrations/authentication/cloudIntegrationService.js +6 -1
  41. package/dist/plus/integrations/authentication/cloudIntegrationService.js.map +1 -1
  42. package/dist/plus/integrations/authentication/configuredIntegrationService.js +2 -2
  43. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +18 -5
  44. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js.map +1 -1
  45. package/dist/plus/integrations/authentication/integrationAuthenticationService.d.ts +3 -1
  46. package/dist/plus/integrations/authentication/integrationAuthenticationService.js +4 -0
  47. package/dist/plus/integrations/authentication/integrationAuthenticationService.js.map +1 -1
  48. package/dist/plus/integrations/authentication/models.d.ts +2 -0
  49. package/dist/plus/integrations/authentication/models.js +2 -1
  50. package/dist/plus/integrations/authentication/models.js.map +1 -1
  51. package/dist/plus/integrations/authentication/observedAuthenticationProvider.js +1 -1
  52. package/dist/plus/integrations/authentication/rejectedTokenTracker.d.ts +5 -4
  53. package/dist/plus/integrations/authentication/rejectedTokenTracker.js +5 -4
  54. package/dist/plus/integrations/authentication/rejectedTokenTracker.js.map +1 -1
  55. package/dist/plus/integrations/index.d.ts +2 -2
  56. package/dist/plus/integrations/index.js +1 -1
  57. package/dist/plus/integrations/index.js.map +1 -1
  58. package/dist/plus/integrations/integrationService.d.ts +6 -0
  59. package/dist/plus/integrations/integrationService.js +3 -3
  60. package/dist/plus/integrations/integrationService.js.map +1 -1
  61. package/dist/plus/integrations/lite.js +1 -1
  62. package/dist/plus/integrations/manager.d.ts +51 -3
  63. package/dist/plus/integrations/models/gitHostIntegration.d.ts +14 -0
  64. package/dist/plus/integrations/models/gitHostIntegration.js +50 -41
  65. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  66. package/dist/plus/integrations/models/integration.d.ts +27 -5
  67. package/dist/plus/integrations/models/integration.js +57 -15
  68. package/dist/plus/integrations/models/integration.js.map +1 -1
  69. package/dist/plus/integrations/models/issueReads.d.ts +6 -0
  70. package/dist/plus/integrations/models/issuesIntegration.js +11 -11
  71. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
  72. package/dist/plus/integrations/providers/azure/azure.js +1 -1
  73. package/dist/plus/integrations/providers/azureDevOps.js +1 -1
  74. package/dist/plus/integrations/providers/azureDevOpsServer.js +1 -1
  75. package/dist/plus/integrations/providers/bitbucket/bitbucket.js +1 -1
  76. package/dist/plus/integrations/providers/bitbucket/bitbucket.js.map +1 -1
  77. package/dist/plus/integrations/providers/bitbucket-server.js +1 -1
  78. package/dist/plus/integrations/providers/bitbucket.js +1 -1
  79. package/dist/plus/integrations/providers/github.js +1 -1
  80. package/dist/plus/integrations/providers/gitlab/gitlab.js +2 -2
  81. package/dist/plus/integrations/providers/gitlab/gitlab.js.map +1 -1
  82. package/dist/plus/integrations/providers/gitlab.js +1 -1
  83. package/dist/plus/integrations/providers/jira-server.js +2 -2
  84. package/dist/plus/integrations/providers/jira.js +1 -1
  85. package/dist/plus/integrations/providers/linear.js +2 -2
  86. package/dist/plus/integrations/providers/linear.js.map +1 -1
  87. package/dist/plus/integrations/providers/models.js +2 -1
  88. package/dist/plus/integrations/providers/models.js.map +1 -1
  89. package/dist/plus/integrations/providers/providerErrors.js +9 -0
  90. package/dist/plus/integrations/providers/providerErrors.js.map +1 -1
  91. package/dist/plus/integrations/providers/providersApi.js +1 -1
  92. package/dist/plus/integrations/providers/trello.js +1 -1
  93. package/dist/plus/integrations/providers/utils.js +1 -1
  94. package/dist/plus/integrations/reads/counts.js +1 -1
  95. package/dist/plus/integrations/reads/cursors.js +1 -1
  96. package/dist/plus/integrations/reads/drains.d.ts +5 -4
  97. package/dist/plus/integrations/reads/drains.js +28 -8
  98. package/dist/plus/integrations/reads/drains.js.map +1 -1
  99. package/dist/plus/integrations/reads/hierarchy.js +1 -1
  100. package/dist/plus/integrations/reads/issueBatch.d.ts +1 -1
  101. package/dist/plus/integrations/reads/issueBatch.js.map +1 -1
  102. package/dist/plus/integrations/reads/issueTracker.js +1 -1
  103. package/dist/plus/integrations/reads/issues.d.ts +2 -0
  104. package/dist/plus/integrations/reads/issues.js +5 -3
  105. package/dist/plus/integrations/reads/issues.js.map +1 -1
  106. package/dist/plus/integrations/reads/paging.d.ts +5 -6
  107. package/dist/plus/integrations/reads/paging.js +5 -6
  108. package/dist/plus/integrations/reads/paging.js.map +1 -1
  109. package/dist/plus/integrations/reads/pullRequests.d.ts +4 -0
  110. package/dist/plus/integrations/reads/pullRequests.js +4 -2
  111. package/dist/plus/integrations/reads/pullRequests.js.map +1 -1
  112. package/dist/plus/integrations/reads/resolveRepository.js +1 -1
  113. package/dist/plus/integrations/reads/sweeps.js +42 -10
  114. package/dist/plus/integrations/reads/sweeps.js.map +1 -1
  115. package/dist/plus/integrations/results.d.ts +23 -3
  116. package/dist/plus/integrations/results.js.map +1 -1
  117. package/dist/plus/notifications/api.d.ts +20 -0
  118. package/dist/plus/notifications/api.js +89 -0
  119. package/dist/plus/notifications/api.js.map +1 -0
  120. package/dist/plus/notifications/context.d.ts +28 -0
  121. package/dist/plus/notifications/context.js +0 -0
  122. package/dist/plus/notifications/errors.d.ts +13 -0
  123. package/dist/plus/notifications/errors.js +22 -0
  124. package/dist/plus/notifications/errors.js.map +1 -0
  125. package/dist/plus/notifications/models.d.ts +94 -0
  126. package/dist/plus/notifications/models.js +0 -0
  127. package/dist/plus/notifications/notificationsService.d.ts +115 -0
  128. package/dist/plus/notifications/notificationsService.js +381 -0
  129. package/dist/plus/notifications/notificationsService.js.map +1 -0
  130. package/dist/plus/notifications/parse.d.ts +12 -0
  131. package/dist/plus/notifications/parse.js +152 -0
  132. package/dist/plus/notifications/parse.js.map +1 -0
  133. package/dist/plus/notifications/presentation.d.ts +28 -0
  134. package/dist/plus/notifications/presentation.js +55 -0
  135. package/dist/plus/notifications/presentation.js.map +1 -0
  136. package/dist/utils/env/node/exec.d.ts +10 -1
  137. package/dist/utils/env/node/exec.js +39 -2
  138. package/dist/utils/env/node/exec.js.map +1 -1
  139. package/docs/integrations.md +21 -2
  140. package/docs/notifications.md +239 -0
  141. package/package.json +21 -1
@@ -0,0 +1,55 @@
1
+ //#region ../plus/notifications/src/presentation.ts
2
+ /** The category chip shown on a notification card; `feature` notifications get none. */
3
+ function getCategoryChip(contentType) {
4
+ switch (contentType) {
5
+ case "help": return "help";
6
+ case "marketing": return "offer";
7
+ case "security": return "security";
8
+ case "system": return "system";
9
+ default: return;
10
+ }
11
+ }
12
+ /** Only security and system notices may interrupt with a toast; everything else arrives quietly. */
13
+ function isToastEligible(notification) {
14
+ return notification.contentType === "security" || notification.contentType === "system";
15
+ }
16
+ /**
17
+ * Server-supplied links may only open `https:` URLs (never `command:`, `javascript:`, `file:`, ...). Deliberately
18
+ * stricter than the inline-link policy for untrusted markdown (`getUntrustedLinkHref`, which also allows `http:`
19
+ * and `mailto:`): a call-to-action is a prominent, single-click action.
20
+ */
21
+ function isSafeExternalUrl(url) {
22
+ if (!URL.canParse(url)) return false;
23
+ return new URL(url).protocol === "https:";
24
+ }
25
+ function formatUnreadCount(count) {
26
+ return count > 99 ? "99+" : String(count);
27
+ }
28
+ /**
29
+ * The body to show under a notification's headline. Service-generated notices often open their body with the
30
+ * headline itself (`jane@acme.dev has requested to join _Acme_. …`), so a first paragraph that only repeats the
31
+ * headline (ignoring markdown emphasis/code markers, whitespace and a trailing period) is dropped. Only the
32
+ * first paragraph is ever dropped; `undefined` when nothing remains.
33
+ */
34
+ function getDisplayBody(headline, body) {
35
+ const trimmed = body?.trim();
36
+ if (!trimmed) return void 0;
37
+ if (!headline) return trimmed;
38
+ const [first, ...rest] = trimmed.split(/\n\s*\n/);
39
+ if (normalizeForComparison(first) !== normalizeForComparison(headline)) return trimmed;
40
+ return rest.join("\n\n").trim() || void 0;
41
+ }
42
+ function normalizeForComparison(text) {
43
+ return text.replace(/[*_`]/g, "").replace(/\s+/g, " ").trim().replace(/\.$/, "");
44
+ }
45
+ /**
46
+ * A best-effort conversion of markdown to a single line of plain text, for headers that cannot render markdown.
47
+ * Not a parser: it handles the common inline and line-start constructs and leaves anything exotic as-is.
48
+ */
49
+ function toPlainText(markdown) {
50
+ return markdown.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/<[^>]*>/g, " ").replace(/^[ \t]*(?:#{1,6}[ \t]+|>[ \t]*|[-*+][ \t]+|\d+\.[ \t]+)/gm, "").replace(/(?<!\w)[*_]+|[*_]+(?!\w)/g, "").replace(/`|~~/g, "").replace(/\s+/g, " ").trim();
51
+ }
52
+ //#endregion
53
+ export { formatUnreadCount, getCategoryChip, getDisplayBody, isSafeExternalUrl, isToastEligible, toPlainText };
54
+
55
+ //# sourceMappingURL=presentation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presentation.js","names":[],"sources":["../../../../plus/notifications/src/presentation.ts"],"sourcesContent":["import type { GkNotification, NotificationContentType } from './models.js';\n\nexport type NotificationCategoryChip = 'help' | 'offer' | 'security' | 'system';\n\n/** The category chip shown on a notification card; `feature` notifications get none. */\nexport function getCategoryChip(contentType: NotificationContentType): NotificationCategoryChip | undefined {\n\tswitch (contentType) {\n\t\tcase 'help':\n\t\t\treturn 'help';\n\t\tcase 'marketing':\n\t\t\treturn 'offer';\n\t\tcase 'security':\n\t\t\treturn 'security';\n\t\tcase 'system':\n\t\t\treturn 'system';\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n\n/** Only security and system notices may interrupt with a toast; everything else arrives quietly. */\nexport function isToastEligible(notification: Pick<GkNotification, 'contentType'>): boolean {\n\treturn notification.contentType === 'security' || notification.contentType === 'system';\n}\n\n/**\n * Server-supplied links may only open `https:` URLs (never `command:`, `javascript:`, `file:`, ...). Deliberately\n * stricter than the inline-link policy for untrusted markdown (`getUntrustedLinkHref`, which also allows `http:`\n * and `mailto:`): a call-to-action is a prominent, single-click action.\n */\nexport function isSafeExternalUrl(url: string): boolean {\n\tif (!URL.canParse(url)) return false;\n\n\treturn new URL(url).protocol === 'https:';\n}\n\nexport function formatUnreadCount(count: number): string {\n\treturn count > 99 ? '99+' : String(count);\n}\n\n/**\n * The body to show under a notification's headline. Service-generated notices often open their body with the\n * headline itself (`jane@acme.dev has requested to join _Acme_. …`), so a first paragraph that only repeats the\n * headline (ignoring markdown emphasis/code markers, whitespace and a trailing period) is dropped. Only the\n * first paragraph is ever dropped; `undefined` when nothing remains.\n */\nexport function getDisplayBody(headline: string | undefined, body: string | undefined): string | undefined {\n\tconst trimmed = body?.trim();\n\tif (!trimmed) return undefined;\n\tif (!headline) return trimmed;\n\n\tconst [first, ...rest] = trimmed.split(/\\n\\s*\\n/);\n\tif (normalizeForComparison(first) !== normalizeForComparison(headline)) return trimmed;\n\n\treturn rest.join('\\n\\n').trim() || undefined;\n}\n\nfunction normalizeForComparison(text: string): string {\n\treturn text.replace(/[*_`]/g, '').replace(/\\s+/g, ' ').trim().replace(/\\.$/, '');\n}\n\n/**\n * A best-effort conversion of markdown to a single line of plain text, for headers that cannot render markdown.\n * Not a parser: it handles the common inline and line-start constructs and leaves anything exotic as-is.\n */\nexport function toPlainText(markdown: string): string {\n\treturn (\n\t\tmarkdown\n\t\t\t.replace(/!\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n\t\t\t.replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n\t\t\t.replace(/<[^>]*>/g, ' ')\n\t\t\t.replace(/^[ \\t]*(?:#{1,6}[ \\t]+|>[ \\t]*|[-*+][ \\t]+|\\d+\\.[ \\t]+)/gm, '')\n\t\t\t// Emphasis markers only at word edges, so `snake_case` names keep their underscores\n\t\t\t.replace(/(?<!\\w)[*_]+|[*_]+(?!\\w)/g, '')\n\t\t\t.replace(/`|~~/g, '')\n\t\t\t.replace(/\\s+/g, ' ')\n\t\t\t.trim()\n\t);\n}\n"],"mappings":";;AAKA,SAAgB,gBAAgB,aAA4E;CAC3G,QAAQ,aAAR;EACC,KAAK,QACJ,OAAO;EACR,KAAK,aACJ,OAAO;EACR,KAAK,YACJ,OAAO;EACR,KAAK,UACJ,OAAO;EACR,SACC;CACF;AACD;;AAGA,SAAgB,gBAAgB,cAA4D;CAC3F,OAAO,aAAa,gBAAgB,cAAc,aAAa,gBAAgB;AAChF;;;;;;AAOA,SAAgB,kBAAkB,KAAsB;CACvD,IAAI,CAAC,IAAI,SAAS,GAAG,GAAG,OAAO;CAE/B,OAAO,IAAI,IAAI,GAAG,CAAC,CAAC,aAAa;AAClC;AAEA,SAAgB,kBAAkB,OAAuB;CACxD,OAAO,QAAQ,KAAK,QAAQ,OAAO,KAAK;AACzC;;;;;;;AAQA,SAAgB,eAAe,UAA8B,MAA8C;CAC1G,MAAM,UAAU,MAAM,KAAK;CAC3B,IAAI,CAAC,SAAS,OAAO,KAAA;CACrB,IAAI,CAAC,UAAU,OAAO;CAEtB,MAAM,CAAC,OAAO,GAAG,QAAQ,QAAQ,MAAM,SAAS;CAChD,IAAI,uBAAuB,KAAK,MAAM,uBAAuB,QAAQ,GAAG,OAAO;CAE/E,OAAO,KAAK,KAAK,MAAM,CAAC,CAAC,KAAK,KAAK,KAAA;AACpC;AAEA,SAAS,uBAAuB,MAAsB;CACrD,OAAO,KAAK,QAAQ,UAAU,EAAE,CAAC,CAAC,QAAQ,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,QAAQ,OAAO,EAAE;AAChF;;;;;AAMA,SAAgB,YAAY,UAA0B;CACrD,OACC,SACE,QAAQ,2BAA2B,IAAI,CAAC,CACxC,QAAQ,0BAA0B,IAAI,CAAC,CACvC,QAAQ,YAAY,GAAG,CAAC,CACxB,QAAQ,6DAA6D,EAAE,CAAC,CAExE,QAAQ,6BAA6B,EAAE,CAAC,CACxC,QAAQ,SAAS,EAAE,CAAC,CACpB,QAAQ,QAAQ,GAAG,CAAC,CACpB,KAAK;AAET"}
@@ -1,4 +1,5 @@
1
1
  import { CancellationReason } from "../../cancellation.js";
2
+ import { ChildProcess } from "child_process";
2
3
  import { Readable, Writable } from "stream";
3
4
  //#region ../utils/src/env/node/exec.d.ts
4
5
  interface RunErrorContext {
@@ -70,6 +71,14 @@ export interface ProcessSpawnOptions {
70
71
  * - close the child's stdout when `stdout` is destroyed, which is how a reader that stops early ends the process
71
72
  */
72
73
  export type ProcessSpawner = (command: string, args: readonly string[], options: ProcessSpawnOptions) => SpawnedProcess;
74
+ /**
75
+ * The default {@link ProcessSpawner}: Node's `spawn`, but with its `timeout` handled here. Node clears its timeout
76
+ * timer only on `'exit'`, which a failed launch (e.g. a missing `cwd`) never emits, so the timer outlives the
77
+ * process and keeps Node alive for the full timeout. This mirrors Node's timeout handling but also clears on
78
+ * `'close'`, as https://github.com/nodejs/node/pull/65506 does; remove it once the minimum supported Node/Electron
79
+ * includes that fix.
80
+ */
81
+ export declare function spawnProcess(command: string, args: readonly string[], options: ProcessSpawnOptions): ChildProcess;
73
82
  export interface RunOptions<TEncoding = BufferEncoding | 'buffer'> {
74
83
  cwd?: string;
75
84
  readonly env?: Record<string, string | undefined>;
@@ -108,7 +117,7 @@ export interface RunOptions<TEncoding = BufferEncoding | 'buffer'> {
108
117
  * Use for commands whose failure is expected (e.g. paired with `errors: 'ignore'`).
109
118
  */
110
119
  readonly quiet?: boolean;
111
- /** Launches the process; defaults to Node's `spawn`. Only used by `runSpawn` */
120
+ /** Launches the process; defaults to {@link spawnProcess}. Only used by `runSpawn` */
112
121
  readonly spawn?: ProcessSpawner;
113
122
  }
114
123
  type ExitCodeOnlyRunOptions<TEncoding = BufferEncoding | 'buffer'> = RunOptions<TEncoding> & {
@@ -142,6 +142,43 @@ async function getWindowsShortPath(path) {
142
142
  });
143
143
  });
144
144
  }
145
+ /**
146
+ * The default {@link ProcessSpawner}: Node's `spawn`, but with its `timeout` handled here. Node clears its timeout
147
+ * timer only on `'exit'`, which a failed launch (e.g. a missing `cwd`) never emits, so the timer outlives the
148
+ * process and keeps Node alive for the full timeout. This mirrors Node's timeout handling but also clears on
149
+ * `'close'`, as https://github.com/nodejs/node/pull/65506 does; remove it once the minimum supported Node/Electron
150
+ * includes that fix.
151
+ */
152
+ function spawnProcess(command, args, options) {
153
+ const { timeout, ...spawnOptions } = options;
154
+ if (timeout != null && !(Number.isInteger(timeout) && timeout >= 0)) {
155
+ const ex = /* @__PURE__ */ new RangeError(`The value of "timeout" is out of range. It must be an unsigned integer. Received ${String(timeout)}`);
156
+ ex.code = "ERR_OUT_OF_RANGE";
157
+ throw ex;
158
+ }
159
+ const child = spawn(command, args, spawnOptions);
160
+ if (timeout != null && timeout > 0) {
161
+ let timeoutId = setTimeout(() => {
162
+ if (timeoutId != null) {
163
+ try {
164
+ child.kill();
165
+ } catch (ex) {
166
+ child.emit("error", ex);
167
+ }
168
+ timeoutId = void 0;
169
+ }
170
+ }, timeout);
171
+ const clear = () => {
172
+ if (timeoutId != null) {
173
+ clearTimeout(timeoutId);
174
+ timeoutId = void 0;
175
+ }
176
+ };
177
+ child.once("exit", clear);
178
+ child.once("close", clear);
179
+ }
180
+ return child;
181
+ }
145
182
  const bufferExceededRegex = /stdout maxBuffer( length)? exceeded/;
146
183
  function run(command, args, encoding, options) {
147
184
  const scope = getScopedLogger() ?? maybeStartScopedLogger("Exec.run");
@@ -203,7 +240,7 @@ function runSpawn(command, args, encoding, options) {
203
240
  const scope = getScopedLogger() ?? maybeStartScopedLogger("Exec.runSpawn");
204
241
  const { stdin, stdinEncoding, cancellation, decode, spawn: spawner, ...opts } = options;
205
242
  return new Promise((resolve, reject) => {
206
- const proc = (spawner ?? spawn)(command, args, {
243
+ const proc = (spawner ?? spawnProcess)(command, args, {
207
244
  cwd: opts.cwd,
208
245
  env: opts.env,
209
246
  timeout: opts.timeout === 0 ? void 0 : opts.timeout,
@@ -287,6 +324,6 @@ async function fsExists(path) {
287
324
  return new Promise((resolve) => access(path, constants.F_OK, (err) => resolve(err == null)));
288
325
  }
289
326
  //#endregion
290
- export { CancelledRunError, RunError, endStdin, findExecutable, fsExists, getWindowsShortPath, run, runSpawn };
327
+ export { CancelledRunError, RunError, endStdin, findExecutable, fsExists, getWindowsShortPath, run, runSpawn, spawnProcess };
291
328
 
292
329
  //# sourceMappingURL=exec.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"exec.js","names":["joinPaths"],"sources":["../../../../../utils/src/env/node/exec.ts"],"sourcesContent":["import type { ExecFileException, ExecFileOptions } from 'child_process';\nimport { exec, execFile, spawn } from 'child_process';\nimport type { Stats } from 'fs';\nimport { access, constants } from 'fs';\nimport { stat } from 'fs/promises';\nimport { join as joinPaths } from 'path';\nimport * as process from 'process';\nimport type { Readable, Writable } from 'stream';\nimport type { CancellationReason } from '../../cancellation.js';\nimport { getScopedLogger, maybeStartScopedLogger } from '../../logger.scoped.js';\nimport { normalizePath } from '../../path.js';\nimport { isWindows } from './platform.js';\n\n// -- Error types --\n\ninterface RunErrorContext {\n\tmessage: string;\n\tcmd?: string | undefined;\n\tkilled?: boolean | undefined;\n\tcode?: string | number | null | undefined;\n\tsignal?: NodeJS.Signals | undefined;\n}\n\nexport class RunError extends Error {\n\treadonly cmd?: string | undefined;\n\treadonly killed?: boolean | undefined;\n\treadonly code?: string | number | undefined;\n\treadonly signal?: NodeJS.Signals | undefined;\n\treadonly stdout: string;\n\treadonly stderr: string;\n\n\tconstructor(context: RunErrorContext, stdout: string, stderr: string) {\n\t\tsuper(context.message);\n\n\t\tthis.cmd = context.cmd;\n\t\tthis.killed = context.killed;\n\t\tthis.code = context.code ?? undefined;\n\t\tthis.signal = context.signal;\n\t\tthis.stdout = stdout?.trim() ?? '';\n\t\tthis.stderr = stderr?.trim() ?? '';\n\n\t\tthis.name = 'RunError';\n\t\tError.captureStackTrace?.(this, new.target);\n\t}\n}\n\nexport class CancelledRunError extends RunError {\n\treadonly reason: CancellationReason;\n\n\tconstructor(\n\t\tcmd: string,\n\t\tkilled: boolean,\n\t\tcode?: number | string | undefined,\n\t\tsignal: NodeJS.Signals = 'SIGTERM',\n\t\treason: CancellationReason = 'unknown',\n\t) {\n\t\tsuper(\n\t\t\t{ message: `Operation cancelled; command=${cmd}`, cmd: cmd, killed: killed, code: code, signal: signal },\n\t\t\t'',\n\t\t\t'',\n\t\t);\n\n\t\tthis.reason = reason;\n\n\t\tthis.name = 'CancelledRunError';\n\t\tError.captureStackTrace?.(this, new.target);\n\t}\n}\n\n// -- Execution utilities --\n\nconst slashesRegex = /[\\\\/]/;\nconst ps1Regex = /\\.ps1$/i;\nconst batOrCmdRegex = /\\.(bat|cmd)$/i;\nconst jsRegex = /\\.(js)$/i;\n\n/**\n * Search PATH to see if a file exists in any of the path folders.\n *\n * @param {string} exe The file to search for\n * @return {string} A fully qualified path, or the original path if nothing\n * is found\n *\n * @private\n */\nasync function runDownPath(exe: string): Promise<string> {\n\t// NB: Windows won't search PATH looking for executables in spawn like\n\t// Posix does\n\n\t// Files with any directory path don't get this applied\n\tif (slashesRegex.test(exe)) return exe;\n\n\tconst target = joinPaths('.', exe);\n\ttry {\n\t\tconst stats = await stat(target);\n\t\tif (stats?.isFile() && isExecutable(stats)) return target;\n\t} catch {}\n\n\tconst path = process.env.PATH;\n\tif (path != null && path.length !== 0) {\n\t\tconst haystack = path.split(isWindows ? ';' : ':');\n\t\tfor (const p of haystack) {\n\t\t\tconst needle = joinPaths(p, exe);\n\t\t\ttry {\n\t\t\t\tconst stats = await stat(needle);\n\t\t\t\tif (stats?.isFile() && isExecutable(stats)) return needle;\n\t\t\t} catch {}\n\t\t}\n\t}\n\n\treturn exe;\n}\n\nfunction isExecutable(stats: Stats) {\n\tif (isWindows) return true;\n\n\tconst isGroup = stats.gid ? stats.gid === process.getgid?.() : true;\n\tconst isUser = stats.uid ? stats.uid === process.getuid?.() : true;\n\n\treturn Boolean(stats.mode & 0o0001 || (stats.mode & 0o0010 && isGroup) || (stats.mode & 0o0100 && isUser));\n}\n\n/**\n * Finds the executable and parameters to run on Windows. This method\n * mimics the POSIX behavior of being able to run scripts as executables by\n * replacing the passed-in executable with the script runner, for PowerShell,\n * CMD, and node scripts.\n *\n * This method also does the work of running down PATH, which spawn on Windows\n * also doesn't do, unlike on POSIX.\n */\nexport async function findExecutable(exe: string, args: string[]): Promise<{ cmd: string; args: string[] }> {\n\t// POSIX can just execute scripts directly, no need for silly goosery\n\tif (!isWindows) return { cmd: await runDownPath(exe), args: args };\n\n\tif (!(await fsExists(exe))) {\n\t\t// NB: When you write something like `surf-client ... -- surf-build` on Windows,\n\t\t// a shell would normally convert that to surf-build.cmd, but since it's passed\n\t\t// in as an argument, it doesn't happen\n\t\tconst possibleExts = ['.exe', '.bat', '.cmd', '.ps1'];\n\t\tfor (const ext of possibleExts) {\n\t\t\tconst possibleFullPath = await runDownPath(`${exe}${ext}`);\n\n\t\t\tif (await fsExists(possibleFullPath)) return findExecutable(possibleFullPath, args);\n\t\t}\n\t}\n\n\tif (ps1Regex.test(exe)) {\n\t\tconst cmd = joinPaths(\n\t\t\tprocess.env.SYSTEMROOT ?? 'C:\\\\WINDOWS',\n\t\t\t'System32',\n\t\t\t'WindowsPowerShell',\n\t\t\t'v1.0',\n\t\t\t'PowerShell.exe',\n\t\t);\n\t\tconst psargs = ['-ExecutionPolicy', 'Unrestricted', '-NoLogo', '-NonInteractive', '-File', exe];\n\n\t\treturn { cmd: cmd, args: [...psargs, ...args] };\n\t}\n\n\tif (batOrCmdRegex.test(exe)) {\n\t\tconst cmd = joinPaths(process.env.SYSTEMROOT ?? 'C:\\\\WINDOWS', 'System32', 'cmd.exe');\n\t\tconst cmdArgs = ['/C', exe, ...args];\n\n\t\treturn { cmd: cmd, args: cmdArgs };\n\t}\n\n\tif (jsRegex.test(exe)) {\n\t\tconst cmd = process.execPath;\n\t\tconst nodeArgs = [exe];\n\n\t\treturn { cmd: cmd, args: [...nodeArgs, ...args] };\n\t}\n\n\treturn { cmd: exe, args: args };\n}\n\nexport async function getWindowsShortPath(path: string): Promise<string> {\n\treturn new Promise<string>((resolve, reject) => {\n\t\texec(`for %I in (\"${path}\") do @echo %~sI`, (error, stdout, _stderr) => {\n\t\t\tif (error != null) {\n\t\t\t\treject(error);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tresolve(normalizePath(stdout.trim()));\n\t\t});\n\t});\n}\n\n/** What core uses of a spawned process; Node's `ChildProcess` satisfies it */\nexport interface SpawnedProcess {\n\treadonly stdin: Writable | null;\n\treadonly stdout: Readable | null;\n\treadonly stderr: Readable | null;\n\treadonly killed: boolean;\n\treadonly exitCode: number | null;\n\treadonly spawnargs: string[];\n\tonce(event: 'error', listener: (error: Error) => void): this;\n\tonce(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;\n\tremoveAllListeners(): this;\n}\n\nexport interface ProcessSpawnOptions {\n\treadonly cwd?: string;\n\t/** The child's whole environment, not additions to the spawner's own; an `undefined` value leaves the key out */\n\treadonly env?: Record<string, string | undefined>;\n\t/** Milliseconds; absent means none */\n\treadonly timeout?: number;\n\treadonly signal?: AbortSignal;\n\t/** A `null` entry means `'pipe'` */\n\treadonly stdio?: ['pipe' | 'ignore', null, null];\n}\n\n/**\n * Launches a process in place of Node's `spawn`, and must behave as it does:\n * - pipe stdout and stderr, and stdin unless `stdio[0]` is `'ignore'`, which leaves `stdin` null\n * - honour `signal`, including one already aborted: kill with SIGTERM, then emit `'error'` with `name` `AbortError`,\n * unless the process has already exited\n * - honour `timeout`: kill with SIGTERM and set `killed`, without emitting `'error'`\n * - report a killed process's `'close'` with a null code and its signal, not an exit code such as 143\n * - emit no event before returning, `'error'` at most once, and `'close'` exactly once and last: also after `'error'`\n * (a failed launch, an abort), and only after `stdout` and `stderr` have ended or been destroyed, with `exitCode`\n * and `killed` already set\n * - close the child's stdout when `stdout` is destroyed, which is how a reader that stops early ends the process\n */\nexport type ProcessSpawner = (command: string, args: readonly string[], options: ProcessSpawnOptions) => SpawnedProcess;\n\nexport interface RunOptions<TEncoding = BufferEncoding | 'buffer'> {\n\tcwd?: string;\n\treadonly env?: Record<string, string | undefined>;\n\treadonly encoding?: TEncoding;\n\t/**\n\t * Decodes non-UTF-8 output. Called when `encoding` is not utf8/binary/buffer.\n\t * Provided by the host via the `GitOptions.decode` option.\n\t * If absent and a non-UTF-8 encoding is requested, throws at runtime.\n\t */\n\treadonly decode?: (data: Uint8Array, options?: { readonly encoding: string }) => string | Promise<string>;\n\t/**\n\t * The size the output buffer to allocate to the spawned process. Set this\n\t * if you are anticipating a large amount of output.\n\t *\n\t * If not specified, this will be 10MB (10485760 bytes) which should be\n\t * enough for most Git operations.\n\t */\n\treadonly maxBuffer?: number;\n\treadonly cancellation?: AbortSignal;\n\t/**\n\t * An optional string or buffer which will be written to\n\t * the child process stdin stream immediately immediately\n\t * after spawning the process.\n\t */\n\treadonly stdin?: string | Buffer;\n\t/**\n\t * The encoding to use when writing to stdin, if the stdin\n\t * parameter is a string.\n\t */\n\treadonly stdinEncoding?: string;\n\treadonly timeout?: number;\n\t/**\n\t * When true, suppresses the `[EXEC]` stderr warning emitted on debug-enabled scopes.\n\t * Use for commands whose failure is expected (e.g. paired with `errors: 'ignore'`).\n\t */\n\treadonly quiet?: boolean;\n\t/** Launches the process; defaults to Node's `spawn`. Only used by `runSpawn` */\n\treadonly spawn?: ProcessSpawner;\n}\n\nconst bufferExceededRegex = /stdout maxBuffer( length)? exceeded/;\n\ntype ExitCodeOnlyRunOptions<TEncoding = BufferEncoding | 'buffer'> = RunOptions<TEncoding> & { exitCodeOnly: true };\n\nexport function run(\n\tcommand: string,\n\targs: readonly string[],\n\tencoding: BufferEncoding | string,\n\toptions: ExitCodeOnlyRunOptions<BufferEncoding>,\n): Promise<number>;\nexport function run(\n\tcommand: string,\n\targs: readonly string[],\n\tencoding: BufferEncoding | string,\n\toptions?: RunOptions<BufferEncoding>,\n): Promise<string>;\nexport function run<T extends number | string>(\n\tcommand: string,\n\targs: readonly string[],\n\tencoding: BufferEncoding | string,\n\toptions?: RunOptions<BufferEncoding> & { exitCodeOnly?: boolean },\n): Promise<T> {\n\tconst scope = getScopedLogger() ?? maybeStartScopedLogger('Exec.run');\n\n\tconst { stdin, stdinEncoding, cancellation, decode, ...opts }: RunOptions<BufferEncoding> & ExecFileOptions = {\n\t\tmaxBuffer: 1000 * 1024 * 1024,\n\t\t...options,\n\t};\n\n\tconst promise = new Promise<T>((resolve, reject) => {\n\t\tconst proc = execFile(\n\t\t\tcommand,\n\t\t\targs,\n\t\t\t{ ...opts, signal: cancellation },\n\t\t\tasync (error: ExecFileException | null, stdout, stderr) => {\n\t\t\t\tif (options?.exitCodeOnly) {\n\t\t\t\t\tresolve((error?.code ?? proc.exitCode) as T);\n\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (error != null) {\n\t\t\t\t\tif (error.signal === 'SIGTERM') {\n\t\t\t\t\t\treject(\n\t\t\t\t\t\t\tnew CancelledRunError(\n\t\t\t\t\t\t\t\t`${command} ${args.join(' ')}`,\n\t\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\t\terror.code ?? undefined,\n\t\t\t\t\t\t\t\terror.signal,\n\t\t\t\t\t\t\t\tcancellation?.aborted ? 'aborted' : error.killed ? 'timeout' : 'unknown',\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (bufferExceededRegex.test(error.message)) {\n\t\t\t\t\t\terror.message = `Command output exceeded the allocated stdout buffer. Set 'options.maxBuffer' to a larger value than ${opts.maxBuffer} bytes`;\n\t\t\t\t\t}\n\n\t\t\t\t\tlet stdoutDecoded: string;\n\t\t\t\t\tlet stderrDecoded: string;\n\t\t\t\t\tif (encoding === 'utf8' || encoding === 'binary' || encoding === 'buffer') {\n\t\t\t\t\t\t// stdout & stderr can be `Buffer` or `string\n\t\t\t\t\t\t// oxlint-disable-next-line typescript/no-unnecessary-type-conversion\n\t\t\t\t\t\tstdoutDecoded = stdout.toString();\n\t\t\t\t\t\t// oxlint-disable-next-line typescript/no-unnecessary-type-conversion\n\t\t\t\t\t\tstderrDecoded = stderr.toString();\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (decode == null) {\n\t\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t\t`Non-UTF-8 encoding '${encoding}' requested but no decode function configured`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tstdoutDecoded = await decode(\n\t\t\t\t\t\t\tBuffer.from(stdout, 'binary'),\n\t\t\t\t\t\t\tencoding ? { encoding: encoding } : undefined,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tstderrDecoded = await decode(\n\t\t\t\t\t\t\tBuffer.from(stderr, 'binary'),\n\t\t\t\t\t\t\tencoding ? { encoding: encoding } : undefined,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\treject(new RunError(error, stdoutDecoded, stderrDecoded));\n\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (stderr && !options?.quiet && scope?.enabled('debug')) {\n\t\t\t\t\tscope?.warn(`[EXEC] '${command} ${args.join(' ')}' \\u2022 ${stderr}`);\n\t\t\t\t}\n\n\t\t\t\tif (encoding === 'utf8' || encoding === 'binary' || encoding === 'buffer') {\n\t\t\t\t\tresolve(stdout as T);\n\t\t\t\t} else {\n\t\t\t\t\tif (decode == null) {\n\t\t\t\t\t\tthrow new Error(`Non-UTF-8 encoding '${encoding}' requested but no decode function configured`);\n\t\t\t\t\t}\n\n\t\t\t\t\tresolve(\n\t\t\t\t\t\t(await decode(\n\t\t\t\t\t\t\tBuffer.from(stdout, 'binary'),\n\t\t\t\t\t\t\tencoding ? { encoding: encoding } : undefined,\n\t\t\t\t\t\t)) as T,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t},\n\t\t);\n\n\t\tif (stdin != null) {\n\t\t\tendStdin(proc.stdin, stdin, stdinEncoding);\n\t\t}\n\t});\n\n\treturn promise.finally(() => scope?.[Symbol.dispose]());\n}\n\n/**\n * Writes a child's whole input and closes it. A child can exit, or be killed on cancellation, before reading all of\n * it, and the pipe then errors (EPIPE, or EOF on Windows). Unlistened, that error is thrown as an uncaught exception\n * and takes down the whole host process, so it is dropped: the child's exit status and stderr already report the\n * failure, and a child that chose to stop reading answered from what it read.\n */\nexport function endStdin(stream: Writable | null, stdin: string | Buffer, stdinEncoding: string | undefined): void {\n\tif (stream == null) return;\n\n\tstream.on('error', () => {});\n\tif (typeof stdin === 'string') {\n\t\tstream.end(stdin, (stdinEncoding ?? 'utf8') as BufferEncoding);\n\t} else {\n\t\tstream.end(stdin);\n\t}\n}\n\nexport interface RunExitResult {\n\t/**\n\t * The process's exit code. ABSENT when the process never exited normally — killed by a signal, in which\n\t * case {@link signal} is set. Deliberately optional rather than defaulted to `0`: a signalled process\n\t * reported as `exitCode: 0` is indistinguishable from a clean success, so callers would read a partial\n\t * or empty stdout as a complete answer.\n\t */\n\treadonly exitCode?: number;\n\t/** Set when the process was terminated by a signal instead of exiting; {@link exitCode} is then absent. */\n\treadonly signal?: NodeJS.Signals;\n\t/** Whether Node itself killed the process (its spawn `timeout`), as opposed to an external signal. */\n\treadonly killed?: boolean;\n}\n\nexport interface RunResult<T extends string | Buffer> extends RunExitResult {\n\tstdout: T;\n\tstderr: T;\n}\n\nexport function runSpawn(\n\tcommand: string,\n\targs: readonly string[],\n\tencoding: BufferEncoding | 'buffer' | string,\n\toptions: ExitCodeOnlyRunOptions,\n): Promise<RunExitResult>;\nexport function runSpawn<T extends string | Buffer>(\n\tcommand: string,\n\targs: readonly string[],\n\tencoding: BufferEncoding | 'buffer' | string,\n\toptions: RunOptions,\n): Promise<RunResult<T>>;\nexport function runSpawn<T extends string | Buffer>(\n\tcommand: string,\n\targs: readonly string[],\n\tencoding: BufferEncoding | 'buffer' | string,\n\toptions: RunOptions & { exitCodeOnly?: boolean },\n): Promise<RunExitResult | RunResult<T>> {\n\tconst scope = getScopedLogger() ?? maybeStartScopedLogger('Exec.runSpawn');\n\n\tconst { stdin, stdinEncoding, cancellation, decode, spawn: spawner, ...opts }: RunOptions = options;\n\n\tconst promise = new Promise<RunExitResult | RunResult<T>>((resolve, reject) => {\n\t\tconst proc = (spawner ?? spawn)(command, args, {\n\t\t\tcwd: opts.cwd,\n\t\t\tenv: opts.env,\n\t\t\t// 0 disables it, which the spawner contract spells as absent\n\t\t\ttimeout: opts.timeout === 0 ? undefined : opts.timeout,\n\t\t\tsignal: cancellation,\n\t\t\tstdio: ['pipe', null, null],\n\t\t});\n\t\tconst errorEncoding = encoding === 'buffer' ? 'utf8' : encoding;\n\n\t\tconst stdoutBuffers: Buffer[] = [];\n\t\tproc.stdout?.on('data', (data: Buffer) => stdoutBuffers.push(data));\n\n\t\tconst stderrBuffers: Buffer[] = [];\n\t\tproc.stderr?.on('data', (data: Buffer) => stderrBuffers.push(data));\n\n\t\tfunction getStdio<T>(\n\t\t\tencoding: BufferEncoding | 'buffer' | string,\n\t\t): { stdout: T; stderr: T } | Promise<{ stdout: T; stderr: T }> {\n\t\t\tconst stdout = Buffer.concat(stdoutBuffers);\n\t\t\tconst stderr = Buffer.concat(stderrBuffers);\n\t\t\tif (encoding === 'utf8' || encoding === 'binary') {\n\t\t\t\treturn { stdout: stdout.toString(encoding) as T, stderr: stderr.toString(encoding) as T };\n\t\t\t}\n\t\t\tif (encoding === 'buffer') {\n\t\t\t\treturn { stdout: stdout as T, stderr: stderr as T };\n\t\t\t}\n\n\t\t\tif (decode == null) {\n\t\t\t\tthrow new Error(`Non-UTF-8 encoding '${encoding}' requested but no decode function configured`);\n\t\t\t}\n\n\t\t\treturn Promise.all([\n\t\t\t\tdecode(stdout, encoding ? { encoding: encoding } : undefined),\n\t\t\t\tdecode(stderr, encoding ? { encoding: encoding } : undefined),\n\t\t\t]).then(([decodedStdout, decodedStderr]) => ({\n\t\t\t\tstdout: decodedStdout as T,\n\t\t\t\tstderr: decodedStderr as T,\n\t\t\t}));\n\t\t}\n\n\t\tproc.once('error', async ex => {\n\t\t\tif (ex?.name === 'AbortError') {\n\t\t\t\treject(new CancelledRunError(`${command} ${args.join(' ')}`, true, undefined, undefined, 'aborted'));\n\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst stdio = getStdio<string>(errorEncoding);\n\t\t\tconst { stdout, stderr } = stdio instanceof Promise ? await stdio : stdio;\n\n\t\t\treject(new RunError(ex, stdout, stderr));\n\t\t});\n\n\t\tproc.once('close', async (code, signal) => {\n\t\t\tif (options?.exitCodeOnly) {\n\t\t\t\t// Resolves unconditionally, so this is the one path that can hand back a signalled run. `code`\n\t\t\t\t// is null exactly when the process was killed — report the signal rather than coercing to `0`,\n\t\t\t\t// which would claim a clean success for a command that never finished. Note this returns BEFORE\n\t\t\t\t// the SIGTERM-to-cancellation diversion below, so callers have to classify that themselves.\n\t\t\t\tresolve({ exitCode: code ?? undefined, signal: signal ?? undefined, killed: proc.killed });\n\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (code !== 0 || signal) {\n\t\t\t\tconst stdio = getStdio<string>(errorEncoding);\n\t\t\t\tconst { stdout, stderr } = stdio instanceof Promise ? await stdio : stdio;\n\t\t\t\tif (stderr.length && !options?.quiet && scope?.enabled('debug')) {\n\t\t\t\t\tscope?.warn(`[EXEC] '${command} ${args.join(' ')}' \\u2022 ${stderr}`);\n\t\t\t\t}\n\n\t\t\t\tif (signal === 'SIGTERM') {\n\t\t\t\t\t// Only Node's spawn `timeout` kills it (a caller abort rejected above, from `'error'`); an\n\t\t\t\t\t// external SIGTERM leaves `proc.killed` false\n\t\t\t\t\treject(\n\t\t\t\t\t\tnew CancelledRunError(\n\t\t\t\t\t\t\t`${command} ${args.join(' ')}`,\n\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\tcode ?? undefined,\n\t\t\t\t\t\t\tsignal,\n\t\t\t\t\t\t\tproc.killed ? 'timeout' : 'unknown',\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\treject(\n\t\t\t\t\tnew RunError(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tmessage: `Command failed with exit code ${code}`,\n\t\t\t\t\t\t\tcode: code,\n\t\t\t\t\t\t\tsignal: signal ?? undefined,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tstdout,\n\t\t\t\t\t\tstderr,\n\t\t\t\t\t),\n\t\t\t\t);\n\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst stdio = getStdio<T>(encoding);\n\t\t\tconst { stdout, stderr } = stdio instanceof Promise ? await stdio : stdio;\n\t\t\tif (stderr.length && !options?.quiet && scope?.enabled('debug')) {\n\t\t\t\tscope?.warn(\n\t\t\t\t\t`[EXEC] '${command} ${args.join(' ')}' \\u2022 ${typeof stderr === 'string' ? stderr : stderr.toString()}`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// The guard above already rejected every non-zero and every signalled close, so this is a clean\n\t\t\t// zero exit — `code` cannot be null here. (The optional typing exists for the `exitCodeOnly`\n\t\t\t// branch, which is where a codeless result can genuinely surface.)\n\t\t\tresolve({ exitCode: code ?? undefined, signal: signal ?? undefined, stdout: stdout, stderr: stderr });\n\t\t});\n\n\t\t// Only a host spawner can leave these out; reading no output as empty would pass for a clean answer\n\t\tif (proc.stdout == null || proc.stderr == null) {\n\t\t\treject(new Error(`'${command}' was spawned without stdout and stderr pipes`));\n\t\t}\n\n\t\tif (stdin != null) {\n\t\t\tendStdin(proc.stdin, stdin, stdinEncoding);\n\t\t}\n\t});\n\n\treturn promise.finally(() => scope?.[Symbol.dispose]());\n}\n\nexport async function fsExists(path: string): Promise<boolean> {\n\treturn new Promise<boolean>(resolve => access(path, constants.F_OK, err => resolve(err == null)));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"exec.js","names":["joinPaths"],"sources":["../../../../../utils/src/env/node/exec.ts"],"sourcesContent":["import type { ChildProcess, ExecFileException, ExecFileOptions } from 'child_process';\nimport { exec, execFile, spawn } from 'child_process';\nimport type { Stats } from 'fs';\nimport { access, constants } from 'fs';\nimport { stat } from 'fs/promises';\nimport { join as joinPaths } from 'path';\nimport * as process from 'process';\nimport type { Readable, Writable } from 'stream';\nimport type { CancellationReason } from '../../cancellation.js';\nimport { getScopedLogger, maybeStartScopedLogger } from '../../logger.scoped.js';\nimport { normalizePath } from '../../path.js';\nimport { isWindows } from './platform.js';\n\n// -- Error types --\n\ninterface RunErrorContext {\n\tmessage: string;\n\tcmd?: string | undefined;\n\tkilled?: boolean | undefined;\n\tcode?: string | number | null | undefined;\n\tsignal?: NodeJS.Signals | undefined;\n}\n\nexport class RunError extends Error {\n\treadonly cmd?: string | undefined;\n\treadonly killed?: boolean | undefined;\n\treadonly code?: string | number | undefined;\n\treadonly signal?: NodeJS.Signals | undefined;\n\treadonly stdout: string;\n\treadonly stderr: string;\n\n\tconstructor(context: RunErrorContext, stdout: string, stderr: string) {\n\t\tsuper(context.message);\n\n\t\tthis.cmd = context.cmd;\n\t\tthis.killed = context.killed;\n\t\tthis.code = context.code ?? undefined;\n\t\tthis.signal = context.signal;\n\t\tthis.stdout = stdout?.trim() ?? '';\n\t\tthis.stderr = stderr?.trim() ?? '';\n\n\t\tthis.name = 'RunError';\n\t\tError.captureStackTrace?.(this, new.target);\n\t}\n}\n\nexport class CancelledRunError extends RunError {\n\treadonly reason: CancellationReason;\n\n\tconstructor(\n\t\tcmd: string,\n\t\tkilled: boolean,\n\t\tcode?: number | string | undefined,\n\t\tsignal: NodeJS.Signals = 'SIGTERM',\n\t\treason: CancellationReason = 'unknown',\n\t) {\n\t\tsuper(\n\t\t\t{ message: `Operation cancelled; command=${cmd}`, cmd: cmd, killed: killed, code: code, signal: signal },\n\t\t\t'',\n\t\t\t'',\n\t\t);\n\n\t\tthis.reason = reason;\n\n\t\tthis.name = 'CancelledRunError';\n\t\tError.captureStackTrace?.(this, new.target);\n\t}\n}\n\n// -- Execution utilities --\n\nconst slashesRegex = /[\\\\/]/;\nconst ps1Regex = /\\.ps1$/i;\nconst batOrCmdRegex = /\\.(bat|cmd)$/i;\nconst jsRegex = /\\.(js)$/i;\n\n/**\n * Search PATH to see if a file exists in any of the path folders.\n *\n * @param {string} exe The file to search for\n * @return {string} A fully qualified path, or the original path if nothing\n * is found\n *\n * @private\n */\nasync function runDownPath(exe: string): Promise<string> {\n\t// NB: Windows won't search PATH looking for executables in spawn like\n\t// Posix does\n\n\t// Files with any directory path don't get this applied\n\tif (slashesRegex.test(exe)) return exe;\n\n\tconst target = joinPaths('.', exe);\n\ttry {\n\t\tconst stats = await stat(target);\n\t\tif (stats?.isFile() && isExecutable(stats)) return target;\n\t} catch {}\n\n\tconst path = process.env.PATH;\n\tif (path != null && path.length !== 0) {\n\t\tconst haystack = path.split(isWindows ? ';' : ':');\n\t\tfor (const p of haystack) {\n\t\t\tconst needle = joinPaths(p, exe);\n\t\t\ttry {\n\t\t\t\tconst stats = await stat(needle);\n\t\t\t\tif (stats?.isFile() && isExecutable(stats)) return needle;\n\t\t\t} catch {}\n\t\t}\n\t}\n\n\treturn exe;\n}\n\nfunction isExecutable(stats: Stats) {\n\tif (isWindows) return true;\n\n\tconst isGroup = stats.gid ? stats.gid === process.getgid?.() : true;\n\tconst isUser = stats.uid ? stats.uid === process.getuid?.() : true;\n\n\treturn Boolean(stats.mode & 0o0001 || (stats.mode & 0o0010 && isGroup) || (stats.mode & 0o0100 && isUser));\n}\n\n/**\n * Finds the executable and parameters to run on Windows. This method\n * mimics the POSIX behavior of being able to run scripts as executables by\n * replacing the passed-in executable with the script runner, for PowerShell,\n * CMD, and node scripts.\n *\n * This method also does the work of running down PATH, which spawn on Windows\n * also doesn't do, unlike on POSIX.\n */\nexport async function findExecutable(exe: string, args: string[]): Promise<{ cmd: string; args: string[] }> {\n\t// POSIX can just execute scripts directly, no need for silly goosery\n\tif (!isWindows) return { cmd: await runDownPath(exe), args: args };\n\n\tif (!(await fsExists(exe))) {\n\t\t// NB: When you write something like `surf-client ... -- surf-build` on Windows,\n\t\t// a shell would normally convert that to surf-build.cmd, but since it's passed\n\t\t// in as an argument, it doesn't happen\n\t\tconst possibleExts = ['.exe', '.bat', '.cmd', '.ps1'];\n\t\tfor (const ext of possibleExts) {\n\t\t\tconst possibleFullPath = await runDownPath(`${exe}${ext}`);\n\n\t\t\tif (await fsExists(possibleFullPath)) return findExecutable(possibleFullPath, args);\n\t\t}\n\t}\n\n\tif (ps1Regex.test(exe)) {\n\t\tconst cmd = joinPaths(\n\t\t\tprocess.env.SYSTEMROOT ?? 'C:\\\\WINDOWS',\n\t\t\t'System32',\n\t\t\t'WindowsPowerShell',\n\t\t\t'v1.0',\n\t\t\t'PowerShell.exe',\n\t\t);\n\t\tconst psargs = ['-ExecutionPolicy', 'Unrestricted', '-NoLogo', '-NonInteractive', '-File', exe];\n\n\t\treturn { cmd: cmd, args: [...psargs, ...args] };\n\t}\n\n\tif (batOrCmdRegex.test(exe)) {\n\t\tconst cmd = joinPaths(process.env.SYSTEMROOT ?? 'C:\\\\WINDOWS', 'System32', 'cmd.exe');\n\t\tconst cmdArgs = ['/C', exe, ...args];\n\n\t\treturn { cmd: cmd, args: cmdArgs };\n\t}\n\n\tif (jsRegex.test(exe)) {\n\t\tconst cmd = process.execPath;\n\t\tconst nodeArgs = [exe];\n\n\t\treturn { cmd: cmd, args: [...nodeArgs, ...args] };\n\t}\n\n\treturn { cmd: exe, args: args };\n}\n\nexport async function getWindowsShortPath(path: string): Promise<string> {\n\treturn new Promise<string>((resolve, reject) => {\n\t\texec(`for %I in (\"${path}\") do @echo %~sI`, (error, stdout, _stderr) => {\n\t\t\tif (error != null) {\n\t\t\t\treject(error);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tresolve(normalizePath(stdout.trim()));\n\t\t});\n\t});\n}\n\n/** What core uses of a spawned process; Node's `ChildProcess` satisfies it */\nexport interface SpawnedProcess {\n\treadonly stdin: Writable | null;\n\treadonly stdout: Readable | null;\n\treadonly stderr: Readable | null;\n\treadonly killed: boolean;\n\treadonly exitCode: number | null;\n\treadonly spawnargs: string[];\n\tonce(event: 'error', listener: (error: Error) => void): this;\n\tonce(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;\n\tremoveAllListeners(): this;\n}\n\nexport interface ProcessSpawnOptions {\n\treadonly cwd?: string;\n\t/** The child's whole environment, not additions to the spawner's own; an `undefined` value leaves the key out */\n\treadonly env?: Record<string, string | undefined>;\n\t/** Milliseconds; absent means none */\n\treadonly timeout?: number;\n\treadonly signal?: AbortSignal;\n\t/** A `null` entry means `'pipe'` */\n\treadonly stdio?: ['pipe' | 'ignore', null, null];\n}\n\n/**\n * Launches a process in place of Node's `spawn`, and must behave as it does:\n * - pipe stdout and stderr, and stdin unless `stdio[0]` is `'ignore'`, which leaves `stdin` null\n * - honour `signal`, including one already aborted: kill with SIGTERM, then emit `'error'` with `name` `AbortError`,\n * unless the process has already exited\n * - honour `timeout`: kill with SIGTERM and set `killed`, without emitting `'error'`\n * - report a killed process's `'close'` with a null code and its signal, not an exit code such as 143\n * - emit no event before returning, `'error'` at most once, and `'close'` exactly once and last: also after `'error'`\n * (a failed launch, an abort), and only after `stdout` and `stderr` have ended or been destroyed, with `exitCode`\n * and `killed` already set\n * - close the child's stdout when `stdout` is destroyed, which is how a reader that stops early ends the process\n */\nexport type ProcessSpawner = (command: string, args: readonly string[], options: ProcessSpawnOptions) => SpawnedProcess;\n\n/**\n * The default {@link ProcessSpawner}: Node's `spawn`, but with its `timeout` handled here. Node clears its timeout\n * timer only on `'exit'`, which a failed launch (e.g. a missing `cwd`) never emits, so the timer outlives the\n * process and keeps Node alive for the full timeout. This mirrors Node's timeout handling but also clears on\n * `'close'`, as https://github.com/nodejs/node/pull/65506 does; remove it once the minimum supported Node/Electron\n * includes that fix.\n */\nexport function spawnProcess(command: string, args: readonly string[], options: ProcessSpawnOptions): ChildProcess {\n\tconst { timeout, ...spawnOptions } = options;\n\t// Node's own validation, thrown before anything spawns\n\tif (timeout != null && !(Number.isInteger(timeout) && timeout >= 0)) {\n\t\tconst ex = new RangeError(\n\t\t\t`The value of \"timeout\" is out of range. It must be an unsigned integer. Received ${String(timeout)}`,\n\t\t);\n\t\t(ex as RangeError & { code: string }).code = 'ERR_OUT_OF_RANGE';\n\t\tthrow ex;\n\t}\n\n\tconst child = spawn(command, args, spawnOptions);\n\n\tif (timeout != null && timeout > 0) {\n\t\tlet timeoutId: ReturnType<typeof setTimeout> | undefined = setTimeout(() => {\n\t\t\tif (timeoutId != null) {\n\t\t\t\ttry {\n\t\t\t\t\tchild.kill();\n\t\t\t\t} catch (ex) {\n\t\t\t\t\tchild.emit('error', ex);\n\t\t\t\t}\n\t\t\t\ttimeoutId = undefined;\n\t\t\t}\n\t\t}, timeout);\n\n\t\tconst clear = () => {\n\t\t\tif (timeoutId != null) {\n\t\t\t\tclearTimeout(timeoutId);\n\t\t\t\ttimeoutId = undefined;\n\t\t\t}\n\t\t};\n\t\t// `'exit'` keeps Node's timing; `'close'` covers a launch that fails and never exits\n\t\tchild.once('exit', clear);\n\t\tchild.once('close', clear);\n\t}\n\n\treturn child;\n}\n\nexport interface RunOptions<TEncoding = BufferEncoding | 'buffer'> {\n\tcwd?: string;\n\treadonly env?: Record<string, string | undefined>;\n\treadonly encoding?: TEncoding;\n\t/**\n\t * Decodes non-UTF-8 output. Called when `encoding` is not utf8/binary/buffer.\n\t * Provided by the host via the `GitOptions.decode` option.\n\t * If absent and a non-UTF-8 encoding is requested, throws at runtime.\n\t */\n\treadonly decode?: (data: Uint8Array, options?: { readonly encoding: string }) => string | Promise<string>;\n\t/**\n\t * The size the output buffer to allocate to the spawned process. Set this\n\t * if you are anticipating a large amount of output.\n\t *\n\t * If not specified, this will be 10MB (10485760 bytes) which should be\n\t * enough for most Git operations.\n\t */\n\treadonly maxBuffer?: number;\n\treadonly cancellation?: AbortSignal;\n\t/**\n\t * An optional string or buffer which will be written to\n\t * the child process stdin stream immediately immediately\n\t * after spawning the process.\n\t */\n\treadonly stdin?: string | Buffer;\n\t/**\n\t * The encoding to use when writing to stdin, if the stdin\n\t * parameter is a string.\n\t */\n\treadonly stdinEncoding?: string;\n\treadonly timeout?: number;\n\t/**\n\t * When true, suppresses the `[EXEC]` stderr warning emitted on debug-enabled scopes.\n\t * Use for commands whose failure is expected (e.g. paired with `errors: 'ignore'`).\n\t */\n\treadonly quiet?: boolean;\n\t/** Launches the process; defaults to {@link spawnProcess}. Only used by `runSpawn` */\n\treadonly spawn?: ProcessSpawner;\n}\n\nconst bufferExceededRegex = /stdout maxBuffer( length)? exceeded/;\n\ntype ExitCodeOnlyRunOptions<TEncoding = BufferEncoding | 'buffer'> = RunOptions<TEncoding> & { exitCodeOnly: true };\n\nexport function run(\n\tcommand: string,\n\targs: readonly string[],\n\tencoding: BufferEncoding | string,\n\toptions: ExitCodeOnlyRunOptions<BufferEncoding>,\n): Promise<number>;\nexport function run(\n\tcommand: string,\n\targs: readonly string[],\n\tencoding: BufferEncoding | string,\n\toptions?: RunOptions<BufferEncoding>,\n): Promise<string>;\nexport function run<T extends number | string>(\n\tcommand: string,\n\targs: readonly string[],\n\tencoding: BufferEncoding | string,\n\toptions?: RunOptions<BufferEncoding> & { exitCodeOnly?: boolean },\n): Promise<T> {\n\tconst scope = getScopedLogger() ?? maybeStartScopedLogger('Exec.run');\n\n\tconst { stdin, stdinEncoding, cancellation, decode, ...opts }: RunOptions<BufferEncoding> & ExecFileOptions = {\n\t\tmaxBuffer: 1000 * 1024 * 1024,\n\t\t...options,\n\t};\n\n\tconst promise = new Promise<T>((resolve, reject) => {\n\t\tconst proc = execFile(\n\t\t\tcommand,\n\t\t\targs,\n\t\t\t{ ...opts, signal: cancellation },\n\t\t\tasync (error: ExecFileException | null, stdout, stderr) => {\n\t\t\t\tif (options?.exitCodeOnly) {\n\t\t\t\t\tresolve((error?.code ?? proc.exitCode) as T);\n\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (error != null) {\n\t\t\t\t\tif (error.signal === 'SIGTERM') {\n\t\t\t\t\t\treject(\n\t\t\t\t\t\t\tnew CancelledRunError(\n\t\t\t\t\t\t\t\t`${command} ${args.join(' ')}`,\n\t\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\t\terror.code ?? undefined,\n\t\t\t\t\t\t\t\terror.signal,\n\t\t\t\t\t\t\t\tcancellation?.aborted ? 'aborted' : error.killed ? 'timeout' : 'unknown',\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (bufferExceededRegex.test(error.message)) {\n\t\t\t\t\t\terror.message = `Command output exceeded the allocated stdout buffer. Set 'options.maxBuffer' to a larger value than ${opts.maxBuffer} bytes`;\n\t\t\t\t\t}\n\n\t\t\t\t\tlet stdoutDecoded: string;\n\t\t\t\t\tlet stderrDecoded: string;\n\t\t\t\t\tif (encoding === 'utf8' || encoding === 'binary' || encoding === 'buffer') {\n\t\t\t\t\t\t// stdout & stderr can be `Buffer` or `string\n\t\t\t\t\t\t// oxlint-disable-next-line typescript/no-unnecessary-type-conversion\n\t\t\t\t\t\tstdoutDecoded = stdout.toString();\n\t\t\t\t\t\t// oxlint-disable-next-line typescript/no-unnecessary-type-conversion\n\t\t\t\t\t\tstderrDecoded = stderr.toString();\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (decode == null) {\n\t\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t\t`Non-UTF-8 encoding '${encoding}' requested but no decode function configured`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tstdoutDecoded = await decode(\n\t\t\t\t\t\t\tBuffer.from(stdout, 'binary'),\n\t\t\t\t\t\t\tencoding ? { encoding: encoding } : undefined,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tstderrDecoded = await decode(\n\t\t\t\t\t\t\tBuffer.from(stderr, 'binary'),\n\t\t\t\t\t\t\tencoding ? { encoding: encoding } : undefined,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\treject(new RunError(error, stdoutDecoded, stderrDecoded));\n\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (stderr && !options?.quiet && scope?.enabled('debug')) {\n\t\t\t\t\tscope?.warn(`[EXEC] '${command} ${args.join(' ')}' \\u2022 ${stderr}`);\n\t\t\t\t}\n\n\t\t\t\tif (encoding === 'utf8' || encoding === 'binary' || encoding === 'buffer') {\n\t\t\t\t\tresolve(stdout as T);\n\t\t\t\t} else {\n\t\t\t\t\tif (decode == null) {\n\t\t\t\t\t\tthrow new Error(`Non-UTF-8 encoding '${encoding}' requested but no decode function configured`);\n\t\t\t\t\t}\n\n\t\t\t\t\tresolve(\n\t\t\t\t\t\t(await decode(\n\t\t\t\t\t\t\tBuffer.from(stdout, 'binary'),\n\t\t\t\t\t\t\tencoding ? { encoding: encoding } : undefined,\n\t\t\t\t\t\t)) as T,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t},\n\t\t);\n\n\t\tif (stdin != null) {\n\t\t\tendStdin(proc.stdin, stdin, stdinEncoding);\n\t\t}\n\t});\n\n\treturn promise.finally(() => scope?.[Symbol.dispose]());\n}\n\n/**\n * Writes a child's whole input and closes it. A child can exit, or be killed on cancellation, before reading all of\n * it, and the pipe then errors (EPIPE, or EOF on Windows). Unlistened, that error is thrown as an uncaught exception\n * and takes down the whole host process, so it is dropped: the child's exit status and stderr already report the\n * failure, and a child that chose to stop reading answered from what it read.\n */\nexport function endStdin(stream: Writable | null, stdin: string | Buffer, stdinEncoding: string | undefined): void {\n\tif (stream == null) return;\n\n\tstream.on('error', () => {});\n\tif (typeof stdin === 'string') {\n\t\tstream.end(stdin, (stdinEncoding ?? 'utf8') as BufferEncoding);\n\t} else {\n\t\tstream.end(stdin);\n\t}\n}\n\nexport interface RunExitResult {\n\t/**\n\t * The process's exit code. ABSENT when the process never exited normally — killed by a signal, in which\n\t * case {@link signal} is set. Deliberately optional rather than defaulted to `0`: a signalled process\n\t * reported as `exitCode: 0` is indistinguishable from a clean success, so callers would read a partial\n\t * or empty stdout as a complete answer.\n\t */\n\treadonly exitCode?: number;\n\t/** Set when the process was terminated by a signal instead of exiting; {@link exitCode} is then absent. */\n\treadonly signal?: NodeJS.Signals;\n\t/** Whether Node itself killed the process (its spawn `timeout`), as opposed to an external signal. */\n\treadonly killed?: boolean;\n}\n\nexport interface RunResult<T extends string | Buffer> extends RunExitResult {\n\tstdout: T;\n\tstderr: T;\n}\n\nexport function runSpawn(\n\tcommand: string,\n\targs: readonly string[],\n\tencoding: BufferEncoding | 'buffer' | string,\n\toptions: ExitCodeOnlyRunOptions,\n): Promise<RunExitResult>;\nexport function runSpawn<T extends string | Buffer>(\n\tcommand: string,\n\targs: readonly string[],\n\tencoding: BufferEncoding | 'buffer' | string,\n\toptions: RunOptions,\n): Promise<RunResult<T>>;\nexport function runSpawn<T extends string | Buffer>(\n\tcommand: string,\n\targs: readonly string[],\n\tencoding: BufferEncoding | 'buffer' | string,\n\toptions: RunOptions & { exitCodeOnly?: boolean },\n): Promise<RunExitResult | RunResult<T>> {\n\tconst scope = getScopedLogger() ?? maybeStartScopedLogger('Exec.runSpawn');\n\n\tconst { stdin, stdinEncoding, cancellation, decode, spawn: spawner, ...opts }: RunOptions = options;\n\n\tconst promise = new Promise<RunExitResult | RunResult<T>>((resolve, reject) => {\n\t\tconst proc = (spawner ?? spawnProcess)(command, args, {\n\t\t\tcwd: opts.cwd,\n\t\t\tenv: opts.env,\n\t\t\t// 0 disables it, which the spawner contract spells as absent\n\t\t\ttimeout: opts.timeout === 0 ? undefined : opts.timeout,\n\t\t\tsignal: cancellation,\n\t\t\tstdio: ['pipe', null, null],\n\t\t});\n\t\tconst errorEncoding = encoding === 'buffer' ? 'utf8' : encoding;\n\n\t\tconst stdoutBuffers: Buffer[] = [];\n\t\tproc.stdout?.on('data', (data: Buffer) => stdoutBuffers.push(data));\n\n\t\tconst stderrBuffers: Buffer[] = [];\n\t\tproc.stderr?.on('data', (data: Buffer) => stderrBuffers.push(data));\n\n\t\tfunction getStdio<T>(\n\t\t\tencoding: BufferEncoding | 'buffer' | string,\n\t\t): { stdout: T; stderr: T } | Promise<{ stdout: T; stderr: T }> {\n\t\t\tconst stdout = Buffer.concat(stdoutBuffers);\n\t\t\tconst stderr = Buffer.concat(stderrBuffers);\n\t\t\tif (encoding === 'utf8' || encoding === 'binary') {\n\t\t\t\treturn { stdout: stdout.toString(encoding) as T, stderr: stderr.toString(encoding) as T };\n\t\t\t}\n\t\t\tif (encoding === 'buffer') {\n\t\t\t\treturn { stdout: stdout as T, stderr: stderr as T };\n\t\t\t}\n\n\t\t\tif (decode == null) {\n\t\t\t\tthrow new Error(`Non-UTF-8 encoding '${encoding}' requested but no decode function configured`);\n\t\t\t}\n\n\t\t\treturn Promise.all([\n\t\t\t\tdecode(stdout, encoding ? { encoding: encoding } : undefined),\n\t\t\t\tdecode(stderr, encoding ? { encoding: encoding } : undefined),\n\t\t\t]).then(([decodedStdout, decodedStderr]) => ({\n\t\t\t\tstdout: decodedStdout as T,\n\t\t\t\tstderr: decodedStderr as T,\n\t\t\t}));\n\t\t}\n\n\t\tproc.once('error', async ex => {\n\t\t\tif (ex?.name === 'AbortError') {\n\t\t\t\treject(new CancelledRunError(`${command} ${args.join(' ')}`, true, undefined, undefined, 'aborted'));\n\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst stdio = getStdio<string>(errorEncoding);\n\t\t\tconst { stdout, stderr } = stdio instanceof Promise ? await stdio : stdio;\n\n\t\t\treject(new RunError(ex, stdout, stderr));\n\t\t});\n\n\t\tproc.once('close', async (code, signal) => {\n\t\t\tif (options?.exitCodeOnly) {\n\t\t\t\t// Resolves unconditionally, so this is the one path that can hand back a signalled run. `code`\n\t\t\t\t// is null exactly when the process was killed — report the signal rather than coercing to `0`,\n\t\t\t\t// which would claim a clean success for a command that never finished. Note this returns BEFORE\n\t\t\t\t// the SIGTERM-to-cancellation diversion below, so callers have to classify that themselves.\n\t\t\t\tresolve({ exitCode: code ?? undefined, signal: signal ?? undefined, killed: proc.killed });\n\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (code !== 0 || signal) {\n\t\t\t\tconst stdio = getStdio<string>(errorEncoding);\n\t\t\t\tconst { stdout, stderr } = stdio instanceof Promise ? await stdio : stdio;\n\t\t\t\tif (stderr.length && !options?.quiet && scope?.enabled('debug')) {\n\t\t\t\t\tscope?.warn(`[EXEC] '${command} ${args.join(' ')}' \\u2022 ${stderr}`);\n\t\t\t\t}\n\n\t\t\t\tif (signal === 'SIGTERM') {\n\t\t\t\t\t// Only Node's spawn `timeout` kills it (a caller abort rejected above, from `'error'`); an\n\t\t\t\t\t// external SIGTERM leaves `proc.killed` false\n\t\t\t\t\treject(\n\t\t\t\t\t\tnew CancelledRunError(\n\t\t\t\t\t\t\t`${command} ${args.join(' ')}`,\n\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\tcode ?? undefined,\n\t\t\t\t\t\t\tsignal,\n\t\t\t\t\t\t\tproc.killed ? 'timeout' : 'unknown',\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\treject(\n\t\t\t\t\tnew RunError(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tmessage: `Command failed with exit code ${code}`,\n\t\t\t\t\t\t\tcode: code,\n\t\t\t\t\t\t\tsignal: signal ?? undefined,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tstdout,\n\t\t\t\t\t\tstderr,\n\t\t\t\t\t),\n\t\t\t\t);\n\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst stdio = getStdio<T>(encoding);\n\t\t\tconst { stdout, stderr } = stdio instanceof Promise ? await stdio : stdio;\n\t\t\tif (stderr.length && !options?.quiet && scope?.enabled('debug')) {\n\t\t\t\tscope?.warn(\n\t\t\t\t\t`[EXEC] '${command} ${args.join(' ')}' \\u2022 ${typeof stderr === 'string' ? stderr : stderr.toString()}`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// The guard above already rejected every non-zero and every signalled close, so this is a clean\n\t\t\t// zero exit — `code` cannot be null here. (The optional typing exists for the `exitCodeOnly`\n\t\t\t// branch, which is where a codeless result can genuinely surface.)\n\t\t\tresolve({ exitCode: code ?? undefined, signal: signal ?? undefined, stdout: stdout, stderr: stderr });\n\t\t});\n\n\t\t// Only a host spawner can leave these out; reading no output as empty would pass for a clean answer\n\t\tif (proc.stdout == null || proc.stderr == null) {\n\t\t\treject(new Error(`'${command}' was spawned without stdout and stderr pipes`));\n\t\t}\n\n\t\tif (stdin != null) {\n\t\t\tendStdin(proc.stdin, stdin, stdinEncoding);\n\t\t}\n\t});\n\n\treturn promise.finally(() => scope?.[Symbol.dispose]());\n}\n\nexport async function fsExists(path: string): Promise<boolean> {\n\treturn new Promise<boolean>(resolve => access(path, constants.F_OK, err => resolve(err == null)));\n}\n"],"mappings":";;;;;;;;;AAuBA,IAAa,WAAb,cAA8B,MAAM;CACnC;CACA;CACA;CACA;CACA;CACA;CAEA,YAAY,SAA0B,QAAgB,QAAgB;EACrE,MAAM,QAAQ,OAAO;EAErB,KAAK,MAAM,QAAQ;EACnB,KAAK,SAAS,QAAQ;EACtB,KAAK,OAAO,QAAQ,QAAQ,KAAA;EAC5B,KAAK,SAAS,QAAQ;EACtB,KAAK,SAAS,QAAQ,KAAK,KAAK;EAChC,KAAK,SAAS,QAAQ,KAAK,KAAK;EAEhC,KAAK,OAAO;EACZ,MAAM,oBAAoB,MAAM,UAAU;CAC3C;AACD;AAEA,IAAa,oBAAb,cAAuC,SAAS;CAC/C;CAEA,YACC,KACA,QACA,MACA,SAAyB,WACzB,SAA6B,WAC5B;EACD,MACC;GAAE,SAAS,gCAAgC;GAAY;GAAa;GAAc;GAAc;EAAO,GACvG,IACA,EACD;EAEA,KAAK,SAAS;EAEd,KAAK,OAAO;EACZ,MAAM,oBAAoB,MAAM,UAAU;CAC3C;AACD;AAIA,MAAM,eAAe;AACrB,MAAM,WAAW;AACjB,MAAM,gBAAgB;AACtB,MAAM,UAAU;;;;;;;;;;AAWhB,eAAe,YAAY,KAA8B;CAKxD,IAAI,aAAa,KAAK,GAAG,GAAG,OAAO;CAEnC,MAAM,SAASA,KAAU,KAAK,GAAG;CACjC,IAAI;EACH,MAAM,QAAQ,MAAM,KAAK,MAAM;EAC/B,IAAI,OAAO,OAAO,KAAK,aAAa,KAAK,GAAG,OAAO;CACpD,QAAQ,CAAC;CAET,MAAM,OAAO,QAAQ,IAAI;CACzB,IAAI,QAAQ,QAAQ,KAAK,WAAW,GAAG;EACtC,MAAM,WAAW,KAAK,MAAM,YAAY,MAAM,GAAG;EACjD,KAAK,MAAM,KAAK,UAAU;GACzB,MAAM,SAASA,KAAU,GAAG,GAAG;GAC/B,IAAI;IACH,MAAM,QAAQ,MAAM,KAAK,MAAM;IAC/B,IAAI,OAAO,OAAO,KAAK,aAAa,KAAK,GAAG,OAAO;GACpD,QAAQ,CAAC;EACV;CACD;CAEA,OAAO;AACR;AAEA,SAAS,aAAa,OAAc;CACnC,IAAI,WAAW,OAAO;CAEtB,MAAM,UAAU,MAAM,MAAM,MAAM,QAAQ,QAAQ,SAAS,IAAI;CAC/D,MAAM,SAAS,MAAM,MAAM,MAAM,QAAQ,QAAQ,SAAS,IAAI;CAE9D,OAAO,QAAQ,MAAM,OAAO,KAAW,MAAM,OAAO,KAAU,WAAa,MAAM,OAAO,MAAU,MAAO;AAC1G;;;;;;;;;;AAWA,eAAsB,eAAe,KAAa,MAA0D;CAE3G,IAAI,CAAC,WAAW,OAAO;EAAE,KAAK,MAAM,YAAY,GAAG;EAAS;CAAK;CAEjE,IAAI,CAAE,MAAM,SAAS,GAAG,GAKvB,KAAK,MAAM,OAAO;EADI;EAAQ;EAAQ;EAAQ;CACjB,GAAG;EAC/B,MAAM,mBAAmB,MAAM,YAAY,GAAG,MAAM,KAAK;EAEzD,IAAI,MAAM,SAAS,gBAAgB,GAAG,OAAO,eAAe,kBAAkB,IAAI;CACnF;CAGD,IAAI,SAAS,KAAK,GAAG,GAUpB,OAAO;EAAE,KATGA,KACX,QAAQ,IAAI,cAAc,eAC1B,YACA,qBACA,QACA,gBAIe;EAAG,MAAM,CAAC,GAAG;GAFb;GAAoB;GAAgB;GAAW;GAAmB;GAAS;EAEzD,GAAG,GAAG,IAAI;CAAE;CAG/C,IAAI,cAAc,KAAK,GAAG,GAIzB,OAAO;EAAE,KAHGA,KAAU,QAAQ,IAAI,cAAc,eAAe,YAAY,SAG3D;EAAG,MAAM;GAFR;GAAM;GAAK,GAAG;EAEA;CAAE;CAGlC,IAAI,QAAQ,KAAK,GAAG,GAInB,OAAO;EAAE,KAHG,QAAQ;EAGD,MAAM,CAAC,GAAG,CAFX,GAEkB,GAAG,GAAG,IAAI;CAAE;CAGjD,OAAO;EAAE,KAAK;EAAW;CAAK;AAC/B;AAEA,eAAsB,oBAAoB,MAA+B;CACxE,OAAO,IAAI,SAAiB,SAAS,WAAW;EAC/C,KAAK,eAAe,KAAK,oBAAoB,OAAO,QAAQ,YAAY;GACvE,IAAI,SAAS,MAAM;IAClB,OAAO,KAAK;IACZ;GACD;GAEA,QAAQ,cAAc,OAAO,KAAK,CAAC,CAAC;EACrC,CAAC;CACF,CAAC;AACF;;;;;;;;AA+CA,SAAgB,aAAa,SAAiB,MAAyB,SAA4C;CAClH,MAAM,EAAE,SAAS,GAAG,iBAAiB;CAErC,IAAI,WAAW,QAAQ,EAAE,OAAO,UAAU,OAAO,KAAK,WAAW,IAAI;EACpE,MAAM,qBAAK,IAAI,WACd,oFAAoF,OAAO,OAAO,GACnG;EACA,GAAsC,OAAO;EAC7C,MAAM;CACP;CAEA,MAAM,QAAQ,MAAM,SAAS,MAAM,YAAY;CAE/C,IAAI,WAAW,QAAQ,UAAU,GAAG;EACnC,IAAI,YAAuD,iBAAiB;GAC3E,IAAI,aAAa,MAAM;IACtB,IAAI;KACH,MAAM,KAAK;IACZ,SAAS,IAAI;KACZ,MAAM,KAAK,SAAS,EAAE;IACvB;IACA,YAAY,KAAA;GACb;EACD,GAAG,OAAO;EAEV,MAAM,cAAc;GACnB,IAAI,aAAa,MAAM;IACtB,aAAa,SAAS;IACtB,YAAY,KAAA;GACb;EACD;EAEA,MAAM,KAAK,QAAQ,KAAK;EACxB,MAAM,KAAK,SAAS,KAAK;CAC1B;CAEA,OAAO;AACR;AA0CA,MAAM,sBAAsB;AAgB5B,SAAgB,IACf,SACA,MACA,UACA,SACa;CACb,MAAM,QAAQ,gBAAgB,KAAK,uBAAuB,UAAU;CAEpE,MAAM,EAAE,OAAO,eAAe,cAAc,QAAQ,GAAG,SAAuD;EAC7G,WAAW;EACX,GAAG;CACJ;CAwFA,OAAO,IAtFa,SAAY,SAAS,WAAW;EACnD,MAAM,OAAO,SACZ,SACA,MACA;GAAE,GAAG;GAAM,QAAQ;EAAa,GAChC,OAAO,OAAiC,QAAQ,WAAW;GAC1D,IAAI,SAAS,cAAc;IAC1B,QAAS,OAAO,QAAQ,KAAK,QAAc;IAE3C;GACD;GAEA,IAAI,SAAS,MAAM;IAClB,IAAI,MAAM,WAAW,WAAW;KAC/B,OACC,IAAI,kBACH,GAAG,QAAQ,GAAG,KAAK,KAAK,GAAG,KAC3B,MACA,MAAM,QAAQ,KAAA,GACd,MAAM,QACN,cAAc,UAAU,YAAY,MAAM,SAAS,YAAY,SAChE,CACD;KAEA;IACD;IAEA,IAAI,oBAAoB,KAAK,MAAM,OAAO,GACzC,MAAM,UAAU,uGAAuG,KAAK,UAAU;IAGvI,IAAI;IACJ,IAAI;IACJ,IAAI,aAAa,UAAU,aAAa,YAAY,aAAa,UAAU;KAG1E,gBAAgB,OAAO,SAAS;KAEhC,gBAAgB,OAAO,SAAS;IACjC,OAAO;KACN,IAAI,UAAU,MACb,MAAM,IAAI,MACT,uBAAuB,SAAS,8CACjC;KAGD,gBAAgB,MAAM,OACrB,OAAO,KAAK,QAAQ,QAAQ,GAC5B,WAAW,EAAY,SAAS,IAAI,KAAA,CACrC;KACA,gBAAgB,MAAM,OACrB,OAAO,KAAK,QAAQ,QAAQ,GAC5B,WAAW,EAAY,SAAS,IAAI,KAAA,CACrC;IACD;IACA,OAAO,IAAI,SAAS,OAAO,eAAe,aAAa,CAAC;IAExD;GACD;GAEA,IAAI,UAAU,CAAC,SAAS,SAAS,OAAO,QAAQ,OAAO,GACtD,OAAO,KAAK,WAAW,QAAQ,GAAG,KAAK,KAAK,GAAG,EAAE,WAAW,QAAQ;GAGrE,IAAI,aAAa,UAAU,aAAa,YAAY,aAAa,UAChE,QAAQ,MAAW;QACb;IACN,IAAI,UAAU,MACb,MAAM,IAAI,MAAM,uBAAuB,SAAS,8CAA8C;IAG/F,QACE,MAAM,OACN,OAAO,KAAK,QAAQ,QAAQ,GAC5B,WAAW,EAAY,SAAS,IAAI,KAAA,CACrC,CACD;GACD;EACD,CACD;EAEA,IAAI,SAAS,MACZ,SAAS,KAAK,OAAO,OAAO,aAAa;CAE3C,CAEa,CAAC,CAAC,cAAc,QAAQ,OAAO,QAAQ,CAAC,CAAC;AACvD;;;;;;;AAQA,SAAgB,SAAS,QAAyB,OAAwB,eAAyC;CAClH,IAAI,UAAU,MAAM;CAEpB,OAAO,GAAG,eAAe,CAAC,CAAC;CAC3B,IAAI,OAAO,UAAU,UACpB,OAAO,IAAI,OAAQ,iBAAiB,MAAyB;MAE7D,OAAO,IAAI,KAAK;AAElB;AAiCA,SAAgB,SACf,SACA,MACA,UACA,SACwC;CACxC,MAAM,QAAQ,gBAAgB,KAAK,uBAAuB,eAAe;CAEzE,MAAM,EAAE,OAAO,eAAe,cAAc,QAAQ,OAAO,SAAS,GAAG,SAAqB;CAkI5F,OAAO,IAhIa,SAAuC,SAAS,WAAW;EAC9E,MAAM,QAAQ,WAAW,aAAA,CAAc,SAAS,MAAM;GACrD,KAAK,KAAK;GACV,KAAK,KAAK;GAEV,SAAS,KAAK,YAAY,IAAI,KAAA,IAAY,KAAK;GAC/C,QAAQ;GACR,OAAO;IAAC;IAAQ;IAAM;GAAI;EAC3B,CAAC;EACD,MAAM,gBAAgB,aAAa,WAAW,SAAS;EAEvD,MAAM,gBAA0B,CAAC;EACjC,KAAK,QAAQ,GAAG,SAAS,SAAiB,cAAc,KAAK,IAAI,CAAC;EAElE,MAAM,gBAA0B,CAAC;EACjC,KAAK,QAAQ,GAAG,SAAS,SAAiB,cAAc,KAAK,IAAI,CAAC;EAElE,SAAS,SACR,UAC+D;GAC/D,MAAM,SAAS,OAAO,OAAO,aAAa;GAC1C,MAAM,SAAS,OAAO,OAAO,aAAa;GAC1C,IAAI,aAAa,UAAU,aAAa,UACvC,OAAO;IAAE,QAAQ,OAAO,SAAS,QAAQ;IAAQ,QAAQ,OAAO,SAAS,QAAQ;GAAO;GAEzF,IAAI,aAAa,UAChB,OAAO;IAAU;IAAqB;GAAY;GAGnD,IAAI,UAAU,MACb,MAAM,IAAI,MAAM,uBAAuB,SAAS,8CAA8C;GAG/F,OAAO,QAAQ,IAAI,CAClB,OAAO,QAAQ,WAAW,EAAY,SAAS,IAAI,KAAA,CAAS,GAC5D,OAAO,QAAQ,WAAW,EAAY,SAAS,IAAI,KAAA,CAAS,CAC7D,CAAC,CAAC,CAAC,MAAM,CAAC,eAAe,oBAAoB;IAC5C,QAAQ;IACR,QAAQ;GACT,EAAE;EACH;EAEA,KAAK,KAAK,SAAS,OAAM,OAAM;GAC9B,IAAI,IAAI,SAAS,cAAc;IAC9B,OAAO,IAAI,kBAAkB,GAAG,QAAQ,GAAG,KAAK,KAAK,GAAG,KAAK,MAAM,KAAA,GAAW,KAAA,GAAW,SAAS,CAAC;IAEnG;GACD;GAEA,MAAM,QAAQ,SAAiB,aAAa;GAC5C,MAAM,EAAE,QAAQ,WAAW,iBAAiB,UAAU,MAAM,QAAQ;GAEpE,OAAO,IAAI,SAAS,IAAI,QAAQ,MAAM,CAAC;EACxC,CAAC;EAED,KAAK,KAAK,SAAS,OAAO,MAAM,WAAW;GAC1C,IAAI,SAAS,cAAc;IAK1B,QAAQ;KAAE,UAAU,QAAQ,KAAA;KAAW,QAAQ,UAAU,KAAA;KAAW,QAAQ,KAAK;IAAO,CAAC;IAEzF;GACD;GAEA,IAAI,SAAS,KAAK,QAAQ;IACzB,MAAM,QAAQ,SAAiB,aAAa;IAC5C,MAAM,EAAE,QAAQ,WAAW,iBAAiB,UAAU,MAAM,QAAQ;IACpE,IAAI,OAAO,UAAU,CAAC,SAAS,SAAS,OAAO,QAAQ,OAAO,GAC7D,OAAO,KAAK,WAAW,QAAQ,GAAG,KAAK,KAAK,GAAG,EAAE,WAAW,QAAQ;IAGrE,IAAI,WAAW,WAAW;KAGzB,OACC,IAAI,kBACH,GAAG,QAAQ,GAAG,KAAK,KAAK,GAAG,KAC3B,MACA,QAAQ,KAAA,GACR,QACA,KAAK,SAAS,YAAY,SAC3B,CACD;KAEA;IACD;IAEA,OACC,IAAI,SACH;KACC,SAAS,iCAAiC;KACpC;KACN,QAAQ,UAAU,KAAA;IACnB,GACA,QACA,MACD,CACD;IAEA;GACD;GAEA,MAAM,QAAQ,SAAY,QAAQ;GAClC,MAAM,EAAE,QAAQ,WAAW,iBAAiB,UAAU,MAAM,QAAQ;GACpE,IAAI,OAAO,UAAU,CAAC,SAAS,SAAS,OAAO,QAAQ,OAAO,GAC7D,OAAO,KACN,WAAW,QAAQ,GAAG,KAAK,KAAK,GAAG,EAAE,WAAW,OAAO,WAAW,WAAW,SAAS,OAAO,SAAS,GACvG;GAMD,QAAQ;IAAE,UAAU,QAAQ,KAAA;IAAW,QAAQ,UAAU,KAAA;IAAmB;IAAgB;GAAO,CAAC;EACrG,CAAC;EAGD,IAAI,KAAK,UAAU,QAAQ,KAAK,UAAU,MACzC,uBAAO,IAAI,MAAM,IAAI,QAAQ,8CAA8C,CAAC;EAG7E,IAAI,SAAS,MACZ,SAAS,KAAK,OAAO,OAAO,aAAa;CAE3C,CAEa,CAAC,CAAC,cAAc,QAAQ,OAAO,QAAQ,CAAC,CAAC;AACvD;AAEA,eAAsB,SAAS,MAAgC;CAC9D,OAAO,IAAI,SAAiB,YAAW,OAAO,MAAM,UAAU,OAAM,QAAO,QAAQ,OAAO,IAAI,CAAC,CAAC;AACjG"}
@@ -251,7 +251,7 @@ never a false `unchanged`. A target the cheap check fails falls through to the f
251
251
  the warning if it fails too — except on an `auth`, `rate-limit` or `no-connection` failure, which the full read
252
252
  would only hit again: those targets are dropped with that warning and `fetchFailed`, exactly as a failed full read
253
253
  drops them. Each target is judged by its own failure, so one target's rate limit never drops another the check only
254
- failed to answer. The cheap check counts toward the integration's disconnect budget only when the credential is
254
+ failed to answer. The cheap check counts toward its connection's failure budget only when the credential is
255
255
  refused; any other failure of it is left to the full reads, so a call counts at most twice, once per full read.
256
256
 
257
257
  `getPullRequestsForBranches` answers "which pull requests have this branch as their head" without the user's
@@ -594,9 +594,26 @@ Invariants worth relying on:
594
594
  - `page.itemsPerPage` describes the page that came back; don't infer totals or "last page" from it.
595
595
  - Composite fan-out cursors retain each scope's exact continuation or retry position. Round-trip the cursor
596
596
  even after a partial result: healthy scopes advance while a failed scope retries the page it missed.
597
+ - A repo-scoped read on a host that reads repository by repository (GitLab, GitLab self-managed, Bitbucket Cloud,
598
+ Bitbucket Data Center and Azure DevOps; Azure DevOps issues per project) keeps at most 6 of those requests in
599
+ flight, whatever the repo count. `scopeConcurrency` on `listPullRequestsPage`, `listIssuesPage` and the pull
600
+ request sweeps lowers or raises it; a sweep runs up to 6 targets at once, each with this bound. GitHub/GHE send
601
+ one query regardless.
602
+ - A repo-scoped Azure DevOps pull request read makes one extra request per repository (and fork source) per page to
603
+ fill each row's clone URLs (`refs.base.url`, `refs.head.url`, `cloneHttps`/`cloneSsh`), which `summary` does not
604
+ skip. `includeRemoteInfo: false` on `listPullRequestsPage` and the pull request sweeps drops that lookup and
605
+ those fields; leave it on for rows that will be checked out or matched to local remotes. Other providers ignore it.
597
606
  - Sweeps drain internally and expose **no** cursor: `hasMore` is always `false`. Gate "this is the complete
598
607
  set" on `page.allPages === true`, which is false for _both_ truncation and failure — unlike
599
608
  `page.truncated`, which can be misread as a benign cap.
609
+ - A sweep's `targets` may hold several targets of one provider, one per connection (two GitHub Enterprise
610
+ installations, two Bitbucket Data Center servers, two github.com accounts), each with its own `repos` and
611
+ `filters` that override the sweep-level ones. Every target of such a provider must name a `connectionId` or,
612
+ on a self-managed host, a `domain` (a cloud provider's read ignores it), since an unnamed one reads the primary
613
+ connection and would read it twice; a repeat throws, matched by host however its `domain` is spelled.
614
+ Read failures per connection from `failedTargets`/`incompleteTargets` — each ref carries the `connectionId`
615
+ and resolved `domain` its `onTargetSettled` event does — and treat `failedProviderIds`/`incompleteProviderIds`
616
+ as the per-provider summary: a provider is failed only when all its targets failed.
600
617
 
601
618
  ### The filtered pull request search
602
619
 
@@ -967,7 +984,7 @@ not, and need no confirmed credential:
967
984
  A page served instead of data keeps its own message, with no cause. `unexpected-response` is only named where the failed
968
985
  read goes through core's provider error handling. Two kinds of failure still carry the error's text and no cause: a
969
986
  scope that one of provider-apis' own fan-outs fails (GitLab issues or pull requests when core hands it several
970
- repositories at once, Bitbucket Data Center pull requests), and the current-user lookup a repository-scoped read makes
987
+ repositories at once), and the current-user lookup a repository-scoped read makes
971
988
  to filter by the user.
972
989
 
973
990
  ### `omission` — succeeded, but withheld results
@@ -1046,6 +1063,8 @@ scope stay two warnings even if their messages ever converge.
1046
1063
  | `page.allPages` | Sweeps only: `true` iff every page of every target drained cleanly. |
1047
1064
  | `failedProviderIds` | Sweeps/broadens: providers whose requested scopes produced no usable result. |
1048
1065
  | `incompleteProviderIds` | Sweeps/broadens: providers with a usable result plus a failed, partial, or truncated sibling scope. |
1066
+ | `failedTargets` | Sweeps only: the targets (provider, `connectionId`, `domain`) that produced no usable result. Authoritative when a provider has several. |
1067
+ | `incompleteTargets` | Sweeps only: the targets that returned a slice with a gap. Never also in `failedTargets`. |
1049
1068
 
1050
1069
  An omission pairs with `page.truncated: true` — results are missing — and typically with `fetchFailed: false`,
1051
1070
  since nothing failed. But the flags are **per result** and `omission` is **per warning**, so the two can differ
@@ -0,0 +1,239 @@
1
+ # Consuming `@gitkraken/core-gitlens` notifications
2
+
3
+ How to show GitKraken notifications (the `notifications-service-v2` inbox) in a second host, such as GitKraken
4
+ Desktop or the `gk` CLI, using the `plus/notifications` subpaths of `@gitkraken/core-gitlens`.
5
+
6
+ Scope: the **client and state** for one user's notifications, filtered by a `productType`. It fetches, validates,
7
+ caches in memory and mutates (read, unread, delete, settings). It does not poll, persist, toast, render or open
8
+ links; those are the host's job (see §5). GitLens is the reference host:
9
+ [`src/plus/notifications/context.ts`](https://github.com/gitkraken/vscode-gitlens/blob/main/src/plus/notifications/context.ts)
10
+ is the adapter and
11
+ [`notificationsController.ts`](https://github.com/gitkraken/vscode-gitlens/blob/main/src/plus/notifications/notificationsController.ts)
12
+ is the lifecycle.
13
+
14
+ ---
15
+
16
+ ## 1. Entry points
17
+
18
+ | Subpath | Use it for |
19
+ | -------------------------------------------------------------------- | -------------------------------------------------------------------------- |
20
+ | `@gitkraken/core-gitlens/plus/notifications/notificationsService.js` | `NotificationsService`: the state holder and the actions. |
21
+ | `@gitkraken/core-gitlens/plus/notifications/context.js` | `NotificationsContext` (what you implement) and `NotificationsLogger`. |
22
+ | `@gitkraken/core-gitlens/plus/notifications/models.js` | `GkNotification`, `NotificationsState`, `NotificationsSnapshot`, settings. |
23
+ | `@gitkraken/core-gitlens/plus/notifications/presentation.js` | Pure display and safety helpers (§6). |
24
+ | `@gitkraken/core-gitlens/plus/notifications/errors.js` | `NotificationsApiError` (§7). |
25
+
26
+ Everything else (the raw API client, the parser) is internal. The package has no `vscode` or `l10n` dependency and
27
+ runs in Node and in the browser; it cannot localize, so every user-facing string is yours to produce.
28
+
29
+ ## 2. Implementing `NotificationsContext`
30
+
31
+ ```ts
32
+ interface NotificationsContext {
33
+ readonly productType: string;
34
+ fetchGkApi(path: string, init?: RequestInit, query?: URLSearchParams): Promise<Response>;
35
+ now?(): number;
36
+ readonly log?: NotificationsLogger;
37
+ }
38
+ ```
39
+
40
+ - **`productType`**: sent as the `productType` query parameter on every list request. The package never
41
+ hardcodes one (GitLens passes `gitlens`).
42
+ - **`fetchGkApi`**: an authenticated request against the GK API root.
43
+ - `path` is relative, has no leading slash and **never carries a query string** (e.g. `v1/notifications`,
44
+ `v1/notifications/user-settings`). Any query arrives separately as a `URLSearchParams`; merge it yourself.
45
+ - A non-2xx response must be **returned, not thrown**: the package turns it into a `NotificationsApiError`.
46
+ Throw only for transport failures.
47
+ - Authentication is yours. The service doesn't scope notifications by organization: it lists the user's
48
+ notifications (organization notifications are delivered to each member) whatever `gk-org-id` says.
49
+ - The package sets only `Content-Type: application/json` plus the method and body.
50
+ - **`now`** (optional): epoch ms; defaults to `Date.now()`. A test seam.
51
+ - **`log`** (optional): `debug`, `warn` and `error` sinks. The package never logs on its own, and logs dropped
52
+ notifications without their content.
53
+
54
+ GitLens makes these requests with `background: true`, so a failing poll never toasts an error or trips its
55
+ too-many-failures breaker. Do the equivalent if your API layer surfaces errors to the user.
56
+
57
+ ## 3. Using `NotificationsService`
58
+
59
+ One service instance holds one account's notifications for one `productType`.
60
+
61
+ ```ts
62
+ const service = new NotificationsService(context);
63
+ service.onDidChange(() => render(service.getState()));
64
+ ```
65
+
66
+ ### State
67
+
68
+ `getState()` returns `{ items, hasMore, status, lastSyncedAt, settings, unreadCount }`. `items` is newest first;
69
+ `status` is `'idle' | 'loading' | 'ready' | 'error'`; `unreadCount` counts the **loaded** items only.
70
+ `onDidChange` fires on every state change (a `void` event: re-read `getState()`).
71
+
72
+ ### `refresh()`
73
+
74
+ Re-fetches every page already loaded (capped at five), so read state, deletions and expiry from elsewhere reach a
75
+ list the user paged through. Expired items are pruned and local actions still in flight are kept.
76
+
77
+ - **Never rejects.** On failure the cached items stay and `status` becomes `'error'`; check `getState().status`.
78
+ - **Resolves to the notices to toast**: the unread, toast-eligible (§6) notices that newly arrived with this
79
+ refresh, newest first. Empty when the refresh failed, when nothing had synced before it, or when nothing is new.
80
+ - **No backfill on a first sync.** The first load for an account is the existing inbox, not news. A restored
81
+ snapshot that has a `lastSyncedAt` counts as synced, so notices that arrived while the host was closed do toast.
82
+ - "New" means the id was not held before the refresh **and** its `createdAt` is later than the newest held one,
83
+ less `newToastableSkewMargin` (5 minutes). Only server timestamps are compared.
84
+ - **Concurrent calls share one request and resolve to the same list.** Act on the result from exactly one place
85
+ (your refresh loop); every other caller should ignore it, or you will toast twice.
86
+
87
+ ### `loadMore()`
88
+
89
+ Loads the next page (after any refresh in flight). Concurrent calls share one request. Unlike `refresh()`, it
90
+ **rejects** on failure and leaves the loaded items as they were. A no-op when `hasMore` is false.
91
+
92
+ ### Mutations
93
+
94
+ `markRead(ids)`, `markUnread(ids)`, `delete(ids)`, `markAllRead()` and `deleteAllRead()` (the last two act on
95
+ **loaded** items) apply to the state immediately and reject, after rolling back, if the request fails. A `404` is
96
+ not a failure: the server answers it only when none of the ids exist any more, so the targets are treated as
97
+ already gone and removed. A refresh that was fetching while an action ran will not undo it. Empty id lists are
98
+ no-ops.
99
+
100
+ ### Settings
101
+
102
+ `getSettings()` fetches and caches `{ marketingEnabled, helpEnabled, securityEnabled }` (a flag missing from the
103
+ server counts as enabled). `updateSettings(partial)` applies optimistically and saves. The server stores the whole
104
+ object per request, so saves are serialized: at most one is in flight, and calls made meanwhile share one
105
+ follow-up request carrying the latest state. A failed save rolls back to the last acknowledged settings unless a
106
+ newer change is queued behind it, and rejects.
107
+
108
+ ### Snapshots and disposal
109
+
110
+ `toSnapshot()` returns `{ items, hasMore, page, lastSyncedAt, settings }`: plain data, safe to JSON-serialize.
111
+ `restore(snapshot)` seeds a service from one, pruning expired items; it only checks that each item has a string
112
+ `id` and numeric `createdAt`, and does **not** re-validate the rest (see the CTA note in §5). `dispose()` (also
113
+ `Symbol.dispose`) disposes the change emitter; it does not cancel requests in flight.
114
+
115
+ ## 4. Minimal wiring
116
+
117
+ ```ts
118
+ import type { NotificationsContext } from '@gitkraken/core-gitlens/plus/notifications/context.js';
119
+ import { NotificationsService } from '@gitkraken/core-gitlens/plus/notifications/notificationsService.js';
120
+ import { isSafeExternalUrl } from '@gitkraken/core-gitlens/plus/notifications/presentation.js';
121
+
122
+ const context: NotificationsContext = {
123
+ productType: '<your-product-type>', // the product type notifications-service-v2 targets for your host
124
+ fetchGkApi: (path, init, query) => {
125
+ const url = new URL(path, gkApiRoot); // path has no leading slash; gkApiRoot ends with '/'
126
+ if (query != null) {
127
+ url.search = query.toString();
128
+ }
129
+ return fetch(url, { ...init, headers: { ...init?.headers, ...authAndOrgHeaders() } }); // no throw on non-2xx
130
+ },
131
+ log: { warn: m => logger.warn(m), error: (ex, m) => logger.error(ex, m) },
132
+ };
133
+
134
+ const service = new NotificationsService(context);
135
+ const cached = loadCache()?.[identity]; // { snapshot, writtenAt } or undefined
136
+ if (cached != null) {
137
+ service.restore(cached.snapshot);
138
+ }
139
+
140
+ async function poll(): Promise<void> {
141
+ const arrived = await service.refresh(); // never rejects
142
+ if (service.getState().status !== 'ready') return;
143
+
144
+ for (const n of arrived) {
145
+ await maybeToast(n); // §5: dedupe across windows, skip while the inbox is showing
146
+ }
147
+ }
148
+ ```
149
+
150
+ ## 5. What the host owns
151
+
152
+ The package deliberately leaves these to you. Each item names how GitLens does it.
153
+
154
+ - **Polling.** The service never refreshes on its own. GitLens refreshes 5 seconds after starting, then every 30
155
+ minutes, **only while the window is focused**; losing focus cancels the timer. On regaining focus it refreshes
156
+ immediately if the last attempt (successful or failed) was 5 or more minutes ago, otherwise it waits out the rest
157
+ of the 30 minutes, so a down service is not hit on every focus change.
158
+ - **Persistence.** Debounce `toSnapshot()` on `onDidChange` (GitLens: 1 second) and store it keyed by an
159
+ **account id**, one entry per account, so windows (or sessions) on different accounts don't overwrite each
160
+ other's `lastSyncedAt`. Restore only the current account's entry; otherwise
161
+ start empty. `onDidChange` also fires for status flips on every refresh, so skip the write when the snapshot is
162
+ unchanged (GitLens compares it serialized). GitLens keeps the 5 most recently written accounts, re-reading
163
+ the shared entry right before each write so another window's account isn't dropped.
164
+ - **Sign-out.** Delete every persisted snapshot, so no account's notifications are left behind, and dispose the
165
+ service.
166
+ - **Account switch.** Dispose the service and create a new one for the new account, then refresh right away. An
167
+ organization switch needs nothing: the list doesn't depend on the active organization. Ignore the result of a
168
+ refresh that was in flight for the replaced service: GitLens compares the service it started with to the
169
+ current one before toasting.
170
+ - **De-duplicate toasts across windows and processes.** `refresh()` de-duplicates only within one service. GitLens
171
+ keeps a shared `{ [notificationId]: toastedAt }` map in global storage, re-reads it right before toasting,
172
+ records the new ids **before** showing the toast (and skips toasting if the write fails), and prunes entries older
173
+ than 30 days.
174
+ - **Suppress toasts while an inbox UI is showing.** GitLens tracks which views show the inbox and records the
175
+ notices as toasted but does not show them, since the user is looking at them.
176
+ - **Re-check CTA URLs.** The parser already drops a `callToAction` notice whose link is not `https:`, but
177
+ `restore()` does not re-parse a snapshot. Call `isSafeExternalUrl(cta.url)` immediately before opening the link
178
+ externally. GitLens also marks the notice read after opening its CTA.
179
+ - **Neutralize markdown links in toast text.** A toast is plain host UI, and some hosts render `[text](target)`
180
+ there as a link, including command targets. GitLens breaks the syntax in server-supplied toast text
181
+ (`text.replace(/\]\s*\(/g, '] (')`). Do the equivalent for whatever your toast API does.
182
+ - **Render bodies as untrusted markdown.** `body` and `fallback` text come from the server. GitLens renders them
183
+ with its `gl-markdown` element in `untrusted` mode: links only for `http:`, `https:` and `mailto:` (otherwise
184
+ just their text), images dropped, raw HTML shown as escaped text, and no theme icons. If you render markdown
185
+ yourself, apply the same policy. That inline-link policy is looser than `isSafeExternalUrl` on purpose: a CTA
186
+ is a prominent single-click action, so it is `https:` only.
187
+ - **Localization and layout.** Titles of toasts, chips and button labels, the `projectShared` sentence, the
188
+ "needs a newer version" message, and relative dates are all yours.
189
+
190
+ ## 6. Presentation helpers and display rules
191
+
192
+ Pure functions from `presentation.js`:
193
+
194
+ | Helper | What it does |
195
+ | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
196
+ | `isToastEligible(n)` | `true` only for `security` and `system` content types; everything else arrives quietly. `refresh()` already applies it. |
197
+ | `getCategoryChip(contentType)` | `'help'`, `'offer'` (marketing), `'security'` or `'system'`; `undefined` for `feature`. |
198
+ | `isSafeExternalUrl(url)` | `true` only for a parseable `https:` URL. |
199
+ | `getDisplayBody(headline, body)` | Trims the body, and drops its first paragraph when that paragraph only repeats the headline (ignoring `*`, `_`, `` ` ``, whitespace and a trailing period). `undefined` when nothing remains. |
200
+ | `toPlainText(markdown)` | Best-effort single-line plain text (links and images reduced to their text, tags, headings, quotes, list markers and emphasis removed). Not a parser. |
201
+ | `formatUnreadCount(count)` | The count as a string, `'99+'` above 99. |
202
+
203
+ Display rules the model encodes:
204
+
205
+ - **Headline is the server's `title` only**, trimmed. It is never the internal `name`, which is an identifier, not
206
+ display text. A missing `title` does not drop a notice; `headline` is simply `undefined` and you supply your own
207
+ header.
208
+ - **`summary`** is a plain-text one-liner (`payload.desktopMessage`, else `fallback` run through `toPlainText`).
209
+ Use it where there is no `headline`, for example a single-notice toast. Never markdown-render it.
210
+ - **`body`** is markdown, or `undefined` for `projectShared`.
211
+ - **`projectShared`** carries `{ organization, owner, project, teams }` and no body: assemble the sentence
212
+ yourself (GitLens: "In {organization}, {owner} created a new Team Workspace: {project}", plus the teams when
213
+ there are any).
214
+ - **`needsNewerVersion`** is set when the event is one this package does not know. It is then shown through its
215
+ `fallback` text as `body`, and `event` is `'unknown'`. Use the flag to prompt the user to update. An unknown event
216
+ with no `fallback` is dropped. An unknown `contentType` is treated as `feature` (no chip, no toast).
217
+ - **`cta`** exists only for `callToAction` notices and its `url` is always `https:`.
218
+
219
+ ## 7. Classifying errors
220
+
221
+ Mutations and `loadMore()` reject with `NotificationsApiError`; `refresh()` never does (it logs through
222
+ `context.log` instead).
223
+
224
+ ```ts
225
+ import { NotificationsApiError } from '@gitkraken/core-gitlens/plus/notifications/errors.js';
226
+
227
+ try {
228
+ await service.markRead([id]);
229
+ } catch (ex) {
230
+ if (NotificationsApiError.is(ex)) {
231
+ // ex.status: the HTTP status; ex.operation: 'list' | 'markRead' | 'markUnread' | 'delete'
232
+ // | 'getSettings' | 'updateSettings'
233
+ }
234
+ }
235
+ ```
236
+
237
+ `status` is the response status, including for a 2xx response whose body is not valid JSON (the `cause` then holds
238
+ the parse error). `operation` names the request. `message` is for logs, not for display; show your own
239
+ localized text. Transport failures from your `fetchGkApi` propagate as whatever you threw.