@rathnasgala/cli 1.1.9 → 1.1.11
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 +8 -8
- package/package.json +2 -2
- package/src/api/gala.js +2 -2
- package/src/api/github.js +2 -2
- package/src/api/http.js +2 -2
- package/src/auth/gala.js +1 -1
- package/src/auth/github.js +2 -2
- package/src/auth/store.js +1 -1
- package/src/cli/terminal.js +2 -2
- package/src/commands/auth.js +4 -4
- package/src/commands/doctor.js +5 -5
- package/src/commands/init.js +5 -5
- package/src/commands/preview.js +41 -9
- package/src/commands/publish.js +4 -5
- package/src/commands-manifest.js +1 -1
- package/src/content.js +29 -7
- package/src/git.js +4 -6
- package/src/publication.js +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Gala CLI
|
|
2
2
|
|
|
3
|
-
Create a publication, write posts, preview them, and publish
|
|
3
|
+
Create a publication, write posts, preview them, and publish - from your terminal.
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
@@ -39,7 +39,7 @@ On the first run, it opens GitHub's Gala App installation page. Choose the accou
|
|
|
39
39
|
finish the installation, and return to the terminal; the same command resumes automatically.
|
|
40
40
|
|
|
41
41
|
If the Gala GitHub App has not been given access to the new repository, it says so and links to the
|
|
42
|
-
one page that grants it
|
|
42
|
+
one page that grants it - GitHub has no way for an app to grant itself access, so that click is
|
|
43
43
|
unavoidable. Everything else is automatic.
|
|
44
44
|
|
|
45
45
|
## Write
|
|
@@ -63,7 +63,7 @@ npx --yes @rathnasgala/cli@latest preview
|
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
Builds the publication and serves it locally, using the exact framework version the repository is
|
|
66
|
-
pinned to
|
|
66
|
+
pinned to - so what you see is what gets published. The first run installs that tooling, which takes
|
|
67
67
|
a moment. Stop it with Ctrl-C.
|
|
68
68
|
|
|
69
69
|
```console
|
|
@@ -118,7 +118,7 @@ npx --yes @rathnasgala/cli@latest upgrade
|
|
|
118
118
|
|
|
119
119
|
Reports on your sign-ins, the publication folder, the publishing workflow, and anything you have
|
|
120
120
|
written but not sent. Each check either passes, names what is wrong and how to fix it, or says it
|
|
121
|
-
could not be determined
|
|
121
|
+
could not be determined - never one of those disguised as another.
|
|
122
122
|
|
|
123
123
|
## Commands
|
|
124
124
|
|
|
@@ -134,13 +134,13 @@ Run any command with `--help`.
|
|
|
134
134
|
| `prism` | Manage author-approved reading configurations | `--root`, `--language`, `--depth`, `--intent`, `--modality`, `--file`, `--reason`, `--yes` |
|
|
135
135
|
| `upgrade` | Inspect and apply a verified managed-theme update | `--root`, `--channel`, `--yes` |
|
|
136
136
|
| `doctor` | Check a publication and say what is wrong | `--root` |
|
|
137
|
-
| `auth` | Sign in to Gala and GitHub |
|
|
137
|
+
| `auth` | Sign in to Gala and GitHub | - |
|
|
138
138
|
|
|
139
139
|
`auth` is never a prerequisite you have to remember: any command that needs a credential obtains
|
|
140
|
-
one. It exists for when you want to do it deliberately
|
|
140
|
+
one. It exists for when you want to do it deliberately - a new machine, or a different account.
|
|
141
141
|
|
|
142
142
|
Every command prompts for what it needs when run in a terminal, and every prompt has an option that
|
|
143
|
-
supplies it instead. With no terminal attached
|
|
143
|
+
supplies it instead. With no terminal attached - in CI - nothing is ever prompted for: a value that
|
|
144
144
|
cannot be worked out is an error naming the option, so an automated run fails immediately rather
|
|
145
145
|
than waiting for an answer that will not come.
|
|
146
146
|
|
|
@@ -155,7 +155,7 @@ directory, with private file permissions. Your GitHub sign-in expires after eigh
|
|
|
155
155
|
asks you to sign in again rather than quietly using a credential the server will refuse.
|
|
156
156
|
|
|
157
157
|
Git operations use that same sign-in, not whatever credential your machine happens to have
|
|
158
|
-
configured
|
|
158
|
+
configured - so publishing works on a machine where those differ, or where none is configured.
|
|
159
159
|
|
|
160
160
|
## Troubleshooting
|
|
161
161
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rathnasgala/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src"
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"url": "git+https://github.com/rathnasgala/cli.git"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@rathnasgala/content-validation": "0.0.
|
|
29
|
+
"@rathnasgala/content-validation": "0.0.28",
|
|
30
30
|
"libsodium-wrappers": "0.8.4",
|
|
31
31
|
"tar": "7.5.22",
|
|
32
32
|
"yaml": "2.9.0"
|
package/src/api/gala.js
CHANGED
|
@@ -3,8 +3,8 @@ import { request, requestJson } from './http.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* The Gala API.
|
|
5
5
|
*
|
|
6
|
-
* Only the calls the six commands make. v0 carried a generated client covering the whole surface
|
|
7
|
-
* comments, reactions, admin, moderation
|
|
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
8
|
*/
|
|
9
9
|
export const DEFAULT_API_BASE_URL = 'https://api.gala67.com';
|
|
10
10
|
|
package/src/api/github.js
CHANGED
|
@@ -5,7 +5,7 @@ import { requestJson } from './http.js';
|
|
|
5
5
|
*
|
|
6
6
|
* The CLI holds a GitHub App user token, not an OAuth App token. That single difference is what
|
|
7
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
|
|
8
|
+
* OAuth App restrictions, and reaches only repositories the App has been given - rather than every
|
|
9
9
|
* repository the writer can see, which is what `repo` scope meant.
|
|
10
10
|
*/
|
|
11
11
|
const API = 'https://api.github.com';
|
|
@@ -33,7 +33,7 @@ export function githubApi(token) {
|
|
|
33
33
|
* Whether a repository has content yet.
|
|
34
34
|
*
|
|
35
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
|
|
36
|
+
* lands. Cloning into that window produces an empty checkout and a missing site.config.yml -
|
|
37
37
|
* a confusing error about a file the template certainly contains. `size` is not usable as the
|
|
38
38
|
* signal; GitHub still reported 0 for a repository that already had commits.
|
|
39
39
|
*/
|
package/src/api/http.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Every HTTP call the CLI makes, and every failure it reports.
|
|
3
3
|
*
|
|
4
|
-
* v0 threw away the response body at all twenty failure sites
|
|
4
|
+
* v0 threw away the response body at all twenty failure sites - `failed with HTTP 403` and nothing
|
|
5
5
|
* else. GitHub explains which 403 it is in that body and nowhere else: an organisation's OAuth App
|
|
6
6
|
* restrictions, a missing permission, a rename and a rate limit all arrive as 403 with a sentence
|
|
7
7
|
* telling them apart. Diagnosing anything meant guessing between causes the server had already
|
|
@@ -67,6 +67,6 @@ function describe(body) {
|
|
|
67
67
|
if (parts.length === 0 && typeof body.raw === 'string') parts.push(body.raw);
|
|
68
68
|
if (typeof body.documentation_url === 'string') parts.push(`See ${body.documentation_url}`);
|
|
69
69
|
|
|
70
|
-
const detail = parts.join('
|
|
70
|
+
const detail = parts.join(' - ').replace(/\s+/g, ' ').trim();
|
|
71
71
|
return detail.length > MAX_DETAIL ? `${detail.slice(0, MAX_DETAIL)}…` : detail;
|
|
72
72
|
}
|
package/src/auth/gala.js
CHANGED
|
@@ -5,7 +5,7 @@ import { credentialPath, forgetCredential, readCredential, writeCredential } fro
|
|
|
5
5
|
* The Gala sign-in.
|
|
6
6
|
*
|
|
7
7
|
* A stored credential is checked against the server before anything depends on it, because one
|
|
8
|
-
* that parses and has not expired can still be one the API refuses
|
|
8
|
+
* that parses and has not expired can still be one the API refuses - and discovering that four
|
|
9
9
|
* calls later, as an opaque 401 from whichever endpoint got there first, is how a "sign in again"
|
|
10
10
|
* became a stack trace.
|
|
11
11
|
*/
|
package/src/auth/github.js
CHANGED
|
@@ -26,7 +26,7 @@ export async function githubCredential({
|
|
|
26
26
|
if (stored != null) return stored;
|
|
27
27
|
|
|
28
28
|
// No scopes. A GitHub App's permissions are fixed on the app and granted when the writer installs
|
|
29
|
-
// it, so there is nothing to negotiate
|
|
29
|
+
// it, so there is nothing to negotiate - and nothing to warn them about, which is why the broad
|
|
30
30
|
// "read/write on every repository you can access" notice is gone.
|
|
31
31
|
const authorization = await post(DEVICE_CODE_URL, { client_id: clientId }, 'GitHub sign-in');
|
|
32
32
|
terminal.step('Sign in to GitHub');
|
|
@@ -37,7 +37,7 @@ export async function githubCredential({
|
|
|
37
37
|
await writeCredential(target, {
|
|
38
38
|
accessToken: token.access_token,
|
|
39
39
|
// The app expires user tokens after eight hours and issues a refresh token with each. Exchanging
|
|
40
|
-
// one needs the app's client secret, which a published CLI cannot hold
|
|
40
|
+
// one needs the app's client secret, which a published CLI cannot hold - so it is kept for the
|
|
41
41
|
// API-side refresh, and until that exists an expired credential asks for one sign-in rather
|
|
42
42
|
// than failing as an unexplained 401 somewhere deeper.
|
|
43
43
|
...(token.expires_in ? { expiresAt: new Date(now() + token.expires_in * 1000).toISOString() } : {}),
|
package/src/auth/store.js
CHANGED
|
@@ -8,7 +8,7 @@ import path from 'node:path';
|
|
|
8
8
|
* Two rules matter more than the storage itself, both learned the hard way:
|
|
9
9
|
*
|
|
10
10
|
* A credential is only valid if the server still accepts it. v0 checked expiry and nothing else, so
|
|
11
|
-
* a token the API had already decided to refuse
|
|
11
|
+
* a token the API had already decided to refuse - it stopped issuing a claim these carried - was
|
|
12
12
|
* handed out for weeks, and every command failed as an unexplained 401 several calls deep.
|
|
13
13
|
*
|
|
14
14
|
* A credential whose shape has changed is not upgradable in place. Bumping `schemaVersion` and
|
package/src/cli/terminal.js
CHANGED
|
@@ -7,7 +7,7 @@ import { spawn } from 'node:child_process';
|
|
|
7
7
|
* v0 wrote to stdout from a dozen modules with no shared shape, so a scaffold emitted raw git
|
|
8
8
|
* output, bare status codes and half-sentences in whatever order they happened to occur. The
|
|
9
9
|
* failure mode that cost the most was subtler: prompts and browser launches were decided
|
|
10
|
-
* separately in each module, so behaviour with no terminal attached varied by code path
|
|
10
|
+
* separately in each module, so behaviour with no terminal attached varied by code path - some
|
|
11
11
|
* hung, some crashed, some silently skipped.
|
|
12
12
|
*
|
|
13
13
|
* One object, created once, knows whether there is a terminal. Nothing else has to ask.
|
|
@@ -50,7 +50,7 @@ export function createTerminal({
|
|
|
50
50
|
async ask(question, { fallback } = {}) {
|
|
51
51
|
if (!interactive) {
|
|
52
52
|
if (fallback !== undefined) return fallback;
|
|
53
|
-
throw new Error(`${question}
|
|
53
|
+
throw new Error(`${question} - no terminal to ask; pass it as an option instead`);
|
|
54
54
|
}
|
|
55
55
|
const reader = createInterface({ input, output });
|
|
56
56
|
try {
|
package/src/commands/auth.js
CHANGED
|
@@ -5,7 +5,7 @@ import { githubCredential } from '../auth/github.js';
|
|
|
5
5
|
* Signs in to both, and says so.
|
|
6
6
|
*
|
|
7
7
|
* Not a prerequisite the writer has to remember: every command that needs a credential obtains one.
|
|
8
|
-
* This exists for the times they want to do it deliberately
|
|
8
|
+
* This exists for the times they want to do it deliberately - a new machine, a different account,
|
|
9
9
|
* or after a token has expired.
|
|
10
10
|
*/
|
|
11
11
|
export async function auth({ terminal, options }) {
|
|
@@ -13,8 +13,8 @@ export async function auth({ terminal, options }) {
|
|
|
13
13
|
const github = await githubCredential({ terminal });
|
|
14
14
|
|
|
15
15
|
terminal.blank();
|
|
16
|
-
terminal.done(`Gala
|
|
16
|
+
terminal.done(`Gala - valid until ${new Date(gala.expiresAt).toLocaleString()}`);
|
|
17
17
|
terminal.done(github.expiresAt
|
|
18
|
-
? `GitHub
|
|
19
|
-
: 'GitHub
|
|
18
|
+
? `GitHub - valid until ${new Date(github.expiresAt).toLocaleString()}`
|
|
19
|
+
: 'GitHub - signed in');
|
|
20
20
|
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -12,7 +12,7 @@ import { createGit } from '../git.js';
|
|
|
12
12
|
*
|
|
13
13
|
* Every check reports one of three things and never guesses between them: it is fine, it is wrong
|
|
14
14
|
* and here is the fix, or it could not be determined. That third state is the one v0 kept
|
|
15
|
-
* collapsing into the second
|
|
15
|
+
* collapsing into the second - an unreachable GitHub reported as "the App is not installed" sent
|
|
16
16
|
* writers to install something that was already installed, repeatedly.
|
|
17
17
|
*/
|
|
18
18
|
export async function doctor({ terminal, options, cwd = process.cwd() }) {
|
|
@@ -46,7 +46,7 @@ export async function doctor({ terminal, options, cwd = process.cwd() }) {
|
|
|
46
46
|
const source = await readFile(workflow, 'utf8');
|
|
47
47
|
const siteId = /site-id:\s*([0-9A-Z]{26})/.exec(source)?.[1];
|
|
48
48
|
return siteId == null
|
|
49
|
-
? wrong('no site id
|
|
49
|
+
? wrong('no site id - this publication may not be registered', cliCommand('init'))
|
|
50
50
|
: ok(siteId);
|
|
51
51
|
}, 'the workflow is missing; registration writes it'));
|
|
52
52
|
|
|
@@ -61,9 +61,9 @@ export async function doctor({ terminal, options, cwd = process.cwd() }) {
|
|
|
61
61
|
|
|
62
62
|
terminal.blank();
|
|
63
63
|
for (const { name, state, detail, fix } of checks) {
|
|
64
|
-
if (state === 'ok') terminal.done(`${name}
|
|
65
|
-
else if (state === 'wrong') terminal.fail(`${name}
|
|
66
|
-
else terminal.step(`${name}
|
|
64
|
+
if (state === 'ok') terminal.done(`${name} - ${detail}`);
|
|
65
|
+
else if (state === 'wrong') terminal.fail(`${name} - ${detail}`);
|
|
66
|
+
else terminal.step(`${name} - could not be determined: ${detail}`);
|
|
67
67
|
if (fix) terminal.note(fix);
|
|
68
68
|
}
|
|
69
69
|
|
package/src/commands/init.js
CHANGED
|
@@ -22,7 +22,7 @@ import { customDomain } from '../domain.js';
|
|
|
22
22
|
* which is why repositories it created never appeared in the web UI.
|
|
23
23
|
* - The **server** writes `site.config.yml` and the publish workflow during registration. The CLI
|
|
24
24
|
* used to write its own versions afterwards and commit them, producing a second commit whose
|
|
25
|
-
* entire content was rewriting one line and stripping comments
|
|
25
|
+
* entire content was rewriting one line and stripping comments - and a second workflow run that
|
|
26
26
|
* collided with the first one's deployment record and failed.
|
|
27
27
|
* - **GitHub** turns on Pages by itself once publishing creates a `gh-pages` branch. The CLI used
|
|
28
28
|
* to poll ten minutes for a run it had caused, then call an API that changed nothing.
|
|
@@ -120,7 +120,7 @@ export async function init({ terminal, options, cwd = process.cwd() }) {
|
|
|
120
120
|
* Creation is a conversation, not a single call.
|
|
121
121
|
*
|
|
122
122
|
* `NEEDS_SHARING` means the repository exists with the right content and the App installation
|
|
123
|
-
* simply cannot see it
|
|
123
|
+
* simply cannot see it - an installation scoped to selected repositories, which is the right way to
|
|
124
124
|
* have it. That is one grant away from working, and GitHub offers no API to do it on the writer's
|
|
125
125
|
* behalf: adding a repository to an installation is documented as classic-PAT-only. So it is asked
|
|
126
126
|
* for, with a link to the one page that grants it.
|
|
@@ -152,7 +152,7 @@ async function createPublication({ terminal, api, capability, name, github, inst
|
|
|
152
152
|
const owner = result?.owner ?? '';
|
|
153
153
|
terminal.blank();
|
|
154
154
|
terminal.step(`${owner}/${result?.name ?? name} exists, but Gala cannot reach it yet`);
|
|
155
|
-
terminal.note('its installation covers only selected repositories
|
|
155
|
+
terminal.note('its installation covers only selected repositories - add this one');
|
|
156
156
|
terminal.openUrl(result?.recoveryUrl
|
|
157
157
|
?? installationUrl(result?.installationId, owner, await viewerOf(github)));
|
|
158
158
|
if (!await terminal.waitForEnter('Once Gala can access it')) {
|
|
@@ -218,7 +218,7 @@ export function installationUrl(installationId, owner, selfLogin) {
|
|
|
218
218
|
|
|
219
219
|
/**
|
|
220
220
|
* GitHub answers the creation call before the template content lands. Cloning into that window
|
|
221
|
-
* gives an empty checkout and a missing site.config.yml
|
|
221
|
+
* gives an empty checkout and a missing site.config.yml - a confusing error about a file the
|
|
222
222
|
* template certainly contains.
|
|
223
223
|
*/
|
|
224
224
|
async function waitForContent(github, owner, name, { attempts = 30, intervalMs = 1000 } = {}) {
|
|
@@ -234,7 +234,7 @@ async function publicationName({ terminal, explicitName, directory }) {
|
|
|
234
234
|
const answer = proposed ?? await terminal.ask('What should this publication be called?');
|
|
235
235
|
const name = slugify(answer);
|
|
236
236
|
if (name == null) {
|
|
237
|
-
throw new UsageError(`"${answer}" cannot be a repository name
|
|
237
|
+
throw new UsageError(`"${answer}" cannot be a repository name - use letters, numbers and hyphens`);
|
|
238
238
|
}
|
|
239
239
|
return name;
|
|
240
240
|
}
|
package/src/commands/preview.js
CHANGED
|
@@ -9,7 +9,7 @@ import { readPublication } from '../publication.js';
|
|
|
9
9
|
* Builds the site and serves it locally.
|
|
10
10
|
*
|
|
11
11
|
* Eleventy is run from the publication's own `node_modules`, so the preview uses the exact
|
|
12
|
-
* framework version the repository is pinned to
|
|
12
|
+
* framework version the repository is pinned to - the same one the publish workflow will use. A
|
|
13
13
|
* preview that agrees with the local machine but not with production is worse than no preview.
|
|
14
14
|
*
|
|
15
15
|
* That pin is also why this installs dependencies when they are missing. A freshly cloned
|
|
@@ -24,21 +24,27 @@ export async function preview({
|
|
|
24
24
|
const today = options.value('today');
|
|
25
25
|
|
|
26
26
|
terminal.step('Checking content');
|
|
27
|
-
await checkContent({
|
|
27
|
+
await checkContent({
|
|
28
|
+
terminal, root, today, preview: true, ...(regenerate == null ? {} : { regenerate })
|
|
29
|
+
});
|
|
28
30
|
terminal.done('Content is valid');
|
|
29
31
|
|
|
30
32
|
const eleventy = path.join(root, 'node_modules', '@11ty', 'eleventy', 'cmd.cjs');
|
|
31
33
|
if (!await exists(eleventy)) {
|
|
32
|
-
terminal.step('Installing what this publication needs
|
|
34
|
+
terminal.step('Installing what this publication needs - first time only');
|
|
33
35
|
await install(root, spawnProcess);
|
|
34
36
|
if (!await exists(eleventy)) {
|
|
35
|
-
throw new Error('
|
|
37
|
+
throw new Error('Gala installed the publication dependencies, but the preview tool is still missing. Restore the managed package.json and package-lock.json files, then run preview again.');
|
|
36
38
|
}
|
|
37
39
|
terminal.done('Installed');
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
terminal.step('Preparing the preview');
|
|
43
|
+
await buildReader(root, spawnProcess);
|
|
44
|
+
terminal.done('Preview is ready');
|
|
45
|
+
|
|
40
46
|
const publication = await readPublication(root);
|
|
41
|
-
terminal.step('Starting the preview
|
|
47
|
+
terminal.step('Starting the preview - stop it with Ctrl-C');
|
|
42
48
|
if (publication != null) terminal.note(`this is ${publication.name ?? 'your publication'} as it will look`);
|
|
43
49
|
terminal.blank();
|
|
44
50
|
|
|
@@ -50,12 +56,38 @@ export async function preview({
|
|
|
50
56
|
});
|
|
51
57
|
|
|
52
58
|
return new Promise((resolve, reject) => {
|
|
53
|
-
child.once('error',
|
|
59
|
+
child.once('error', (error) => {
|
|
60
|
+
reject(new Error(`The preview could not start: ${error.message}`));
|
|
61
|
+
});
|
|
54
62
|
child.once('exit', (code, signal) => {
|
|
55
63
|
// Ctrl-C is how a writer stops a preview; it is not a failure to report.
|
|
56
64
|
if (signal === 'SIGINT' || signal === 'SIGTERM' || code === 0 || code === 130) resolve();
|
|
57
|
-
else if (signal)
|
|
58
|
-
|
|
65
|
+
else if (signal) {
|
|
66
|
+
reject(new Error(`The preview process was stopped by ${signal}. Review the build output above, correct the reported problem, and run preview again.`));
|
|
67
|
+
} else {
|
|
68
|
+
reject(new Error(`The preview build failed with exit code ${code}. Review the build output above, correct the reported problem, and run preview again.`));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildReader(root, spawnProcess) {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
const child = spawnProcess('npm', ['run', 'build:reader'], {
|
|
77
|
+
cwd: root, shell: false, stdio: ['ignore', 'pipe', 'pipe']
|
|
78
|
+
});
|
|
79
|
+
let said = '';
|
|
80
|
+
child.stdout?.on('data', (chunk) => { said += chunk; });
|
|
81
|
+
child.stderr?.on('data', (chunk) => { said += chunk; });
|
|
82
|
+
child.once('error', reject);
|
|
83
|
+
child.once('exit', (code) => {
|
|
84
|
+
if (code === 0) {
|
|
85
|
+
resolve();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const failure = new Error('Gala could not prepare the publication styles and reader tools. The build output below contains the cause.');
|
|
89
|
+
failure.detail = said.trim();
|
|
90
|
+
reject(failure);
|
|
59
91
|
});
|
|
60
92
|
});
|
|
61
93
|
}
|
|
@@ -84,7 +116,7 @@ function install(root, spawnProcess) {
|
|
|
84
116
|
resolve();
|
|
85
117
|
return;
|
|
86
118
|
}
|
|
87
|
-
const failure = new Error('
|
|
119
|
+
const failure = new Error('Gala could not install the preview dependencies. The package-manager output below contains the cause.');
|
|
88
120
|
failure.detail = said.trim();
|
|
89
121
|
reject(failure);
|
|
90
122
|
});
|
package/src/commands/publish.js
CHANGED
|
@@ -8,16 +8,15 @@ import { readPublication } from '../publication.js';
|
|
|
8
8
|
/**
|
|
9
9
|
* Validates, records the writer's work, and sends it to GitHub.
|
|
10
10
|
*
|
|
11
|
-
* v0 only sent
|
|
11
|
+
* v0 only sent - the writer had to remember to record their changes first, and a run that appeared
|
|
12
12
|
* to succeed could ship nothing at all. It also used the machine's git credential rather than the
|
|
13
13
|
* one the CLI holds, which fails for anyone whose accounts differ.
|
|
14
14
|
*
|
|
15
15
|
* The order below is the whole of the difficulty. Every successful publish adds a deployment record
|
|
16
16
|
* to the branch from the workflow, so the checkout is behind by one before the writer has touched
|
|
17
|
-
* anything
|
|
18
|
-
* validation, because validation assigns a content id to any post missing one
|
|
19
|
-
*
|
|
20
|
-
* ids, which git can only report as a conflict.
|
|
17
|
+
* anything - catching up is the normal condition, not a race fix. It has to happen *before*
|
|
18
|
+
* validation, because publish validation assigns a content id to any post still missing one. Local
|
|
19
|
+
* preview is deliberately read-only and can therefore never create a competing ID before this sync.
|
|
21
20
|
*
|
|
22
21
|
* Publishing itself happens on GitHub: the workflow in the repository builds and deploys. This
|
|
23
22
|
* command's job ends when the work is on the branch.
|
package/src/commands-manifest.js
CHANGED
|
@@ -18,7 +18,7 @@ import { cliCommand } from './cli/invocation.js';
|
|
|
18
18
|
* Each was a surface to keep correct and a way to be wrong.
|
|
19
19
|
*
|
|
20
20
|
* This lives apart from the dispatcher so the README can be checked against it. v0's README
|
|
21
|
-
* outlived its commands by weeks
|
|
21
|
+
* outlived its commands by weeks - it taught a command that never existed - because nothing tied
|
|
22
22
|
* the two together.
|
|
23
23
|
*
|
|
24
24
|
* Each entry carries its own options. Nothing is parsed globally, so an option cannot mean two
|
package/src/content.js
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
1
3
|
import { regenerateBuildManifest } from '@rathnasgala/content-validation';
|
|
2
4
|
|
|
5
|
+
function shownPath(root, file) {
|
|
6
|
+
if (!path.isAbsolute(file)) return file;
|
|
7
|
+
const relative = path.relative(root, file);
|
|
8
|
+
return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative)
|
|
9
|
+
? relative
|
|
10
|
+
: file;
|
|
11
|
+
}
|
|
12
|
+
|
|
3
13
|
/**
|
|
4
14
|
* Content validation, shared by preview and publish.
|
|
5
15
|
*
|
|
6
16
|
* v0 exposed this as its own `validate` command and then called it from two others, so a writer had
|
|
7
|
-
* three ways to learn the same thing and one of them
|
|
17
|
+
* three ways to learn the same thing and one of them - the pre-push hook - ran it behind their back.
|
|
8
18
|
* Validation is not a task; it is a precondition of showing or shipping. It runs where those happen
|
|
9
19
|
* and nowhere else.
|
|
10
20
|
*
|
|
@@ -12,20 +22,32 @@ import { regenerateBuildManifest } from '@rathnasgala/content-validation';
|
|
|
12
22
|
* theme package installed in the publication's node_modules. That belongs in a test of the
|
|
13
23
|
* validator, not of how this reports what it found.
|
|
14
24
|
*/
|
|
15
|
-
export async function checkContent({
|
|
16
|
-
|
|
25
|
+
export async function checkContent({
|
|
26
|
+
terminal, root, today, preview = false, regenerate = regenerateBuildManifest
|
|
27
|
+
}) {
|
|
28
|
+
const { results } = await regenerate({ root, today, preview });
|
|
17
29
|
const failed = results.filter(({ errors }) => errors.length > 0);
|
|
18
30
|
|
|
19
31
|
for (const result of results) {
|
|
20
|
-
for (const warning of result.warnings ?? [])
|
|
32
|
+
for (const warning of result.warnings ?? []) {
|
|
33
|
+
terminal.note(`${shownPath(root, result.file)} - ${warning}`);
|
|
34
|
+
}
|
|
21
35
|
}
|
|
22
36
|
if (failed.length === 0) return results;
|
|
23
37
|
|
|
24
38
|
// Every problem, in one pass. Stopping at the first means fixing one thing, running again, and
|
|
25
39
|
// only then learning about the next.
|
|
26
|
-
|
|
40
|
+
const details = [];
|
|
27
41
|
for (const result of failed) {
|
|
28
|
-
|
|
42
|
+
details.push(shownPath(root, result.file));
|
|
43
|
+
for (const error of result.errors) details.push(` - ${error}`);
|
|
29
44
|
}
|
|
30
|
-
|
|
45
|
+
const problemCount = failed.reduce((count, result) => count + result.errors.length, 0);
|
|
46
|
+
details.push(`Fix ${problemCount === 1 ? 'this problem' : 'these problems'}, then run the command again.`);
|
|
47
|
+
const failure = new Error(
|
|
48
|
+
`Content check failed: ${problemCount} problem${problemCount === 1 ? '' : 's'} `
|
|
49
|
+
+ `in ${failed.length} post${failed.length === 1 ? '' : 's'}`
|
|
50
|
+
);
|
|
51
|
+
failure.detail = details.join('\n');
|
|
52
|
+
throw failure;
|
|
31
53
|
}
|
package/src/git.js
CHANGED
|
@@ -8,7 +8,7 @@ import path from 'node:path';
|
|
|
8
8
|
* which is a different identity from the one the CLI just authenticated with. On the machine where
|
|
9
9
|
* this surfaced the token belonged to one account and git's stored credential to another, so a
|
|
10
10
|
* scaffold created the repository through the API and was then refused when it tried to write to
|
|
11
|
-
* it. Anyone with no credential configured at all
|
|
11
|
+
* it. Anyone with no credential configured at all - a fresh machine, or SSH-only - had no chance.
|
|
12
12
|
*
|
|
13
13
|
* The token travels in the environment, never in the argument list, because arguments are readable
|
|
14
14
|
* machine-wide through `ps`. Nothing is written to `.git/config`.
|
|
@@ -100,11 +100,9 @@ export function createGit({ root, token, spawnProcess = spawn } = {}) {
|
|
|
100
100
|
* Brings the remote's commits in, over the top of anything uncommitted.
|
|
101
101
|
*
|
|
102
102
|
* `--autostash` matters: this runs before the writer's work is recorded, and a rebase refuses a
|
|
103
|
-
* dirty tree. Doing it the other way round
|
|
104
|
-
*
|
|
105
|
-
* the publish
|
|
106
|
-
* ids, and git can only call that a conflict. Taking the remote first means the local pass sees
|
|
107
|
-
* ids that already exist and changes nothing.
|
|
103
|
+
* dirty tree. Doing it the other way round - record first, then rebase - lets the publication
|
|
104
|
+
* workflow and the local publish independently update the same file. Taking the remote first
|
|
105
|
+
* makes the post-publish validation pass the sole local writer of any missing content ID.
|
|
108
106
|
*/
|
|
109
107
|
async takeRemote() {
|
|
110
108
|
const branch = await git.branch();
|
package/src/publication.js
CHANGED
|
@@ -8,8 +8,8 @@ import { parse } from 'yaml';
|
|
|
8
8
|
*
|
|
9
9
|
* The CLI knew every part of this and never said it: `new` printed a file path, `publish` said the
|
|
10
10
|
* site would appear shortly, and the writer was left to assemble
|
|
11
|
-
* `{canonicalBaseUrl}{pathPrefix}/{language}/{slug}/` themselves. That is easy to get wrong
|
|
12
|
-
* language segment is not obvious from anything they typed
|
|
11
|
+
* `{canonicalBaseUrl}{pathPrefix}/{language}/{slug}/` themselves. That is easy to get wrong - the
|
|
12
|
+
* language segment is not obvious from anything they typed - and being unable to find your own post
|
|
13
13
|
* is a poor first minute with a publishing tool.
|
|
14
14
|
*/
|
|
15
15
|
export async function readPublication(root) {
|