@rathnasgala/cli 1.1.8 → 1.1.10
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 +1 -1
- 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/invocation.js +9 -0
- package/src/cli/terminal.js +2 -2
- package/src/commands/auth.js +4 -4
- package/src/commands/doctor.js +9 -8
- package/src/commands/domain.js +6 -5
- package/src/commands/init.js +7 -10
- package/src/commands/new.js +2 -1
- package/src/commands/preview.js +16 -9
- package/src/commands/prism.js +17 -13
- package/src/commands/publish.js +4 -5
- package/src/commands/upgrade.js +4 -2
- package/src/commands-manifest.js +7 -6
- package/src/content.js +29 -7
- package/src/git.js +4 -6
- package/src/index.js +4 -2
- 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
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
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const CLI_INVOCATION = 'npx --yes @rathnasgala/cli@latest';
|
|
2
|
+
|
|
3
|
+
export function cliCommand(argumentsText = '') {
|
|
4
|
+
return argumentsText === '' ? CLI_INVOCATION : `${CLI_INVOCATION} ${argumentsText}`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function shellArgument(value) {
|
|
8
|
+
return /^[A-Za-z0-9_./-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
9
|
+
}
|
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
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { galaApi } from '../api/gala.js';
|
|
5
5
|
import { galaCredential } from '../auth/gala.js';
|
|
6
6
|
import { githubCredential } from '../auth/github.js';
|
|
7
|
+
import { cliCommand } from '../cli/invocation.js';
|
|
7
8
|
import { createGit } from '../git.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -11,7 +12,7 @@ import { createGit } from '../git.js';
|
|
|
11
12
|
*
|
|
12
13
|
* Every check reports one of three things and never guesses between them: it is fine, it is wrong
|
|
13
14
|
* and here is the fix, or it could not be determined. That third state is the one v0 kept
|
|
14
|
-
* collapsing into the second
|
|
15
|
+
* collapsing into the second - an unreachable GitHub reported as "the App is not installed" sent
|
|
15
16
|
* writers to install something that was already installed, repeatedly.
|
|
16
17
|
*/
|
|
17
18
|
export async function doctor({ terminal, options, cwd = process.cwd() }) {
|
|
@@ -23,7 +24,7 @@ export async function doctor({ terminal, options, cwd = process.cwd() }) {
|
|
|
23
24
|
const accepted = await galaApi({ baseUrl: gala.apiBaseUrl, token: gala.accessToken }).accepted();
|
|
24
25
|
return accepted
|
|
25
26
|
? ok(`valid until ${new Date(gala.expiresAt).toLocaleString()}`)
|
|
26
|
-
: wrong('the API no longer accepts it', '
|
|
27
|
+
: wrong('the API no longer accepts it', cliCommand('auth'));
|
|
27
28
|
}));
|
|
28
29
|
|
|
29
30
|
checks.push(await checkCredential('GitHub sign-in', async () => {
|
|
@@ -45,7 +46,7 @@ export async function doctor({ terminal, options, cwd = process.cwd() }) {
|
|
|
45
46
|
const source = await readFile(workflow, 'utf8');
|
|
46
47
|
const siteId = /site-id:\s*([0-9A-Z]{26})/.exec(source)?.[1];
|
|
47
48
|
return siteId == null
|
|
48
|
-
? wrong('no site id
|
|
49
|
+
? wrong('no site id - this publication may not be registered', cliCommand('init'))
|
|
49
50
|
: ok(siteId);
|
|
50
51
|
}, 'the workflow is missing; registration writes it'));
|
|
51
52
|
|
|
@@ -53,16 +54,16 @@ export async function doctor({ terminal, options, cwd = process.cwd() }) {
|
|
|
53
54
|
const git = createGit({ root });
|
|
54
55
|
const dirty = await git.run(['status', '--porcelain'], { capture: true });
|
|
55
56
|
const ahead = await git.run(['rev-list', '--count', '@{upstream}..HEAD'], { capture: true, allow: [0, 128] });
|
|
56
|
-
if (dirty !== '') return wrong(`${dirty.split('\n').length} file(s) not recorded`, '
|
|
57
|
-
if (ahead !== '' && ahead !== '0') return wrong(`${ahead} commit(s) not sent`, '
|
|
57
|
+
if (dirty !== '') return wrong(`${dirty.split('\n').length} file(s) not recorded`, cliCommand('publish'));
|
|
58
|
+
if (ahead !== '' && ahead !== '0') return wrong(`${ahead} commit(s) not sent`, cliCommand('publish'));
|
|
58
59
|
return ok('everything is on GitHub');
|
|
59
60
|
}, 'this folder is not a git checkout'));
|
|
60
61
|
|
|
61
62
|
terminal.blank();
|
|
62
63
|
for (const { name, state, detail, fix } of checks) {
|
|
63
|
-
if (state === 'ok') terminal.done(`${name}
|
|
64
|
-
else if (state === 'wrong') terminal.fail(`${name}
|
|
65
|
-
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}`);
|
|
66
67
|
if (fix) terminal.note(fix);
|
|
67
68
|
}
|
|
68
69
|
|
package/src/commands/domain.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { galaApi } from '../api/gala.js';
|
|
4
4
|
import { galaCredential } from '../auth/gala.js';
|
|
5
5
|
import { UsageError } from '../cli/args.js';
|
|
6
|
+
import { cliCommand } from '../cli/invocation.js';
|
|
6
7
|
import { customDomain } from '../domain.js';
|
|
7
8
|
import { readPublication } from '../publication.js';
|
|
8
9
|
|
|
@@ -16,7 +17,7 @@ export async function domain({ terminal, options, cwd = process.cwd() }) {
|
|
|
16
17
|
}
|
|
17
18
|
const [action = 'status', value, ...extra] = options.positional;
|
|
18
19
|
if (extra.length > 0 || !['status', 'set', 'check', 'cancel', 'remove'].includes(action)) {
|
|
19
|
-
throw new UsageError(
|
|
20
|
+
throw new UsageError(`Use: ${cliCommand('domain [status|set <hostname>|check|cancel|remove]')}`);
|
|
20
21
|
}
|
|
21
22
|
if (action === 'set' && value == null) throw new UsageError('domain set needs a hostname');
|
|
22
23
|
if (action !== 'set' && value != null) throw new UsageError(`domain ${action} takes no hostname`);
|
|
@@ -45,7 +46,7 @@ export async function domain({ terminal, options, cwd = process.cwd() }) {
|
|
|
45
46
|
pathPrefix: '/',
|
|
46
47
|
});
|
|
47
48
|
terminal.done(`Reserved ${checked.host}`);
|
|
48
|
-
terminal.note(
|
|
49
|
+
terminal.note(`Verify it in the repository owner’s GitHub account, then run: ${cliCommand('domain check')}`);
|
|
49
50
|
terminal.openUrl('https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/verifying-your-custom-domain-for-github-pages');
|
|
50
51
|
return change;
|
|
51
52
|
}
|
|
@@ -83,7 +84,7 @@ export async function domain({ terminal, options, cwd = process.cwd() }) {
|
|
|
83
84
|
const configured = await api.configureTopologyChange(publication.siteId, pending.changeId);
|
|
84
85
|
terminal.done(`GitHub verified ${configured.cname}`);
|
|
85
86
|
terminal.note(dnsInstruction(configured.cname, await providerHost(api, publication.siteId)));
|
|
86
|
-
terminal.note(
|
|
87
|
+
terminal.note(`After DNS propagates, run: ${cliCommand('domain check')}`);
|
|
87
88
|
return configured;
|
|
88
89
|
}
|
|
89
90
|
const committed = await api.commitTopologyChange(publication.siteId, pending.changeId);
|
|
@@ -119,6 +120,6 @@ function dnsInstruction(host, target) {
|
|
|
119
120
|
function showPending(terminal, pending) {
|
|
120
121
|
terminal.result(`${pending.canonicalBaseUrl}${pending.pathPrefix}`);
|
|
121
122
|
terminal.note(`State: ${pending.state}`);
|
|
122
|
-
if (pending.state === 'PREPARED') terminal.note(
|
|
123
|
-
else terminal.note(
|
|
123
|
+
if (pending.state === 'PREPARED') terminal.note(`Next: verify ownership, then run ${cliCommand('domain check')}.`);
|
|
124
|
+
else terminal.note(`Next: configure DNS, then run ${cliCommand('domain check')}.`);
|
|
124
125
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -7,6 +7,7 @@ import { galaCredential } from '../auth/gala.js';
|
|
|
7
7
|
import { githubCredential } from '../auth/github.js';
|
|
8
8
|
import { cloneRepository, createGit, populateEmptyRepository } from '../git.js';
|
|
9
9
|
import { UsageError } from '../cli/args.js';
|
|
10
|
+
import { CLI_INVOCATION, shellArgument } from '../cli/invocation.js';
|
|
10
11
|
import { customDomain } from '../domain.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -21,7 +22,7 @@ import { customDomain } from '../domain.js';
|
|
|
21
22
|
* which is why repositories it created never appeared in the web UI.
|
|
22
23
|
* - The **server** writes `site.config.yml` and the publish workflow during registration. The CLI
|
|
23
24
|
* used to write its own versions afterwards and commit them, producing a second commit whose
|
|
24
|
-
* 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
|
|
25
26
|
* collided with the first one's deployment record and failed.
|
|
26
27
|
* - **GitHub** turns on Pages by itself once publishing creates a `gh-pages` branch. The CLI used
|
|
27
28
|
* to poll ten minutes for a run it had caused, then call an API that changed nothing.
|
|
@@ -119,7 +120,7 @@ export async function init({ terminal, options, cwd = process.cwd() }) {
|
|
|
119
120
|
* Creation is a conversation, not a single call.
|
|
120
121
|
*
|
|
121
122
|
* `NEEDS_SHARING` means the repository exists with the right content and the App installation
|
|
122
|
-
* simply cannot see it
|
|
123
|
+
* simply cannot see it - an installation scoped to selected repositories, which is the right way to
|
|
123
124
|
* have it. That is one grant away from working, and GitHub offers no API to do it on the writer's
|
|
124
125
|
* behalf: adding a repository to an installation is documented as classic-PAT-only. So it is asked
|
|
125
126
|
* for, with a link to the one page that grants it.
|
|
@@ -151,7 +152,7 @@ async function createPublication({ terminal, api, capability, name, github, inst
|
|
|
151
152
|
const owner = result?.owner ?? '';
|
|
152
153
|
terminal.blank();
|
|
153
154
|
terminal.step(`${owner}/${result?.name ?? name} exists, but Gala cannot reach it yet`);
|
|
154
|
-
terminal.note('its installation covers only selected repositories
|
|
155
|
+
terminal.note('its installation covers only selected repositories - add this one');
|
|
155
156
|
terminal.openUrl(result?.recoveryUrl
|
|
156
157
|
?? installationUrl(result?.installationId, owner, await viewerOf(github)));
|
|
157
158
|
if (!await terminal.waitForEnter('Once Gala can access it')) {
|
|
@@ -217,7 +218,7 @@ export function installationUrl(installationId, owner, selfLogin) {
|
|
|
217
218
|
|
|
218
219
|
/**
|
|
219
220
|
* GitHub answers the creation call before the template content lands. Cloning into that window
|
|
220
|
-
* 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
|
|
221
222
|
* template certainly contains.
|
|
222
223
|
*/
|
|
223
224
|
async function waitForContent(github, owner, name, { attempts = 30, intervalMs = 1000 } = {}) {
|
|
@@ -233,7 +234,7 @@ async function publicationName({ terminal, explicitName, directory }) {
|
|
|
233
234
|
const answer = proposed ?? await terminal.ask('What should this publication be called?');
|
|
234
235
|
const name = slugify(answer);
|
|
235
236
|
if (name == null) {
|
|
236
|
-
throw new UsageError(`"${answer}" cannot be a repository name
|
|
237
|
+
throw new UsageError(`"${answer}" cannot be a repository name - use letters, numbers and hyphens`);
|
|
237
238
|
}
|
|
238
239
|
return name;
|
|
239
240
|
}
|
|
@@ -291,7 +292,7 @@ export function reportCreatedPublication({
|
|
|
291
292
|
directoryLabel,
|
|
292
293
|
hasDomainChange = false
|
|
293
294
|
}) {
|
|
294
|
-
const run =
|
|
295
|
+
const run = CLI_INVOCATION;
|
|
295
296
|
terminal.done(`Created ${owner}/${name}`);
|
|
296
297
|
terminal.note('The first deployment is running in GitHub Actions. The public site is not live yet.');
|
|
297
298
|
terminal.note(`track it at https://github.com/${owner}/${name}/actions`);
|
|
@@ -313,10 +314,6 @@ export function reportCreatedPublication({
|
|
|
313
314
|
terminal.note('lists every available command');
|
|
314
315
|
}
|
|
315
316
|
|
|
316
|
-
function shellArgument(value) {
|
|
317
|
-
return /^[A-Za-z0-9_./-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
317
|
function idempotencyKey(owner, name) {
|
|
321
318
|
return `init-${createHash('sha256').update(`${owner.toLowerCase()}/${name.toLowerCase()}`).digest('hex')}`;
|
|
322
319
|
}
|
package/src/commands/new.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { stringify } from 'yaml';
|
|
12
12
|
|
|
13
13
|
import { UsageError } from '../cli/args.js';
|
|
14
|
+
import { cliCommand } from '../cli/invocation.js';
|
|
14
15
|
import { postUrl, readPublication } from '../publication.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -54,7 +55,7 @@ export async function createPost({ terminal, options, cwd = process.cwd(), now =
|
|
|
54
55
|
if (address != null) terminal.note(`will appear at ${address}`);
|
|
55
56
|
|
|
56
57
|
terminal.blank();
|
|
57
|
-
terminal.note(
|
|
58
|
+
terminal.note(`write below the second --- line, then: ${cliCommand('preview')}`);
|
|
58
59
|
return { file, metadata };
|
|
59
60
|
}
|
|
60
61
|
|
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,23 @@ 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
|
|
|
40
42
|
const publication = await readPublication(root);
|
|
41
|
-
terminal.step('Starting the preview
|
|
43
|
+
terminal.step('Starting the preview - stop it with Ctrl-C');
|
|
42
44
|
if (publication != null) terminal.note(`this is ${publication.name ?? 'your publication'} as it will look`);
|
|
43
45
|
terminal.blank();
|
|
44
46
|
|
|
@@ -50,12 +52,17 @@ export async function preview({
|
|
|
50
52
|
});
|
|
51
53
|
|
|
52
54
|
return new Promise((resolve, reject) => {
|
|
53
|
-
child.once('error',
|
|
55
|
+
child.once('error', (error) => {
|
|
56
|
+
reject(new Error(`The preview could not start: ${error.message}`));
|
|
57
|
+
});
|
|
54
58
|
child.once('exit', (code, signal) => {
|
|
55
59
|
// Ctrl-C is how a writer stops a preview; it is not a failure to report.
|
|
56
60
|
if (signal === 'SIGINT' || signal === 'SIGTERM' || code === 0 || code === 130) resolve();
|
|
57
|
-
else if (signal)
|
|
58
|
-
|
|
61
|
+
else if (signal) {
|
|
62
|
+
reject(new Error(`The preview process was stopped by ${signal}. Review the build output above, correct the reported problem, and run preview again.`));
|
|
63
|
+
} else {
|
|
64
|
+
reject(new Error(`The preview build failed with exit code ${code}. Review the build output above, correct the reported problem, and run preview again.`));
|
|
65
|
+
}
|
|
59
66
|
});
|
|
60
67
|
});
|
|
61
68
|
}
|
|
@@ -84,7 +91,7 @@ function install(root, spawnProcess) {
|
|
|
84
91
|
resolve();
|
|
85
92
|
return;
|
|
86
93
|
}
|
|
87
|
-
const failure = new Error('
|
|
94
|
+
const failure = new Error('Gala could not install the preview dependencies. The package-manager output below contains the cause.');
|
|
88
95
|
failure.detail = said.trim();
|
|
89
96
|
reject(failure);
|
|
90
97
|
});
|
package/src/commands/prism.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { galaApi } from '../api/gala.js';
|
|
5
5
|
import { galaCredential } from '../auth/gala.js';
|
|
6
6
|
import { UsageError } from '../cli/args.js';
|
|
7
|
+
import { cliCommand } from '../cli/invocation.js';
|
|
7
8
|
import { createGit } from '../git.js';
|
|
8
9
|
import { readPublication } from '../publication.js';
|
|
9
10
|
|
|
@@ -25,7 +26,7 @@ export async function prism({ terminal, options, cwd = process.cwd() }) {
|
|
|
25
26
|
const [action = 'status', ...args] = options.positional;
|
|
26
27
|
|
|
27
28
|
if (action === 'status') {
|
|
28
|
-
requireArgs(args, 0, '
|
|
29
|
+
requireArgs(args, 0, cliCommand('prism status'));
|
|
29
30
|
const state = await api.json(`/v1/sites/${publication.siteId}/prism`, { action: 'Prism status' });
|
|
30
31
|
terminal.result(`Prism ${state.publishedMode}`);
|
|
31
32
|
terminal.note(`Mode: requested ${state.requestedMode}; published ${state.publishedMode}`);
|
|
@@ -39,7 +40,7 @@ export async function prism({ terminal, options, cwd = process.cwd() }) {
|
|
|
39
40
|
const expectedRepositoryHeadSha = inventory.headSha;
|
|
40
41
|
|
|
41
42
|
if (action === 'mode') {
|
|
42
|
-
requireArgs(args, 1, '
|
|
43
|
+
requireArgs(args, 1, cliCommand('prism mode <off|presentation-only|manual|assisted>'));
|
|
43
44
|
const mode = MODES.get(args[0]);
|
|
44
45
|
if (!mode) throw new UsageError('Prism mode must be off, presentation-only, manual, or assisted.');
|
|
45
46
|
if (mode === 'OFF' || mode === 'PRESENTATION_ONLY') await confirm(terminal, options, `Change Prism mode to ${args[0]}?`);
|
|
@@ -52,7 +53,7 @@ export async function prism({ terminal, options, cwd = process.cwd() }) {
|
|
|
52
53
|
if (action === 'link-policy') {
|
|
53
54
|
const [scope, target, value] = args;
|
|
54
55
|
if (scope === 'site') {
|
|
55
|
-
requireArgs(args, 2, '
|
|
56
|
+
requireArgs(args, 2, cliCommand('prism link-policy site <nofollow|follow>'));
|
|
56
57
|
const policy = policyValue(target);
|
|
57
58
|
const result = await mutate(api, `/v1/sites/${publication.siteId}/prism`, 'PUT', {
|
|
58
59
|
configurationLinkPolicy: policy, expectedRepositoryHeadSha,
|
|
@@ -60,7 +61,7 @@ export async function prism({ terminal, options, cwd = process.cwd() }) {
|
|
|
60
61
|
return settleMutation(api, terminal, publication, result);
|
|
61
62
|
}
|
|
62
63
|
if (scope === 'work') {
|
|
63
|
-
requireArgs(args, 3, '
|
|
64
|
+
requireArgs(args, 3, cliCommand('prism link-policy work <slug> <inherit|nofollow|follow>'));
|
|
64
65
|
const post = resolvePost(inventory, target, options.value('language'), publication.defaultLanguage);
|
|
65
66
|
if (value === 'inherit') {
|
|
66
67
|
const result = await mutate(api,
|
|
@@ -76,11 +77,14 @@ export async function prism({ terminal, options, cwd = process.cwd() }) {
|
|
|
76
77
|
}, 'Prism work link policy');
|
|
77
78
|
return settleMutation(api, terminal, publication, result);
|
|
78
79
|
}
|
|
79
|
-
throw new UsageError(
|
|
80
|
+
throw new UsageError(
|
|
81
|
+
`Use: ${cliCommand('prism link-policy site <nofollow|follow>')} or `
|
|
82
|
+
+ cliCommand('prism link-policy work <slug> <inherit|nofollow|follow>')
|
|
83
|
+
);
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
if (action === 'list') {
|
|
83
|
-
requireArgs(args, 1, '
|
|
87
|
+
requireArgs(args, 1, cliCommand('prism list <slug> [--language en]'));
|
|
84
88
|
const post = resolvePost(inventory, args[0], options.value('language'), publication.defaultLanguage);
|
|
85
89
|
const result = await configurations(api, publication.siteId, post);
|
|
86
90
|
terminal.result(`${result.configurations.length} configuration${result.configurations.length === 1 ? '' : 's'}`);
|
|
@@ -91,7 +95,7 @@ export async function prism({ terminal, options, cwd = process.cwd() }) {
|
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
if (action === 'create') {
|
|
94
|
-
requireArgs(args, 1, '
|
|
98
|
+
requireArgs(args, 1, cliCommand('prism create <slug> --language en --depth brief --intent orientation'));
|
|
95
99
|
const post = resolvePost(inventory, args[0], options.value('language'), publication.defaultLanguage);
|
|
96
100
|
const current = await configurations(api, publication.siteId, post);
|
|
97
101
|
const result = await mutate(api,
|
|
@@ -108,7 +112,7 @@ export async function prism({ terminal, options, cwd = process.cwd() }) {
|
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
if (!['edit', 'generate', 'submit', 'approve', 'reject', 'revoke'].includes(action)) {
|
|
111
|
-
throw new UsageError(
|
|
115
|
+
throw new UsageError(`Unknown Prism action. Run ${cliCommand('prism --help')}.`);
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
const configurationId = args[0];
|
|
@@ -119,7 +123,7 @@ export async function prism({ terminal, options, cwd = process.cwd() }) {
|
|
|
119
123
|
const base = `/v1/sites/${publication.siteId}/articles/${post.articleId}/configurations/${configurationId}`;
|
|
120
124
|
|
|
121
125
|
if (action === 'edit') {
|
|
122
|
-
requireArgs(args, 1, '
|
|
126
|
+
requireArgs(args, 1, cliCommand('prism edit <configuration-id> --file proposal.md'));
|
|
123
127
|
const filename = options.value('file');
|
|
124
128
|
if (!filename) throw new UsageError('Prism edit needs --file proposal.md.');
|
|
125
129
|
const markdown = await readFile(path.resolve(root, filename), 'utf8');
|
|
@@ -132,7 +136,7 @@ export async function prism({ terminal, options, cwd = process.cwd() }) {
|
|
|
132
136
|
}
|
|
133
137
|
|
|
134
138
|
if (action === 'generate') {
|
|
135
|
-
requireArgs(args, 1, '
|
|
139
|
+
requireArgs(args, 1, cliCommand('prism generate <configuration-id>'));
|
|
136
140
|
const result = await mutate(api, `${base}/generation-jobs`, 'POST', {
|
|
137
141
|
expectedSourceContentHash: collection.sourceRevisionHash,
|
|
138
142
|
hashContract: collection.hashContract,
|
|
@@ -146,7 +150,7 @@ export async function prism({ terminal, options, cwd = process.cwd() }) {
|
|
|
146
150
|
throw new UsageError(`${action} needs a revision ID when there is no working revision.`);
|
|
147
151
|
}
|
|
148
152
|
if (action === 'submit') {
|
|
149
|
-
if (args.length > 2) throw new UsageError(
|
|
153
|
+
if (args.length > 2) throw new UsageError(`Use: ${cliCommand('prism submit <configuration-id> [revision-id]')}`);
|
|
150
154
|
if (revisionId !== configuration.workingRevision?.revisionId) {
|
|
151
155
|
throw new UsageError('Only the current working revision can be submitted. Refresh the configuration and try again.');
|
|
152
156
|
}
|
|
@@ -165,7 +169,7 @@ export async function prism({ terminal, options, cwd = process.cwd() }) {
|
|
|
165
169
|
|
|
166
170
|
const reason = options.value('reason');
|
|
167
171
|
if (action === 'approve') {
|
|
168
|
-
if (args.length > 2) throw new UsageError(
|
|
172
|
+
if (args.length > 2) throw new UsageError(`Use: ${cliCommand('prism approve <configuration-id> [revision-id] [--yes]')}`);
|
|
169
173
|
if (revisionId !== configuration.workingRevision?.revisionId) {
|
|
170
174
|
throw new UsageError('Only the current working revision can be approved. Refresh the configuration and try again.');
|
|
171
175
|
}
|
|
@@ -292,7 +296,7 @@ async function settleMutation(api, terminal, publication, result) {
|
|
|
292
296
|
throw new UsageError(`Repository update failed (${state.errorCode ?? 'unknown error'}). Run the command again after correcting the cause.`);
|
|
293
297
|
}
|
|
294
298
|
if (state.status !== 'COMMITTED') {
|
|
295
|
-
throw new UsageError(
|
|
299
|
+
throw new UsageError(`Repository update did not finish before the 31-minute tracking deadline. Check ${cliCommand('prism status')} before retrying.`);
|
|
296
300
|
}
|
|
297
301
|
if (!state.publicationAttemptSha) {
|
|
298
302
|
terminal.result('Repository updated. No publication attempt was returned.');
|
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/upgrade.js
CHANGED
|
@@ -4,6 +4,8 @@ import { tmpdir } from 'node:os';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { x as extractTar } from 'tar';
|
|
6
6
|
|
|
7
|
+
import { cliCommand } from '../cli/invocation.js';
|
|
8
|
+
|
|
7
9
|
const PACKAGE = '@rathnasgala/theme';
|
|
8
10
|
const REGISTRY = 'https://registry.npmjs.org';
|
|
9
11
|
const PROTECTED = ['.git/', 'content/', 'custom.css', 'site.config.yml'];
|
|
@@ -81,7 +83,7 @@ async function assertNoManagedDrift(root, installed) {
|
|
|
81
83
|
for (const [managed, expected] of Object.entries(installed.files)) {
|
|
82
84
|
const file = path.join(root, managed);
|
|
83
85
|
if (!await exists(file) || (await lstat(file)).isSymbolicLink() || sha256(await readFile(file)) !== expected) {
|
|
84
|
-
throw new Error(`${managed} has local changes; restore it with
|
|
86
|
+
throw new Error(`${managed} has local changes; restore it with ${cliCommand('doctor')} before upgrading`);
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
89
|
}
|
|
@@ -168,6 +170,6 @@ export async function upgrade({ terminal, options, cwd = process.cwd(), fetchImp
|
|
|
168
170
|
await rm(unpacked.temporary, { recursive: true, force: true });
|
|
169
171
|
}
|
|
170
172
|
terminal.done(`Upgraded managed theme to ${release.version}`);
|
|
171
|
-
terminal.note(
|
|
173
|
+
terminal.note(`Run ${cliCommand('preview')}, then ${cliCommand('publish')} when the result is approved.`);
|
|
172
174
|
return { changed: true, version: release.version };
|
|
173
175
|
}
|
package/src/commands-manifest.js
CHANGED
|
@@ -7,6 +7,7 @@ import { preview } from './commands/preview.js';
|
|
|
7
7
|
import { publish } from './commands/publish.js';
|
|
8
8
|
import { prism } from './commands/prism.js';
|
|
9
9
|
import { upgrade } from './commands/upgrade.js';
|
|
10
|
+
import { cliCommand } from './cli/invocation.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Commands in the order a writer meets them.
|
|
@@ -17,7 +18,7 @@ import { upgrade } from './commands/upgrade.js';
|
|
|
17
18
|
* Each was a surface to keep correct and a way to be wrong.
|
|
18
19
|
*
|
|
19
20
|
* This lives apart from the dispatcher so the README can be checked against it. v0's README
|
|
20
|
-
* outlived its commands by weeks
|
|
21
|
+
* outlived its commands by weeks - it taught a command that never existed - because nothing tied
|
|
21
22
|
* the two together.
|
|
22
23
|
*
|
|
23
24
|
* Each entry carries its own options. Nothing is parsed globally, so an option cannot mean two
|
|
@@ -31,19 +32,19 @@ export const COMMANDS = {
|
|
|
31
32
|
},
|
|
32
33
|
init: {
|
|
33
34
|
summary: 'Create a publication in an empty directory',
|
|
34
|
-
usage: '
|
|
35
|
+
usage: cliCommand('init [directory] [--name my-notes] [--domain blog.example.com]'),
|
|
35
36
|
flags: ['name', 'domain', 'api-base-url'],
|
|
36
37
|
run: init
|
|
37
38
|
},
|
|
38
39
|
domain: {
|
|
39
40
|
summary: 'Inspect or change this publication’s custom domain',
|
|
40
|
-
usage: '
|
|
41
|
+
usage: cliCommand('domain [status|set <hostname>|check|cancel|remove] [--root path]'),
|
|
41
42
|
flags: ['root', 'api-base-url'],
|
|
42
43
|
run: domain
|
|
43
44
|
},
|
|
44
45
|
new: {
|
|
45
46
|
summary: 'Start a post',
|
|
46
|
-
usage: '
|
|
47
|
+
usage: cliCommand('new "A durable idea" [--language en]'),
|
|
47
48
|
flags: ['root', 'language', 'today'],
|
|
48
49
|
run: createPost
|
|
49
50
|
},
|
|
@@ -60,14 +61,14 @@ export const COMMANDS = {
|
|
|
60
61
|
},
|
|
61
62
|
prism: {
|
|
62
63
|
summary: 'Manage author-approved Prism configurations',
|
|
63
|
-
usage: '
|
|
64
|
+
usage: cliCommand('prism <status|mode|link-policy|list|create|edit|generate|submit|approve|reject|revoke> [arguments]'),
|
|
64
65
|
flags: ['root', 'api-base-url', 'language', 'depth', 'intent', 'modality', 'file', 'reason'],
|
|
65
66
|
switches: ['yes'],
|
|
66
67
|
run: prism
|
|
67
68
|
},
|
|
68
69
|
upgrade: {
|
|
69
70
|
summary: 'Inspect and apply a verified managed-theme update',
|
|
70
|
-
usage: '
|
|
71
|
+
usage: cliCommand('upgrade [--channel latest|next] [--yes]'),
|
|
71
72
|
flags: ['root', 'channel'],
|
|
72
73
|
switches: ['yes'],
|
|
73
74
|
run: upgrade
|
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/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { UsageError, parseArguments } from './cli/args.js';
|
|
3
|
+
import { CLI_INVOCATION, cliCommand } from './cli/invocation.js';
|
|
3
4
|
import { createTerminal } from './cli/terminal.js';
|
|
4
5
|
import { COMMANDS } from './commands-manifest.js';
|
|
5
6
|
|
|
@@ -20,7 +21,7 @@ if (command == null) {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
if (argv.includes('--help') || argv.includes('-h')) {
|
|
23
|
-
process.stdout.write(`\n ${command.usage ??
|
|
24
|
+
process.stdout.write(`\n ${command.usage ?? cliCommand(name)}\n ${command.summary}\n\n`);
|
|
24
25
|
process.exit(0);
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -56,5 +57,6 @@ function usage() {
|
|
|
56
57
|
const lines = Object.entries(COMMANDS)
|
|
57
58
|
.map(([key, { summary }]) => ` ${key.padEnd(9)} ${summary}`)
|
|
58
59
|
.join('\n');
|
|
59
|
-
process.stdout.write(`\n
|
|
60
|
+
process.stdout.write(`\n ${CLI_INVOCATION} <command>\n\n${lines}\n\n`
|
|
61
|
+
+ ` Run ${CLI_INVOCATION} <command> --help for details.\n\n`);
|
|
60
62
|
}
|
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) {
|