@rathnasgala/cli 0.0.21 → 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 (64) hide show
  1. package/README.md +66 -212
  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 -297
  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/github-auth-command.js +0 -29
  34. package/src/github-credential-store.js +0 -65
  35. package/src/github-device-flow.js +0 -130
  36. package/src/github-empty-repository.js +0 -76
  37. package/src/github-identity.js +0 -32
  38. package/src/github-pages-provisioning.js +0 -107
  39. package/src/github-repository-secret.js +0 -82
  40. package/src/github-repository-variable.js +0 -56
  41. package/src/github-template-repository.js +0 -165
  42. package/src/hook-command.js +0 -64
  43. package/src/http-failure.js +0 -55
  44. package/src/new-command.js +0 -54
  45. package/src/open-browser.js +0 -40
  46. package/src/preview-command.js +0 -60
  47. package/src/publication-creation-client.js +0 -144
  48. package/src/publication-state.js +0 -7
  49. package/src/publish-command.js +0 -37
  50. package/src/record-deployment-command.js +0 -147
  51. package/src/refresh-command.js +0 -104
  52. package/src/repository-limits.js +0 -94
  53. package/src/scaffold-git.js +0 -41
  54. package/src/scaffold-options.js +0 -58
  55. package/src/scaffold-preflight.js +0 -147
  56. package/src/scaffold-site.js +0 -162
  57. package/src/site-config-registration.js +0 -47
  58. package/src/site-registration-client.js +0 -138
  59. package/src/theme-package.js +0 -128
  60. package/src/topology-client.js +0 -43
  61. package/src/topology-command.js +0 -70
  62. package/src/upgrade-command.js +0 -81
  63. package/src/validate-command.js +0 -5
  64. package/src/workflow-command.js +0 -87
package/src/index.js CHANGED
@@ -1,313 +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
- const result = await authenticateGithub({
88
- showScopeWarning: ({ explanation }) => process.stdout.write(`GitHub authorization: ${explanation}\n`),
89
- showInstructions: ({ verificationUri, userCode }) => {
90
- process.stdout.write(announce(verificationUri, userCode));
91
- }
92
- });
93
- process.stdout.write(`GitHub authentication stored securely with scopes: ${result.scopes.join(', ')}.\n`);
94
- } else {
95
- const apiIndex = args.indexOf('--api-base-url');
96
- const apiBaseUrl = apiIndex === -1 ? 'https://api.gala67.com' : args[apiIndex + 1];
97
- const result = await authenticateGala({
98
- apiBaseUrl,
99
- showInstructions: ({ verificationUri, userCode }) => {
100
- process.stdout.write(announce(verificationUri, userCode));
101
- }
102
- });
103
- process.stdout.write(`Gala authentication stored securely until ${result.expiresAt.toISOString()}.\n`);
104
- }
105
- } else if (command === 'scaffold') {
106
- const valueFor = (name) => {
107
- const index = args.indexOf(name);
108
- return index === -1 ? undefined : args[index + 1];
109
- };
110
- const explicitInstallationId = valueFor('--installation-id');
111
- const topology = valueFor('--topology') ?? 'provider-default';
112
- const siteOptions = parseScaffoldOptions(args);
113
- // Prompting only makes sense at a terminal. In CI there is nobody to answer, so a missing value
114
- // has to stay a clear error rather than a process that hangs waiting for enter.
115
- const interactive = process.stdin.isTTY === true;
116
- const ask = interactive
117
- ? async (question) => {
118
- const terminal = createInterface({ input: process.stdin, output: process.stdout });
119
- try { return await terminal.question(question); } finally { terminal.close(); }
120
- }
121
- : undefined;
122
- const prepared = await prepareScaffold({
123
- owner: valueFor('--owner'),
124
- repository: valueFor('--repository'),
125
- target: valueFor('--target'),
126
- githubInstallationId: explicitInstallationId == null ? undefined : Number(explicitInstallationId),
127
- siteName: siteOptions.siteName,
128
- apiBaseUrl: valueFor('--api-base-url') ?? 'https://api.gala67.com',
129
- notify: (message) => process.stdout.write(`${message}\n`),
130
- ask
131
- });
132
- const result = await scaffoldSite({
133
- notify: (message) => process.stdout.write(`${message}\n`),
134
- ask,
135
- openUrl: openInBrowser,
136
- owner: prepared.owner,
137
- repository: prepared.repository,
138
- target: prepared.target,
139
- githubInstallationId: prepared.githubInstallationId,
140
- topology,
141
- canonicalBaseUrl: valueFor('--canonical-base-url'),
142
- actionRef: valueFor('--action-ref'),
143
- siteOptions,
144
- buildMode: valueFor('--mode') ?? 'build-and-deploy',
145
- emptyExistingRepository: args.includes('--empty-existing-repository'),
146
- resumeExistingCheckout: args.includes('--resume')
147
- });
148
- await reportRepositoryLimitWarnings(result.root);
149
- process.stdout.write(`Scaffolded ${result.fullName} as Gala site ${result.siteId} in ${result.root}.\n`);
150
- } else if (command === 'configure') {
151
- const rootIndex = args.indexOf('--root');
152
- const root = rootIndex === -1 ? process.cwd() : args[rootIndex + 1];
153
- const options = parseScaffoldOptions(args);
154
- const config = await configureSite(root, options);
155
- process.stdout.write(`${JSON.stringify(config.design, null, 2)}\n`);
156
- } else if (command === 'topology') {
157
- const valueFor = (name) => {
158
- const index = args.indexOf(name);
159
- return index === -1 ? undefined : args[index + 1];
160
- };
161
- const result = await switchTopology({
162
- root: valueFor('--root') ?? process.cwd(),
163
- owner: valueFor('--owner'),
164
- repository: valueFor('--repository'),
165
- canonicalBaseUrl: valueFor('--canonical-base-url'),
166
- pathPrefix: valueFor('--path-prefix') ?? '/'
167
- });
168
- process.stdout.write(`Committed topology ${result.changeId} at ${result.commitSha}.\n`);
169
- } else if (command === 'entitlement') {
170
- const rootIndex = args.indexOf('--root');
171
- const result = await acquireAttributionEntitlement({
172
- root: rootIndex === -1 ? process.cwd() : args[rootIndex + 1]
173
- });
174
- process.stdout.write(result.changed
175
- ? `Stored the signed attribution entitlement for ${result.siteId}.\n`
176
- : `Attribution entitlement for ${result.siteId} is current.\n`);
177
- } else if (command === 'validate') {
178
- const todayIndex = args.indexOf('--today');
179
- const today = todayIndex === -1 ? undefined : args[todayIndex + 1];
180
- const root = args.find((argument) => !argument.startsWith('--') && argument !== today) ?? process.cwd();
181
- const { results } = await regenerateBuildManifest({ root, today });
182
- const failures = results.filter(({ errors }) => errors.length > 0);
183
-
184
- for (const result of failures) {
185
- for (const error of result.errors) process.stderr.write(`${result.file}: ${error}\n`);
186
- }
187
- for (const result of results) {
188
- 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;
189
46
  }
190
- process.stdout.write(`Validated ${results.length} post variant(s); ${failures.length} failed.\n`);
191
- if (failures.length > 0) process.exitCode = 1;
192
- } else if (command === 'new') {
193
- const valueFor = (name) => {
194
- const index = args.indexOf(name);
195
- return index === -1 ? undefined : args[index + 1];
196
- };
197
- const title = valueFor('--title');
198
- const language = valueFor('--language');
199
- const today = valueFor('--today');
200
- const root = valueFor('--root') ?? process.cwd();
201
- const result = await createPost({ root, title, language, today });
202
- process.stdout.write(`Created ${result.postPath}\n`);
203
- } else if (command === 'doctor') {
204
- const positional = args.filter((argument, index) =>
205
- !argument.startsWith('--') && args[index - 1] !== '--source'
206
- );
207
- const root = positional[0] ?? process.cwd();
208
- if (args.includes('--fix')) {
209
- const sourceIndex = args.indexOf('--source');
210
- const sourceRoot = sourceIndex === -1 ? undefined : args[sourceIndex + 1];
211
- if (!sourceRoot) throw new Error('doctor --fix requires --source <trusted-template-root>');
212
- const repaired = await repairFramework(root, sourceRoot);
213
- 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);
214
51
  }
215
- const findings = await diagnoseFramework(root);
216
- const drift = findings.filter(({ status }) => status !== 'intact');
217
- findings.forEach(({ path: file, status }) => process.stdout.write(`${status}\t${file}\n`));
218
- const publicationState = await diagnosePublicationState(root);
219
- process.stdout.write(`${publicationState.status}\t${publicationState.path}\n`);
220
- if (publicationState.status === 'invalid') process.exitCode = 1;
221
- if (drift.length > 0) process.exitCode = 1;
222
- } else if (command === 'preview') {
223
- const valueFor = (name) => {
224
- const index = args.indexOf(name);
225
- return index === -1 ? undefined : args[index + 1];
226
- };
227
- await previewSite({
228
- root: valueFor('--root') ?? process.cwd(),
229
- today: valueFor('--today')
230
- });
231
- } else if (command === 'workflow') {
232
- const valueFor = (name) => {
233
- const index = args.indexOf(name);
234
- return index === -1 ? undefined : args[index + 1];
235
- };
236
- const result = await writePublishWorkflow({
237
- root: valueFor('--root') ?? process.cwd(),
238
- siteId: valueFor('--site-id'),
239
- timezone: valueFor('--timezone'),
240
- actionRef: valueFor('--action-ref'),
241
- defaultBranch: valueFor('--default-branch') ?? 'main',
242
- buildMode: valueFor('--mode') ?? 'build-and-deploy'
243
- });
244
- process.stdout.write(`Wrote ${result.target} (${result.minute} ${result.hour} * * *)\n`);
245
- } else if (command === 'publish') {
246
- const valueFor = (name) => {
247
- const index = args.indexOf(name);
248
- return index === -1 ? undefined : args[index + 1];
249
- };
250
- await publishSite({
251
- root: valueFor('--root') ?? process.cwd(),
252
- today: valueFor('--today'),
253
- force: args.includes('--force')
254
- });
255
- } else if (command === 'record-deployment') {
256
- const valueFor = (name) => {
257
- const index = args.indexOf(name);
258
- return index === -1 ? undefined : args[index + 1];
259
- };
260
- const root = valueFor('--root') ?? process.cwd();
261
- const result = await recordDeployment({
262
- root,
263
- deployedOn: valueFor('--today'),
264
- deployedCommitSha: valueFor('--commit-sha')
265
- });
266
- process.stdout.write(
267
- `${result.pushed ? 'Pushed' : 'No change for'} successful deployment `
268
- + `of ${result.state.posts.length} article(s).\n`
269
- + `Recorded state SHA: ${result.recordedStateSha}\n`
270
- );
271
- } else if (command === 'refresh') {
272
- const rootIndex = args.indexOf('--root');
273
- const root = rootIndex === -1 ? process.cwd() : args[rootIndex + 1];
274
- const result = await refreshEngagementSnapshot({ root });
275
- process.stdout.write(result.changed
276
- ? 'Refreshed, committed, and pushed the engagement snapshot.\n'
277
- : 'Engagement snapshot is already current.\n');
278
- } else if (command === 'upgrade') {
279
- const valueFor = (name) => {
280
- const index = args.indexOf(name);
281
- return index === -1 ? undefined : args[index + 1];
282
- };
283
- const terminalConfirm = async ({ installed, version, channel }) => {
284
- if (args.includes('--yes')) return true;
285
- const terminal = createInterface({ input: process.stdin, output: process.stdout });
286
- try {
287
- const answer = await terminal.question(`Upgrade theme ${installed} -> ${version} (${channel})? [y/N] `);
288
- return /^(?:y|yes)$/i.test(answer.trim());
289
- } finally { terminal.close(); }
290
- };
291
- const result = await upgradeTheme({
292
- root: valueFor('--root') ?? process.cwd(),
293
- channel: valueFor('--channel'),
294
- confirm: terminalConfirm
295
- });
296
- process.stdout.write(result.cancelled ? 'Theme upgrade cancelled.\n'
297
- : result.changed ? `Upgraded theme to ${result.version}.\n`
298
- : `Theme ${result.version} is already installed.\n`);
299
- process.stdout.write(
300
- `Action major v${result.action.currentMajor}; `
301
- + (result.action.newerAvailable
302
- ? `v${result.action.latestMajor} is available.\n`
303
- : 'no newer major is available.\n')
304
- );
305
- } else if (command === 'hook' && args[0] === 'install') {
306
- const rootIndex = args.indexOf('--root');
307
- const root = rootIndex === -1 ? process.cwd() : args[rootIndex + 1];
308
- const result = await installPrePushHook(root);
309
- process.stdout.write(`${result.installed ? 'Installed' : 'Already installed'} ${result.target}\n`);
310
- } else {
311
- process.stderr.write(`${usage}\n`);
312
- 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`);
313
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';