@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.
- package/README.md +66 -212
- package/package.json +3 -3
- package/src/api/gala.js +126 -0
- package/src/api/github.js +63 -0
- package/src/api/http.js +72 -0
- package/src/auth/gala.js +52 -0
- package/src/auth/github.js +78 -0
- package/src/auth/store.js +88 -0
- package/src/cli/args.js +56 -0
- package/src/cli/terminal.js +104 -0
- package/src/commands/auth.js +20 -0
- package/src/commands/doctor.js +98 -0
- package/src/commands/init.js +197 -0
- package/src/commands/new.js +76 -0
- package/src/commands/preview.js +92 -0
- package/src/commands/publish.js +57 -0
- package/src/commands-manifest.js +58 -0
- package/src/content.js +31 -0
- package/src/git.js +143 -0
- package/src/index.js +44 -297
- package/src/publication.js +37 -0
- package/src/assign-content-ids.js +0 -1
- package/src/auth-command.js +0 -36
- package/src/configure-site.js +0 -102
- package/src/content-files.js +0 -1
- package/src/doctor-command.js +0 -214
- package/src/entitlement-client.js +0 -26
- package/src/entitlement-command.js +0 -74
- package/src/evaluation-date.js +0 -1
- package/src/gala-credential-health.js +0 -34
- package/src/gala-credential-store.js +0 -115
- package/src/gala-device-flow.js +0 -121
- package/src/github-auth-command.js +0 -29
- package/src/github-credential-store.js +0 -65
- package/src/github-device-flow.js +0 -130
- package/src/github-empty-repository.js +0 -76
- package/src/github-identity.js +0 -32
- package/src/github-pages-provisioning.js +0 -107
- package/src/github-repository-secret.js +0 -82
- package/src/github-repository-variable.js +0 -56
- package/src/github-template-repository.js +0 -165
- package/src/hook-command.js +0 -64
- package/src/http-failure.js +0 -55
- package/src/new-command.js +0 -54
- package/src/open-browser.js +0 -40
- package/src/preview-command.js +0 -60
- package/src/publication-creation-client.js +0 -144
- package/src/publication-state.js +0 -7
- package/src/publish-command.js +0 -37
- package/src/record-deployment-command.js +0 -147
- package/src/refresh-command.js +0 -104
- package/src/repository-limits.js +0 -94
- package/src/scaffold-git.js +0 -41
- package/src/scaffold-options.js +0 -58
- package/src/scaffold-preflight.js +0 -147
- package/src/scaffold-site.js +0 -162
- package/src/site-config-registration.js +0 -47
- package/src/site-registration-client.js +0 -138
- package/src/theme-package.js +0 -128
- package/src/topology-client.js +0 -43
- package/src/topology-command.js +0 -70
- package/src/upgrade-command.js +0 -81
- package/src/validate-command.js +0 -5
- 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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
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';
|
package/src/auth-command.js
DELETED
|
@@ -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
|
-
}
|
package/src/configure-site.js
DELETED
|
@@ -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
|
-
}
|
package/src/content-files.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { markdownPostFiles } from '@rathnasgala/content-validation';
|