@rathnasgala/cli 0.0.22 → 1.1.4

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 (69) hide show
  1. package/README.md +100 -187
  2. package/package.json +3 -3
  3. package/src/api/gala.js +163 -0
  4. package/src/api/github.js +63 -0
  5. package/src/api/http.js +72 -0
  6. package/src/auth/gala.js +52 -0
  7. package/src/auth/github.js +78 -0
  8. package/src/auth/store.js +88 -0
  9. package/src/cli/args.js +56 -0
  10. package/src/cli/terminal.js +104 -0
  11. package/src/commands/auth.js +20 -0
  12. package/src/commands/doctor.js +98 -0
  13. package/src/commands/domain.js +124 -0
  14. package/src/commands/init.js +276 -0
  15. package/src/commands/new.js +76 -0
  16. package/src/commands/preview.js +92 -0
  17. package/src/commands/prism.js +360 -0
  18. package/src/commands/publish.js +57 -0
  19. package/src/commands/upgrade.js +173 -0
  20. package/src/commands-manifest.js +80 -0
  21. package/src/content.js +31 -0
  22. package/src/domain.js +33 -0
  23. package/src/git.js +160 -0
  24. package/src/index.js +44 -294
  25. package/src/publication.js +39 -0
  26. package/src/assign-content-ids.js +0 -1
  27. package/src/auth-command.js +0 -36
  28. package/src/configure-site.js +0 -102
  29. package/src/content-files.js +0 -1
  30. package/src/doctor-command.js +0 -214
  31. package/src/entitlement-client.js +0 -26
  32. package/src/entitlement-command.js +0 -74
  33. package/src/evaluation-date.js +0 -1
  34. package/src/gala-credential-health.js +0 -34
  35. package/src/gala-credential-store.js +0 -115
  36. package/src/gala-device-flow.js +0 -121
  37. package/src/git-credentials.js +0 -37
  38. package/src/github-auth-command.js +0 -50
  39. package/src/github-credential-store.js +0 -104
  40. package/src/github-device-flow.js +0 -153
  41. package/src/github-empty-repository.js +0 -89
  42. package/src/github-identity.js +0 -32
  43. package/src/github-pages-provisioning.js +0 -107
  44. package/src/github-repository-secret.js +0 -82
  45. package/src/github-repository-variable.js +0 -56
  46. package/src/github-template-repository.js +0 -171
  47. package/src/hook-command.js +0 -64
  48. package/src/http-failure.js +0 -55
  49. package/src/new-command.js +0 -54
  50. package/src/open-browser.js +0 -40
  51. package/src/preview-command.js +0 -60
  52. package/src/publication-creation-client.js +0 -155
  53. package/src/publication-state.js +0 -7
  54. package/src/publish-command.js +0 -37
  55. package/src/record-deployment-command.js +0 -147
  56. package/src/refresh-command.js +0 -104
  57. package/src/repository-limits.js +0 -94
  58. package/src/scaffold-git.js +0 -76
  59. package/src/scaffold-options.js +0 -58
  60. package/src/scaffold-preflight.js +0 -146
  61. package/src/scaffold-site.js +0 -185
  62. package/src/site-config-registration.js +0 -47
  63. package/src/site-registration-client.js +0 -138
  64. package/src/theme-package.js +0 -128
  65. package/src/topology-client.js +0 -43
  66. package/src/topology-command.js +0 -70
  67. package/src/upgrade-command.js +0 -81
  68. package/src/validate-command.js +0 -5
  69. package/src/workflow-command.js +0 -87
package/README.md CHANGED
@@ -1,267 +1,180 @@
1
1
  # Gala CLI
2
2
 
3
- Create, validate, preview, publish, and maintain a GitHub-backed Gala publication from your terminal.
4
-
5
- The quick start below begins with the required accounts and tools and does not assume a global CLI installation.
3
+ Create a publication, write posts, preview them, and publish from your terminal.
6
4
 
7
5
  ## Requirements
8
6
 
9
7
  - [Git](https://git-scm.com/downloads)
10
- - [Node.js 24](https://nodejs.org/en/download) recommended; the CLI package supports Node.js 18 or newer
8
+ - [Node.js 20](https://nodejs.org/en/download) or newer
11
9
  - A [GitHub account](https://github.com/signup)
12
- - The [Gala GitHub App](https://github.com/apps/gala67-app/installations/new) — `scaffold` walks you through installing it if it is not already
13
-
14
- Check your local tools:
15
10
 
16
- ```console
17
- node --version
18
- npm --version
19
- git --version
20
- ```
21
-
22
- You should see something like this:
23
- ```console
24
- v22.18.0
25
- 10.9.3
26
- git version 2.50.1 (Apple Git-155)
27
- ```
11
+ Nothing to install. Every command runs through `npx`.
28
12
 
29
- ## Quick start
13
+ ## Start a publication
30
14
 
31
- One command, run inside an empty folder named after the publication you want:
15
+ Create it in the current empty folder:
32
16
 
33
17
  ```console
34
18
  mkdir field-notes && cd field-notes
35
- npx --yes @rathnasgala/cli@latest scaffold --target ./
19
+ npx --yes @rathnasgala/cli@latest init
36
20
  ```
37
21
 
38
- That single command does all of the following, and asks only for what it cannot work out:
39
-
40
- 1. **Signs you in to Gala** if no valid token is stored, showing a code to enter in the browser.
41
- 2. **Signs you in to GitHub** the same way, as the Gala GitHub App. It requests no scopes: a GitHub
42
- App's permissions are fixed on the app and granted when you install it, so Gala reaches only the
43
- repositories you have shared with it — never every repository you can access.
44
- 3. **Reads your GitHub account** from that token, so there is no username to type.
45
- 4. **Finds the Gala GitHub App installation** for your account. If the App is not installed yet it
46
- prints the installation page, waits while you install it, and carries on — the installation ID
47
- is never something you have to read out of a URL.
48
- 5. **Names the publication** after the folder you are standing in.
49
- 6. **Creates the repository** from the site template, registers it, installs its one-time secret,
50
- writes the publication workflow, commits, and enables GitHub Pages.
51
-
52
- Both sign-ins are skipped when a valid credential is already stored, so re-running is cheap.
53
-
54
- After scaffolding succeeds, open [GitHub App settings](https://github.com/settings/installations)
55
- and restrict the App to the publication repository if you installed it against all of them.
56
-
57
- ### Write, preview, and publish
22
+ Or name a new destination directly:
58
23
 
59
24
  ```console
60
- npx --yes @rathnasgala/cli@latest new --title "My first post" --language en
61
- npx --yes @rathnasgala/cli@latest preview
62
- npx --yes @rathnasgala/cli@latest publish
25
+ npx --yes @rathnasgala/cli@latest init field-notes
63
26
  ```
64
27
 
65
- `new` prints the Markdown file it created. Write below the second `---` line, save the file,
66
- preview it locally, then publish it through GitHub.
28
+ The destination must be empty. An initialized Git repository with no commits and no files is also
29
+ accepted and its `.git` directory is preserved. To reserve a custom domain during setup, add
30
+ `--domain blog.example.com`; Gala still requires GitHub ownership verification and healthy DNS
31
+ before activating it.
67
32
 
68
- ### Overriding what scaffold works out
33
+ It signs you in to Gala and to GitHub if you are not already, creates the repository, registers the
34
+ publication, and leaves a working checkout in the folder. When it finishes it prints the address
35
+ your publication will live at.
69
36
 
70
- Every derived value is still an explicit flag, for the cases where the default is wrong a
71
- publication owned by an organisation, a folder named differently from the repository, or more than
72
- one App installation on the account:
37
+ If the Gala GitHub App has not been given access to the new repository, it says so and links to the
38
+ one page that grants it GitHub has no way for an app to grant itself access, so that click is
39
+ unavoidable. Everything else is automatic.
73
40
 
74
- ```console
75
- npx --yes @rathnasgala/cli@latest scaffold \
76
- --owner YOUR_GITHUB_USERNAME \
77
- --repository YOUR_REPOSITORY_NAME \
78
- --target ./YOUR_REPOSITORY_NAME \
79
- --installation-id YOUR_INSTALLATION_ID
80
- ```
81
-
82
- `--repository` is otherwise taken from `--target`, then from `--site-name`, and only then asked
83
- for. Outside a terminal — in CI — nothing is ever prompted for: a value that cannot be derived is
84
- an error, so an automated run fails fast instead of waiting for an answer that will not come.
85
-
86
- ## Command reference
87
-
88
- Run commands through `npx` without installing a global package:
41
+ ## Write
89
42
 
90
43
  ```console
91
- npx --yes @rathnasgala/cli@latest COMMAND [options]
44
+ npx --yes @rathnasgala/cli@latest new "The places we return to"
92
45
  ```
93
46
 
94
- Inside the table below, `gala` is shorthand for that prefix.
95
-
96
- | Command | Purpose | Common options |
97
- | --- | --- | --- |
98
- | `gala auth` | Authenticate the author with Gala | `--api-base-url URL` for a non-production API |
99
- | `gala auth github` | Authenticate the CLI with GitHub | Browser device flow; requests `repo workflow` |
100
- | `gala scaffold` | Sign in if needed, then create and register a publication | All derived; override with `--owner`, `--repository`, `--target`, `--installation-id` |
101
- | `gala configure` | Update author-owned site and design settings | `--root`, plus the configuration options below |
102
- | `gala new` | Create a Markdown post variant | `--root`, `--title`, `--language`, `--today` |
103
- | `gala validate` | Validate repository content without publishing | optional root path, `--today` |
104
- | `gala preview` | Validate and run the local Eleventy preview | `--root`, `--today` |
105
- | `gala publish` | Validate, commit, and push publication changes | `--root`, `--today`, `--force` |
106
- | `gala doctor` | Report managed-framework drift and publication-state validity | optional root path; `--fix --source TRUSTED_ROOT` |
107
- | `gala hook install` | Install the pre-push validation hook | `--root` |
108
- | `gala refresh` | Refresh and commit the engagement snapshot | `--root` |
109
- | `gala upgrade` | Verify and install an exact theme-package release | `--root`, `--channel`, `--yes` |
110
- | `gala topology` | Switch canonical origin/path topology transactionally | `--root`, `--owner`, `--repository`, `--canonical-base-url`, `--path-prefix` |
111
- | `gala entitlement` | Retrieve and commit the current paid attribution artifact | `--root` |
112
- | `gala workflow` | Write the reusable GitHub Actions workflow | `--root`, `--site-id`, `--timezone`, `--action-ref`, `--default-branch`, `--mode` |
113
- | `gala record-deployment` | Record state after a successful deployment | `--root`, `--today`, `--commit-sha` |
114
-
115
- ### Scaffold and configure options
116
-
117
- The same author-owned options are accepted by `scaffold` and `configure`:
118
-
119
- ```text
120
- --site-name
121
- --author
122
- --language
123
- --timezone
124
- --theme
125
- --layout
126
- --palette
127
- --typography
128
- --spacing
129
- --radius
130
- --density
131
- --motion
132
- --componentStyle
133
- --share-target repeatable
134
- --social-profile repeatable
135
- ```
136
-
137
- Use only identities supported by the installed theme package. Validation rejects unavailable layout, palette, and theme identities instead of silently substituting another design.
138
-
139
- ### Scaffold an existing empty repository
140
-
141
- Use this only when the exact GitHub repository already exists and has no branches or content:
47
+ This creates the Markdown file and tells you the address the post will appear at. Write below the
48
+ second `---` line.
142
49
 
143
50
  ```console
144
- npx --yes @rathnasgala/cli@latest scaffold \
145
- --repository YOUR_REPOSITORY_NAME \
146
- --empty-existing-repository
51
+ npx --yes @rathnasgala/cli@latest preview
147
52
  ```
148
53
 
149
- ### Resume interrupted scaffolding
150
-
151
- The target must already be a checkout whose HTTPS origin exactly matches the requested repository:
54
+ Builds the publication and serves it locally, using the exact framework version the repository is
55
+ pinned to — so what you see is what gets published. The first run installs that tooling, which takes
56
+ a moment. Stop it with Ctrl-C.
152
57
 
153
58
  ```console
154
- npx --yes @rathnasgala/cli@latest scaffold \
155
- --repository YOUR_REPOSITORY_NAME \
156
- --target ./YOUR_REPOSITORY_NAME \
157
- --resume
59
+ npx --yes @rathnasgala/cli@latest publish
158
60
  ```
159
61
 
160
- Scaffolding is designed to converge after partial failure. It will not adopt a non-empty unrelated repository.
62
+ Checks your content, records it, and sends it to GitHub. GitHub builds and deploys from there; the
63
+ site updates a minute or two later.
161
64
 
162
- ## Everyday workflow
65
+ ## Prism configurations
163
66
 
164
- Create another post:
67
+ Prism keeps one canonical work while letting you explicitly approve alternate reading depths and
68
+ intents. The CLI uses Gala's public lifecycle API; it never writes approval artifacts itself.
165
69
 
166
70
  ```console
167
- npx --yes @rathnasgala/cli@latest new --title "A durable idea" --language en
71
+ npx --yes @rathnasgala/cli@latest prism status
72
+ npx --yes @rathnasgala/cli@latest prism create my-post --language en --depth brief --intent orientation
73
+ npx --yes @rathnasgala/cli@latest prism list my-post --language en
168
74
  ```
169
75
 
170
- Validate without running a preview server:
76
+ Use `prism edit`, `submit`, `approve`, `reject`, and `revoke` to advance a configuration. Approval,
77
+ rejection, revocation, and reducing the publication mode require terminal confirmation or `--yes`.
78
+ Configuration links default to `nofollow`; change the publication or one work with
79
+ `prism link-policy` when ordinary followed links are intentional. Commands that change repository
80
+ artifacts stay attached through Gala's materialization and GitHub Pages publication states, then
81
+ print the live publication address or a concrete terminal failure. Proposal generation likewise
82
+ waits until its revision is ready for review or generation fails.
171
83
 
172
- ```console
173
- npx --yes @rathnasgala/cli@latest validate
174
- ```
84
+ ## Custom domain
175
85
 
176
- Preview locally:
86
+ Reserve a domain after setup, then advance the verified GitHub Pages flow as DNS propagates:
177
87
 
178
88
  ```console
179
- npx --yes @rathnasgala/cli@latest preview
89
+ npx --yes @rathnasgala/cli@latest domain set blog.example.com
90
+ npx --yes @rathnasgala/cli@latest domain check
180
91
  ```
181
92
 
182
- Publish:
183
-
184
- ```console
185
- npx --yes @rathnasgala/cli@latest publish
186
- ```
93
+ `domain status` resumes an interrupted change, `domain cancel` abandons it, and `domain remove`
94
+ returns the publication to its GitHub Pages address. After removal, delete the old DNS records.
187
95
 
188
- Check managed files and recorded publication state:
96
+ ## When something is wrong
189
97
 
190
98
  ```console
191
99
  npx --yes @rathnasgala/cli@latest doctor
192
100
  ```
193
101
 
194
- ## Security and ownership
195
-
196
- - Your repository remains the canonical source for publication content and configuration.
197
- - Gala credentials and GitHub App credentials are stored outside the repository.
198
- - Credential directories are created with private permissions; credential files use mode `0600` on operating systems that support POSIX modes.
199
- - The site signing secret is returned once by the API and sealed directly into GitHub Actions secrets.
200
- - Do not copy credential files into the repository, dotfiles, cloud-sync folders, or `/tmp`.
201
- - The generated workflow pins the public Gala Action contract; managed framework files are integrity-checked before repair or upgrade.
202
-
203
- ## Troubleshooting
204
-
205
- ### `GitHub authentication is missing`
206
-
207
- Run:
102
+ Inspect a verified managed-theme release without changing anything unless you confirm it:
208
103
 
209
104
  ```console
210
- npx --yes @rathnasgala/cli@latest auth github
105
+ npx --yes @rathnasgala/cli@latest upgrade
211
106
  ```
212
107
 
213
- ### Gala authentication expired
108
+ Reports on your sign-ins, the publication folder, the publishing workflow, and anything you have
109
+ written but not sent. Each check either passes, names what is wrong and how to fix it, or says it
110
+ could not be determined — never one of those disguised as another.
214
111
 
215
- Gala author tokens expire and do not use a refresh token. Run:
112
+ ## Commands
216
113
 
217
- ```console
218
- npx --yes @rathnasgala/cli@latest auth
219
- ```
114
+ Run any command with `--help`.
220
115
 
221
- ### `The Gala GitHub App is not installed on YOUR_ACCOUNT`
116
+ | Command | What it does | Options |
117
+ | --- | --- | --- |
118
+ | `init` | Create a publication in the current or named empty directory | `--name`, `--domain` |
119
+ | `domain` | Inspect or change the custom domain | `--root` |
120
+ | `new` | Start a post | `--language`, `--root`, `--today` |
121
+ | `preview` | Build and serve the publication locally | `--root`, `--today` |
122
+ | `publish` | Check, record and send your work to GitHub | `--root`, `--today`, `--skip-checks` |
123
+ | `prism` | Manage author-approved reading configurations | `--root`, `--language`, `--depth`, `--intent`, `--modality`, `--file`, `--reason`, `--yes` |
124
+ | `upgrade` | Inspect and apply a verified managed-theme update | `--root`, `--channel`, `--yes` |
125
+ | `doctor` | Check a publication and say what is wrong | `--root` |
126
+ | `auth` | Sign in to Gala and GitHub | — |
222
127
 
223
- `scaffold` could not find an installation covering that account. At a terminal it prints the
224
- installation page and waits; in CI it stops, because there is nobody to install it. Install the App
225
- at [the installation page](https://github.com/apps/gala67-app/installations/new) and run `scaffold`
226
- again, or pass `--installation-id` explicitly.
128
+ `auth` is never a prerequisite you have to remember: any command that needs a credential obtains
129
+ one. It exists for when you want to do it deliberately a new machine, or a different account.
227
130
 
228
- ### The App cannot access the new repository
131
+ Every command prompts for what it needs when run in a terminal, and every prompt has an option that
132
+ supplies it instead. With no terminal attached — in CI — nothing is ever prompted for: a value that
133
+ cannot be worked out is an error naming the option, so an automated run fails immediately rather
134
+ than waiting for an answer that will not come.
229
135
 
230
- Open [GitHub App settings](https://github.com/settings/installations) and add the publication repository to the Gala installation. The platform verifies access to the exact repository; the existence of an installation alone is insufficient.
136
+ ## Access
231
137
 
232
- ### The target folder already exists
138
+ The CLI signs in as the **Gala GitHub App**. It reaches only the repositories you have given that
139
+ App, and never asks for the broad `repo` scope, which would have meant read and write access to
140
+ every repository you can see.
233
141
 
234
- Do not delete or overwrite it blindly. Use `--resume` only when it is the intended repository checkout. Use `--empty-existing-repository` only when the remote GitHub repository is genuinely empty.
142
+ Credentials are stored outside the publication, in your operating system's application-config
143
+ directory, with private file permissions. Your GitHub sign-in expires after eight hours and the CLI
144
+ asks you to sign in again rather than quietly using a credential the server will refuse.
235
145
 
236
- ### Validation refuses a post
146
+ Git operations use that same sign-in, not whatever credential your machine happens to have
147
+ configured — so publishing works on a machine where those differ, or where none is configured.
237
148
 
238
- The error includes the source file and violated rule. Correct the file and run:
149
+ ## Troubleshooting
150
+
151
+ ### `GitHub authentication expired`
152
+
153
+ Sign in again:
239
154
 
240
155
  ```console
241
- npx --yes @rathnasgala/cli@latest validate
156
+ npx --yes @rathnasgala/cli@latest auth
242
157
  ```
243
158
 
244
- Do not use `publish --force` as a routine bypass. It skips content validation but does not force-push Git history.
159
+ ### Gala cannot reach the repository
245
160
 
246
- ### Managed files have drifted
161
+ The Gala GitHub App is installed but has not been given this repository. The command prints a link
162
+ to the installation that needs it; add the one repository and continue. Nothing else needs granting.
247
163
 
248
- Inspect first:
164
+ ### A post is not appearing
249
165
 
250
- ```console
251
- npx --yes @rathnasgala/cli@latest doctor
252
- ```
166
+ Run `doctor`. The most common cause is work written but never sent, which it reports along with the
167
+ command to fix it.
253
168
 
254
- Repair requires a trusted, hash-verified theme source:
169
+ ### Something failed and the message was not enough
255
170
 
256
171
  ```console
257
- npx --yes @rathnasgala/cli@latest doctor --fix --source PATH_TO_TRUSTED_THEME
172
+ GALA_DEBUG=1 npx --yes @rathnasgala/cli@latest publish
258
173
  ```
259
174
 
260
- ## Package and source
261
-
262
- - npm: [`@rathnasgala/cli`](https://www.npmjs.com/package/@rathnasgala/cli)
263
- - source: [`rathnasgala/cli`](https://github.com/rathnasgala/cli)
175
+ Prints the full stack. Without it, failures are one line you can act on.
264
176
 
265
- ## License
177
+ ## Package and source
266
178
 
267
- The repository does not currently declare a license. Copyright remains with its owner unless and until a license is added.
179
+ Published as [`@rathnasgala/cli`](https://www.npmjs.com/package/@rathnasgala/cli). Source at
180
+ [rathnasgala/cli](https://github.com/rathnasgala/cli).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rathnasgala/cli",
3
- "version": "0.0.22",
3
+ "version": "1.1.4",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src"
@@ -13,13 +13,13 @@
13
13
  "gala": "src/index.js"
14
14
  },
15
15
  "scripts": {
16
- "test": "node --test",
16
+ "test": "node --test test/*.test.js",
17
17
  "lint": "node scripts/lint.js",
18
18
  "preversion": "npm test && npm run lint",
19
19
  "push": "node scripts/push.js"
20
20
  },
21
21
  "engines": {
22
- "node": ">=18"
22
+ "node": ">=20"
23
23
  },
24
24
  "repository": {
25
25
  "type": "git",
@@ -0,0 +1,163 @@
1
+ import { request, requestJson } from './http.js';
2
+
3
+ /**
4
+ * The Gala API.
5
+ *
6
+ * Only the calls the six commands make. v0 carried a generated client covering the whole surface —
7
+ * comments, reactions, admin, moderation — none of which a CLI ever touches.
8
+ */
9
+ export const DEFAULT_API_BASE_URL = 'https://api.gala67.com';
10
+
11
+ export function galaApi({ baseUrl = DEFAULT_API_BASE_URL, token } = {}) {
12
+ const root = String(baseUrl).replace(/\/$/, '');
13
+ const authorized = (action, extra = {}) => ({
14
+ action,
15
+ ...extra,
16
+ headers: {
17
+ accept: 'application/json',
18
+ ...(token == null ? {} : { authorization: `Bearer ${token}` }),
19
+ ...extra.headers
20
+ }
21
+ });
22
+
23
+ return {
24
+ baseUrl: root,
25
+
26
+ /** Cheap authenticated call, used to find out whether a stored credential is still accepted. */
27
+ async accepted() {
28
+ try {
29
+ const response = await fetch(`${root}/v1/me/sites`, {
30
+ headers: { accept: 'application/json', authorization: `Bearer ${token}` }
31
+ });
32
+ // 403 means "not an author yet", which is a stage of the product, not a dead credential.
33
+ return response.status !== 401;
34
+ } catch {
35
+ // Offline is not an answer. Forcing a sign-in the writer does not need is worse than
36
+ // letting the real call fail with its own error.
37
+ return true;
38
+ }
39
+ },
40
+
41
+ /** Exchanges the GitHub token for the short-lived capability the GitHub-scoped routes require. */
42
+ async githubCapability(githubToken) {
43
+ const body = await requestJson(`${root}/v1/auth/github/device-authorizations`,
44
+ authorized('GitHub authorization', {
45
+ method: 'POST',
46
+ headers: { 'content-type': 'application/json' },
47
+ body: JSON.stringify({ accessToken: githubToken })
48
+ }));
49
+ if (typeof body?.authorization !== 'string') {
50
+ throw new TypeError('GitHub authorization returned no capability');
51
+ }
52
+ return body.authorization;
53
+ },
54
+
55
+ /**
56
+ * Creates the publication repository, the same call the browser editor makes.
57
+ *
58
+ * v0 called GitHub's template endpoint itself: a second implementation with no fallback and no
59
+ * wait for the App installation to reach the result, which is why repositories the CLI created
60
+ * never appeared in the web UI.
61
+ */
62
+ createPublication({ capability, name, installationId }) {
63
+ return requestJson(`${root}/v1/auth/github/publications`,
64
+ authorized('Publication creation', {
65
+ method: 'POST',
66
+ headers: { 'content-type': 'application/json', 'GitHub-Authorization': capability },
67
+ body: JSON.stringify({ name, installationId })
68
+ }));
69
+ },
70
+
71
+ githubInstallationAccounts({ capability }) {
72
+ return requestJson(`${root}/v1/auth/github/accounts`,
73
+ authorized('GitHub installation accounts', {
74
+ headers: { 'GitHub-Authorization': capability }
75
+ }));
76
+ },
77
+
78
+ registerSite({ capability, idempotencyKey, repositoryOwner, repositoryName, topology, canonicalBaseUrl }) {
79
+ return requestJson(`${root}/v1/sites`,
80
+ authorized('Site registration', {
81
+ method: 'POST',
82
+ headers: {
83
+ 'content-type': 'application/json',
84
+ 'GitHub-Authorization': capability,
85
+ 'idempotency-key': idempotencyKey
86
+ },
87
+ body: JSON.stringify({ repositoryOwner, repositoryName, topology, canonicalBaseUrl })
88
+ }));
89
+ },
90
+
91
+ listPublications() {
92
+ return requestJson(`${root}/v1/me/sites`, authorized('Publication list'));
93
+ },
94
+
95
+ prepareTopologyChange(siteId, body) {
96
+ return requestJson(`${root}/v1/sites/${encodeURIComponent(siteId)}/topology-changes/prepare`,
97
+ authorized('Custom domain reservation', {
98
+ method: 'POST',
99
+ headers: { 'content-type': 'application/json' },
100
+ body: JSON.stringify(body)
101
+ }));
102
+ },
103
+
104
+ pendingTopologyChange(siteId) {
105
+ return requestJson(`${root}/v1/sites/${encodeURIComponent(siteId)}/topology-changes/pending`,
106
+ authorized('Custom domain status'));
107
+ },
108
+
109
+ configureTopologyChange(siteId, changeId) {
110
+ return requestJson(`${root}/v1/sites/${encodeURIComponent(siteId)}/topology-changes/${encodeURIComponent(changeId)}/configure`,
111
+ authorized('GitHub Pages domain verification', { method: 'POST' }));
112
+ },
113
+
114
+ commitTopologyChange(siteId, changeId) {
115
+ return requestJson(`${root}/v1/sites/${encodeURIComponent(siteId)}/topology-changes/${encodeURIComponent(changeId)}/commit`,
116
+ authorized('Custom domain activation', { method: 'POST' }));
117
+ },
118
+
119
+ discardTopologyChange(siteId, changeId) {
120
+ return request(`${root}/v1/sites/${encodeURIComponent(siteId)}/topology-changes/${encodeURIComponent(changeId)}`,
121
+ authorized('Custom domain cancellation', { method: 'DELETE' }));
122
+ },
123
+
124
+ /** Not in the OpenAPI document, though the endpoint exists and is public. */
125
+ async signInConfiguration() {
126
+ return requestJson(`${root}/v1/auth/configuration`, { action: 'Sign-in configuration' });
127
+ },
128
+
129
+ /*
130
+ * Form-encoded, not JSON. These are RFC 8628 device-flow endpoints and the spec declares them
131
+ * as `application/x-www-form-urlencoded`; sending JSON gets a bare "Authentication is required",
132
+ * which reads as a credential problem and is nothing of the kind.
133
+ */
134
+ async startDeviceAuthorization() {
135
+ return requestJson(`${root}/v1/auth/device/code`, {
136
+ action: 'Gala sign-in',
137
+ method: 'POST',
138
+ headers: { accept: 'application/json', 'content-type': 'application/x-www-form-urlencoded' },
139
+ body: new URLSearchParams({ client_id: 'gala-cli' }).toString()
140
+ });
141
+ },
142
+
143
+ /** Returns the token, or null while the writer has not finished authorizing. */
144
+ async pollDeviceAuthorization(deviceCode) {
145
+ const response = await fetch(`${root}/v1/auth/device/token`, {
146
+ method: 'POST',
147
+ headers: { accept: 'application/json', 'content-type': 'application/x-www-form-urlencoded' },
148
+ body: new URLSearchParams({
149
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
150
+ device_code: deviceCode,
151
+ client_id: 'gala-cli'
152
+ }).toString()
153
+ });
154
+ const body = await response.json().catch(() => null);
155
+ if (response.ok) return body;
156
+ if (body?.error === 'authorization_pending' || body?.error === 'slow_down') return null;
157
+ throw new Error(`Gala sign-in failed: ${body?.error_description ?? body?.error ?? response.status}`);
158
+ },
159
+
160
+ request: (path, options) => request(`${root}${path}`, authorized(options?.action ?? path, options)),
161
+ json: (path, options) => requestJson(`${root}${path}`, authorized(options?.action ?? path, options))
162
+ };
163
+ }
@@ -0,0 +1,63 @@
1
+ import { requestJson } from './http.js';
2
+
3
+ /**
4
+ * GitHub, as the Gala App.
5
+ *
6
+ * The CLI holds a GitHub App user token, not an OAuth App token. That single difference is what
7
+ * separates this from v0: an App token can list installations, is not blocked by an organisation's
8
+ * OAuth App restrictions, and reaches only repositories the App has been given — rather than every
9
+ * repository the writer can see, which is what `repo` scope meant.
10
+ */
11
+ const API = 'https://api.github.com';
12
+ const API_VERSION = '2026-03-10';
13
+
14
+ export function githubApi(token) {
15
+ const headers = {
16
+ accept: 'application/vnd.github+json',
17
+ authorization: `Bearer ${token}`,
18
+ 'x-github-api-version': API_VERSION
19
+ };
20
+
21
+ return {
22
+ /** The account behind the token. Works for every token type and needs no permission. */
23
+ async viewer() {
24
+ const body = await requestJson(`${API}/user`, { action: 'GitHub account lookup', headers });
25
+ const login = body?.login;
26
+ if (typeof login !== 'string' || login === '') {
27
+ throw new TypeError('GitHub returned an unusable account login');
28
+ }
29
+ return login;
30
+ },
31
+
32
+ /**
33
+ * Whether a repository has content yet.
34
+ *
35
+ * Creating a repository is asynchronous: GitHub answers with a clone URL before the template
36
+ * lands. Cloning into that window produces an empty checkout and a missing site.config.yml —
37
+ * a confusing error about a file the template certainly contains. `size` is not usable as the
38
+ * signal; GitHub still reported 0 for a repository that already had commits.
39
+ */
40
+ async hasContent(owner, repository) {
41
+ const branches = await requestJson(
42
+ `${API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/branches?per_page=1`,
43
+ { action: 'GitHub branch lookup', headers }
44
+ );
45
+ return Array.isArray(branches) && branches.length > 0;
46
+ },
47
+
48
+ setVariable(owner, repository, name, value) {
49
+ const base = `${API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/actions/variables`;
50
+ return requestJson(`${base}/${encodeURIComponent(name)}`, {
51
+ action: 'GitHub repository variable',
52
+ method: 'PATCH',
53
+ headers: { ...headers, 'content-type': 'application/json' },
54
+ body: JSON.stringify({ name, value })
55
+ }).catch(() => requestJson(base, {
56
+ action: 'GitHub repository variable',
57
+ method: 'POST',
58
+ headers: { ...headers, 'content-type': 'application/json' },
59
+ body: JSON.stringify({ name, value })
60
+ }));
61
+ }
62
+ };
63
+ }