forge-workflow 0.1.0-beta.6 → 0.1.0-beta.8

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 (179) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/bin/forge-gh-proxy.js +15 -0
  3. package/bin/forge-github-credential.js +21 -0
  4. package/bin/forge.js +16 -1
  5. package/docs/guides/MIGRATION.md +4 -3
  6. package/docs/reference/RELEASE.md +5 -3
  7. package/docs/reference/VALIDATION.md +14 -0
  8. package/docs/reference/github-accounts.md +114 -38
  9. package/lib/bun-workflow-pins.js +129 -3
  10. package/lib/commands/github.js +234 -49
  11. package/lib/commands/merge.js +2 -2
  12. package/lib/commands/push.js +308 -47
  13. package/lib/commands/release.js +18 -1
  14. package/lib/commands/shepherd.js +1 -1
  15. package/lib/commands/test.js +8 -0
  16. package/lib/commands/validate.js +219 -67
  17. package/lib/gh-proxy.js +309 -0
  18. package/lib/github-context.js +160 -25
  19. package/lib/github-credential.js +47 -0
  20. package/lib/github-router.js +774 -0
  21. package/lib/kernel/migrations.js +1 -1
  22. package/lib/kernel/sqlite-driver.js +1 -1
  23. package/lib/memory/router.js +1 -1
  24. package/lib/memory/usage-evidence.js +1 -1
  25. package/lib/native-gh.js +177 -0
  26. package/lib/npm-publish-workflow.js +5 -5
  27. package/lib/pr-monitor/flow-monitor.js +127 -28
  28. package/lib/pr-monitor/reconcile-executor.js +1 -1
  29. package/lib/project-memory.js +1 -1
  30. package/lib/protected-state-authority.js +214 -13
  31. package/lib/release-readiness.js +57 -41
  32. package/lib/shell-utils.js +41 -15
  33. package/lib/test-workflow.js +389 -0
  34. package/lib/validation-receipt.js +305 -0
  35. package/lib/workflow-templates/test.yml +654 -0
  36. package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  37. package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  38. package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  39. package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  40. package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  41. package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  42. package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  43. package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  44. package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  45. package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  46. package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  47. package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  48. package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  49. package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  50. package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  51. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  52. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  53. package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  54. package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  55. package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  56. package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  57. package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  58. package/node_modules/@forge/contracts/index.js +32 -0
  59. package/node_modules/@forge/contracts/package.json +35 -0
  60. package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  61. package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  62. package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  63. package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  64. package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  65. package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  66. package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  67. package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  68. package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  69. package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  70. package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  71. package/node_modules/@forge/contracts/src/baseline.js +23 -0
  72. package/node_modules/@forge/contracts/src/canonical.js +151 -0
  73. package/node_modules/@forge/contracts/src/definitions.js +176 -0
  74. package/node_modules/@forge/contracts/src/identity.js +42 -0
  75. package/node_modules/@forge/contracts/src/schema.js +72 -0
  76. package/node_modules/@forge/contracts/src/validate.js +305 -0
  77. package/node_modules/@forge/flow/index.js +114 -0
  78. package/node_modules/@forge/flow/package.json +34 -0
  79. package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
  80. package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
  81. package/node_modules/@forge/flow/src/executor.js +279 -0
  82. package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
  83. package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
  84. package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
  85. package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
  86. package/node_modules/@forge/memory/index.js +331 -0
  87. package/node_modules/@forge/memory/package.json +34 -0
  88. package/node_modules/@forge/memory/src/authority-provider.js +67 -0
  89. package/node_modules/@forge/memory/src/backend-registry.js +166 -0
  90. package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
  91. package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
  92. package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
  93. package/package.json +12 -2
  94. package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  95. package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  96. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  97. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  98. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  99. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  100. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  101. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  102. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  103. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  104. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  105. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  106. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  107. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  108. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  109. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  110. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  111. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  112. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  113. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  114. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  115. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  116. package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
  117. package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
  118. package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  119. package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  120. package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  121. package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  122. package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  123. package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  124. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  125. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  126. package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  127. package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  128. package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  129. package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
  130. package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
  131. package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
  132. package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
  133. package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
  134. package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
  135. package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  136. package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  137. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  138. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  139. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  140. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  141. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  142. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  143. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  144. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  145. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  146. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  147. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  148. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  149. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  150. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  151. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  152. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  153. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  154. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  155. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  156. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  157. package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
  158. package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
  159. package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  160. package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  161. package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  162. package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  163. package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  164. package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  165. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  166. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  167. package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  168. package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  169. package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  170. package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
  171. package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
  172. package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
  173. package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
  174. package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
  175. package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
  176. package/scripts/check-forge-token.js +85 -67
  177. package/scripts/gen-embedded-assets.mjs +31 -5
  178. package/scripts/test-full-suite.js +69 -15
  179. package/scripts/test.js +11 -1
@@ -0,0 +1,309 @@
1
+ 'use strict';
2
+
3
+ const { execFileSync, spawnSync: nativeSpawnSync } = require('node:child_process');
4
+ const { createGithubTokenContext, readGithubAuto } = require('./github-context');
5
+ const {
6
+ commandName, exitCode, isForgeProxy, isLocalGhInvocation, resolveRealGh, runNativeGh, shortOptionValue,
7
+ withoutProxyMarker,
8
+ } = require('./native-gh');
9
+ const { assertGithubRouterCloneRegistered } = require('./github-router');
10
+ const { resolveLocalGithubTarget } = require('./commands/github');
11
+
12
+ const COMMAND_VALUE_FLAGS = new Set(['--hostname', '--repo', '-R', '--jq', '--template']);
13
+ const REPOSITORY_TARGET_POSITIONS = new Map([
14
+ ['extension install', 2],
15
+ ['issue transfer', 3],
16
+ ['label clone', 2],
17
+ ...['archive', 'clone', 'create', 'delete', 'edit', 'fork', 'new', 'set-default', 'sync', 'unarchive', 'view']
18
+ .map(verb => [`repo ${verb}`, 2]),
19
+ ]);
20
+ const RESOURCE_TARGET_POSITIONS = new Map([
21
+ ['co', 1],
22
+ ['api', 1],
23
+ ['agent-task view', 2],
24
+ ...['clone', 'delete', 'edit', 'rename', 'view'].map(verb => [`gist ${verb}`, 2]),
25
+ ]);
26
+
27
+ function capturedRunner(realGh) {
28
+ return (command, args, options = {}) => {
29
+ if (command !== 'gh') return execFileSync(command, args, options);
30
+ const result = nativeSpawnSync(realGh, args, options);
31
+ if (result.error) throw result.error;
32
+ if (result.status !== 0) {
33
+ const error = new Error(`GitHub CLI exited with status ${result.status}.`);
34
+ Object.assign(error, { status: result.status, stdout: result.stdout, stderr: result.stderr });
35
+ throw error;
36
+ }
37
+ return result.stdout || '';
38
+ };
39
+ }
40
+
41
+ function environmentValue(environment, name, platform) {
42
+ if (platform !== 'win32') return environment[name] || undefined;
43
+ const key = Object.keys(environment).find(candidate => candidate.toUpperCase() === name);
44
+ return key ? (environment[key] || undefined) : undefined;
45
+ }
46
+
47
+ function resourceHostname(value) {
48
+ const text = String(value || '');
49
+ if (text.includes('://')) {
50
+ const url = new URL(text);
51
+ if (url.protocol !== 'https:' || url.port) throw new Error('Unsupported GitHub target URL.');
52
+ const hostname = url.hostname.toLowerCase();
53
+ return ['api.github.com', 'gist.github.com', 'uploads.github.com'].includes(hostname) ? 'github.com' : hostname;
54
+ }
55
+ return undefined;
56
+ }
57
+
58
+ function repositoryHostname(value) {
59
+ const text = String(value || '');
60
+ if (text.includes('://')) {
61
+ const url = new URL(text);
62
+ if (!['https:', 'ssh:'].includes(url.protocol) || (url.protocol === 'https:' && url.port)) {
63
+ throw new Error('Unsupported GitHub target URL.');
64
+ }
65
+ return url.hostname.toLowerCase();
66
+ }
67
+ const scpHost = /^(?:[^@\s/:]+@)?([^:\s/]+):[^/\s]+\/[^/\s]+$/.exec(text);
68
+ if (scpHost) return scpHost[1].toLowerCase();
69
+ if (text.includes(':')) throw new Error('Unknown GitHub repository destination.');
70
+ const parts = text.split('/');
71
+ if (parts.length === 2 && parts.every(Boolean)) return undefined;
72
+ if (parts.length === 3 && parts.every(Boolean)) return parts[0].toLowerCase();
73
+ throw new Error('Unknown GitHub repository destination.');
74
+ }
75
+
76
+ function commandPath(args) {
77
+ const result = [];
78
+ let started = false;
79
+ for (let index = 0; index < args.length; index += 1) {
80
+ const arg = args[index];
81
+ if (COMMAND_VALUE_FLAGS.has(arg)) { index += 1; continue; }
82
+ if (/^(?:--hostname|--repo|--jq|--template)=/.test(arg) || /^-R=?\S+/.test(arg)) continue;
83
+ if (arg === '--' || (started && arg.startsWith('-'))) break;
84
+ if (arg.startsWith('-')) continue;
85
+ started = true;
86
+ result.push(arg);
87
+ }
88
+ return result;
89
+ }
90
+
91
+ function flagTakesValue(help, flag) {
92
+ const matches = new Set();
93
+ for (const line of String(help || '').split(/\r?\n/)) {
94
+ const [declaration, description] = line.trim().split(/\s{2,}/, 2);
95
+ if (!description) continue;
96
+ const tokens = declaration.replaceAll(',', '').split(/\s+/);
97
+ if (tokens.includes(flag)) matches.add(tokens.some(token => !token.startsWith('-')));
98
+ }
99
+ return matches.size === 1 ? [...matches][0] : null;
100
+ }
101
+
102
+ function optionValueResolver(args, realGh, baseEnv, options) {
103
+ let help;
104
+ const readHelp = () => {
105
+ if (help !== undefined) return help;
106
+ if (options.readCommandHelp) help = String(options.readCommandHelp(commandPath(args)) || '');
107
+ else {
108
+ const pathParts = commandPath(args);
109
+ for (let length = pathParts.length; length > 0; length -= 1) {
110
+ try {
111
+ help = String((options.execFileSync || execFileSync)(realGh, ['help', ...pathParts.slice(0, length)], {
112
+ encoding: 'utf8', env: baseEnv, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, timeout: 3000,
113
+ }) || '');
114
+ break;
115
+ } catch { /* try the parent command */ }
116
+ }
117
+ }
118
+ return help || '';
119
+ };
120
+ const resolve = flag => flagTakesValue(readHelp(), flag);
121
+ resolve.help = readHelp;
122
+ return resolve;
123
+ }
124
+
125
+ function isOptionValue(args, index, resolveOptionValue) {
126
+ const previous = args[index - 1];
127
+ if (!previous?.startsWith('-') || previous.includes('=')) return false;
128
+ if (COMMAND_VALUE_FLAGS.has(previous)) return true;
129
+ if (!previous.startsWith('--') && previous.length > 2) {
130
+ const option = shortOptionValue(previous, resolveOptionValue);
131
+ return Boolean(option && !option.attached);
132
+ }
133
+ const takesValue = resolveOptionValue(previous);
134
+ if (typeof takesValue !== 'boolean') throw new Error(`Unable to determine GitHub CLI option arity for ${previous}.`);
135
+ return takesValue;
136
+ }
137
+
138
+ function positionalArguments(args, resolveOptionValue) {
139
+ const delimiter = args.indexOf('--');
140
+ return args.filter((arg, index) => arg !== '--' && ((delimiter >= 0 && index > delimiter)
141
+ || (!arg.startsWith('-') && !isOptionValue(args, index, resolveOptionValue))));
142
+ }
143
+
144
+ function namedUrlPositions(help, positionalCount) {
145
+ const positions = new Set();
146
+ for (const line of String(help || '').split(/\r?\n/)) {
147
+ const usage = /^\s*(?:Usage:\s*)?gh\s+(.+)$/i.exec(line)?.[1];
148
+ if (!usage) continue;
149
+ const tokens = usage.match(/\[[^\]]*\]|\{[^}]*\}|<[^>]+>|\S+/g) || [];
150
+ let position = tokens.findIndex(token => /^[\[{<]/.test(token));
151
+ if (position < 0) continue;
152
+ for (const token of tokens.slice(position)) {
153
+ const placeholders = [...token.matchAll(/<([^>]+)>/g)].map(match => match[1]);
154
+ if (!placeholders.length) continue;
155
+ if (placeholders.some(name => /(?:^|[-_])urls?$/i.test(name))) {
156
+ positions.add(position);
157
+ if (placeholders.some(name => /(?:^|[-_])urls$/i.test(name))) {
158
+ for (let index = position + 1; index < positionalCount; index += 1) positions.add(index);
159
+ }
160
+ }
161
+ position += 1;
162
+ }
163
+ }
164
+ return positions;
165
+ }
166
+
167
+ function positionalResourceHosts(args, command, resolveOptionValue, excludedPosition) {
168
+ if (!args.some(arg => arg.includes('://'))) return [];
169
+ const route = command.slice(0, 2).join(' ');
170
+ const position = RESOURCE_TARGET_POSITIONS.get(route) ?? RESOURCE_TARGET_POSITIONS.get(command[0]);
171
+ const help = position === undefined ? resolveOptionValue.help() : '';
172
+ if (position === undefined && !help.trim()) throw new Error('Unable to classify the GitHub CLI URL argument.');
173
+ const positions = position === undefined ? namedUrlPositions(help, args.length) : new Set([position]);
174
+ if (positions.size === 0) return [];
175
+ const values = positionalArguments(args, resolveOptionValue);
176
+ return [...positions].filter(index => index !== excludedPosition)
177
+ .map(index => resourceHostname(values[index])).filter(Boolean);
178
+ }
179
+
180
+ function repositoryFromShortOptions(arg, args, index, resolveOptionValue) {
181
+ if (!arg.startsWith('-') || arg.startsWith('--') || !arg.slice(1).includes('R')) return null;
182
+ const option = shortOptionValue(arg, resolveOptionValue);
183
+ if (option?.flag !== '-R') return null;
184
+ const next = args[index + 1];
185
+ return option.attached || (next && !next.startsWith('-') ? next : '');
186
+ }
187
+
188
+ function argumentDestinations(args, realGh, baseEnv, options) {
189
+ const hosts = [];
190
+ let explicit = false;
191
+ let remoteName;
192
+ const resolveOptionValue = optionValueResolver(args, realGh, baseEnv, options);
193
+ const command = commandPath(args);
194
+ const commandRoute = command.slice(0, 2).join(' ');
195
+ const delimiter = args.indexOf('--');
196
+ const end = delimiter < 0 ? args.length : delimiter;
197
+ for (let index = 0; index < end; index += 1) {
198
+ const arg = args[index];
199
+ let value;
200
+ if (arg === '--hostname' || arg === '--repo' || arg === '-R'
201
+ || (commandRoute === 'repo create' && (arg === '--template' || arg === '-p'))) {
202
+ if (arg !== '--hostname') explicit = true;
203
+ value = args[++index];
204
+ if (!value || value.startsWith('-')) throw new Error(`Missing value for ${arg}.`);
205
+ } else if (arg.startsWith('--hostname=')) {
206
+ value = arg.slice('--hostname='.length);
207
+ if (!value) throw new Error('Missing value for --hostname.');
208
+ } else if (arg.startsWith('--repo=')) {
209
+ explicit = true;
210
+ value = arg.slice('--repo='.length);
211
+ if (!value) throw new Error('Missing value for --repo.');
212
+ } else if (commandRoute === 'repo create' && arg.startsWith('--template=')) {
213
+ explicit = true;
214
+ value = arg.slice('--template='.length);
215
+ if (!value) throw new Error('Missing value for --template.');
216
+ } else if (commandRoute === 'repo create' && /^-p=?\S+/.test(arg)) {
217
+ explicit = true;
218
+ value = arg.slice(2).replace(/^=/, '');
219
+ } else if (/^-R=?\S+/.test(arg)) {
220
+ explicit = true;
221
+ value = arg.slice(2).replace(/^=/, '');
222
+ } else {
223
+ value = repositoryFromShortOptions(arg, args, index, resolveOptionValue);
224
+ if (value === null) continue;
225
+ explicit = true;
226
+ if (!value) throw new Error('Missing value for -R.');
227
+ }
228
+ const host = arg.startsWith('--hostname') ? value.toLowerCase() : repositoryHostname(value);
229
+ if (host) hosts.push(host);
230
+ }
231
+
232
+ const repositoryPosition = REPOSITORY_TARGET_POSITIONS.get(commandRoute);
233
+ if (repositoryPosition !== undefined) {
234
+ const target = positionalArguments(args, resolveOptionValue)[repositoryPosition];
235
+ if (target) {
236
+ explicit = true;
237
+ const localName = ['repo create', 'repo new'].includes(commandRoute) && !/[/:]/.test(target);
238
+ const localRemote = commandRoute === 'repo set-default' && !/[/:]/.test(target);
239
+ if (localRemote) remoteName = target;
240
+ const host = localName || localRemote ? undefined : repositoryHostname(target);
241
+ if (host) hosts.push(host);
242
+ }
243
+ }
244
+ const resourceHosts = positionalResourceHosts(args, command, resolveOptionValue, repositoryPosition);
245
+ if (resourceHosts.length) { explicit = true; hosts.push(...resourceHosts); }
246
+ return { explicit, hosts, remoteName };
247
+ }
248
+
249
+ function selectedTarget(args, projectRoot, baseEnv, options, platform) {
250
+ const destinations = argumentDestinations(args, options.realGh, baseEnv, options);
251
+ const hosts = destinations.hosts;
252
+ const environmentHost = environmentValue(baseEnv, 'GH_HOST', platform);
253
+ const environmentRepository = environmentValue(baseEnv, 'GH_REPO', platform);
254
+ if (environmentHost) hosts.push(environmentHost.toLowerCase());
255
+ const repositoryHost = environmentRepository ? repositoryHostname(environmentRepository) : undefined;
256
+ if (repositoryHost) hosts.push(repositoryHost);
257
+
258
+ let local;
259
+ if (destinations.remoteName || (!destinations.explicit && !environmentRepository)) {
260
+ local = resolveLocalGithubTarget(projectRoot, options, destinations.remoteName);
261
+ if (!local) throw new Error('Unable to resolve the local GitHub repository host.');
262
+ hosts.push(local.hostname.toLowerCase());
263
+ }
264
+ if (hosts.length === 0) hosts.push('github.com');
265
+ const unique = [...new Set(hosts)];
266
+ if (unique.length !== 1 || unique[0] !== 'github.com') throw new Error('Automatic routing supports github.com only.');
267
+ return { local, environmentRepository };
268
+ }
269
+
270
+ function runGhProxy(args, projectRoot = process.cwd(), options = {}) {
271
+ const writeError = options.writeError || (value => process.stderr.write(value));
272
+ const realGh = (options.resolveExecutable || resolveRealGh)(options);
273
+ if (!realGh) {
274
+ writeError('GitHub CLI executable not found outside the Forge proxy.\n');
275
+ return 127;
276
+ }
277
+ const baseEnv = withoutProxyMarker(options.baseEnv || process.env);
278
+ const resolveOptionValue = optionValueResolver(args, realGh, baseEnv, options);
279
+ if (isLocalGhInvocation(args, resolveOptionValue)) {
280
+ return runNativeGh(args, projectRoot, { ...options, resolveExecutable: () => realGh });
281
+ }
282
+ const spawnSync = options.spawnSync || nativeSpawnSync;
283
+ const passthrough = () => exitCode(spawnSync(realGh, args, {
284
+ cwd: projectRoot, env: baseEnv, stdio: 'inherit', shell: false,
285
+ }));
286
+ try {
287
+ const automatic = (options.readAuto || readGithubAuto)(projectRoot, options);
288
+ if (automatic === false) return passthrough();
289
+ if (automatic !== true) throw new Error('Invalid automatic routing state.');
290
+ (options.assertRegistered || assertGithubRouterCloneRegistered)(projectRoot, options);
291
+ const platform = options.platform || process.platform;
292
+ const { local, environmentRepository } = selectedTarget(args, projectRoot, baseEnv, { ...options, realGh }, platform);
293
+ const childRepository = environmentRepository || (local ? `github.com/${local.repository}` : undefined);
294
+ const childRunner = (command, childArgs, childOptions) => spawnSync(command, childArgs, {
295
+ ...childOptions,
296
+ ...(childRepository ? { env: { ...childOptions.env, GH_REPO: childRepository } } : {}),
297
+ });
298
+ const context = (options.createContext || createGithubTokenContext)(projectRoot, {
299
+ ...options, baseEnv, runner: options.runner || capturedRunner(realGh), childRunner,
300
+ });
301
+ if (!context.bound) throw new Error('Automatic routing requires a clone account binding.');
302
+ return exitCode(context.runChild(realGh, args, { cwd: projectRoot, stdio: 'inherit', shell: false }));
303
+ } catch {
304
+ writeError('Forge could not select this clone\'s GitHub account. Run forge github status.\n');
305
+ return 1;
306
+ }
307
+ }
308
+
309
+ module.exports = { commandName, isForgeProxy, resolveRealGh, runGhProxy };
@@ -1,12 +1,16 @@
1
1
  'use strict';
2
2
 
3
3
  const { execFileSync } = require('node:child_process');
4
+ const { isManagedCredentialHelperValue, isOwnedCredentialHelperValue, isUsableCredentialHelperValue } = require('./github-router');
5
+ const { resolveRealGh } = require('./native-gh');
4
6
 
5
7
  const ACCOUNT_KEY = 'github.account';
8
+ const AUTO_KEY = 'github.auto';
9
+ const CREDENTIAL_HELPER_KEY = 'credential.https://github.com.helper';
6
10
  const HOST = 'github.com';
7
11
  const LOGIN_RE = /^(?=.{1,39}$)[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/;
8
- const GITHUB_ENV_NAMES = new Set(['gh_token', 'github_token', 'gh_host', 'gh_repo']);
9
- const GITHUB_SECRET_ENV_NAMES = new Set(['gh_token', 'github_token']);
12
+ const GITHUB_ENV_NAMES = new Set(['gh_token', 'github_token', 'gh_enterprise_token', 'github_enterprise_token', 'gh_host', 'gh_repo']);
13
+ const GITHUB_SECRET_ENV_NAMES = new Set(['gh_token', 'github_token', 'gh_enterprise_token', 'github_enterprise_token']);
10
14
 
11
15
  class GithubContextError extends Error {
12
16
  constructor(code, message) {
@@ -29,16 +33,17 @@ function safeLogin(login) {
29
33
  }
30
34
 
31
35
  function runnerFor(options) {
32
- return options.runner || options._runner || options._exec || options.execFileSync || defaultRunner;
33
- }
34
-
35
- function defaultRunner(command, args, options = {}) {
36
- return execFileSync(command, args, {
37
- encoding: 'utf8',
38
- stdio: ['ignore', 'pipe', 'pipe'],
39
- windowsHide: true,
40
- ...options,
41
- });
36
+ const injected = options.runner || options._runner || options._exec;
37
+ if (injected) return injected;
38
+ const execute = options.execFileSync || execFileSync;
39
+ const resolve = options.resolveRealGh || resolveRealGh;
40
+ return (command, args, runOptions = {}) => {
41
+ const executable = command === 'gh' ? resolve({ env: runOptions.env }) : command;
42
+ if (!executable) throw Object.assign(new Error('GitHub CLI was not found.'), { code: 'ENOENT' });
43
+ return execute(executable, args, {
44
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, ...runOptions,
45
+ });
46
+ };
42
47
  }
43
48
 
44
49
  function errorText(error) {
@@ -86,35 +91,50 @@ function gitConfig(projectRoot, args, options, absentIsOkay = false) {
86
91
  windowsHide: true,
87
92
  });
88
93
  } catch (error) {
89
- if (absentIsOkay && (error?.status === 1 || /key does not contain|not found|no such key/i.test(errorText(error)))) {
90
- return '';
94
+ if (absentIsOkay && (error?.status === 1 || /key does not contain|not found|no such key|not a git repository|--local can only be used/i.test(errorText(error)))) {
95
+ return null;
91
96
  }
92
97
  throw safeCommandFailure('GIT_CONFIG_FAILED', 'Unable to read this clone-local GitHub account binding.');
93
98
  }
94
99
  }
95
100
 
96
- function readGithubAccount(projectRoot, options = {}) {
97
- const output = gitConfig(projectRoot, ['--get', ACCOUNT_KEY], options, true);
98
- const value = output == null ? '' : output.toString().trim();
99
- return value || null;
101
+ function readLocalValues(projectRoot, key, options = {}) {
102
+ const output = gitConfig(projectRoot, ['--get-all', key], options, true);
103
+ if (output == null) return [];
104
+ return output.toString().replace(/\r?\n$/, '').split(/\r?\n/);
100
105
  }
101
106
 
102
- function writeGithubAccount(projectRoot, login, options = {}) {
103
- if (!validateGithubLogin(login)) {
104
- throw safeCommandFailure('INVALID_GITHUB_LOGIN', 'GitHub login must be 1-39 letters, numbers, or internal hyphens.');
105
- }
107
+ function runGitConfig(projectRoot, args, options, message) {
106
108
  try {
107
- runnerFor(options)('git', ['config', '--local', '--replace-all', ACCOUNT_KEY, login], {
109
+ return runnerFor(options)('git', ['config', '--local', ...args], {
108
110
  cwd: projectRoot,
109
111
  encoding: 'utf8',
110
112
  stdio: ['ignore', 'pipe', 'pipe'],
111
113
  windowsHide: true,
112
114
  });
113
- } catch (_error) {
114
- throw safeCommandFailure('GIT_CONFIG_FAILED', 'Unable to write the clone-local GitHub account binding.');
115
+ } catch {
116
+ throw safeCommandFailure('GIT_CONFIG_FAILED', message);
115
117
  }
116
118
  }
117
119
 
120
+ function readGithubAccount(projectRoot, options = {}) {
121
+ const values = readLocalValues(projectRoot, ACCOUNT_KEY, options);
122
+ if (values.length === 0) return null;
123
+ if (values.length !== 1) {
124
+ throw safeCommandFailure('INVALID_GITHUB_LOGIN', 'The clone-local github.account value must be unique.');
125
+ }
126
+ const value = values[0].trim();
127
+ return value || null;
128
+ }
129
+
130
+ function writeGithubAccount(projectRoot, login, options = {}) {
131
+ if (!validateGithubLogin(login)) {
132
+ throw safeCommandFailure('INVALID_GITHUB_LOGIN', 'GitHub login must be 1-39 letters, numbers, or internal hyphens.');
133
+ }
134
+ runGitConfig(projectRoot, ['--replace-all', ACCOUNT_KEY, login], options,
135
+ 'Unable to write the clone-local GitHub account binding.');
136
+ }
137
+
118
138
  function unsetGithubAccount(projectRoot, options = {}) {
119
139
  try {
120
140
  runnerFor(options)('git', ['config', '--local', '--unset-all', ACCOUNT_KEY], {
@@ -129,6 +149,91 @@ function unsetGithubAccount(projectRoot, options = {}) {
129
149
  }
130
150
  }
131
151
 
152
+ function readGithubAuto(projectRoot, options = {}) {
153
+ const values = readLocalValues(projectRoot, AUTO_KEY, options);
154
+ if (values.length === 0) return false;
155
+ if (values.length !== 1) return null;
156
+ const value = values[0].toLowerCase();
157
+ if (value === 'true') return true;
158
+ if (value === 'false') return false;
159
+ return null;
160
+ }
161
+
162
+ function ownsCredentialHelper(value, options) {
163
+ return (options.isOwnedCredentialHelper || isOwnedCredentialHelperValue)(value);
164
+ }
165
+
166
+ function hasGithubAutoCredentialHelper(projectRoot, options = {}) {
167
+ const helpers = readLocalValues(projectRoot, CREDENTIAL_HELPER_KEY, options);
168
+ return helpers.length === 2 && helpers[0] === ''
169
+ && (options.isOwnedCredentialHelper || isUsableCredentialHelperValue)(helpers[1], options);
170
+ }
171
+
172
+ function isForgeCredentialHelper(value, options) {
173
+ return ownsCredentialHelper(value, options)
174
+ || (options.isManagedCredentialHelper || isManagedCredentialHelperValue)(value, options);
175
+ }
176
+
177
+ function restoreLocalValues(projectRoot, snapshots, options) {
178
+ for (const [key, values] of snapshots) {
179
+ try {
180
+ unsetLocalKey(projectRoot, key, options);
181
+ for (const value of values) runnerFor(options)('git', ['config', '--local', '--add', key, value], { cwd: projectRoot, stdio: 'ignore' });
182
+ } catch { /* best effort rollback */ }
183
+ }
184
+ }
185
+
186
+ function unsetLocalKey(projectRoot, key, options) {
187
+ try {
188
+ runnerFor(options)('git', ['config', '--local', '--unset-all', key], {
189
+ cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
190
+ });
191
+ } catch (error) {
192
+ if (error?.status === 5 || error?.status === 1 || /key does not contain|not found|no such key/i.test(errorText(error))) return;
193
+ throw error;
194
+ }
195
+ }
196
+
197
+ function assertGithubAutoAvailable(projectRoot, options = {}) {
198
+ const helpers = readLocalValues(projectRoot, CREDENTIAL_HELPER_KEY, options);
199
+ const replaceable = helpers.length === 2 && helpers[0] === ''
200
+ && isForgeCredentialHelper(helpers[1], options);
201
+ if (helpers.length && !replaceable) {
202
+ throw safeCommandFailure('GIT_CREDENTIAL_HELPER_CONFLICT', 'A clone-local GitHub credential helper is already configured.');
203
+ }
204
+ }
205
+
206
+ function enableGithubAuto(projectRoot, options = {}) {
207
+ assertGithubAutoAvailable(projectRoot, options);
208
+ const helperValue = options.credentialHelperValue;
209
+ if (!ownsCredentialHelper(helperValue || '', options)) {
210
+ throw safeCommandFailure('GIT_CONFIG_FAILED', 'Forge did not provide a valid credential helper command.');
211
+ }
212
+ const previousHelpers = readLocalValues(projectRoot, CREDENTIAL_HELPER_KEY, options);
213
+ const previousAuto = readLocalValues(projectRoot, AUTO_KEY, options);
214
+ try {
215
+ runGitConfig(projectRoot, ['--replace-all', CREDENTIAL_HELPER_KEY, ''], options, 'Unable to enable automatic GitHub routing.');
216
+ runGitConfig(projectRoot, ['--add', CREDENTIAL_HELPER_KEY, helperValue], options, 'Unable to enable automatic GitHub routing.');
217
+ runGitConfig(projectRoot, ['--replace-all', AUTO_KEY, 'true'], options, 'Unable to enable automatic GitHub routing.');
218
+ } catch (error) {
219
+ restoreLocalValues(projectRoot, [[CREDENTIAL_HELPER_KEY, previousHelpers], [AUTO_KEY, previousAuto]], options);
220
+ throw error;
221
+ }
222
+ }
223
+
224
+ function disableGithubAuto(projectRoot, options = {}) {
225
+ const helpers = readLocalValues(projectRoot, CREDENTIAL_HELPER_KEY, options);
226
+ const previousAuto = readLocalValues(projectRoot, AUTO_KEY, options);
227
+ const owned = helpers.length === 2 && helpers[0] === '' && isForgeCredentialHelper(helpers[1], options);
228
+ const keys = [AUTO_KEY, ...(owned ? [CREDENTIAL_HELPER_KEY] : [])];
229
+ try {
230
+ for (const key of keys) unsetLocalKey(projectRoot, key, options);
231
+ } catch {
232
+ restoreLocalValues(projectRoot, [[AUTO_KEY, previousAuto], [CREDENTIAL_HELPER_KEY, helpers]], options);
233
+ throw safeCommandFailure('GIT_CONFIG_FAILED', 'Unable to disable automatic GitHub routing.');
234
+ }
235
+ }
236
+
132
237
  function withoutGithubEnvironment(environment) {
133
238
  const result = {};
134
239
  for (const [key, value] of Object.entries(environment || {})) {
@@ -177,6 +282,20 @@ function retrieveGithubToken(login, options) {
177
282
  return token;
178
283
  }
179
284
 
285
+ function createGithubTokenContext(projectRoot, options = {}) {
286
+ const account = (options.readAccount || readGithubAccount)(projectRoot, options);
287
+ if (!validateGithubLogin(account)) {
288
+ throw safeCommandFailure('GITHUB_NOT_AUTHENTICATED', 'No selected GitHub account is available.');
289
+ }
290
+ const baseEnv = { ...(options.baseEnv || options.env || process.env) };
291
+ const token = retrieveGithubToken(account, { ...options, baseEnv });
292
+ return createPublicContext(publicStatus('selected', account, null), token, baseEnv, options);
293
+ }
294
+
295
+ function writeGithubCredential(projectRoot, write, options = {}) {
296
+ createGithubTokenContext(projectRoot, options).writeCredential(write);
297
+ }
298
+
180
299
  function loginFromOutput(output) {
181
300
  const text = output == null ? '' : output.toString().trim();
182
301
  if (!text) return null;
@@ -270,9 +389,16 @@ function createPublicContext(status, token, baseEnv, options) {
270
389
  throw safeGithubCommandFailure(error);
271
390
  }
272
391
  };
392
+ const writeCredential = (write) => {
393
+ if (status.state === 'unbound' || typeof write !== 'function') {
394
+ throw safeCommandFailure('GITHUB_NOT_AUTHENTICATED', 'No selected GitHub account is available.');
395
+ }
396
+ write(`username=${status.account}\npassword=${token}\n\n`);
397
+ };
273
398
  Object.defineProperties(context, {
274
399
  runChild: { value: runChild, enumerable: false },
275
400
  runGh: { value: runGh, enumerable: false },
401
+ writeCredential: { value: writeCredential, enumerable: false },
276
402
  getStatus: { value: () => ({ ...status }), enumerable: false },
277
403
  });
278
404
  return context;
@@ -296,12 +422,21 @@ function createGithubContext(projectRoot, options = {}) {
296
422
 
297
423
  module.exports = {
298
424
  ACCOUNT_KEY,
425
+ AUTO_KEY,
426
+ CREDENTIAL_HELPER_KEY,
299
427
  GithubContextError,
428
+ assertGithubAutoAvailable,
300
429
  createGithubContext,
430
+ createGithubTokenContext,
431
+ disableGithubAuto,
432
+ enableGithubAuto,
433
+ hasGithubAutoCredentialHelper,
301
434
  prepareGithubAccount,
302
435
  prepareGithubContext: createGithubContext,
303
436
  readGithubAccount,
437
+ readGithubAuto,
304
438
  resolveGithubLogin,
439
+ writeGithubCredential,
305
440
  unsetGithubAccount,
306
441
  validateGithubLogin,
307
442
  writeGithubAccount,
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ const { readGithubAuto, writeGithubCredential } = require('./github-context');
4
+ const { assertGithubRouterCloneRegistered } = require('./github-router');
5
+
6
+ function parseRequest(input) {
7
+ if (typeof input !== 'string' || input.length > 64 * 1024) return null;
8
+ const request = Object.create(null);
9
+ for (const line of input.split(/\r?\n/)) {
10
+ if (!line) break;
11
+ const separator = line.indexOf('=');
12
+ if (separator < 1) return null;
13
+ request[line.slice(0, separator)] = line.slice(separator + 1);
14
+ }
15
+ return request;
16
+ }
17
+
18
+ function runCredentialHelper(operation, options = {}) {
19
+ if (!['get', 'store', 'erase'].includes(operation)) return 1;
20
+ if (operation !== 'get') return 0;
21
+ const request = parseRequest(options.input || '');
22
+ const host = request?.host?.toLowerCase();
23
+ if (request?.protocol !== 'https' || (host !== 'github.com' && host !== 'github.com:443')) return 0;
24
+ const projectRoot = options.projectRoot || process.cwd();
25
+ const write = options.write || (value => process.stdout.write(value));
26
+ const writeError = options.writeError || (value => process.stderr.write(value));
27
+ try {
28
+ const automatic = (options.readAuto || readGithubAuto)(projectRoot, options);
29
+ if (automatic === false) return 0;
30
+ if (automatic !== true) throw new Error('Automatic GitHub routing configuration is invalid.');
31
+ if (!options.createContext || options.assertRegistered) {
32
+ (options.assertRegistered || assertGithubRouterCloneRegistered)(projectRoot, options);
33
+ }
34
+ if (options.createContext) {
35
+ options.createContext(projectRoot, options).writeCredential(write);
36
+ } else {
37
+ writeGithubCredential(projectRoot, write, options);
38
+ }
39
+ return 0;
40
+ } catch {
41
+ write('quit=true\n\n');
42
+ writeError('Forge could not resolve this clone\'s GitHub account. Run forge github status.\n');
43
+ return 1;
44
+ }
45
+ }
46
+
47
+ module.exports = { parseRequest, runCredentialHelper };