@rathnasgala/cli 0.0.12 → 0.0.13
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 +38 -46
- package/package.json +1 -1
- package/src/gala-installation-client.js +75 -0
- package/src/github-identity.js +31 -0
- package/src/index.js +25 -4
- package/src/scaffold-preflight.js +154 -0
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ The quick start below begins with the required accounts and tools and does not a
|
|
|
9
9
|
- [Git](https://git-scm.com/downloads)
|
|
10
10
|
- [Node.js 24](https://nodejs.org/en/download) recommended; the CLI package supports Node.js 18 or newer
|
|
11
11
|
- A [GitHub account](https://github.com/signup)
|
|
12
|
-
- The [Gala GitHub App](https://github.com/apps/gala67-app/installations/new)
|
|
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
13
|
|
|
14
14
|
Check your local tools:
|
|
15
15
|
|
|
@@ -28,42 +28,47 @@ git version 2.50.1 (Apple Git-155)
|
|
|
28
28
|
|
|
29
29
|
## Quick start
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
One command, run inside an empty folder named after the publication you want:
|
|
32
32
|
|
|
33
33
|
```console
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
The CLI displays a short code and opens the platform authorization page. The resulting Gala token is stored in your operating system's application-config directory, never in the publication repository.
|
|
38
|
-
|
|
39
|
-
### 2. Authenticate with GitHub
|
|
40
|
-
|
|
41
|
-
```console
|
|
42
|
-
npx --yes @rathnasgala/cli@latest auth github
|
|
34
|
+
mkdir field-notes && cd field-notes
|
|
35
|
+
npx --yes @rathnasgala/cli@latest scaffold --target ./ --mode build-and-deploy
|
|
43
36
|
```
|
|
44
37
|
|
|
45
|
-
|
|
38
|
+
That single command does all of the following, and asks only for what it cannot work out:
|
|
46
39
|
|
|
47
|
-
|
|
48
|
-
|
|
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, requesting `repo` to create the publication and install
|
|
42
|
+
its Actions secret, and `workflow` for the initial scaffold. Ordinary publishing needs neither.
|
|
43
|
+
3. **Reads your GitHub account** from that token, so there is no username to type.
|
|
44
|
+
4. **Finds the Gala GitHub App installation** for your account. If the App is not installed yet it
|
|
45
|
+
prints the installation page, waits while you install it, and carries on — the installation ID
|
|
46
|
+
is never something you have to read out of a URL.
|
|
47
|
+
5. **Names the publication** after the folder you are standing in.
|
|
48
|
+
6. **Creates the repository** from the site template, registers it, installs its one-time secret,
|
|
49
|
+
writes the publication workflow, commits, and enables GitHub Pages.
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
Both sign-ins are skipped when a valid credential is already stored, so re-running is cheap.
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
After scaffolding succeeds, open [GitHub App settings](https://github.com/settings/installations)
|
|
54
|
+
and restrict the App to the publication repository if you installed it against all of them.
|
|
53
55
|
|
|
54
|
-
|
|
56
|
+
### Write, preview, and publish
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
```console
|
|
59
|
+
npx --yes @rathnasgala/cli@latest new --title "My first post" --language en
|
|
60
|
+
npx --yes @rathnasgala/cli@latest preview
|
|
61
|
+
npx --yes @rathnasgala/cli@latest publish
|
|
60
62
|
```
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
`new` prints the Markdown file it created. Write below the second `---` line, save the file,
|
|
65
|
+
preview it locally, then publish it through GitHub.
|
|
63
66
|
|
|
64
|
-
###
|
|
67
|
+
### Overriding what scaffold works out
|
|
65
68
|
|
|
66
|
-
|
|
69
|
+
Every derived value is still an explicit flag, for the cases where the default is wrong — a
|
|
70
|
+
publication owned by an organisation, a folder named differently from the repository, or more than
|
|
71
|
+
one App installation on the account:
|
|
67
72
|
|
|
68
73
|
```console
|
|
69
74
|
npx --yes @rathnasgala/cli@latest scaffold \
|
|
@@ -74,20 +79,9 @@ npx --yes @rathnasgala/cli@latest scaffold \
|
|
|
74
79
|
--mode build-and-deploy
|
|
75
80
|
```
|
|
76
81
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
### 5. Write, preview, and publish
|
|
82
|
-
|
|
83
|
-
```console
|
|
84
|
-
cd YOUR_REPOSITORY_NAME
|
|
85
|
-
npx --yes @rathnasgala/cli@latest new --title "My first post" --language en
|
|
86
|
-
npx --yes @rathnasgala/cli@latest preview
|
|
87
|
-
npx --yes @rathnasgala/cli@latest publish
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
`new` prints the Markdown file it created. Write below the second `---` line, save the file, preview it locally, then publish it through GitHub.
|
|
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.
|
|
91
85
|
|
|
92
86
|
## Command reference
|
|
93
87
|
|
|
@@ -103,7 +97,7 @@ Inside the table below, `gala` is shorthand for that prefix.
|
|
|
103
97
|
| --- | --- | --- |
|
|
104
98
|
| `gala auth` | Authenticate the author with Gala | `--api-base-url URL` for a non-production API |
|
|
105
99
|
| `gala auth github` | Authenticate the CLI with GitHub | Browser device flow; requests `repo workflow` |
|
|
106
|
-
| `gala scaffold` |
|
|
100
|
+
| `gala scaffold` | Sign in if needed, then create and register a publication | All derived; override with `--owner`, `--repository`, `--target`, `--installation-id`, `--mode` |
|
|
107
101
|
| `gala configure` | Update author-owned site and design settings | `--root`, plus the configuration options below |
|
|
108
102
|
| `gala new` | Create a Markdown post variant | `--root`, `--title`, `--language`, `--today` |
|
|
109
103
|
| `gala validate` | Validate repository content without publishing | optional root path, `--today` |
|
|
@@ -148,10 +142,7 @@ Use this only when the exact GitHub repository already exists and has no branche
|
|
|
148
142
|
|
|
149
143
|
```console
|
|
150
144
|
npx --yes @rathnasgala/cli@latest scaffold \
|
|
151
|
-
--owner YOUR_GITHUB_USERNAME \
|
|
152
145
|
--repository YOUR_REPOSITORY_NAME \
|
|
153
|
-
--target ./YOUR_REPOSITORY_NAME \
|
|
154
|
-
--installation-id YOUR_INSTALLATION_ID \
|
|
155
146
|
--empty-existing-repository
|
|
156
147
|
```
|
|
157
148
|
|
|
@@ -161,10 +152,8 @@ The target must already be a checkout whose HTTPS origin exactly matches the req
|
|
|
161
152
|
|
|
162
153
|
```console
|
|
163
154
|
npx --yes @rathnasgala/cli@latest scaffold \
|
|
164
|
-
--owner YOUR_GITHUB_USERNAME \
|
|
165
155
|
--repository YOUR_REPOSITORY_NAME \
|
|
166
156
|
--target ./YOUR_REPOSITORY_NAME \
|
|
167
|
-
--installation-id YOUR_INSTALLATION_ID \
|
|
168
157
|
--resume
|
|
169
158
|
```
|
|
170
159
|
|
|
@@ -237,9 +226,12 @@ Gala author tokens expire and do not use a refresh token. Run:
|
|
|
237
226
|
npx --yes @rathnasgala/cli@latest auth
|
|
238
227
|
```
|
|
239
228
|
|
|
240
|
-
### `
|
|
229
|
+
### `The Gala GitHub App is not installed on YOUR_ACCOUNT`
|
|
241
230
|
|
|
242
|
-
|
|
231
|
+
`scaffold` could not find an installation covering that account. At a terminal it prints the
|
|
232
|
+
installation page and waits; in CI it stops, because there is nobody to install it. Install the App
|
|
233
|
+
at [the installation page](https://github.com/apps/gala67-app/installations/new) and run `scaffold`
|
|
234
|
+
again, or pass `--installation-id` explicitly.
|
|
243
235
|
|
|
244
236
|
### The App cannot access the new repository
|
|
245
237
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which Gala GitHub App installation covers this writer's account.
|
|
3
|
+
*
|
|
4
|
+
* The installation ID is an internal GitHub identifier that `scaffold` has to send when it
|
|
5
|
+
* registers a site. Until now the writer supplied it by installing the App, watching GitHub
|
|
6
|
+
* redirect to `https://github.com/settings/installations/153144989`, and copying the number out of
|
|
7
|
+
* the address bar — an internal identifier, read out of a URL, by hand.
|
|
8
|
+
*
|
|
9
|
+
* The Gala API already knows it. `GET /v1/auth/github/repositories` answers with
|
|
10
|
+
* `{ installationId, owner, name, status }` per repository, so the CLI can ask the same service it
|
|
11
|
+
* is about to register with rather than guess. Reaching that endpoint needs the bounded capability
|
|
12
|
+
* from `POST /v1/auth/github/device-authorizations`, which is bound to the Gala user and takes the
|
|
13
|
+
* GitHub token the CLI already holds.
|
|
14
|
+
*/
|
|
15
|
+
function endpoint(apiBaseUrl, path) {
|
|
16
|
+
return `${String(apiBaseUrl).replace(/\/$/, '')}${path}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function exchangeGithubAuthorization({
|
|
20
|
+
apiBaseUrl, galaAccessToken, githubAccessToken, fetchImpl = fetch
|
|
21
|
+
}) {
|
|
22
|
+
const response = await fetchImpl(endpoint(apiBaseUrl, '/v1/auth/github/device-authorizations'), {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: {
|
|
25
|
+
accept: 'application/json',
|
|
26
|
+
authorization: `Bearer ${galaAccessToken}`,
|
|
27
|
+
'content-type': 'application/json'
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify({ accessToken: githubAccessToken })
|
|
30
|
+
});
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
throw new Error(`GitHub authorization exchange failed with HTTP ${response.status}`);
|
|
33
|
+
}
|
|
34
|
+
const payload = await response.json();
|
|
35
|
+
if (typeof payload?.authorization !== 'string') {
|
|
36
|
+
throw new TypeError('GitHub authorization exchange returned no capability');
|
|
37
|
+
}
|
|
38
|
+
return payload.authorization;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function listAuthorizedRepositories({ apiBaseUrl, authorization, fetchImpl = fetch }) {
|
|
42
|
+
const response = await fetchImpl(endpoint(apiBaseUrl, '/v1/auth/github/repositories'), {
|
|
43
|
+
headers: { accept: 'application/json', 'GitHub-Authorization': authorization }
|
|
44
|
+
});
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new Error(`Authorized repository lookup failed with HTTP ${response.status}`);
|
|
47
|
+
}
|
|
48
|
+
const payload = await response.json();
|
|
49
|
+
if (!Array.isArray(payload)) throw new TypeError('Authorized repository lookup returned no list');
|
|
50
|
+
return payload;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Returns the installation covering `owner`, or null when the App is not installed there.
|
|
55
|
+
*
|
|
56
|
+
* An installation belongs to an account, not to one repository, so any repository the App can
|
|
57
|
+
* already see under that owner carries the id the new one will use. Null is an ordinary answer —
|
|
58
|
+
* it means "not installed yet" — and the caller turns it into an instruction, not an error.
|
|
59
|
+
*/
|
|
60
|
+
export async function resolveInstallationId({
|
|
61
|
+
apiBaseUrl, galaAccessToken, githubAccessToken, owner, fetchImpl = fetch,
|
|
62
|
+
exchange = exchangeGithubAuthorization, list = listAuthorizedRepositories
|
|
63
|
+
}) {
|
|
64
|
+
const authorization = await exchange({
|
|
65
|
+
apiBaseUrl, galaAccessToken, githubAccessToken, fetchImpl
|
|
66
|
+
});
|
|
67
|
+
const repositories = await list({ apiBaseUrl, authorization, fetchImpl });
|
|
68
|
+
const wanted = String(owner).toLowerCase();
|
|
69
|
+
for (const repository of repositories) {
|
|
70
|
+
if (String(repository?.owner).toLowerCase() !== wanted) continue;
|
|
71
|
+
const installationId = Number(repository?.installationId);
|
|
72
|
+
if (Number.isSafeInteger(installationId) && installationId > 0) return installationId;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const GITHUB_API_VERSION = '2026-03-10';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The GitHub account the stored credential belongs to.
|
|
5
|
+
*
|
|
6
|
+
* `scaffold` used to make the writer pass `--owner`, which is a value the token already knows and
|
|
7
|
+
* they can only get wrong. The credential file holds the token and its scopes and nothing else, so
|
|
8
|
+
* this is a live lookup rather than something cached at `auth github` time — a login can be
|
|
9
|
+
* changed, and a stale one would create the repository under a name that no longer exists.
|
|
10
|
+
*/
|
|
11
|
+
export async function resolveGithubLogin({ accessToken, fetchImpl = fetch }) {
|
|
12
|
+
if (typeof accessToken !== 'string' || accessToken === '') {
|
|
13
|
+
throw new TypeError('accessToken is required');
|
|
14
|
+
}
|
|
15
|
+
const response = await fetchImpl('https://api.github.com/user', {
|
|
16
|
+
headers: {
|
|
17
|
+
accept: 'application/vnd.github+json',
|
|
18
|
+
authorization: `Bearer ${accessToken}`,
|
|
19
|
+
'x-github-api-version': GITHUB_API_VERSION
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) throw new Error(`GitHub account lookup failed with HTTP ${response.status}`);
|
|
23
|
+
const payload = await response.json();
|
|
24
|
+
const login = payload?.login;
|
|
25
|
+
// The same shape `scaffold` demands of `--owner`. Refusing here beats a confusing failure four
|
|
26
|
+
// API calls later.
|
|
27
|
+
if (typeof login !== 'string' || !/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/.test(login)) {
|
|
28
|
+
throw new TypeError('GitHub returned an unusable account login');
|
|
29
|
+
}
|
|
30
|
+
return login;
|
|
31
|
+
}
|
package/src/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import { createInterface } from 'node:readline/promises';
|
|
|
20
20
|
import { upgradeTheme } from './upgrade-command.js';
|
|
21
21
|
import { authenticateGithub } from './github-auth-command.js';
|
|
22
22
|
import { scaffoldSite } from './scaffold-site.js';
|
|
23
|
+
import { prepareScaffold } from './scaffold-preflight.js';
|
|
23
24
|
import { refreshEngagementSnapshot } from './refresh-command.js';
|
|
24
25
|
import { switchTopology } from './topology-command.js';
|
|
25
26
|
import { acquireAttributionEntitlement } from './entitlement-command.js';
|
|
@@ -77,17 +78,37 @@ if (command === 'auth') {
|
|
|
77
78
|
const index = args.indexOf(name);
|
|
78
79
|
return index === -1 ? undefined : args[index + 1];
|
|
79
80
|
};
|
|
80
|
-
const
|
|
81
|
+
const explicitInstallationId = valueFor('--installation-id');
|
|
81
82
|
const topology = valueFor('--topology') ?? 'provider-default';
|
|
82
|
-
const
|
|
83
|
+
const siteOptions = parseScaffoldOptions(args);
|
|
84
|
+
// Prompting only makes sense at a terminal. In CI there is nobody to answer, so a missing value
|
|
85
|
+
// has to stay a clear error rather than a process that hangs waiting for enter.
|
|
86
|
+
const interactive = process.stdin.isTTY === true;
|
|
87
|
+
const ask = interactive
|
|
88
|
+
? async (question) => {
|
|
89
|
+
const terminal = createInterface({ input: process.stdin, output: process.stdout });
|
|
90
|
+
try { return await terminal.question(question); } finally { terminal.close(); }
|
|
91
|
+
}
|
|
92
|
+
: undefined;
|
|
93
|
+
const prepared = await prepareScaffold({
|
|
83
94
|
owner: valueFor('--owner'),
|
|
84
95
|
repository: valueFor('--repository'),
|
|
85
96
|
target: valueFor('--target'),
|
|
86
|
-
githubInstallationId:
|
|
97
|
+
githubInstallationId: explicitInstallationId == null ? undefined : Number(explicitInstallationId),
|
|
98
|
+
siteName: siteOptions.siteName,
|
|
99
|
+
apiBaseUrl: valueFor('--api-base-url') ?? 'https://api.gala67.com',
|
|
100
|
+
notify: (message) => process.stdout.write(`${message}\n`),
|
|
101
|
+
ask
|
|
102
|
+
});
|
|
103
|
+
const result = await scaffoldSite({
|
|
104
|
+
owner: prepared.owner,
|
|
105
|
+
repository: prepared.repository,
|
|
106
|
+
target: prepared.target,
|
|
107
|
+
githubInstallationId: prepared.githubInstallationId,
|
|
87
108
|
topology,
|
|
88
109
|
canonicalBaseUrl: valueFor('--canonical-base-url'),
|
|
89
110
|
actionRef: valueFor('--action-ref'),
|
|
90
|
-
siteOptions
|
|
111
|
+
siteOptions,
|
|
91
112
|
buildMode: valueFor('--mode') ?? 'build-and-deploy',
|
|
92
113
|
emptyExistingRepository: args.includes('--empty-existing-repository'),
|
|
93
114
|
resumeExistingCheckout: args.includes('--resume')
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { authenticateGala } from './auth-command.js';
|
|
4
|
+
import { authenticateGithub } from './github-auth-command.js';
|
|
5
|
+
import { readGalaCredential } from './gala-credential-store.js';
|
|
6
|
+
import { readGithubCredential } from './github-credential-store.js';
|
|
7
|
+
import { resolveGithubLogin } from './github-identity.js';
|
|
8
|
+
import { resolveInstallationId } from './gala-installation-client.js';
|
|
9
|
+
|
|
10
|
+
export const GITHUB_APP_INSTALL_URL = 'https://github.com/apps/gala67-app/installations/new';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_API_BASE_URL = 'https://api.gala67.com';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Everything `scaffold` needs, worked out rather than demanded.
|
|
16
|
+
*
|
|
17
|
+
* `scaffold` used to require four values up front — `--owner`, `--repository`, `--target` and
|
|
18
|
+
* `--installation-id` — and it failed outright if `auth` or `auth github` had not been run first,
|
|
19
|
+
* telling the writer to go and run them. Three of those four are derivable and the two sign-ins
|
|
20
|
+
* can simply happen. Every one of them is still accepted as an explicit override; nothing that
|
|
21
|
+
* worked before stops working.
|
|
22
|
+
*
|
|
23
|
+
* The steps are ordered so nothing is created until everything is known: the App installation is
|
|
24
|
+
* confirmed before a repository exists, rather than after, so an interrupted run leaves no
|
|
25
|
+
* half-connected repository behind.
|
|
26
|
+
*/
|
|
27
|
+
export async function prepareScaffold({
|
|
28
|
+
owner,
|
|
29
|
+
repository,
|
|
30
|
+
target,
|
|
31
|
+
githubInstallationId,
|
|
32
|
+
siteName,
|
|
33
|
+
cwd = process.cwd(),
|
|
34
|
+
notify = () => {},
|
|
35
|
+
ask,
|
|
36
|
+
installUrl = GITHUB_APP_INSTALL_URL,
|
|
37
|
+
installAttempts = 3,
|
|
38
|
+
readGala = readGalaCredential,
|
|
39
|
+
readGithub = readGithubCredential,
|
|
40
|
+
signInGala = authenticateGala,
|
|
41
|
+
signInGithub = authenticateGithub,
|
|
42
|
+
resolveLogin = resolveGithubLogin,
|
|
43
|
+
resolveInstallation = resolveInstallationId,
|
|
44
|
+
apiBaseUrl = DEFAULT_API_BASE_URL
|
|
45
|
+
} = {}) {
|
|
46
|
+
const gala = await ensureGala({ apiBaseUrl, notify, readGala, signInGala });
|
|
47
|
+
const github = await ensureGithub({ notify, readGithub, signInGithub });
|
|
48
|
+
|
|
49
|
+
const resolvedOwner = owner ?? await resolveLogin({ accessToken: github.accessToken });
|
|
50
|
+
|
|
51
|
+
const resolvedRepository = repository
|
|
52
|
+
?? (target == null ? null : path.basename(path.resolve(cwd, target)))
|
|
53
|
+
?? repositoryNameFrom(siteName)
|
|
54
|
+
?? await askForRepository(ask);
|
|
55
|
+
|
|
56
|
+
// `--target ./` is the common case and means "here", so the repository takes its name from the
|
|
57
|
+
// directory the writer is standing in. Everywhere else the repository names its own folder.
|
|
58
|
+
const resolvedTarget = target ?? `./${resolvedRepository}`;
|
|
59
|
+
|
|
60
|
+
const resolvedInstallation = githubInstallationId
|
|
61
|
+
?? await ensureInstallation({
|
|
62
|
+
apiBaseUrl: gala.apiBaseUrl ?? apiBaseUrl,
|
|
63
|
+
galaAccessToken: gala.accessToken,
|
|
64
|
+
githubAccessToken: github.accessToken,
|
|
65
|
+
owner: resolvedOwner,
|
|
66
|
+
notify, ask, installUrl, installAttempts, resolveInstallation
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return Object.freeze({
|
|
70
|
+
owner: resolvedOwner,
|
|
71
|
+
repository: resolvedRepository,
|
|
72
|
+
target: resolvedTarget,
|
|
73
|
+
githubInstallationId: resolvedInstallation
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A missing or expired credential is a step to take, not an error to report. */
|
|
78
|
+
async function ensureGala({ apiBaseUrl, notify, readGala, signInGala }) {
|
|
79
|
+
try {
|
|
80
|
+
return await readGala();
|
|
81
|
+
} catch {
|
|
82
|
+
notify('Signing in to Gala.');
|
|
83
|
+
await signInGala({
|
|
84
|
+
apiBaseUrl,
|
|
85
|
+
showInstructions: ({ verificationUri, userCode }) =>
|
|
86
|
+
notify(`Open ${verificationUri}\nEnter code: ${userCode}`)
|
|
87
|
+
});
|
|
88
|
+
return readGala();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function ensureGithub({ notify, readGithub, signInGithub }) {
|
|
93
|
+
try {
|
|
94
|
+
return await readGithub();
|
|
95
|
+
} catch {
|
|
96
|
+
notify('Signing in to GitHub.');
|
|
97
|
+
await signInGithub({
|
|
98
|
+
showScopeWarning: ({ explanation }) => notify(`GitHub authorization: ${explanation}`),
|
|
99
|
+
showInstructions: ({ verificationUri, userCode }) =>
|
|
100
|
+
notify(`Open ${verificationUri}\nEnter code: ${userCode}`)
|
|
101
|
+
});
|
|
102
|
+
return readGithub();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Confirms the Gala GitHub App is installed, walking the writer through installing it if not.
|
|
108
|
+
*
|
|
109
|
+
* This is the step that used to be a manual detour through GitHub's settings to copy a number out
|
|
110
|
+
* of a redirect URL. The loop is what makes it a step rather than a failure: the writer installs
|
|
111
|
+
* the App in the browser, comes back, presses enter, and the run continues.
|
|
112
|
+
*/
|
|
113
|
+
async function ensureInstallation({
|
|
114
|
+
apiBaseUrl, galaAccessToken, githubAccessToken, owner,
|
|
115
|
+
notify, ask, installUrl, installAttempts, resolveInstallation
|
|
116
|
+
}) {
|
|
117
|
+
for (let attempt = 0; attempt < Math.max(1, installAttempts); attempt += 1) {
|
|
118
|
+
const installationId = await resolveInstallation({
|
|
119
|
+
apiBaseUrl, galaAccessToken, githubAccessToken, owner
|
|
120
|
+
});
|
|
121
|
+
if (installationId != null) return installationId;
|
|
122
|
+
|
|
123
|
+
if (typeof ask !== 'function') {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`The Gala GitHub App is not installed on ${owner}. Install it at ${installUrl} and run scaffold again, `
|
|
126
|
+
+ 'or pass --installation-id explicitly.'
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
notify(`The Gala GitHub App is not installed on ${owner} yet.\nOpen ${installUrl}`);
|
|
130
|
+
await ask('Press enter once the App is installed. ');
|
|
131
|
+
}
|
|
132
|
+
throw new Error(
|
|
133
|
+
`The Gala GitHub App still does not cover ${owner}. Install it at ${installUrl}, then run scaffold again.`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** GitHub repository names allow letters, digits, dot, underscore and hyphen, and nothing else. */
|
|
138
|
+
export function repositoryNameFrom(siteName) {
|
|
139
|
+
if (typeof siteName !== 'string') return null;
|
|
140
|
+
const slug = siteName
|
|
141
|
+
.trim().toLowerCase()
|
|
142
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
143
|
+
.replace(/^-+|-+$/g, '');
|
|
144
|
+
return slug === '' ? null : slug;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function askForRepository(ask) {
|
|
148
|
+
if (typeof ask !== 'function') {
|
|
149
|
+
throw new TypeError('repository is required; pass --repository or --site-name');
|
|
150
|
+
}
|
|
151
|
+
const answer = repositoryNameFrom(await ask('What should the publication repository be called? '));
|
|
152
|
+
if (answer == null) throw new TypeError('A repository name is required');
|
|
153
|
+
return answer;
|
|
154
|
+
}
|