@rathnasgala/cli 0.0.22 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +66 -204
  2. package/package.json +3 -3
  3. package/src/api/gala.js +126 -0
  4. package/src/api/github.js +63 -0
  5. package/src/api/http.js +72 -0
  6. package/src/auth/gala.js +52 -0
  7. package/src/auth/github.js +78 -0
  8. package/src/auth/store.js +88 -0
  9. package/src/cli/args.js +56 -0
  10. package/src/cli/terminal.js +104 -0
  11. package/src/commands/auth.js +20 -0
  12. package/src/commands/doctor.js +98 -0
  13. package/src/commands/init.js +197 -0
  14. package/src/commands/new.js +76 -0
  15. package/src/commands/preview.js +92 -0
  16. package/src/commands/publish.js +57 -0
  17. package/src/commands-manifest.js +58 -0
  18. package/src/content.js +31 -0
  19. package/src/git.js +143 -0
  20. package/src/index.js +44 -294
  21. package/src/publication.js +37 -0
  22. package/src/assign-content-ids.js +0 -1
  23. package/src/auth-command.js +0 -36
  24. package/src/configure-site.js +0 -102
  25. package/src/content-files.js +0 -1
  26. package/src/doctor-command.js +0 -214
  27. package/src/entitlement-client.js +0 -26
  28. package/src/entitlement-command.js +0 -74
  29. package/src/evaluation-date.js +0 -1
  30. package/src/gala-credential-health.js +0 -34
  31. package/src/gala-credential-store.js +0 -115
  32. package/src/gala-device-flow.js +0 -121
  33. package/src/git-credentials.js +0 -37
  34. package/src/github-auth-command.js +0 -50
  35. package/src/github-credential-store.js +0 -104
  36. package/src/github-device-flow.js +0 -153
  37. package/src/github-empty-repository.js +0 -89
  38. package/src/github-identity.js +0 -32
  39. package/src/github-pages-provisioning.js +0 -107
  40. package/src/github-repository-secret.js +0 -82
  41. package/src/github-repository-variable.js +0 -56
  42. package/src/github-template-repository.js +0 -171
  43. package/src/hook-command.js +0 -64
  44. package/src/http-failure.js +0 -55
  45. package/src/new-command.js +0 -54
  46. package/src/open-browser.js +0 -40
  47. package/src/preview-command.js +0 -60
  48. package/src/publication-creation-client.js +0 -155
  49. package/src/publication-state.js +0 -7
  50. package/src/publish-command.js +0 -37
  51. package/src/record-deployment-command.js +0 -147
  52. package/src/refresh-command.js +0 -104
  53. package/src/repository-limits.js +0 -94
  54. package/src/scaffold-git.js +0 -76
  55. package/src/scaffold-options.js +0 -58
  56. package/src/scaffold-preflight.js +0 -146
  57. package/src/scaffold-site.js +0 -185
  58. package/src/site-config-registration.js +0 -47
  59. package/src/site-registration-client.js +0 -138
  60. package/src/theme-package.js +0 -128
  61. package/src/topology-client.js +0 -43
  62. package/src/topology-command.js +0 -70
  63. package/src/upgrade-command.js +0 -81
  64. package/src/validate-command.js +0 -5
  65. package/src/workflow-command.js +0 -87
package/src/index.js CHANGED
@@ -1,310 +1,60 @@
1
1
  #!/usr/bin/env node
2
+ import { UsageError, parseArguments } from './cli/args.js';
3
+ import { createTerminal } from './cli/terminal.js';
4
+ import { COMMANDS } from './commands-manifest.js';
2
5
 
3
- import { parseScaffoldOptions } from './scaffold-options.js';
4
- import { regenerateBuildManifest } from './validate-command.js';
5
- import { createPost } from './new-command.js';
6
- import {
7
- diagnoseFramework,
8
- diagnosePublicationState,
9
- repairFramework
10
- } from './doctor-command.js';
11
- import { configureSite } from './configure-site.js';
12
- import { previewSite } from './preview-command.js';
13
- import { writePublishWorkflow } from './workflow-command.js';
14
- import { publishSite } from './publish-command.js';
15
- import { installPrePushHook } from './hook-command.js';
16
- import { reportRepositoryLimitWarnings } from './repository-limits.js';
17
- import { recordDeployment } from './record-deployment-command.js';
18
- import { authenticateGala } from './auth-command.js';
19
- import { createInterface } from 'node:readline/promises';
20
- import { upgradeTheme } from './upgrade-command.js';
21
- import { authenticateGithub } from './github-auth-command.js';
22
- import { scaffoldSite } from './scaffold-site.js';
23
- import { openInBrowser } from './open-browser.js';
24
- import { prepareScaffold } from './scaffold-preflight.js';
25
- import { refreshEngagementSnapshot } from './refresh-command.js';
26
- import { switchTopology } from './topology-command.js';
27
- import { acquireAttributionEntitlement } from './entitlement-command.js';
28
6
 
29
- /*
30
- * A failed command should say what went wrong and what to do about it. Node's default for a
31
- * rejected top-level await is a stack trace through node_modules, which tells a writer nothing and
32
- * buries the one line that matters. The stack is still available behind GALA_DEBUG for anyone
33
- * debugging the CLI itself.
34
- */
35
- process.on('uncaughtException', reportAndExit);
36
- process.on('unhandledRejection', reportAndExit);
7
+ const [name, ...argv] = process.argv.slice(2);
8
+ const terminal = createTerminal();
37
9
 
38
- function reportAndExit(failure) {
39
- if (process.env.GALA_DEBUG) {
40
- process.stderr.write(`${failure instanceof Error ? failure.stack : String(failure)}\n`);
41
- } else {
42
- const message = failure instanceof Error ? failure.message : String(failure);
43
- process.stderr.write(`${message}\n`);
44
- }
45
- process.exit(1);
10
+ if (name == null || name === 'help' || name === '--help' || name === '-h') {
11
+ usage();
12
+ process.exit(name == null ? 1 : 0);
46
13
  }
47
14
 
48
- /**
49
- * Opens the page and says so, or falls back to asking for it to be opened by hand. The URL is
50
- * printed either way — it is the thing the writer may need to move to another device.
51
- */
52
- function announce(verificationUri, userCode) {
53
- const opened = openInBrowser(verificationUri);
54
- return `${opened ? 'Opened' : 'Open'} ${verificationUri}\nEnter code: ${userCode}\n`;
15
+ const command = COMMANDS[name];
16
+ if (command == null) {
17
+ terminal.fail(`there is no ${name} command`);
18
+ usage();
19
+ process.exit(1);
55
20
  }
56
21
 
57
- const [command, ...args] = process.argv.slice(2);
58
- const usage = 'Usage: gala <auth|configure|entitlement|scaffold|topology|validate|new|doctor|hook|preview|publish|record-deployment|refresh|upgrade|workflow> [options]';
59
-
60
- if (command === 'help' || command === '--help' || command === '-h'
61
- || args.includes('--help') || args.includes('-h')) {
62
- process.stdout.write(`${usage}\n`);
22
+ if (argv.includes('--help') || argv.includes('-h')) {
23
+ process.stdout.write(`\n ${command.usage ?? `gala ${name}`}\n ${command.summary}\n\n`);
63
24
  process.exit(0);
64
25
  }
65
26
 
66
- const recognizedCommands = new Set([
67
- 'auth', 'configure', 'validate', 'new', 'doctor', 'preview',
68
- 'workflow', 'publish', 'record-deployment', 'refresh', 'hook', 'upgrade', 'topology', 'entitlement'
69
- ]);
70
- function commandRoot() {
71
- const rootIndex = args.indexOf('--root');
72
- if (rootIndex !== -1) return args[rootIndex + 1];
73
- if (command === 'doctor' || command === 'validate') {
74
- return args.find((argument, index) =>
75
- !argument.startsWith('--')
76
- && !['--today', '--source'].includes(args[index - 1])
77
- ) ?? process.cwd();
78
- }
79
- return process.cwd();
80
- }
81
- if (recognizedCommands.has(command)) {
82
- await reportRepositoryLimitWarnings(commandRoot());
27
+ try {
28
+ const options = parseArguments(argv, { flags: command.flags ?? [], switches: command.switches ?? [] });
29
+ await command.run({ terminal, options });
30
+ } catch (failure) {
31
+ report(failure);
32
+ process.exit(1);
83
33
  }
84
34
 
85
- if (command === 'auth') {
86
- if (args[0] === 'github') {
87
- await authenticateGithub({
88
- showInstructions: ({ verificationUri, userCode }) => {
89
- process.stdout.write(announce(verificationUri, userCode));
90
- }
91
- });
92
- process.stdout.write('GitHub authentication stored securely.\n');
93
- } else {
94
- const apiIndex = args.indexOf('--api-base-url');
95
- const apiBaseUrl = apiIndex === -1 ? 'https://api.gala67.com' : args[apiIndex + 1];
96
- const result = await authenticateGala({
97
- apiBaseUrl,
98
- showInstructions: ({ verificationUri, userCode }) => {
99
- process.stdout.write(announce(verificationUri, userCode));
100
- }
101
- });
102
- process.stdout.write(`Gala authentication stored securely until ${result.expiresAt.toISOString()}.\n`);
103
- }
104
- } else if (command === 'scaffold') {
105
- const valueFor = (name) => {
106
- const index = args.indexOf(name);
107
- return index === -1 ? undefined : args[index + 1];
108
- };
109
- const explicitInstallationId = valueFor('--installation-id');
110
- const topology = valueFor('--topology') ?? 'provider-default';
111
- const siteOptions = parseScaffoldOptions(args);
112
- // Prompting only makes sense at a terminal. In CI there is nobody to answer, so a missing value
113
- // has to stay a clear error rather than a process that hangs waiting for enter.
114
- const interactive = process.stdin.isTTY === true;
115
- const ask = interactive
116
- ? async (question) => {
117
- const terminal = createInterface({ input: process.stdin, output: process.stdout });
118
- try { return await terminal.question(question); } finally { terminal.close(); }
119
- }
120
- : undefined;
121
- const prepared = await prepareScaffold({
122
- owner: valueFor('--owner'),
123
- repository: valueFor('--repository'),
124
- target: valueFor('--target'),
125
- githubInstallationId: explicitInstallationId == null ? undefined : Number(explicitInstallationId),
126
- siteName: siteOptions.siteName,
127
- apiBaseUrl: valueFor('--api-base-url') ?? 'https://api.gala67.com',
128
- notify: (message) => process.stdout.write(`${message}\n`),
129
- ask
130
- });
131
- const result = await scaffoldSite({
132
- notify: (message) => process.stdout.write(`${message}\n`),
133
- ask,
134
- openUrl: openInBrowser,
135
- owner: prepared.owner,
136
- repository: prepared.repository,
137
- target: prepared.target,
138
- githubInstallationId: prepared.githubInstallationId,
139
- topology,
140
- canonicalBaseUrl: valueFor('--canonical-base-url'),
141
- siteOptions,
142
- emptyExistingRepository: args.includes('--empty-existing-repository'),
143
- resumeExistingCheckout: args.includes('--resume')
144
- });
145
- await reportRepositoryLimitWarnings(result.root);
146
- process.stdout.write(`Scaffolded ${result.fullName} as Gala site ${result.siteId} in ${result.root}.\n`);
147
- } else if (command === 'configure') {
148
- const rootIndex = args.indexOf('--root');
149
- const root = rootIndex === -1 ? process.cwd() : args[rootIndex + 1];
150
- const options = parseScaffoldOptions(args);
151
- const config = await configureSite(root, options);
152
- process.stdout.write(`${JSON.stringify(config.design, null, 2)}\n`);
153
- } else if (command === 'topology') {
154
- const valueFor = (name) => {
155
- const index = args.indexOf(name);
156
- return index === -1 ? undefined : args[index + 1];
157
- };
158
- const result = await switchTopology({
159
- root: valueFor('--root') ?? process.cwd(),
160
- owner: valueFor('--owner'),
161
- repository: valueFor('--repository'),
162
- canonicalBaseUrl: valueFor('--canonical-base-url'),
163
- pathPrefix: valueFor('--path-prefix') ?? '/'
164
- });
165
- process.stdout.write(`Committed topology ${result.changeId} at ${result.commitSha}.\n`);
166
- } else if (command === 'entitlement') {
167
- const rootIndex = args.indexOf('--root');
168
- const result = await acquireAttributionEntitlement({
169
- root: rootIndex === -1 ? process.cwd() : args[rootIndex + 1]
170
- });
171
- process.stdout.write(result.changed
172
- ? `Stored the signed attribution entitlement for ${result.siteId}.\n`
173
- : `Attribution entitlement for ${result.siteId} is current.\n`);
174
- } else if (command === 'validate') {
175
- const todayIndex = args.indexOf('--today');
176
- const today = todayIndex === -1 ? undefined : args[todayIndex + 1];
177
- const root = args.find((argument) => !argument.startsWith('--') && argument !== today) ?? process.cwd();
178
- const { results } = await regenerateBuildManifest({ root, today });
179
- const failures = results.filter(({ errors }) => errors.length > 0);
180
-
181
- for (const result of failures) {
182
- for (const error of result.errors) process.stderr.write(`${result.file}: ${error}\n`);
183
- }
184
- for (const result of results) {
185
- for (const warning of result.warnings) process.stderr.write(`${result.file}: warning: ${warning}\n`);
35
+ /**
36
+ * One line the writer can act on.
37
+ *
38
+ * A rejected top-level await prints a stack through node_modules by default, which buries the only
39
+ * line that matters. The stack is still there behind GALA_DEBUG for anyone debugging the CLI
40
+ * itself, which is a different audience from anyone trying to publish.
41
+ */
42
+ function report(failure) {
43
+ if (process.env.GALA_DEBUG) {
44
+ process.stderr.write(`${failure instanceof Error ? failure.stack : String(failure)}\n`);
45
+ return;
186
46
  }
187
- process.stdout.write(`Validated ${results.length} post variant(s); ${failures.length} failed.\n`);
188
- if (failures.length > 0) process.exitCode = 1;
189
- } else if (command === 'new') {
190
- const valueFor = (name) => {
191
- const index = args.indexOf(name);
192
- return index === -1 ? undefined : args[index + 1];
193
- };
194
- const title = valueFor('--title');
195
- const language = valueFor('--language');
196
- const today = valueFor('--today');
197
- const root = valueFor('--root') ?? process.cwd();
198
- const result = await createPost({ root, title, language, today });
199
- process.stdout.write(`Created ${result.postPath}\n`);
200
- } else if (command === 'doctor') {
201
- const positional = args.filter((argument, index) =>
202
- !argument.startsWith('--') && args[index - 1] !== '--source'
203
- );
204
- const root = positional[0] ?? process.cwd();
205
- if (args.includes('--fix')) {
206
- const sourceIndex = args.indexOf('--source');
207
- const sourceRoot = sourceIndex === -1 ? undefined : args[sourceIndex + 1];
208
- if (!sourceRoot) throw new Error('doctor --fix requires --source <trusted-template-root>');
209
- const repaired = await repairFramework(root, sourceRoot);
210
- process.stdout.write(`Repaired ${repaired.length} managed file(s).\n`);
47
+ terminal.fail(failure instanceof Error ? failure.message : String(failure));
48
+ // Captured output from a subprocess is the most useful thing on screen at exactly this moment.
49
+ if (typeof failure?.detail === 'string' && failure.detail !== '') {
50
+ for (const line of failure.detail.split('\n')) terminal.note(line);
211
51
  }
212
- const findings = await diagnoseFramework(root);
213
- const drift = findings.filter(({ status }) => status !== 'intact');
214
- findings.forEach(({ path: file, status }) => process.stdout.write(`${status}\t${file}\n`));
215
- const publicationState = await diagnosePublicationState(root);
216
- process.stdout.write(`${publicationState.status}\t${publicationState.path}\n`);
217
- if (publicationState.status === 'invalid') process.exitCode = 1;
218
- if (drift.length > 0) process.exitCode = 1;
219
- } else if (command === 'preview') {
220
- const valueFor = (name) => {
221
- const index = args.indexOf(name);
222
- return index === -1 ? undefined : args[index + 1];
223
- };
224
- await previewSite({
225
- root: valueFor('--root') ?? process.cwd(),
226
- today: valueFor('--today')
227
- });
228
- } else if (command === 'workflow') {
229
- const valueFor = (name) => {
230
- const index = args.indexOf(name);
231
- return index === -1 ? undefined : args[index + 1];
232
- };
233
- const result = await writePublishWorkflow({
234
- root: valueFor('--root') ?? process.cwd(),
235
- siteId: valueFor('--site-id'),
236
- timezone: valueFor('--timezone'),
237
- actionRef: valueFor('--action-ref'),
238
- defaultBranch: valueFor('--default-branch') ?? 'main',
239
- buildMode: valueFor('--mode') ?? 'build-and-deploy'
240
- });
241
- process.stdout.write(`Wrote ${result.target} (${result.minute} ${result.hour} * * *)\n`);
242
- } else if (command === 'publish') {
243
- const valueFor = (name) => {
244
- const index = args.indexOf(name);
245
- return index === -1 ? undefined : args[index + 1];
246
- };
247
- await publishSite({
248
- root: valueFor('--root') ?? process.cwd(),
249
- today: valueFor('--today'),
250
- force: args.includes('--force')
251
- });
252
- } else if (command === 'record-deployment') {
253
- const valueFor = (name) => {
254
- const index = args.indexOf(name);
255
- return index === -1 ? undefined : args[index + 1];
256
- };
257
- const root = valueFor('--root') ?? process.cwd();
258
- const result = await recordDeployment({
259
- root,
260
- deployedOn: valueFor('--today'),
261
- deployedCommitSha: valueFor('--commit-sha')
262
- });
263
- process.stdout.write(
264
- `${result.pushed ? 'Pushed' : 'No change for'} successful deployment `
265
- + `of ${result.state.posts.length} article(s).\n`
266
- + `Recorded state SHA: ${result.recordedStateSha}\n`
267
- );
268
- } else if (command === 'refresh') {
269
- const rootIndex = args.indexOf('--root');
270
- const root = rootIndex === -1 ? process.cwd() : args[rootIndex + 1];
271
- const result = await refreshEngagementSnapshot({ root });
272
- process.stdout.write(result.changed
273
- ? 'Refreshed, committed, and pushed the engagement snapshot.\n'
274
- : 'Engagement snapshot is already current.\n');
275
- } else if (command === 'upgrade') {
276
- const valueFor = (name) => {
277
- const index = args.indexOf(name);
278
- return index === -1 ? undefined : args[index + 1];
279
- };
280
- const terminalConfirm = async ({ installed, version, channel }) => {
281
- if (args.includes('--yes')) return true;
282
- const terminal = createInterface({ input: process.stdin, output: process.stdout });
283
- try {
284
- const answer = await terminal.question(`Upgrade theme ${installed} -> ${version} (${channel})? [y/N] `);
285
- return /^(?:y|yes)$/i.test(answer.trim());
286
- } finally { terminal.close(); }
287
- };
288
- const result = await upgradeTheme({
289
- root: valueFor('--root') ?? process.cwd(),
290
- channel: valueFor('--channel'),
291
- confirm: terminalConfirm
292
- });
293
- process.stdout.write(result.cancelled ? 'Theme upgrade cancelled.\n'
294
- : result.changed ? `Upgraded theme to ${result.version}.\n`
295
- : `Theme ${result.version} is already installed.\n`);
296
- process.stdout.write(
297
- `Action major v${result.action.currentMajor}; `
298
- + (result.action.newerAvailable
299
- ? `v${result.action.latestMajor} is available.\n`
300
- : 'no newer major is available.\n')
301
- );
302
- } else if (command === 'hook' && args[0] === 'install') {
303
- const rootIndex = args.indexOf('--root');
304
- const root = rootIndex === -1 ? process.cwd() : args[rootIndex + 1];
305
- const result = await installPrePushHook(root);
306
- process.stdout.write(`${result.installed ? 'Installed' : 'Already installed'} ${result.target}\n`);
307
- } else {
308
- process.stderr.write(`${usage}\n`);
309
- process.exitCode = 1;
52
+ if (failure instanceof UsageError) usage();
53
+ }
54
+
55
+ function usage() {
56
+ const lines = Object.entries(COMMANDS)
57
+ .map(([key, { summary }]) => ` ${key.padEnd(9)} ${summary}`)
58
+ .join('\n');
59
+ process.stdout.write(`\n gala <command>\n\n${lines}\n\n Run any command with --help.\n\n`);
310
60
  }
@@ -0,0 +1,37 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { parse } from 'yaml';
5
+
6
+ /**
7
+ * Where a publication and its posts actually live on the web.
8
+ *
9
+ * The CLI knew every part of this and never said it: `new` printed a file path, `publish` said the
10
+ * site would appear shortly, and the writer was left to assemble
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
+ * is a poor first minute with a publishing tool.
14
+ */
15
+ export async function readPublication(root) {
16
+ try {
17
+ const configuration = parse(await readFile(path.join(root, 'site.config.yml'), 'utf8'));
18
+ const base = configuration?.hosting?.canonicalBaseUrl;
19
+ if (typeof base !== 'string') return null;
20
+ const prefix = typeof configuration?.hosting?.pathPrefix === 'string'
21
+ ? configuration.hosting.pathPrefix
22
+ : '/';
23
+ return {
24
+ name: configuration?.site?.name,
25
+ defaultLanguage: configuration?.site?.defaultLanguage ?? 'en',
26
+ url: `${base.replace(/\/$/, '')}${prefix === '/' ? '' : prefix}/`
27
+ };
28
+ } catch {
29
+ // Not being able to read it is never worth failing a command over; the caller simply says less.
30
+ return null;
31
+ }
32
+ }
33
+
34
+ export function postUrl(publication, slug, language) {
35
+ if (publication == null) return null;
36
+ return `${publication.url}${language ?? publication.defaultLanguage}/${slug}/`;
37
+ }
@@ -1 +0,0 @@
1
- export { assignMissingContentIds } from '@rathnasgala/content-validation';
@@ -1,36 +0,0 @@
1
- import { pollForGalaToken, requestGalaDeviceCode } from './gala-device-flow.js';
2
- import { writeGalaCredential } from './gala-credential-store.js';
3
-
4
- export async function authenticateGala({
5
- apiBaseUrl = 'https://api.gala67.com',
6
- clientId = 'gala-cli',
7
- fetchImpl = fetch,
8
- sleep,
9
- now = Date.now,
10
- showInstructions,
11
- credentialTarget
12
- } = {}) {
13
- if (typeof showInstructions !== 'function') throw new TypeError('showInstructions is required');
14
- const authorization = await requestGalaDeviceCode({ apiBaseUrl, clientId, fetchImpl });
15
- showInstructions({
16
- verificationUri: authorization.verificationUri,
17
- verificationUriComplete: authorization.verificationUriComplete,
18
- userCode: authorization.userCode
19
- });
20
- const token = await pollForGalaToken({
21
- ...authorization,
22
- apiBaseUrl,
23
- clientId,
24
- fetchImpl,
25
- ...(sleep == null ? {} : { sleep }),
26
- now
27
- });
28
- const expiresAt = new Date(now() + token.expiresInSeconds * 1000);
29
- const target = await writeGalaCredential({
30
- accessToken: token.accessToken,
31
- expiresAt,
32
- apiBaseUrl,
33
- ...(credentialTarget == null ? {} : { target: credentialTarget })
34
- });
35
- return Object.freeze({ target, expiresAt });
36
- }
@@ -1,102 +0,0 @@
1
- import { lstat, readFile, rename, rm, writeFile } from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { normalizeSiteConfigurationOptions } from '@rathnasgala/content-validation';
4
- import { parseDocument } from 'yaml';
5
-
6
- import { scaffoldOptionNames } from './scaffold-options.js';
7
-
8
- const IMPLEMENTED_DESIGN_VALUES = Object.freeze({
9
- layout: Object.freeze(['article-first', 'portfolio']),
10
- palette: Object.freeze(['default', 'ocean'])
11
- });
12
-
13
- function nonEmptyString(value, field) {
14
- if (typeof value !== 'string' || value.trim() === '') {
15
- throw new TypeError(`${field} must be a non-empty string`);
16
- }
17
- return value.trim();
18
- }
19
-
20
- export async function configureSite(root, designOptions) {
21
- const configPath = path.resolve(root, 'site.config.yml');
22
- const relation = path.relative(path.resolve(root), configPath);
23
- if (relation.startsWith('..') || path.isAbsolute(relation)) {
24
- throw new TypeError('site.config.yml escapes the site root');
25
- }
26
-
27
- const metadata = await lstat(configPath);
28
- if (metadata.isSymbolicLink() || !metadata.isFile()) {
29
- throw new TypeError('site.config.yml must be a regular file');
30
- }
31
-
32
- let config;
33
- let document;
34
- try {
35
- document = parseDocument(await readFile(configPath, 'utf8'));
36
- if (document.errors.length > 0) throw document.errors[0];
37
- config = document.toJS();
38
- } catch (error) {
39
- throw new TypeError(`Invalid site.config.yml: ${error.message}`);
40
- }
41
- if (config.schemaVersion !== 1 || config.design == null || Array.isArray(config.design)) {
42
- throw new TypeError('Unsupported site configuration schema');
43
- }
44
- if (Object.keys(designOptions).length === 0) return config;
45
-
46
- const siteOptions = Object.fromEntries(
47
- Object.entries(designOptions).filter(([name]) => !scaffoldOptionNames.includes(name))
48
- );
49
- const normalizedSiteOptions = normalizeSiteConfigurationOptions(siteOptions);
50
-
51
- for (const [name, value] of Object.entries(designOptions)) {
52
- if (scaffoldOptionNames.includes(name)) {
53
- config.design[name] = nonEmptyString(value, `Design option ${name}`);
54
- if (IMPLEMENTED_DESIGN_VALUES[name]?.includes(config.design[name]) === false) {
55
- throw new TypeError(`Unsupported design ${name}: ${config.design[name]}`);
56
- }
57
- document.setIn(['design', name], config.design[name]);
58
- }
59
- }
60
- if (normalizedSiteOptions.siteName != null) {
61
- config.site.name = normalizedSiteOptions.siteName;
62
- document.setIn(['site', 'name'], config.site.name);
63
- }
64
- if (normalizedSiteOptions.siteAuthor != null) {
65
- config.site.author = normalizedSiteOptions.siteAuthor;
66
- document.setIn(['site', 'author'], config.site.author);
67
- }
68
- if (normalizedSiteOptions.defaultLanguage != null) {
69
- config.site.defaultLanguage = normalizedSiteOptions.defaultLanguage;
70
- document.setIn(['site', 'defaultLanguage'], config.site.defaultLanguage);
71
- }
72
- if (normalizedSiteOptions.timezone != null) {
73
- config.site.timezone = normalizedSiteOptions.timezone;
74
- document.setIn(['site', 'timezone'], config.site.timezone);
75
- }
76
- if (normalizedSiteOptions.shareTargets != null) {
77
- config.sharing.targets = normalizedSiteOptions.shareTargets;
78
- document.setIn(['sharing', 'targets'], config.sharing.targets);
79
- }
80
- if (normalizedSiteOptions.socialProfiles != null) {
81
- config.sharing.socialProfiles = normalizedSiteOptions.socialProfiles;
82
- document.setIn(['sharing', 'socialProfiles'], config.sharing.socialProfiles);
83
- }
84
-
85
- const temporary = `${configPath}.gala-config-${process.pid}`;
86
- const backup = `${configPath}.gala-backup-${process.pid}`;
87
- try {
88
- await writeFile(temporary, String(document), { flag: 'wx' });
89
- await rename(configPath, backup);
90
- try {
91
- await rename(temporary, configPath);
92
- } catch (error) {
93
- await rename(backup, configPath);
94
- throw error;
95
- }
96
- await rm(backup);
97
- } catch (error) {
98
- await rm(temporary, { force: true });
99
- throw error;
100
- }
101
- return config;
102
- }
@@ -1 +0,0 @@
1
- export { markdownPostFiles } from '@rathnasgala/content-validation';