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.
- package/CHANGELOG.md +36 -0
- package/bin/forge-gh-proxy.js +15 -0
- package/bin/forge-github-credential.js +21 -0
- package/bin/forge.js +16 -1
- package/docs/guides/MIGRATION.md +4 -3
- package/docs/reference/RELEASE.md +5 -3
- package/docs/reference/VALIDATION.md +14 -0
- package/docs/reference/github-accounts.md +114 -38
- package/lib/bun-workflow-pins.js +129 -3
- package/lib/commands/github.js +234 -49
- package/lib/commands/merge.js +2 -2
- package/lib/commands/push.js +308 -47
- package/lib/commands/release.js +18 -1
- package/lib/commands/shepherd.js +1 -1
- package/lib/commands/test.js +8 -0
- package/lib/commands/validate.js +219 -67
- package/lib/gh-proxy.js +309 -0
- package/lib/github-context.js +160 -25
- package/lib/github-credential.js +47 -0
- package/lib/github-router.js +774 -0
- package/lib/kernel/migrations.js +1 -1
- package/lib/kernel/sqlite-driver.js +1 -1
- package/lib/memory/router.js +1 -1
- package/lib/memory/usage-evidence.js +1 -1
- package/lib/native-gh.js +177 -0
- package/lib/npm-publish-workflow.js +5 -5
- package/lib/pr-monitor/flow-monitor.js +127 -28
- package/lib/pr-monitor/reconcile-executor.js +1 -1
- package/lib/project-memory.js +1 -1
- package/lib/protected-state-authority.js +214 -13
- package/lib/release-readiness.js +57 -41
- package/lib/shell-utils.js +41 -15
- package/lib/test-workflow.js +389 -0
- package/lib/validation-receipt.js +305 -0
- package/lib/workflow-templates/test.yml +654 -0
- package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/node_modules/@forge/contracts/index.js +32 -0
- package/node_modules/@forge/contracts/package.json +35 -0
- package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/node_modules/@forge/contracts/src/identity.js +42 -0
- package/node_modules/@forge/contracts/src/schema.js +72 -0
- package/node_modules/@forge/contracts/src/validate.js +305 -0
- package/node_modules/@forge/flow/index.js +114 -0
- package/node_modules/@forge/flow/package.json +34 -0
- package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
- package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
- package/node_modules/@forge/flow/src/executor.js +279 -0
- package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
- package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
- package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
- package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
- package/node_modules/@forge/memory/index.js +331 -0
- package/node_modules/@forge/memory/package.json +34 -0
- package/node_modules/@forge/memory/src/authority-provider.js +67 -0
- package/node_modules/@forge/memory/src/backend-registry.js +166 -0
- package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
- package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
- package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
- package/package.json +12 -2
- package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
- package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
- package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
- package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
- package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
- package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
- package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
- package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
- package/scripts/check-forge-token.js +85 -67
- package/scripts/gen-embedded-assets.mjs +31 -5
- package/scripts/test-full-suite.js +69 -15
- package/scripts/test.js +11 -1
package/lib/gh-proxy.js
ADDED
|
@@ -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 };
|
package/lib/github-context.js
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
97
|
-
const output = gitConfig(projectRoot, ['--get',
|
|
98
|
-
|
|
99
|
-
return
|
|
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
|
|
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',
|
|
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
|
|
114
|
-
throw safeCommandFailure('GIT_CONFIG_FAILED',
|
|
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 };
|