@rathnasgala/cli 0.0.22 → 1.0.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.
- package/README.md +66 -204
- package/package.json +3 -3
- package/src/api/gala.js +126 -0
- package/src/api/github.js +63 -0
- package/src/api/http.js +72 -0
- package/src/auth/gala.js +52 -0
- package/src/auth/github.js +78 -0
- package/src/auth/store.js +88 -0
- package/src/cli/args.js +56 -0
- package/src/cli/terminal.js +104 -0
- package/src/commands/auth.js +20 -0
- package/src/commands/doctor.js +98 -0
- package/src/commands/init.js +197 -0
- package/src/commands/new.js +76 -0
- package/src/commands/preview.js +92 -0
- package/src/commands/publish.js +57 -0
- package/src/commands-manifest.js +58 -0
- package/src/content.js +31 -0
- package/src/git.js +143 -0
- package/src/index.js +44 -294
- package/src/publication.js +37 -0
- package/src/assign-content-ids.js +0 -1
- package/src/auth-command.js +0 -36
- package/src/configure-site.js +0 -102
- package/src/content-files.js +0 -1
- package/src/doctor-command.js +0 -214
- package/src/entitlement-client.js +0 -26
- package/src/entitlement-command.js +0 -74
- package/src/evaluation-date.js +0 -1
- package/src/gala-credential-health.js +0 -34
- package/src/gala-credential-store.js +0 -115
- package/src/gala-device-flow.js +0 -121
- package/src/git-credentials.js +0 -37
- package/src/github-auth-command.js +0 -50
- package/src/github-credential-store.js +0 -104
- package/src/github-device-flow.js +0 -153
- package/src/github-empty-repository.js +0 -89
- package/src/github-identity.js +0 -32
- package/src/github-pages-provisioning.js +0 -107
- package/src/github-repository-secret.js +0 -82
- package/src/github-repository-variable.js +0 -56
- package/src/github-template-repository.js +0 -171
- package/src/hook-command.js +0 -64
- package/src/http-failure.js +0 -55
- package/src/new-command.js +0 -54
- package/src/open-browser.js +0 -40
- package/src/preview-command.js +0 -60
- package/src/publication-creation-client.js +0 -155
- package/src/publication-state.js +0 -7
- package/src/publish-command.js +0 -37
- package/src/record-deployment-command.js +0 -147
- package/src/refresh-command.js +0 -104
- package/src/repository-limits.js +0 -94
- package/src/scaffold-git.js +0 -76
- package/src/scaffold-options.js +0 -58
- package/src/scaffold-preflight.js +0 -146
- package/src/scaffold-site.js +0 -185
- package/src/site-config-registration.js +0 -47
- package/src/site-registration-client.js +0 -138
- package/src/theme-package.js +0 -128
- package/src/topology-client.js +0 -43
- package/src/topology-command.js +0 -70
- package/src/upgrade-command.js +0 -81
- package/src/validate-command.js +0 -5
- package/src/workflow-command.js +0 -87
package/README.md
CHANGED
|
@@ -1,267 +1,129 @@
|
|
|
1
1
|
# Gala CLI
|
|
2
2
|
|
|
3
|
-
Create,
|
|
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
|
|
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
10
|
|
|
14
|
-
|
|
11
|
+
Nothing to install. Every command runs through `npx`.
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
node --version
|
|
18
|
-
npm --version
|
|
19
|
-
git --version
|
|
20
|
-
```
|
|
13
|
+
## Start a publication
|
|
21
14
|
|
|
22
|
-
|
|
23
|
-
```console
|
|
24
|
-
v22.18.0
|
|
25
|
-
10.9.3
|
|
26
|
-
git version 2.50.1 (Apple Git-155)
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Quick start
|
|
30
|
-
|
|
31
|
-
One command, run inside an empty folder named after the publication you want:
|
|
15
|
+
Make a folder named after the publication you want, and run one command inside it:
|
|
32
16
|
|
|
33
17
|
```console
|
|
34
18
|
mkdir field-notes && cd field-notes
|
|
35
|
-
npx --yes @rathnasgala/cli@latest
|
|
19
|
+
npx --yes @rathnasgala/cli@latest init --here
|
|
36
20
|
```
|
|
37
21
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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.
|
|
22
|
+
It signs you in to Gala and to GitHub if you are not already, creates the repository, registers the
|
|
23
|
+
publication, and leaves a working checkout in the folder. When it finishes it prints the address
|
|
24
|
+
your publication will live at.
|
|
53
25
|
|
|
54
|
-
|
|
55
|
-
|
|
26
|
+
If the Gala GitHub App has not been given access to the new repository, it says so and links to the
|
|
27
|
+
one page that grants it — GitHub has no way for an app to grant itself access, so that click is
|
|
28
|
+
unavoidable. Everything else is automatic.
|
|
56
29
|
|
|
57
|
-
|
|
30
|
+
## Write
|
|
58
31
|
|
|
59
32
|
```console
|
|
60
|
-
npx --yes @rathnasgala/cli@latest new
|
|
61
|
-
npx --yes @rathnasgala/cli@latest preview
|
|
62
|
-
npx --yes @rathnasgala/cli@latest publish
|
|
33
|
+
npx --yes @rathnasgala/cli@latest new "The places we return to"
|
|
63
34
|
```
|
|
64
35
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
### Overriding what scaffold works out
|
|
69
|
-
|
|
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:
|
|
36
|
+
This creates the Markdown file and tells you the address the post will appear at. Write below the
|
|
37
|
+
second `---` line.
|
|
73
38
|
|
|
74
39
|
```console
|
|
75
|
-
npx --yes @rathnasgala/cli@latest
|
|
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:
|
|
89
|
-
|
|
90
|
-
```console
|
|
91
|
-
npx --yes @rathnasgala/cli@latest COMMAND [options]
|
|
92
|
-
```
|
|
93
|
-
|
|
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:
|
|
142
|
-
|
|
143
|
-
```console
|
|
144
|
-
npx --yes @rathnasgala/cli@latest scaffold \
|
|
145
|
-
--repository YOUR_REPOSITORY_NAME \
|
|
146
|
-
--empty-existing-repository
|
|
40
|
+
npx --yes @rathnasgala/cli@latest preview
|
|
147
41
|
```
|
|
148
42
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
43
|
+
Builds the publication and serves it locally, using the exact framework version the repository is
|
|
44
|
+
pinned to — so what you see is what gets published. The first run installs that tooling, which takes
|
|
45
|
+
a moment. Stop it with Ctrl-C.
|
|
152
46
|
|
|
153
47
|
```console
|
|
154
|
-
npx --yes @rathnasgala/cli@latest
|
|
155
|
-
--repository YOUR_REPOSITORY_NAME \
|
|
156
|
-
--target ./YOUR_REPOSITORY_NAME \
|
|
157
|
-
--resume
|
|
48
|
+
npx --yes @rathnasgala/cli@latest publish
|
|
158
49
|
```
|
|
159
50
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
## Everyday workflow
|
|
51
|
+
Checks your content, records it, and sends it to GitHub. GitHub builds and deploys from there; the
|
|
52
|
+
site updates a minute or two later.
|
|
163
53
|
|
|
164
|
-
|
|
54
|
+
## When something is wrong
|
|
165
55
|
|
|
166
56
|
```console
|
|
167
|
-
npx --yes @rathnasgala/cli@latest
|
|
57
|
+
npx --yes @rathnasgala/cli@latest doctor
|
|
168
58
|
```
|
|
169
59
|
|
|
170
|
-
|
|
60
|
+
Reports on your sign-ins, the publication folder, the publishing workflow, and anything you have
|
|
61
|
+
written but not sent. Each check either passes, names what is wrong and how to fix it, or says it
|
|
62
|
+
could not be determined — never one of those disguised as another.
|
|
171
63
|
|
|
172
|
-
|
|
173
|
-
npx --yes @rathnasgala/cli@latest validate
|
|
174
|
-
```
|
|
64
|
+
## Commands
|
|
175
65
|
|
|
176
|
-
|
|
66
|
+
Run any command with `--help`.
|
|
177
67
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
68
|
+
| Command | What it does | Options |
|
|
69
|
+
| --- | --- | --- |
|
|
70
|
+
| `init` | Create a publication and clone it here | `--name`, `--here` |
|
|
71
|
+
| `new` | Start a post | `--language`, `--root`, `--today` |
|
|
72
|
+
| `preview` | Build and serve the publication locally | `--root`, `--today` |
|
|
73
|
+
| `publish` | Check, record and send your work to GitHub | `--root`, `--today`, `--skip-checks` |
|
|
74
|
+
| `doctor` | Check a publication and say what is wrong | `--root` |
|
|
75
|
+
| `auth` | Sign in to Gala and GitHub | — |
|
|
181
76
|
|
|
182
|
-
|
|
77
|
+
`auth` is never a prerequisite you have to remember: any command that needs a credential obtains
|
|
78
|
+
one. It exists for when you want to do it deliberately — a new machine, or a different account.
|
|
183
79
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
80
|
+
Every command prompts for what it needs when run in a terminal, and every prompt has an option that
|
|
81
|
+
supplies it instead. With no terminal attached — in CI — nothing is ever prompted for: a value that
|
|
82
|
+
cannot be worked out is an error naming the option, so an automated run fails immediately rather
|
|
83
|
+
than waiting for an answer that will not come.
|
|
187
84
|
|
|
188
|
-
|
|
85
|
+
## Access
|
|
189
86
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
87
|
+
The CLI signs in as the **Gala GitHub App**. It reaches only the repositories you have given that
|
|
88
|
+
App, and never asks for the broad `repo` scope, which would have meant read and write access to
|
|
89
|
+
every repository you can see.
|
|
193
90
|
|
|
194
|
-
|
|
91
|
+
Credentials are stored outside the publication, in your operating system's application-config
|
|
92
|
+
directory, with private file permissions. Your GitHub sign-in expires after eight hours and the CLI
|
|
93
|
+
asks you to sign in again rather than quietly using a credential the server will refuse.
|
|
195
94
|
|
|
196
|
-
|
|
197
|
-
|
|
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.
|
|
95
|
+
Git operations use that same sign-in, not whatever credential your machine happens to have
|
|
96
|
+
configured — so publishing works on a machine where those differ, or where none is configured.
|
|
202
97
|
|
|
203
98
|
## Troubleshooting
|
|
204
99
|
|
|
205
|
-
### `GitHub authentication
|
|
100
|
+
### `GitHub authentication expired`
|
|
206
101
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
```console
|
|
210
|
-
npx --yes @rathnasgala/cli@latest auth github
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
### Gala authentication expired
|
|
214
|
-
|
|
215
|
-
Gala author tokens expire and do not use a refresh token. Run:
|
|
102
|
+
Sign in again:
|
|
216
103
|
|
|
217
104
|
```console
|
|
218
105
|
npx --yes @rathnasgala/cli@latest auth
|
|
219
106
|
```
|
|
220
107
|
|
|
221
|
-
###
|
|
222
|
-
|
|
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.
|
|
227
|
-
|
|
228
|
-
### The App cannot access the new repository
|
|
229
|
-
|
|
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.
|
|
231
|
-
|
|
232
|
-
### The target folder already exists
|
|
233
|
-
|
|
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.
|
|
235
|
-
|
|
236
|
-
### Validation refuses a post
|
|
237
|
-
|
|
238
|
-
The error includes the source file and violated rule. Correct the file and run:
|
|
108
|
+
### Gala cannot reach the repository
|
|
239
109
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
```
|
|
110
|
+
The Gala GitHub App is installed but has not been given this repository. The command prints a link
|
|
111
|
+
to the installation that needs it; add the one repository and continue. Nothing else needs granting.
|
|
243
112
|
|
|
244
|
-
|
|
113
|
+
### A post is not appearing
|
|
245
114
|
|
|
246
|
-
|
|
115
|
+
Run `doctor`. The most common cause is work written but never sent, which it reports along with the
|
|
116
|
+
command to fix it.
|
|
247
117
|
|
|
248
|
-
|
|
118
|
+
### Something failed and the message was not enough
|
|
249
119
|
|
|
250
120
|
```console
|
|
251
|
-
npx --yes @rathnasgala/cli@latest
|
|
121
|
+
GALA_DEBUG=1 npx --yes @rathnasgala/cli@latest publish
|
|
252
122
|
```
|
|
253
123
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
```console
|
|
257
|
-
npx --yes @rathnasgala/cli@latest doctor --fix --source PATH_TO_TRUSTED_THEME
|
|
258
|
-
```
|
|
124
|
+
Prints the full stack. Without it, failures are one line you can act on.
|
|
259
125
|
|
|
260
126
|
## Package and source
|
|
261
127
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
## License
|
|
266
|
-
|
|
267
|
-
The repository does not currently declare a license. Copyright remains with its owner unless and until a license is added.
|
|
128
|
+
Published as [`@rathnasgala/cli`](https://www.npmjs.com/package/@rathnasgala/cli). Source at
|
|
129
|
+
[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
|
|
3
|
+
"version": "1.0.0",
|
|
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": ">=
|
|
22
|
+
"node": ">=20"
|
|
23
23
|
},
|
|
24
24
|
"repository": {
|
|
25
25
|
"type": "git",
|
package/src/api/gala.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
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 }) {
|
|
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 })
|
|
68
|
+
}));
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
registerSite({ capability, idempotencyKey, repositoryOwner, repositoryName, topology, canonicalBaseUrl }) {
|
|
72
|
+
return requestJson(`${root}/v1/sites`,
|
|
73
|
+
authorized('Site registration', {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: {
|
|
76
|
+
'content-type': 'application/json',
|
|
77
|
+
'GitHub-Authorization': capability,
|
|
78
|
+
'idempotency-key': idempotencyKey
|
|
79
|
+
},
|
|
80
|
+
body: JSON.stringify({ repositoryOwner, repositoryName, topology, canonicalBaseUrl })
|
|
81
|
+
}));
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
listPublications() {
|
|
85
|
+
return requestJson(`${root}/v1/me/sites`, authorized('Publication list'));
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
/** Not in the OpenAPI document, though the endpoint exists and is public. */
|
|
89
|
+
async signInConfiguration() {
|
|
90
|
+
return requestJson(`${root}/v1/auth/configuration`, { action: 'Sign-in configuration' });
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/*
|
|
94
|
+
* Form-encoded, not JSON. These are RFC 8628 device-flow endpoints and the spec declares them
|
|
95
|
+
* as `application/x-www-form-urlencoded`; sending JSON gets a bare "Authentication is required",
|
|
96
|
+
* which reads as a credential problem and is nothing of the kind.
|
|
97
|
+
*/
|
|
98
|
+
async startDeviceAuthorization() {
|
|
99
|
+
return requestJson(`${root}/v1/auth/device/code`, {
|
|
100
|
+
action: 'Gala sign-in',
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: { accept: 'application/json', 'content-type': 'application/x-www-form-urlencoded' },
|
|
103
|
+
body: new URLSearchParams({ client_id: 'gala-cli' }).toString()
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
/** Returns the token, or null while the writer has not finished authorizing. */
|
|
108
|
+
async pollDeviceAuthorization(deviceCode) {
|
|
109
|
+
const response = await fetch(`${root}/v1/auth/device/token`, {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
headers: { accept: 'application/json', 'content-type': 'application/x-www-form-urlencoded' },
|
|
112
|
+
body: new URLSearchParams({
|
|
113
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
114
|
+
device_code: deviceCode,
|
|
115
|
+
client_id: 'gala-cli'
|
|
116
|
+
}).toString()
|
|
117
|
+
});
|
|
118
|
+
const body = await response.json().catch(() => null);
|
|
119
|
+
if (response.ok) return body;
|
|
120
|
+
if (body?.error === 'authorization_pending' || body?.error === 'slow_down') return null;
|
|
121
|
+
throw new Error(`Gala sign-in failed: ${body?.error_description ?? body?.error ?? response.status}`);
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
request: (path, options) => request(`${root}${path}`, authorized(options?.action ?? path, options))
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -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
|
+
}
|
package/src/api/http.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every HTTP call the CLI makes, and every failure it reports.
|
|
3
|
+
*
|
|
4
|
+
* v0 threw away the response body at all twenty failure sites — `failed with HTTP 403` and nothing
|
|
5
|
+
* else. GitHub explains which 403 it is in that body and nowhere else: an organisation's OAuth App
|
|
6
|
+
* restrictions, a missing permission, a rename and a rate limit all arrive as 403 with a sentence
|
|
7
|
+
* telling them apart. Diagnosing anything meant guessing between causes the server had already
|
|
8
|
+
* distinguished, and it cost days.
|
|
9
|
+
*
|
|
10
|
+
* Only the response is read here. Request bodies, tokens and secrets never pass through.
|
|
11
|
+
*/
|
|
12
|
+
const MAX_DETAIL = 400;
|
|
13
|
+
|
|
14
|
+
export class HttpError extends Error {
|
|
15
|
+
constructor(status, action, detail, code) {
|
|
16
|
+
super(detail === '' ? `${action} failed with HTTP ${status}` : `${action} failed: ${detail}`);
|
|
17
|
+
this.name = 'HttpError';
|
|
18
|
+
this.status = status;
|
|
19
|
+
this.code = code;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function request(url, { action, ...options } = {}) {
|
|
24
|
+
let response;
|
|
25
|
+
try {
|
|
26
|
+
response = await fetch(url, options);
|
|
27
|
+
} catch (unreachable) {
|
|
28
|
+
throw new Error(`${action} could not reach ${new URL(url).host}: ${unreachable.message}`);
|
|
29
|
+
}
|
|
30
|
+
if (response.ok) return response;
|
|
31
|
+
|
|
32
|
+
const body = await readBody(response);
|
|
33
|
+
throw new HttpError(response.status, action, describe(body), body?.code);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function requestJson(url, options) {
|
|
37
|
+
const response = await request(url, options);
|
|
38
|
+
if (response.status === 204) return undefined;
|
|
39
|
+
return response.json();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function readBody(response) {
|
|
43
|
+
try {
|
|
44
|
+
const text = await response.text();
|
|
45
|
+
if (text.trim() === '') return null;
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(text);
|
|
48
|
+
} catch {
|
|
49
|
+
return { raw: text };
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function describe(body) {
|
|
57
|
+
if (body == null) return '';
|
|
58
|
+
const parts = [];
|
|
59
|
+
if (typeof body.message === 'string' && body.message.trim() !== '') parts.push(body.message.trim());
|
|
60
|
+
// GitHub's `errors` array carries the specific field or reason behind a generic message.
|
|
61
|
+
for (const error of Array.isArray(body.errors) ? body.errors : []) {
|
|
62
|
+
const reason = typeof error === 'string' ? error : error?.message ?? error?.code;
|
|
63
|
+
if (typeof reason === 'string' && reason.trim() !== '') parts.push(reason.trim());
|
|
64
|
+
}
|
|
65
|
+
if (typeof body.error_description === 'string') parts.push(body.error_description.trim());
|
|
66
|
+
if (parts.length === 0 && typeof body.code === 'string') parts.push(body.code);
|
|
67
|
+
if (parts.length === 0 && typeof body.raw === 'string') parts.push(body.raw);
|
|
68
|
+
if (typeof body.documentation_url === 'string') parts.push(`See ${body.documentation_url}`);
|
|
69
|
+
|
|
70
|
+
const detail = parts.join(' — ').replace(/\s+/g, ' ').trim();
|
|
71
|
+
return detail.length > MAX_DETAIL ? `${detail.slice(0, MAX_DETAIL)}…` : detail;
|
|
72
|
+
}
|