@angelmsger/fx 0.10.1 → 0.11.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 (104) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +7 -8
  3. package/defaults/tools.yaml +9 -0
  4. package/dist/adapters/ci/github-actions/ci.js +95 -0
  5. package/dist/adapters/ci/github-actions/ci.js.map +1 -0
  6. package/dist/adapters/ci/github-actions/provider.js +33 -0
  7. package/dist/adapters/ci/github-actions/provider.js.map +1 -0
  8. package/dist/adapters/code-host/github/provider.js +207 -0
  9. package/dist/adapters/code-host/github/provider.js.map +1 -0
  10. package/dist/adapters/github/gh.js +103 -0
  11. package/dist/adapters/github/gh.js.map +1 -0
  12. package/dist/adapters/process/launch.js +55 -0
  13. package/dist/adapters/process/launch.js.map +1 -0
  14. package/dist/adapters/process/node-process.js +3 -3
  15. package/dist/adapters/process/node-process.js.map +1 -1
  16. package/dist/adapters/process/powershell.js +13 -2
  17. package/dist/adapters/process/powershell.js.map +1 -1
  18. package/dist/adapters/process/stdio-bridge.js +2 -2
  19. package/dist/adapters/process/stdio-bridge.js.map +1 -1
  20. package/dist/application/docs/derive.js +14 -5
  21. package/dist/application/docs/derive.js.map +1 -1
  22. package/dist/application/import.js +2 -1
  23. package/dist/application/import.js.map +1 -1
  24. package/dist/application/login/collect.js +12 -6
  25. package/dist/application/login/collect.js.map +1 -1
  26. package/dist/application/release/ci.js +15 -4
  27. package/dist/application/release/ci.js.map +1 -1
  28. package/dist/application/release/errors.js +4 -0
  29. package/dist/application/release/errors.js.map +1 -1
  30. package/dist/application/release/facts.js +24 -3
  31. package/dist/application/release/facts.js.map +1 -1
  32. package/dist/application/release/index.js +14 -5
  33. package/dist/application/release/index.js.map +1 -1
  34. package/dist/application/release/repo.js +109 -0
  35. package/dist/application/release/repo.js.map +1 -0
  36. package/dist/application/sync/checks.js +11 -0
  37. package/dist/application/sync/checks.js.map +1 -1
  38. package/dist/application/sync/stages.js +11 -1
  39. package/dist/application/sync/stages.js.map +1 -1
  40. package/dist/application/workspace/go-work.js +77 -0
  41. package/dist/application/workspace/go-work.js.map +1 -0
  42. package/dist/application/workspace/resources.js +9 -0
  43. package/dist/application/workspace/resources.js.map +1 -1
  44. package/dist/application/workspace/root-entries.js +3 -0
  45. package/dist/application/workspace/root-entries.js.map +1 -1
  46. package/dist/cli/base.js +3 -1
  47. package/dist/cli/base.js.map +1 -1
  48. package/dist/cli/commands/aliases.js +1 -0
  49. package/dist/cli/commands/aliases.js.map +1 -1
  50. package/dist/cli/commands/index.js +3 -3
  51. package/dist/cli/commands/index.js.map +1 -1
  52. package/dist/cli/commands/{release.js → release/check.js} +14 -13
  53. package/dist/cli/commands/release/check.js.map +1 -0
  54. package/dist/cli/commands/self/info.js +2 -2
  55. package/dist/cli/commands/self/info.js.map +1 -1
  56. package/dist/cli/flags.js +2 -2
  57. package/dist/cli/flags.js.map +1 -1
  58. package/dist/cli/help.js +2 -8
  59. package/dist/cli/help.js.map +1 -1
  60. package/dist/cli/release-view.js +105 -17
  61. package/dist/cli/release-view.js.map +1 -1
  62. package/dist/cli/render/report.js +1 -1
  63. package/dist/cli/render/report.js.map +1 -1
  64. package/dist/cli/route/index.js +13 -1
  65. package/dist/cli/route/index.js.map +1 -1
  66. package/dist/composition/bundled.js +4 -1
  67. package/dist/composition/bundled.js.map +1 -1
  68. package/dist/composition/registry.js +4 -0
  69. package/dist/composition/registry.js.map +1 -1
  70. package/dist/domain/manifest/files.js +8 -7
  71. package/dist/domain/manifest/files.js.map +1 -1
  72. package/dist/domain/manifest/lint.js +4 -3
  73. package/dist/domain/manifest/lint.js.map +1 -1
  74. package/dist/domain/manifest/workspace.schema.js +9 -0
  75. package/dist/domain/manifest/workspace.schema.js.map +1 -1
  76. package/dist/domain/release/ci.js +11 -8
  77. package/dist/domain/release/ci.js.map +1 -1
  78. package/dist/domain/release/deps.js +3 -1
  79. package/dist/domain/release/deps.js.map +1 -1
  80. package/dist/domain/release/errors.js +8 -8
  81. package/dist/domain/release/errors.js.map +1 -1
  82. package/dist/domain/release/merge.js +26 -13
  83. package/dist/domain/release/merge.js.map +1 -1
  84. package/dist/domain/release/policy.js +2 -0
  85. package/dist/domain/release/policy.js.map +1 -1
  86. package/dist/domain/release/types.js +2 -1
  87. package/dist/domain/release/types.js.map +1 -1
  88. package/dist/domain/release/version.js +133 -0
  89. package/dist/domain/release/version.js.map +1 -0
  90. package/dist/domain/release/window.js +1 -1
  91. package/dist/domain/release/window.js.map +1 -1
  92. package/dist/domain/setup/planner.js +7 -5
  93. package/dist/domain/setup/planner.js.map +1 -1
  94. package/dist/domain/workspace/commits.js +4 -1
  95. package/dist/domain/workspace/commits.js.map +1 -1
  96. package/dist/domain/workspace/go-work.js +75 -0
  97. package/dist/domain/workspace/go-work.js.map +1 -0
  98. package/dist/domain/workspace/ref.js +3 -0
  99. package/dist/domain/workspace/ref.js.map +1 -1
  100. package/package.json +4 -1
  101. package/skills/fx-workspace/SKILL.md +1 -1
  102. package/skills/fx-workspace/references/commands.md +33 -31
  103. package/skills/fx-workspace/references/workspace-tasks.md +6 -5
  104. package/dist/cli/commands/release.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,62 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## 0.11.0 (2026-10-12)
6
+
7
+ ### Added
8
+
9
+ - Keep a `go.work` at the workspace root when the manifest sets `go.workspace: true`.
10
+ `fx init` and `fx sync` list every selected checkout that has a `go.mod` at the
11
+ newest `go` version they require, so each module builds against its siblings'
12
+ checkouts. A repository not cloned yet is left out, fx replaces or removes only
13
+ the `go.work` it wrote, and `fx sync --only go` refreshes it alone. `go.work` and
14
+ `go.work.sum` no longer count as stray root entries in such a workspace.
15
+ - Bind a workspace to GitHub. The `github` code-host provider and the
16
+ `github-actions` ci provider both drive the GitHub CLI `gh`, which joins the
17
+ tool catalog with one sign-in per host for both. `fx release check` reads
18
+ branches, commits, files and the last workflow run of a branch from GitHub just
19
+ as it does from Bitbucket and Jenkins; a ci environment can narrow the runs it
20
+ counts to one branch and one trigger. Opening pull requests through fx is not
21
+ available on GitHub yet.
22
+
23
+ ### Changed
24
+
25
+ - Rename `fx release` to `fx release check`, so the name says it checks a release
26
+ rather than making one. `fx release` keeps working as a compatibility name.
27
+ - Say what each release would ship. `fx release check` opens each repository with
28
+ the version its target branch declares, the head commits of both branches and how
29
+ many commits on the source branch are not merged into the target yet, by how many
30
+ people; repositories whose checks all passed close the output one line each with the
31
+ same. JSON reports gain `repos[]` with these facts, summaries included; every
32
+ existing field is unchanged.
33
+ - Name the commit a CI build is judged against, say "not merged into main" and "not
34
+ merged back into dev" in the merge check, and use each platform's own name for the
35
+ job or project in CI and error messages. CI evidence gains `head`.
36
+ - Show a repository's `release.note` in the release report, as the manifest schema
37
+ describes.
38
+
39
+ ### Fixed
40
+
41
+ - Resolve every relative link in the source AGENTS.md that names a file of the
42
+ manifest source, such as `CONTRIBUTING.md`, in the guide derived at the
43
+ workspace root. Only links into `README.md`, `docs/` and `skills/` resolved
44
+ before, so the others pointed at files the root does not have.
45
+ - Stop `fx manifest lint` from asking for a code-host provider when every
46
+ repository names its `upstream`, since such a workspace reaches no code host.
47
+ - Pass every argument intact to a companion that Windows starts through a batch
48
+ file (`.cmd`, as npm, Scoop and Chocolatey install them). cmd.exe no longer
49
+ interprets characters such as `&`, `|`, `%` or `^` in an argument.
50
+ - Read a local Git remote given as a Windows path, such as `C:\host\fx\api.git`,
51
+ as project and name, so import, adoption, forks and `fx source push` work with it.
52
+ Manifest documents are named with forward slashes in stdin bundles and source
53
+ contributions on every platform, and import writes repository paths with them.
54
+ - Stop the PowerShell hook from writing task results into the agent's context,
55
+ passing fx its input with a byte order mark, and writing progress records to
56
+ stderr.
57
+ - Link source files with forward slashes in guides derived on Windows, and report
58
+ the bundled tools catalog in `fx self info` as a file path, also when the install
59
+ path contains spaces.
60
+
5
61
  ## 0.10.1 (2026-10-12)
6
62
 
7
63
  ### Added
package/README.md CHANGED
@@ -118,7 +118,7 @@ For everyday updates use `fx sync`; `fx status` shows repository state. To work
118
118
  | List capabilities | `fx can` |
119
119
  | Check a capability | `fx can <capability> --env <environment>` |
120
120
  | Refresh guides and Skills | `fx sync --only docs,skills` |
121
- | Inspect release checks | `fx release --format json` |
121
+ | Check what is ready to release | `fx release check` |
122
122
 
123
123
  Use `repo list`, `env list`, `agent list` and `tool list` to discover valid names.
124
124
  Use `repo select` and `env select` to choose an exact set, `--all` to follow the
@@ -126,7 +126,8 @@ team's full set including future additions, or `--none` to select nothing.
126
126
  `pick/unpick` add or remove individual names. First-time setup can limit cloning
127
127
  with `fx init ./work --from <manifest-repository> --repos example-api,example-web --dry-run`.
128
128
  `where` prints coordinates and commands; it does not execute those commands.
129
- `release` reports on your repositories; it does not publish packages or deploy them.
129
+ `release check` shows what each repository would release and whether it is ready; it
130
+ never publishes or deploys.
130
131
  The [CLI reference](docs/cli/README.md) and `fx <command> --help` own exact syntax.
131
132
 
132
133
  ## For coding agents
@@ -143,12 +144,10 @@ pending writes; inspect them before retrying.
143
144
  step fails. Read those results alongside stderr errors before retrying; the
144
145
  [user guide](docs/user-guide.md) describes the result and recovery fields.
145
146
 
146
- `init`, `sync`, `doctor` and `release` summarize JSON by default. Use `--details`
147
- for full results or `--fields` to select fields from those results in any output
148
- format. Explicit projection emits one report object in JSON/NDJSON and selected
149
- key/value pairs in human output. Keep stdout
150
- separate from diagnostic stderr. Do not repeat a write just to obtain a different
151
- view of its output.
147
+ `init`, `sync`, `doctor` and `release check` summarize JSON by default; the
148
+ [user guide](docs/user-guide.md#for-coding-agents) explains the full and projected
149
+ views. Keep stdout separate from diagnostic stderr. Do not repeat a write just to
150
+ obtain a different view of its output.
152
151
 
153
152
  For `init`, `sync` and `doctor`, read `preparation.state` and the scoped blockers.
154
153
  Incomplete preparation exits 12; continuation is `fx init --resume --workspace <name>`.
@@ -46,6 +46,15 @@ tools:
46
46
  desc: Issues, comments, transitions and worklogs in Jira
47
47
  install: [{ via: npm, spec: '@angelmsger/jira-cli' }]
48
48
  docs: https://github.com/AngelMsger/jira-cli
49
+ - name: gh
50
+ min_version: '2.50.0'
51
+ desc: Repositories, branches and Actions runs on GitHub
52
+ install:
53
+ - { via: brew, spec: gh }
54
+ - { via: manual }
55
+ skill: { command: [] }
56
+ auth: { command: [auth, status] }
57
+ docs: https://cli.github.com/
49
58
  - name: sentry
50
59
  min_version: '0.44.1'
51
60
  desc: Issues, events and releases in Sentry
@@ -0,0 +1,95 @@
1
+ // GitHub Actions through gh: a job is one workflow of one repository, narrowed to the
2
+ // runs of one branch and one trigger when the coordinate names them. One workflow often
3
+ // runs on pushes, pull requests and by hand, and only the coordinate knows which of
4
+ // those builds what ships. A workflow that is not there is an answer (undefined), a
5
+ // workflow that cannot be read is an error.
6
+ import { EXIT, objectOf, queryFailed, stringAt, unreadableError } from '../../family/cli.js';
7
+ import { BINARY, GhCli, qualifiedRepository } from '../../github/gh.js';
8
+ import { TemplatePlatform } from '../../platform/template-platform.js';
9
+ export const ID = 'github-actions';
10
+ const QUERY = { timeoutMs: 15_000 };
11
+ const RUNS = ['run', 'list', '--repo', '{repository}', '--workflow', '{workflow}'];
12
+ const FIELDS = 'number,status,conclusion,startedAt,createdAt,url';
13
+ // A run still on its way, by gh's status; a finished run says how it ended instead.
14
+ const PENDING = new Set(['queued', 'in_progress', 'waiting', 'requested', 'pending']);
15
+ const CONCLUSIONS = {
16
+ success: 'success',
17
+ failure: 'failure',
18
+ timed_out: 'failure',
19
+ startup_failure: 'failure',
20
+ cancelled: 'aborted',
21
+ skipped: 'not_built',
22
+ };
23
+ export class GitHubActionsCi {
24
+ process;
25
+ id = ID;
26
+ capability = 'ci';
27
+ platform = new TemplatePlatform({
28
+ id: ID,
29
+ capability: 'ci',
30
+ label: 'GitHub Actions workflow {workflow} in {repository}',
31
+ optionalFlags: { branch: '--branch', event: '--event' },
32
+ command: [BINARY, ...RUNS],
33
+ probe: { label: 'workflow {workflow} in {repository}', command: [BINARY, ...RUNS] },
34
+ });
35
+ constructor(process) {
36
+ this.process = process;
37
+ }
38
+ locate(vars) {
39
+ return this.platform.locate(qualified(vars));
40
+ }
41
+ probe(vars) {
42
+ return this.platform.probe(qualified(vars));
43
+ }
44
+ async lastBuild(vars) {
45
+ const { repository = '', workflow = '', branch, event } = qualified(vars);
46
+ const what = `the last run of ${workflow} in ${repository}${branch ? ` on ${branch}` : ''}`;
47
+ const call = await new GhCli(this.process).call([
48
+ 'run',
49
+ 'list',
50
+ '--repo',
51
+ repository,
52
+ '--workflow',
53
+ workflow,
54
+ ...(branch ? ['--branch', branch] : []),
55
+ ...(event ? ['--event', event] : []),
56
+ '--limit',
57
+ '1',
58
+ '--json',
59
+ FIELDS,
60
+ ], QUERY);
61
+ if (call.code === EXIT.not_found)
62
+ return undefined;
63
+ if (call.code !== 0 || !Array.isArray(call.data))
64
+ throw queryFailed(BINARY, call, `read ${what}`);
65
+ // A workflow that never ran on that branch has no build to judge.
66
+ if (call.data.length === 0)
67
+ return undefined;
68
+ const run = objectOf(call.data[0]);
69
+ if (run === undefined || typeof run['number'] !== 'number')
70
+ throw unreadableError(BINARY, what);
71
+ return {
72
+ number: run['number'],
73
+ status: statusOf(run),
74
+ startedAt: stringAt(run, 'startedAt') ?? stringAt(run, 'createdAt') ?? null,
75
+ url: stringAt(run, 'url') ?? null,
76
+ };
77
+ }
78
+ }
79
+ // Every command, run or printed, names the repository's host, so GH_HOST cannot send
80
+ // it elsewhere.
81
+ function qualified(vars) {
82
+ const { repository } = vars;
83
+ return repository === undefined
84
+ ? vars
85
+ : { ...vars, repository: qualifiedRepository(repository).repository };
86
+ }
87
+ function statusOf(run) {
88
+ const status = stringAt(run, 'status') ?? '';
89
+ if (PENDING.has(status))
90
+ return 'building';
91
+ if (status !== 'completed')
92
+ return 'unknown';
93
+ return CONCLUSIONS[stringAt(run, 'conclusion') ?? ''] ?? 'unknown';
94
+ }
95
+ //# sourceMappingURL=ci.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ci.js","sourceRoot":"","sources":["../../../../src/adapters/ci/github-actions/ci.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ // GitHub Actions through gh: one sign-in, shared with the github code host; each
2
+ // environment names the workflow, branch and trigger that build it.
3
+ import { BINARY, GH_RECIPES, qualifiedRepository } from '../../github/gh.js';
4
+ import { GitHubActionsCi, ID } from './ci.js';
5
+ export const githubActionsProvider = {
6
+ id: ID,
7
+ capability: 'ci',
8
+ tools: [BINARY],
9
+ recipes: GH_RECIPES,
10
+ vars: [
11
+ {
12
+ name: 'repository',
13
+ desc: '[HOST/]OWNER/NAME of the repository the workflows live in (github.com without a host), may use {repo}',
14
+ required: true,
15
+ },
16
+ ],
17
+ envVars: [
18
+ { name: 'workflow', desc: 'workflow file name or name, e.g. ci.yml', required: true },
19
+ {
20
+ name: 'branch',
21
+ desc: 'only runs of this branch; without it, the latest run on any branch',
22
+ required: false,
23
+ },
24
+ {
25
+ name: 'event',
26
+ desc: 'only runs this trigger started, e.g. push; without it, any trigger',
27
+ required: false,
28
+ },
29
+ ],
30
+ recipeVars: (vars) => ({ hostname: qualifiedRepository(vars['repository'] ?? '').hostname }),
31
+ create: (deps) => new GitHubActionsCi(deps.process),
32
+ };
33
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../../../../src/adapters/ci/github-actions/provider.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,oEAAoE;AAIpE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE7E,OAAO,EAAE,eAAe,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAE9C,MAAM,CAAC,MAAM,qBAAqB,GAA2B;IAC3D,EAAE,EAAE,EAAE;IACN,UAAU,EAAE,IAAI;IAChB,KAAK,EAAE,CAAC,MAAM,CAAC;IACf,OAAO,EAAE,UAAU;IACnB,IAAI,EAAE;QACJ;YACE,IAAI,EAAE,YAAY;YAClB,IAAI,EAAE,uGAAuG;YAC7G,QAAQ,EAAE,IAAI;SACf;KACF;IACD,OAAO,EAAE;QACP,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,yCAAyC,EAAE,QAAQ,EAAE,IAAI,EAAE;QACrF;YACE,IAAI,EAAE,QAAQ;YACd,IAAI,EAAE,oEAAoE;YAC1E,QAAQ,EAAE,KAAK;SAChB;QACD;YACE,IAAI,EAAE,OAAO;YACb,IAAI,EAAE,oEAAoE;YAC1E,QAAQ,EAAE,KAAK;SAChB;KACF;IACD,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,mBAAmB,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC5F,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC;CACpD,CAAC"}
@@ -0,0 +1,207 @@
1
+ // GitHub as the code host, through gh. Branches, ranges, commits and files come from
2
+ // the REST API by way of `gh api`, so gh holds the credential and fx never sees it.
3
+ // Pull requests are not wired yet: contribution workflows report this host as
4
+ // unsupported instead of half-working.
5
+ import { DevkitError } from '../../../domain/errors.js';
6
+ import { repoUrl } from '../../../domain/workspace/ref.js';
7
+ import { EXIT, objectOf, queryFailed, stringAt, unreadableError } from '../../family/cli.js';
8
+ import { BINARY, DEFAULT_HOSTNAME, GH_RECIPES, GhCli, jsonLines, } from '../../github/gh.js';
9
+ const QUERY = { timeoutMs: 15_000 };
10
+ // The fields of a commit the release check reads, so a commit's diff never crosses the pipe.
11
+ const COMMIT = '{sha, author: .commit.author.name, date: .commit.committer.date, message: .commit.message, parents: [.parents[].sha]}';
12
+ export class GitHubCodeHost {
13
+ vars;
14
+ supportsContributions = false;
15
+ gh;
16
+ constructor(deps, vars) {
17
+ this.vars = vars;
18
+ this.gh = new GhCli(deps.process);
19
+ }
20
+ async repositoryIdentity(_ref) {
21
+ throw contributionUnsupported();
22
+ }
23
+ async pullRequest(_input) {
24
+ throw contributionUnsupported();
25
+ }
26
+ async createPullRequest(_input) {
27
+ throw contributionUnsupported();
28
+ }
29
+ async whoami() {
30
+ const call = await this.api('user', ['--jq', '.login'], { timeoutMs: 5000 });
31
+ const user = call.stdout.trim();
32
+ if (call.code !== 0 || user === '')
33
+ throw queryFailed(BINARY, call, 'tell who is signed in');
34
+ return { user };
35
+ }
36
+ async canRead(ref) {
37
+ const call = await this.api(`repos/${slug(ref)}`, ['--jq', '.full_name'], { timeoutMs: 5000 });
38
+ if (call.code === 0)
39
+ return 'readable';
40
+ if (call.code === EXIT.permission)
41
+ return 'forbidden';
42
+ // GitHub answers 404 for a private repository the person cannot see, too.
43
+ if (call.code === EXIT.not_found)
44
+ return 'missing';
45
+ return 'unknown';
46
+ }
47
+ async fork(ref) {
48
+ const { user } = await this.whoami();
49
+ const url = this.forkUrl(user, ref);
50
+ const existing = await this.api(`repos/${user}/${ref.name}`, ['--jq', '{fork, parent: .parent.full_name, source: .source.full_name}'], QUERY);
51
+ if (existing.code === 0) {
52
+ // Being a fork does not say of which repository: only one forked from this
53
+ // upstream, or from a fork whose network starts at it, can carry its pull requests.
54
+ const found = objectOf(existing.data) ?? {};
55
+ const of = [stringAt(found, 'parent'), stringAt(found, 'source')];
56
+ if (of.some((name) => name?.toLowerCase() === slug(ref).toLowerCase()))
57
+ return { url, created: false };
58
+ // A repository of that name that is not this fork would make gh pick another name.
59
+ throw forkFailed(ref, found['fork'] === true
60
+ ? `${user}/${ref.name} is a fork of ${of[0] ?? 'another repository'}`
61
+ : `${user}/${ref.name} exists and is not a fork`);
62
+ }
63
+ const call = await this.gh.call(['repo', 'fork', `${this.hostname}/${slug(ref)}`, '--clone=false', '--remote=false'], QUERY);
64
+ if (call.code !== 0)
65
+ throw forkFailed(ref, call.error?.message ?? `exit ${call.code}`);
66
+ return { url, created: true };
67
+ }
68
+ async branches(ref) {
69
+ const [list, repo] = await Promise.all([
70
+ this.api(`repos/${slug(ref)}/branches?per_page=100`, [
71
+ '--paginate',
72
+ '--jq',
73
+ '.[] | {name, sha: .commit.sha}',
74
+ ]),
75
+ this.api(`repos/${slug(ref)}`, ['--jq', '.default_branch']),
76
+ ]);
77
+ if (list.code !== 0)
78
+ throw queryFailed(BINARY, list, `list the branches of ${slug(ref)}`);
79
+ if (repo.code !== 0)
80
+ throw queryFailed(BINARY, repo, `read the default branch of ${slug(ref)}`);
81
+ const defaultBranch = repo.stdout.trim();
82
+ return records(list, `the branches of ${slug(ref)}`).map((item) => {
83
+ const name = stringAt(item, 'name');
84
+ const sha = stringAt(item, 'sha');
85
+ if (name === undefined || sha === undefined)
86
+ throw unreadableError(BINARY, `a branch of ${slug(ref)}`);
87
+ return { name, sha, default: name === defaultBranch };
88
+ });
89
+ }
90
+ // Every page, checked against the total GitHub states: a list shorter than the range
91
+ // would read as branches that agree.
92
+ async compare(ref, range) {
93
+ const what = `${range.from}..${range.to} of ${slug(ref)}`;
94
+ const call = await this.api(`repos/${slug(ref)}/compare/${refPath(range.from)}...${refPath(range.to)}?per_page=100`, ['--paginate', '--jq', `{total: .total_commits, commits: [.commits[] | ${COMMIT}]}`]);
95
+ if (call.code !== 0)
96
+ throw queryFailed(BINARY, call, `compare ${what}`);
97
+ const pages = records(call, what);
98
+ const commits = pages.flatMap((page) => Array.isArray(page['commits']) ? page['commits'].map(objectOf) : [undefined]);
99
+ const total = pages[0]?.['total'];
100
+ if (typeof total !== 'number' || commits.length !== total)
101
+ throw unreadableError(BINARY, `an incomplete list of the commits in ${what}`);
102
+ return commits.map((item) => {
103
+ const commit = item === undefined ? undefined : commitOf(item);
104
+ if (commit === undefined)
105
+ throw unreadableError(BINARY, `a commit of ${what}`);
106
+ return commit;
107
+ });
108
+ }
109
+ async commit(ref, sha) {
110
+ const call = await this.api(`repos/${slug(ref)}/commits/${refPath(sha)}`, ['--jq', COMMIT]);
111
+ const commit = call.code === 0 ? commitOf(objectOf(call.data) ?? {}) : undefined;
112
+ if (commit === undefined)
113
+ throw queryFailed(BINARY, call, `read commit ${sha} of ${slug(ref)}`);
114
+ return commit;
115
+ }
116
+ async readFile(ref, path, at) {
117
+ const call = await this.api(`repos/${slug(ref)}/contents/${refPath(path)}?ref=${encodeURIComponent(at)}`, ['-H', 'Accept: application/vnd.github.raw']);
118
+ if (call.code === 0)
119
+ return call.stdout;
120
+ if (call.code === EXIT.not_found)
121
+ return undefined;
122
+ throw queryFailed(BINARY, call, `read ${path} at ${at} of ${slug(ref)}`);
123
+ }
124
+ upstreamUrl(ref) {
125
+ return repoUrl(this.host, ref.project, ref.name);
126
+ }
127
+ forkUrl(user, ref) {
128
+ return repoUrl(this.host, user, ref.name);
129
+ }
130
+ webUrl(ref) {
131
+ return `https://${this.hostname}/${slug(ref)}`;
132
+ }
133
+ // gh signs in per host and would otherwise pick one from GH_HOST or its configuration,
134
+ // so every call names the host the binding names, github.com included.
135
+ api(path, args, options = QUERY) {
136
+ return this.gh.call(['api', '--hostname', this.hostname, ...args, path], options);
137
+ }
138
+ get host() {
139
+ return this.vars['host'] ?? '';
140
+ }
141
+ get hostname() {
142
+ return hostnameOf(this.host);
143
+ }
144
+ }
145
+ export const githubProvider = {
146
+ id: 'github',
147
+ capability: 'code-host',
148
+ tools: [BINARY],
149
+ recipes: GH_RECIPES,
150
+ vars: [
151
+ {
152
+ name: 'host',
153
+ desc: 'git clone prefix, e.g. https://github.com, or an ssh:// prefix for SSH clones',
154
+ required: true,
155
+ },
156
+ ],
157
+ recipeVars: (vars) => ({ hostname: hostnameOf(vars['host'] ?? '') }),
158
+ create: (deps, vars) => new GitHubCodeHost(deps, vars),
159
+ };
160
+ // An https:// or an ssh:// clone prefix names its host; anything else means github.com.
161
+ function hostnameOf(host) {
162
+ try {
163
+ return new URL(host).hostname || DEFAULT_HOSTNAME;
164
+ }
165
+ catch {
166
+ return DEFAULT_HOSTNAME;
167
+ }
168
+ }
169
+ function records(call, what) {
170
+ const rows = jsonLines(call.stdout)?.map(objectOf);
171
+ if (rows === undefined || rows.some((row) => row === undefined))
172
+ throw unreadableError(BINARY, what);
173
+ return rows;
174
+ }
175
+ function slug(ref) {
176
+ return `${ref.project}/${ref.name}`;
177
+ }
178
+ // A branch or path keeps its slashes; anything else that means something in a URL is escaped.
179
+ function refPath(value) {
180
+ return value.split('/').map(encodeURIComponent).join('/');
181
+ }
182
+ function commitOf(item) {
183
+ const sha = stringAt(item, 'sha');
184
+ const date = stringAt(item, 'date');
185
+ if (sha === undefined || date === undefined)
186
+ return undefined;
187
+ const parents = Array.isArray(item['parents'])
188
+ ? item['parents'].filter((p) => typeof p === 'string')
189
+ : [];
190
+ return {
191
+ sha,
192
+ author: stringAt(item, 'author') ?? '',
193
+ date,
194
+ message: stringAt(item, 'message') ?? '',
195
+ parents,
196
+ };
197
+ }
198
+ function forkFailed(ref, reason) {
199
+ return new DevkitError('permission', 'FORK_FAILED', `${BINARY} could not fork ${slug(ref)}: ${reason}`, {
200
+ hint: 'Forking needs read access to the repository and a free name in your account.',
201
+ nextSteps: [`${BINARY} repo view ${slug(ref)}`, 'fx repo list --unreadable'],
202
+ });
203
+ }
204
+ function contributionUnsupported() {
205
+ return new DevkitError('config', 'SOURCE_CONTRIBUTION_UNSUPPORTED', 'The github code host does not open pull requests through fx yet', { nextSteps: ['Open the pull request with gh pr create.'] });
206
+ }
207
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../../../../src/adapters/code-host/github/provider.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,oFAAoF;AACpF,8EAA8E;AAC9E,uCAAuC;AAQvC,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACxD,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAW3D,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC7F,OAAO,EACL,MAAM,EACN,gBAAgB,EAChB,UAAU,EACV,KAAK,EACL,SAAS,GAGV,MAAM,oBAAoB,CAAC;AAE5B,MAAM,KAAK,GAAG,EAAE,SAAS,EAAE,MAAM,EAAW,CAAC;AAC7C,6FAA6F;AAC7F,MAAM,MAAM,GACV,uHAAuH,CAAC;AAE1H,MAAM,OAAO,cAAc;IAMN;IALV,qBAAqB,GAAG,KAAK,CAAC;IACtB,EAAE,CAAQ;IAE3B,YACE,IAAiB,EACA,IAAU;QAAV,SAAI,GAAJ,IAAI,CAAM;QAE3B,IAAI,CAAC,EAAE,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACpC,CAAC;IAED,KAAK,CAAC,kBAAkB,CAAC,IAAa;QACpC,MAAM,uBAAuB,EAAE,CAAC;IAClC,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,MAAyB;QACzC,MAAM,uBAAuB,EAAE,CAAC;IAClC,CAAC;IAED,KAAK,CAAC,iBAAiB,CAAC,MAAyB;QAC/C,MAAM,uBAAuB,EAAE,CAAC;IAClC,CAAC;IAED,KAAK,CAAC,MAAM;QACV,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7E,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAChC,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,EAAE;YAAE,MAAM,WAAW,CAAC,MAAM,EAAE,IAAI,EAAE,uBAAuB,CAAC,CAAC;QAC7F,OAAO,EAAE,IAAI,EAAE,CAAC;IAClB,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,GAAY;QACxB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,MAAM,EAAE,YAAY,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/F,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO,UAAU,CAAC;QACvC,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,UAAU;YAAE,OAAO,WAAW,CAAC;QACtD,0EAA0E;QAC1E,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,SAAS;YAAE,OAAO,SAAS,CAAC;QACnD,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,GAAY;QACrB,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;QACrC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QACpC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,GAAG,CAC7B,SAAS,IAAI,IAAI,GAAG,CAAC,IAAI,EAAE,EAC3B,CAAC,MAAM,EAAE,8DAA8D,CAAC,EACxE,KAAK,CACN,CAAC;QACF,IAAI,QAAQ,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YACxB,2EAA2E;YAC3E,oFAAoF;YACpF,MAAM,KAAK,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YAC5C,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC;YAClE,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,IAAI,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;gBACpE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;YACjC,mFAAmF;YACnF,MAAM,UAAU,CACd,GAAG,EACH,KAAK,CAAC,MAAM,CAAC,KAAK,IAAI;gBACpB,CAAC,CAAC,GAAG,IAAI,IAAI,GAAG,CAAC,IAAI,iBAAiB,EAAE,CAAC,CAAC,CAAC,IAAI,oBAAoB,EAAE;gBACrE,CAAC,CAAC,GAAG,IAAI,IAAI,GAAG,CAAC,IAAI,2BAA2B,CACnD,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,IAAI,CAC7B,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,eAAe,EAAE,gBAAgB,CAAC,EACpF,KAAK,CACN,CAAC;QACF,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC;YAAE,MAAM,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,EAAE,OAAO,IAAI,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QACvF,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAChC,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,GAAY;QACzB,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YACrC,IAAI,CAAC,GAAG,CAAC,SAAS,IAAI,CAAC,GAAG,CAAC,wBAAwB,EAAE;gBACnD,YAAY;gBACZ,MAAM;gBACN,gCAAgC;aACjC,CAAC;YACF,IAAI,CAAC,GAAG,CAAC,SAAS,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;SAC5D,CAAC,CAAC;QACH,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC;YAAE,MAAM,WAAW,CAAC,MAAM,EAAE,IAAI,EAAE,wBAAwB,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC1F,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC;YAAE,MAAM,WAAW,CAAC,MAAM,EAAE,IAAI,EAAE,8BAA8B,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAChG,MAAM,aAAa,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACzC,OAAO,OAAO,CAAC,IAAI,EAAE,mBAAmB,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;YAChE,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YACpC,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YAClC,IAAI,IAAI,KAAK,SAAS,IAAI,GAAG,KAAK,SAAS;gBACzC,MAAM,eAAe,CAAC,MAAM,EAAE,eAAe,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC5D,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,KAAK,aAAa,EAAE,CAAC;QACxD,CAAC,CAAC,CAAC;IACL,CAAC;IAED,qFAAqF;IACrF,qCAAqC;IACrC,KAAK,CAAC,OAAO,CAAC,GAAY,EAAE,KAAkB;QAC5C,MAAM,IAAI,GAAG,GAAG,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC1D,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CACzB,SAAS,IAAI,CAAC,GAAG,CAAC,YAAY,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,eAAe,EACvF,CAAC,YAAY,EAAE,MAAM,EAAE,kDAAkD,MAAM,IAAI,CAAC,CACrF,CAAC;QACF,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC;YAAE,MAAM,WAAW,CAAC,MAAM,EAAE,IAAI,EAAE,WAAW,IAAI,EAAE,CAAC,CAAC;QACxE,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAClC,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CACrC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAC7E,CAAC;QACF,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC;QAClC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK;YACvD,MAAM,eAAe,CAAC,MAAM,EAAE,wCAAwC,IAAI,EAAE,CAAC,CAAC;QAChF,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;YAC1B,MAAM,MAAM,GAAG,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;YAC/D,IAAI,MAAM,KAAK,SAAS;gBAAE,MAAM,eAAe,CAAC,MAAM,EAAE,eAAe,IAAI,EAAE,CAAC,CAAC;YAC/E,OAAO,MAAM,CAAC;QAChB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAY,EAAE,GAAW;QACpC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,IAAI,CAAC,GAAG,CAAC,YAAY,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;QAC5F,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACjF,IAAI,MAAM,KAAK,SAAS;YAAE,MAAM,WAAW,CAAC,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAChG,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,GAAY,EAAE,IAAY,EAAE,EAAU;QACnD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CACzB,SAAS,IAAI,CAAC,GAAG,CAAC,aAAa,OAAO,CAAC,IAAI,CAAC,QAAQ,kBAAkB,CAAC,EAAE,CAAC,EAAE,EAC5E,CAAC,IAAI,EAAE,oCAAoC,CAAC,CAC7C,CAAC;QACF,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC,MAAM,CAAC;QACxC,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,SAAS;YAAE,OAAO,SAAS,CAAC;QACnD,MAAM,WAAW,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ,IAAI,OAAO,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC3E,CAAC;IAED,WAAW,CAAC,GAAY;QACtB,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;IACnD,CAAC;IAED,OAAO,CAAC,IAAY,EAAE,GAAY;QAChC,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;IAC5C,CAAC;IAED,MAAM,CAAC,GAAY;QACjB,OAAO,WAAW,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IACjD,CAAC;IAED,uFAAuF;IACvF,uEAAuE;IAC/D,GAAG,CACT,IAAY,EACZ,IAAuB,EACvB,UAAyB,KAAK;QAE9B,OAAO,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,CAAC,QAAQ,EAAE,GAAG,IAAI,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;IACpF,CAAC;IAED,IAAY,IAAI;QACd,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IACjC,CAAC;IAED,IAAY,QAAQ;QAClB,OAAO,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;CACF;AAED,MAAM,CAAC,MAAM,cAAc,GAAiC;IAC1D,EAAE,EAAE,QAAQ;IACZ,UAAU,EAAE,WAAW;IACvB,KAAK,EAAE,CAAC,MAAM,CAAC;IACf,OAAO,EAAE,UAAU;IACnB,IAAI,EAAE;QACJ;YACE,IAAI,EAAE,MAAM;YACZ,IAAI,EAAE,+EAA+E;YACrF,QAAQ,EAAE,IAAI;SACf;KACF;IACD,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;IACpE,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC;CACvD,CAAC;AAEF,wFAAwF;AACxF,SAAS,UAAU,CAAC,IAAY;IAC9B,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,IAAI,gBAAgB,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,gBAAgB,CAAC;IAC1B,CAAC;AACH,CAAC;AAED,SAAS,OAAO,CAAC,IAAa,EAAE,IAAY;IAC1C,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC;IACnD,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,SAAS,CAAC;QAC7D,MAAM,eAAe,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACtC,OAAO,IAAiC,CAAC;AAC3C,CAAC;AAED,SAAS,IAAI,CAAC,GAAY;IACxB,OAAO,GAAG,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;AACtC,CAAC;AAED,8FAA8F;AAC9F,SAAS,OAAO,CAAC,KAAa;IAC5B,OAAO,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC5D,CAAC;AAED,SAAS,QAAQ,CAAC,IAA6B;IAC7C,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAClC,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACpC,IAAI,GAAG,KAAK,SAAS,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC9D,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC5C,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC;QACnE,CAAC,CAAC,EAAE,CAAC;IACP,OAAO;QACL,GAAG;QACH,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE;QACtC,IAAI;QACJ,OAAO,EAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE;QACxC,OAAO;KACR,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,GAAY,EAAE,MAAc;IAC9C,OAAO,IAAI,WAAW,CACpB,YAAY,EACZ,aAAa,EACb,GAAG,MAAM,mBAAmB,IAAI,CAAC,GAAG,CAAC,KAAK,MAAM,EAAE,EAClD;QACE,IAAI,EAAE,8EAA8E;QACpF,SAAS,EAAE,CAAC,GAAG,MAAM,cAAc,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,2BAA2B,CAAC;KAC7E,CACF,CAAC;AACJ,CAAC;AAED,SAAS,uBAAuB;IAC9B,OAAO,IAAI,WAAW,CACpB,QAAQ,EACR,iCAAiC,EACjC,iEAAiE,EACjE,EAAE,SAAS,EAAE,CAAC,0CAA0C,CAAC,EAAE,CAC5D,CAAC;AACJ,CAAC"}
@@ -0,0 +1,103 @@
1
+ // gh, the GitHub CLI, serves two providers: the github code host and GitHub Actions
2
+ // ci. This is the one place that runs it. gh reports a failed request as an HTTP
3
+ // status on stderr, so a failure is translated into the family's exit categories and
4
+ // classified by the same `queryFailed` every companion's failures go through.
5
+ import { stderrReason } from '../../domain/toolchain/stderr.js';
6
+ import { EXIT, toolMissing } from '../family/cli.js';
7
+ export const BINARY = 'gh';
8
+ export const DEFAULT_HOSTNAME = 'github.com';
9
+ // gh signs in per host, so the step is per host too: a binding names its host through
10
+ // `hostname` (see `recipeVars`), the same host from both providers is planned once, and
11
+ // two hosts are two steps. Only the active account of that host is checked, the one gh
12
+ // will use.
13
+ export const GH_RECIPES = [
14
+ {
15
+ id: 'login-{hostname}',
16
+ tool: BINARY,
17
+ perEnv: false,
18
+ kind: 'run',
19
+ phase: 'authenticate',
20
+ title: 'Log in to GitHub at {hostname}',
21
+ instructions: 'Signs gh in through the browser or with a token. fx reads repositories and Actions runs with that sign-in; private repositories need the repo scope gh asks for by default.',
22
+ run: ['auth', 'login', '--hostname', '{hostname}'],
23
+ verify: { command: ['auth', 'status', '--active', '--hostname', '{hostname}'] },
24
+ },
25
+ ];
26
+ // gh's own [HOST/]OWNER/NAME. Left without a host, gh would pick one from GH_HOST or
27
+ // its configuration, so a bare OWNER/NAME is spelled out as github.com's.
28
+ export function qualifiedRepository(repository) {
29
+ const parts = repository.split('/');
30
+ if (parts.length > 2)
31
+ return { hostname: parts[0] ?? DEFAULT_HOSTNAME, repository };
32
+ return { hostname: DEFAULT_HOSTNAME, repository: `${DEFAULT_HOSTNAME}/${repository}` };
33
+ }
34
+ export class GhCli {
35
+ process;
36
+ constructor(process) {
37
+ this.process = process;
38
+ }
39
+ async call(args, options = {}) {
40
+ const result = await this.process.run([BINARY, ...args], {
41
+ timeoutMs: options.timeoutMs,
42
+ diagnostic: { phase: `${BINARY} ${args.slice(0, 2).join(' ')}`.trim() },
43
+ });
44
+ if (result.notStarted === true || result.code === EXIT.missing_binary)
45
+ throw toolMissing(BINARY);
46
+ if (result.code === 0)
47
+ return { code: 0, stdout: result.stdout, data: parse(result.stdout), error: undefined };
48
+ const status = httpStatus(result.stderr);
49
+ const message = stderrReason(result.stderr, result.code).replace(/^gh: /, '');
50
+ return {
51
+ code: exitOf(result.code, status, message),
52
+ stdout: result.stdout,
53
+ data: undefined,
54
+ error: { message, ...(status === undefined ? {} : { http_status: status }) },
55
+ };
56
+ }
57
+ }
58
+ // `--paginate --jq` prints one JSON value per line. A line that does not parse makes
59
+ // the whole answer unreadable rather than shorter.
60
+ export function jsonLines(text) {
61
+ const values = [];
62
+ for (const line of text.split('\n')) {
63
+ if (line.trim() === '')
64
+ continue;
65
+ const value = parse(line);
66
+ if (value === undefined)
67
+ return undefined;
68
+ values.push(value);
69
+ }
70
+ return values;
71
+ }
72
+ // `gh api` ends its message with "(HTTP 404)"; other commands open it with "HTTP 404:".
73
+ function httpStatus(stderr) {
74
+ const match = /\(HTTP (\d{3})\)|^HTTP (\d{3}):/m.exec(stderr);
75
+ const status = match?.[1] ?? match?.[2];
76
+ return status === undefined ? undefined : Number(status);
77
+ }
78
+ // gh's own exit 4 means it holds no credential at all. Any other failure is a plain
79
+ // failure: gh's remaining codes (cancelled, pending checks) would read as categories.
80
+ function exitOf(code, status, message) {
81
+ if (code === 4 || status === 401)
82
+ return EXIT.auth;
83
+ if (status === 429 || (status === 403 && /rate limit/i.test(message)))
84
+ return EXIT.rate_limit;
85
+ if (status === 403)
86
+ return EXIT.permission;
87
+ if (status === 404)
88
+ return EXIT.not_found;
89
+ if (status !== undefined && status >= 500)
90
+ return EXIT.server;
91
+ if (/error connecting to/i.test(message))
92
+ return EXIT.network;
93
+ return 1;
94
+ }
95
+ function parse(text) {
96
+ try {
97
+ return JSON.parse(text);
98
+ }
99
+ catch {
100
+ return undefined;
101
+ }
102
+ }
103
+ //# sourceMappingURL=gh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gh.js","sourceRoot":"","sources":["../../../src/adapters/github/gh.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ // Start an executable that findExecutable resolved. A batch file runs under cmd.exe,
2
+ // which reads the command line first. The batch then reads each argument itself
3
+ // (`%~1`, as mvn.cmd does) or hands them all on (`%*`, as the shims npm, Scoop and
4
+ // Chocolatey install do), and cmd reads that line again. Caret escaping throughout,
5
+ // as cross-spawn applies it, suits only one of those readers, so fx starts every batch
6
+ // file itself and leaves everything else to cross-spawn.
7
+ import { spawn as spawnNode } from 'node:child_process';
8
+ import path from 'node:path';
9
+ import spawn from 'cross-spawn';
10
+ const BATCH = /\.(?:bat|cmd)$/i;
11
+ // What cmd acts on outside quotes.
12
+ const SPECIAL = /[&|<>^()!]/;
13
+ export function launch(executable, args, options) {
14
+ if (process.platform !== 'win32' || !BATCH.test(executable))
15
+ return spawn(executable, [...args], options);
16
+ // /v:off keeps !name! literal whatever delayed expansion the registry turns on.
17
+ return spawnNode(process.env['comspec'] ?? 'cmd.exe', ['/d', '/v:off', '/s', '/c', `"${batchLine([path.normalize(executable), ...args])}"`], { ...options, windowsVerbatimArguments: true });
18
+ }
19
+ // Every argument is quoted for the program at the end of the chain, and cmd leaves
20
+ // whatever sits inside quotes alone, so an argument without a quote of its own reaches
21
+ // `%~1` unchanged. A quote inside an argument turns cmd's quoting off until the next
22
+ // one; there, cmd's special characters are escaped for both of its reads. Each percent
23
+ // sign is followed by an empty expansion, so no %name% survives to expand.
24
+ function batchLine(vector) {
25
+ let line = '';
26
+ let quoted = false;
27
+ for (const char of vector.map(programArgument).join(' ')) {
28
+ if (char === '"')
29
+ quoted = !quoted;
30
+ if (char === '%')
31
+ line += '%%cd:~,%';
32
+ else if (!quoted && SPECIAL.test(char))
33
+ line += '^^^' + char;
34
+ else
35
+ line += char;
36
+ }
37
+ return line;
38
+ }
39
+ // The program's own reading of its command line: backslashes are literal except in
40
+ // a run before a quote, so such a run is doubled, before an inner quote and before
41
+ // the closing one.
42
+ function programArgument(arg) {
43
+ let text = '"';
44
+ let slashes = 0;
45
+ for (const char of arg) {
46
+ if (char === '\\') {
47
+ slashes += 1;
48
+ continue;
49
+ }
50
+ text += '\\'.repeat(char === '"' ? slashes * 2 + 1 : slashes) + char;
51
+ slashes = 0;
52
+ }
53
+ return text + '\\'.repeat(slashes * 2) + '"';
54
+ }
55
+ //# sourceMappingURL=launch.js.map