@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
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { lstat } from 'node:fs/promises';
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
4
|
-
import { createHash } from 'node:crypto';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import { parseFrontmatter } from '@rathnasgala/content-validation';
|
|
7
|
-
|
|
8
|
-
import { repositoryEvaluationDate } from './evaluation-date.js';
|
|
9
|
-
import { recordSuccessfulDeployment } from './publication-state.js';
|
|
10
|
-
import { BUILD_MANIFEST_PATH } from './validate-command.js';
|
|
11
|
-
|
|
12
|
-
function runGit(root, args, spawnProcess) {
|
|
13
|
-
return new Promise((resolve, reject) => {
|
|
14
|
-
const child = spawnProcess('git', ['-C', root, ...args], {
|
|
15
|
-
cwd: root,
|
|
16
|
-
shell: false,
|
|
17
|
-
stdio: 'inherit'
|
|
18
|
-
});
|
|
19
|
-
child.once('error', reject);
|
|
20
|
-
child.once('exit', (code, signal) => {
|
|
21
|
-
if (signal) reject(new Error(`Git terminated by signal ${signal}`));
|
|
22
|
-
else resolve(code);
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function readHead(root, spawnProcess) {
|
|
28
|
-
return new Promise((resolve, reject) => {
|
|
29
|
-
const child = spawnProcess('git', ['-C', root, 'rev-parse', '--verify', 'HEAD'], {
|
|
30
|
-
cwd: root,
|
|
31
|
-
shell: false,
|
|
32
|
-
stdio: ['ignore', 'pipe', 'inherit']
|
|
33
|
-
});
|
|
34
|
-
let output = '';
|
|
35
|
-
child.stdout?.on('data', (chunk) => { output += chunk; });
|
|
36
|
-
child.once('error', reject);
|
|
37
|
-
child.once('exit', (code, signal) => {
|
|
38
|
-
if (signal) reject(new Error(`Git terminated by signal ${signal}`));
|
|
39
|
-
else if (code !== 0) reject(new Error(`Git rev-parse exited with code ${code}`));
|
|
40
|
-
else {
|
|
41
|
-
const sha = output.trim();
|
|
42
|
-
if (!/^[0-9a-f]{40}$/.test(sha)) reject(new Error('Git returned an invalid HEAD SHA'));
|
|
43
|
-
else resolve(sha);
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function assignedContentPaths(root, manifest) {
|
|
50
|
-
const assigned = manifest.assignedContentIds ?? [];
|
|
51
|
-
if (!Array.isArray(assigned)) throw new TypeError('assignedContentIds must be a list');
|
|
52
|
-
const sources = new Set();
|
|
53
|
-
for (const item of assigned) {
|
|
54
|
-
if (item == null
|
|
55
|
-
|| typeof item.source !== 'string'
|
|
56
|
-
|| !/^content\/posts\/[a-z0-9]+(?:-[a-z0-9]+)*\/index\.[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*\.md$/.test(item.source)
|
|
57
|
-
|| typeof item.id !== 'string'
|
|
58
|
-
|| !/^[0-7][0-9A-HJKMNP-TV-Z]{25}$/.test(item.id)
|
|
59
|
-
|| typeof item.fileHash !== 'string'
|
|
60
|
-
|| !/^[a-f0-9]{64}$/.test(item.fileHash)
|
|
61
|
-
|| sources.has(item.source)) {
|
|
62
|
-
throw new TypeError('assignedContentIds contains an invalid entry');
|
|
63
|
-
}
|
|
64
|
-
const file = path.resolve(root, item.source);
|
|
65
|
-
const metadata = await lstat(file);
|
|
66
|
-
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
67
|
-
throw new TypeError(`Assigned-ID source must be a regular file: ${item.source}`);
|
|
68
|
-
}
|
|
69
|
-
const bytes = await readFile(file);
|
|
70
|
-
if (createHash('sha256').update(bytes).digest('hex') !== item.fileHash) {
|
|
71
|
-
throw new Error(`Assigned-ID source changed after the deployed build: ${item.source}`);
|
|
72
|
-
}
|
|
73
|
-
const parsed = parseFrontmatter(bytes.toString('utf8'));
|
|
74
|
-
if (parsed.errors.length > 0 || parsed.data.id !== item.id) {
|
|
75
|
-
throw new Error(`Assigned-ID source no longer contains its deployed ULID: ${item.source}`);
|
|
76
|
-
}
|
|
77
|
-
sources.add(item.source);
|
|
78
|
-
}
|
|
79
|
-
return [...sources].sort();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export async function recordDeployment({
|
|
83
|
-
root,
|
|
84
|
-
deployedOn,
|
|
85
|
-
now,
|
|
86
|
-
deployedCommitSha,
|
|
87
|
-
spawnProcess = spawn
|
|
88
|
-
}) {
|
|
89
|
-
const siteRoot = path.resolve(root);
|
|
90
|
-
const manifestPath = path.join(siteRoot, BUILD_MANIFEST_PATH);
|
|
91
|
-
let manifest;
|
|
92
|
-
try {
|
|
93
|
-
manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
94
|
-
} catch (error) {
|
|
95
|
-
if (error.code === 'ENOENT') {
|
|
96
|
-
throw new Error('Current validated build manifest is missing; deployment cannot be recorded');
|
|
97
|
-
}
|
|
98
|
-
throw new TypeError(`Current validated build manifest is invalid: ${error.message}`);
|
|
99
|
-
}
|
|
100
|
-
const date = deployedOn ?? await repositoryEvaluationDate({ root: siteRoot, now });
|
|
101
|
-
if (typeof deployedCommitSha !== 'string' || !/^[0-9a-f]{40}$/.test(deployedCommitSha)) {
|
|
102
|
-
throw new TypeError('record-deployment requires --commit-sha <lowercase 40-character SHA>');
|
|
103
|
-
}
|
|
104
|
-
const head = await readHead(siteRoot, spawnProcess);
|
|
105
|
-
if (deployedCommitSha !== head) {
|
|
106
|
-
throw new Error(`Deployment SHA ${deployedCommitSha} does not match checkout HEAD ${head}`);
|
|
107
|
-
}
|
|
108
|
-
const state = await recordSuccessfulDeployment({
|
|
109
|
-
root: siteRoot,
|
|
110
|
-
manifest,
|
|
111
|
-
deployedOn: date,
|
|
112
|
-
deployedCommitSha
|
|
113
|
-
});
|
|
114
|
-
const statePath = '.gala/publication-state.yml';
|
|
115
|
-
const contentPaths = await assignedContentPaths(siteRoot, manifest);
|
|
116
|
-
const committedPaths = [statePath, ...contentPaths];
|
|
117
|
-
const addCode = await runGit(siteRoot, ['add', '--', ...committedPaths], spawnProcess);
|
|
118
|
-
if (addCode !== 0) throw new Error(`Git add exited with code ${addCode}`);
|
|
119
|
-
const diffCode = await runGit(
|
|
120
|
-
siteRoot,
|
|
121
|
-
['diff', '--cached', '--quiet', '--exit-code', '--', ...committedPaths],
|
|
122
|
-
spawnProcess
|
|
123
|
-
);
|
|
124
|
-
if (diffCode === 0) return { state, pushed: false, recordedStateSha: head };
|
|
125
|
-
if (diffCode !== 1) throw new Error(`Git diff exited with code ${diffCode}`);
|
|
126
|
-
const assignmentTrailers = (manifest.assignedContentIds ?? []).map(
|
|
127
|
-
({ id, source }) => `Gala-Assigned-ID: ${id} ${source}`
|
|
128
|
-
);
|
|
129
|
-
const commitMessage = [
|
|
130
|
-
'chore(gala): record successful deployment [skip ci]',
|
|
131
|
-
'',
|
|
132
|
-
`Gala-Deployed-SHA: ${deployedCommitSha}`,
|
|
133
|
-
...assignmentTrailers
|
|
134
|
-
].join('\n');
|
|
135
|
-
const commitCode = await runGit(siteRoot, [
|
|
136
|
-
'commit', '--only', '-m', commitMessage,
|
|
137
|
-
'--', ...committedPaths
|
|
138
|
-
], spawnProcess);
|
|
139
|
-
if (commitCode !== 0) throw new Error(`Git commit exited with code ${commitCode}`);
|
|
140
|
-
const recordedStateSha = await readHead(siteRoot, spawnProcess);
|
|
141
|
-
if (recordedStateSha === deployedCommitSha) {
|
|
142
|
-
throw new Error('Git did not create a distinct recorded-state commit');
|
|
143
|
-
}
|
|
144
|
-
const pushCode = await runGit(siteRoot, ['push'], spawnProcess);
|
|
145
|
-
if (pushCode !== 0) throw new Error(`Git push exited with code ${pushCode}`);
|
|
146
|
-
return { state, pushed: true, recordedStateSha };
|
|
147
|
-
}
|
package/src/refresh-command.js
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { lstat, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
4
|
-
import { parse } from 'yaml';
|
|
5
|
-
|
|
6
|
-
import { readGalaCredential } from './gala-credential-store.js';
|
|
7
|
-
|
|
8
|
-
const ULID = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/;
|
|
9
|
-
const UTC_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
|
|
10
|
-
const SNAPSHOT_PATH = '.engagement-snapshot.json';
|
|
11
|
-
|
|
12
|
-
async function runGit(root, args) {
|
|
13
|
-
return new Promise((resolve, reject) => {
|
|
14
|
-
const child = spawn('git', ['-C', root, ...args], { shell: false, stdio: 'inherit' });
|
|
15
|
-
child.once('error', reject);
|
|
16
|
-
child.once('exit', (code, signal) => {
|
|
17
|
-
if (signal) reject(new Error(`git terminated by signal ${signal}`));
|
|
18
|
-
else if (code !== 0) reject(new Error(`git ${args[0]} exited with code ${code}`));
|
|
19
|
-
else resolve();
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function commitRefreshedSnapshot(root, relativePath) {
|
|
25
|
-
await runGit(root, [
|
|
26
|
-
'commit', '--only', '--message', 'chore(gala): refresh engagement snapshot', '--', relativePath
|
|
27
|
-
]);
|
|
28
|
-
await runGit(root, ['push']);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function validateSnapshot(payload) {
|
|
32
|
-
if (payload?.schemaVersion !== 1 || !UTC_INSTANT.test(payload.refreshedAt)
|
|
33
|
-
|| payload.articles == null || Array.isArray(payload.articles)
|
|
34
|
-
|| typeof payload.articles !== 'object') {
|
|
35
|
-
throw new TypeError('Engagement snapshot response is invalid');
|
|
36
|
-
}
|
|
37
|
-
for (const [articleId, counts] of Object.entries(payload.articles)) {
|
|
38
|
-
if (!ULID.test(articleId) || counts == null || Array.isArray(counts)
|
|
39
|
-
|| typeof counts !== 'object'
|
|
40
|
-
|| Object.keys(counts).sort().join(',') !== 'comments,reactions,views'
|
|
41
|
-
|| !['reactions', 'comments', 'views'].every(
|
|
42
|
-
(field) => Number.isSafeInteger(counts[field]) && counts[field] >= 0
|
|
43
|
-
)) {
|
|
44
|
-
throw new TypeError('Engagement snapshot response is invalid');
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return payload;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function requireRegularFile(target, label, { allowMissing = false } = {}) {
|
|
51
|
-
try {
|
|
52
|
-
const metadata = await lstat(target);
|
|
53
|
-
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
54
|
-
throw new TypeError(`${label} must be a regular file`);
|
|
55
|
-
}
|
|
56
|
-
return true;
|
|
57
|
-
} catch (error) {
|
|
58
|
-
if (allowMissing && error.code === 'ENOENT') return false;
|
|
59
|
-
throw error;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export async function refreshEngagementSnapshot({
|
|
64
|
-
root = process.cwd(),
|
|
65
|
-
readCredential = readGalaCredential,
|
|
66
|
-
fetchImpl = fetch,
|
|
67
|
-
commitSnapshot = commitRefreshedSnapshot
|
|
68
|
-
} = {}) {
|
|
69
|
-
const siteRoot = path.resolve(root);
|
|
70
|
-
const configPath = path.join(siteRoot, 'site.config.yml');
|
|
71
|
-
await requireRegularFile(configPath, 'site.config.yml');
|
|
72
|
-
const config = parse(await readFile(configPath, 'utf8'));
|
|
73
|
-
const siteId = config?.site?.id;
|
|
74
|
-
if (!ULID.test(siteId)) throw new TypeError('site.config.yml site.id must be a canonical ULID');
|
|
75
|
-
|
|
76
|
-
const credential = await readCredential();
|
|
77
|
-
const endpoint = new URL(`/v1/sites/${siteId}/engagement-snapshot`, credential.apiBaseUrl);
|
|
78
|
-
const loopback = endpoint.protocol === 'http:'
|
|
79
|
-
&& ['127.0.0.1', 'localhost', '::1'].includes(endpoint.hostname);
|
|
80
|
-
if ((endpoint.protocol !== 'https:' && !loopback) || endpoint.username || endpoint.password) {
|
|
81
|
-
throw new TypeError('Gala API URL must be credential-free HTTPS or HTTP loopback');
|
|
82
|
-
}
|
|
83
|
-
const response = await fetchImpl(endpoint, {
|
|
84
|
-
method: 'GET',
|
|
85
|
-
headers: { Authorization: `Bearer ${credential.accessToken}`, Accept: 'application/json' }
|
|
86
|
-
});
|
|
87
|
-
if (!response.ok) throw new Error(`Engagement snapshot refresh failed with HTTP ${response.status}`);
|
|
88
|
-
const snapshot = validateSnapshot(await response.json());
|
|
89
|
-
const next = `${JSON.stringify(snapshot, null, 2)}\n`;
|
|
90
|
-
const target = path.join(siteRoot, SNAPSHOT_PATH);
|
|
91
|
-
const exists = await requireRegularFile(target, 'Engagement snapshot', { allowMissing: true });
|
|
92
|
-
if (exists && await readFile(target, 'utf8') === next) return Object.freeze({ changed: false });
|
|
93
|
-
|
|
94
|
-
const temporary = `${target}.gala-${process.pid}`;
|
|
95
|
-
try {
|
|
96
|
-
await writeFile(temporary, next, { flag: 'wx' });
|
|
97
|
-
await rename(temporary, target);
|
|
98
|
-
} catch (error) {
|
|
99
|
-
await rm(temporary, { force: true });
|
|
100
|
-
throw error;
|
|
101
|
-
}
|
|
102
|
-
await commitSnapshot(siteRoot, SNAPSHOT_PATH);
|
|
103
|
-
return Object.freeze({ changed: true });
|
|
104
|
-
}
|
package/src/repository-limits.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { lstat, readdir } from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
const MEBIBYTE = 1024 * 1024;
|
|
6
|
-
|
|
7
|
-
function gitRepositoryBytes(root, spawnProcess) {
|
|
8
|
-
return new Promise((resolve, reject) => {
|
|
9
|
-
const child = spawnProcess('git', ['-C', root, 'count-objects', '-v'], {
|
|
10
|
-
cwd: root,
|
|
11
|
-
shell: false,
|
|
12
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
13
|
-
});
|
|
14
|
-
let output = '';
|
|
15
|
-
let errors = '';
|
|
16
|
-
child.stdout.on('data', (chunk) => { output += chunk; });
|
|
17
|
-
child.stderr.on('data', (chunk) => { errors += chunk; });
|
|
18
|
-
child.once('error', reject);
|
|
19
|
-
child.once('exit', (code, signal) => {
|
|
20
|
-
if (signal) return reject(new Error(`Repository inspection terminated by signal ${signal}`));
|
|
21
|
-
if (code !== 0) return reject(new Error(`Repository inspection failed: ${errors.trim()}`));
|
|
22
|
-
const values = Object.fromEntries(output.trim().split('\n').map((line) => {
|
|
23
|
-
const separator = line.indexOf(':');
|
|
24
|
-
return [line.slice(0, separator), Number(line.slice(separator + 1).trim())];
|
|
25
|
-
}));
|
|
26
|
-
if (!Number.isFinite(values.size) || !Number.isFinite(values['size-pack'])) {
|
|
27
|
-
return reject(new Error('Git returned invalid repository size metadata'));
|
|
28
|
-
}
|
|
29
|
-
resolve((values.size + values['size-pack']) * 1024);
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function countPosts(directory) {
|
|
35
|
-
const entries = await readdir(directory, { withFileTypes: true });
|
|
36
|
-
const counts = await Promise.all(entries.map((entry) => {
|
|
37
|
-
if (entry.isDirectory()) return countPosts(path.join(directory, entry.name));
|
|
38
|
-
return Promise.resolve(entry.isFile() && /^index\.[^.]+\.md$/.test(entry.name) ? 1 : 0);
|
|
39
|
-
}));
|
|
40
|
-
return counts.reduce((total, count) => total + count, 0);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function repositoryLimitWarnings({ repositoryBytes, postCount, buildDurationMs }) {
|
|
44
|
-
const warnings = [];
|
|
45
|
-
if (repositoryBytes > 800 * MEBIBYTE) {
|
|
46
|
-
warnings.push({ severity: 'critical', code: 'repository-size-800mb' });
|
|
47
|
-
} else if (repositoryBytes > 500 * MEBIBYTE) {
|
|
48
|
-
warnings.push({ severity: 'warning', code: 'repository-size-500mb' });
|
|
49
|
-
}
|
|
50
|
-
if (postCount > 1000) warnings.push({ severity: 'warning', code: 'post-count-1000' });
|
|
51
|
-
if (buildDurationMs != null && buildDurationMs > 5 * 60 * 1000) {
|
|
52
|
-
warnings.push({ severity: 'warning', code: 'build-duration-5m' });
|
|
53
|
-
}
|
|
54
|
-
return warnings;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export async function inspectRepositoryLimits(root, { spawnProcess = spawn, buildDurationMs } = {}) {
|
|
58
|
-
const resolvedRoot = path.resolve(root);
|
|
59
|
-
const [repositoryBytes, postCount] = await Promise.all([
|
|
60
|
-
gitRepositoryBytes(resolvedRoot, spawnProcess),
|
|
61
|
-
countPosts(path.join(resolvedRoot, 'content', 'posts'))
|
|
62
|
-
]);
|
|
63
|
-
return {
|
|
64
|
-
repositoryBytes,
|
|
65
|
-
postCount,
|
|
66
|
-
warnings: repositoryLimitWarnings({ repositoryBytes, postCount, buildDurationMs })
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export async function reportRepositoryLimitWarnings(
|
|
71
|
-
root,
|
|
72
|
-
{ spawnProcess = spawn, output = process.stderr } = {}
|
|
73
|
-
) {
|
|
74
|
-
const resolvedRoot = path.resolve(root);
|
|
75
|
-
try {
|
|
76
|
-
const [gitMetadata, config, posts] = await Promise.all([
|
|
77
|
-
lstat(path.join(resolvedRoot, '.git')),
|
|
78
|
-
lstat(path.join(resolvedRoot, 'site.config.yml')),
|
|
79
|
-
lstat(path.join(resolvedRoot, 'content', 'posts'))
|
|
80
|
-
]);
|
|
81
|
-
if ((!gitMetadata.isDirectory() && !gitMetadata.isFile())
|
|
82
|
-
|| !config.isFile() || config.isSymbolicLink()
|
|
83
|
-
|| !posts.isDirectory() || posts.isSymbolicLink()) {
|
|
84
|
-
return [];
|
|
85
|
-
}
|
|
86
|
-
} catch (error) {
|
|
87
|
-
if (error.code === 'ENOENT') return [];
|
|
88
|
-
throw error;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const { warnings } = await inspectRepositoryLimits(resolvedRoot, { spawnProcess });
|
|
92
|
-
for (const { severity, code } of warnings) output.write(`${severity}\t${code}\n`);
|
|
93
|
-
return warnings;
|
|
94
|
-
}
|
package/src/scaffold-git.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
|
|
3
|
-
function run(root, args, spawnProcess, acceptedExitCodes = [0]) {
|
|
4
|
-
return new Promise((resolve, reject) => {
|
|
5
|
-
const child = spawnProcess('git', ['-C', root, ...args], { cwd: root, shell: false, stdio: 'inherit' });
|
|
6
|
-
child.once('error', reject);
|
|
7
|
-
child.once('exit', (code, signal) => {
|
|
8
|
-
if (signal) reject(new Error(`Git ${args[0]} terminated by signal ${signal}`));
|
|
9
|
-
else if (!acceptedExitCodes.includes(code)) reject(new Error(`Git ${args[0]} exited with code ${code}`));
|
|
10
|
-
else resolve(code);
|
|
11
|
-
});
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function capture(root, args, spawnProcess) {
|
|
16
|
-
return new Promise((resolve, reject) => {
|
|
17
|
-
const child = spawnProcess('git', ['-C', root, ...args], {
|
|
18
|
-
cwd: root, shell: false, stdio: ['ignore', 'pipe', 'inherit']
|
|
19
|
-
});
|
|
20
|
-
let stdout = '';
|
|
21
|
-
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
22
|
-
child.once('error', reject);
|
|
23
|
-
child.once('exit', (code, signal) => {
|
|
24
|
-
if (signal) reject(new Error(`Git ${args[0]} terminated by signal ${signal}`));
|
|
25
|
-
else if (code !== 0) reject(new Error(`Git ${args[0]} exited with code ${code}`));
|
|
26
|
-
else resolve(stdout.trim());
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export async function commitScaffold(root, { spawnProcess = spawn } = {}) {
|
|
32
|
-
await run(root, ['add', '--', 'site.config.yml', '.github/workflows/publish.yml'], spawnProcess);
|
|
33
|
-
const unchanged = await run(root, ['diff', '--cached', '--quiet', '--exit-code'], spawnProcess, [0, 1]);
|
|
34
|
-
if (unchanged === 1) {
|
|
35
|
-
await run(root, ['commit', '-m', 'chore(gala): configure site'], spawnProcess);
|
|
36
|
-
}
|
|
37
|
-
await run(root, ['push', 'origin', 'HEAD'], spawnProcess);
|
|
38
|
-
const commitSha = await capture(root, ['rev-parse', 'HEAD'], spawnProcess);
|
|
39
|
-
if (!/^[0-9a-f]{40}$/.test(commitSha)) throw new Error('Git returned an invalid scaffold commit SHA');
|
|
40
|
-
return commitSha;
|
|
41
|
-
}
|
package/src/scaffold-options.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
export const scaffoldOptionNames = Object.freeze([
|
|
2
|
-
'theme',
|
|
3
|
-
'layout',
|
|
4
|
-
'palette',
|
|
5
|
-
'typography',
|
|
6
|
-
'spacing',
|
|
7
|
-
'radius',
|
|
8
|
-
'density',
|
|
9
|
-
'motion',
|
|
10
|
-
'componentStyle'
|
|
11
|
-
]);
|
|
12
|
-
|
|
13
|
-
const singleValueOptions = Object.freeze({
|
|
14
|
-
'site-name': 'siteName',
|
|
15
|
-
author: 'siteAuthor',
|
|
16
|
-
language: 'defaultLanguage',
|
|
17
|
-
timezone: 'timezone'
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
function requireValue(args, index, name) {
|
|
21
|
-
const value = args[index + 1];
|
|
22
|
-
if (!value || value.startsWith('--')) {
|
|
23
|
-
throw new Error(`Missing value for --${name}`);
|
|
24
|
-
}
|
|
25
|
-
return value;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function parseScaffoldOptions(args) {
|
|
29
|
-
const values = {};
|
|
30
|
-
|
|
31
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
32
|
-
const argument = args[index];
|
|
33
|
-
if (!argument.startsWith('--')) continue;
|
|
34
|
-
|
|
35
|
-
const name = argument.slice(2);
|
|
36
|
-
if (name === 'share-target') {
|
|
37
|
-
const value = requireValue(args, index, name);
|
|
38
|
-
values.shareTargets = [...(values.shareTargets ?? []), value];
|
|
39
|
-
index += 1;
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
if (name === 'social-profile') {
|
|
43
|
-
const value = requireValue(args, index, name);
|
|
44
|
-
values.socialProfiles = [...(values.socialProfiles ?? []), value];
|
|
45
|
-
index += 1;
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const targetName = singleValueOptions[name] ?? name;
|
|
50
|
-
if (!scaffoldOptionNames.includes(name) && singleValueOptions[name] == null) continue;
|
|
51
|
-
|
|
52
|
-
const value = requireValue(args, index, name);
|
|
53
|
-
values[targetName] = value;
|
|
54
|
-
index += 1;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return values;
|
|
58
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
|
|
3
|
-
import { authenticateGala } from './auth-command.js';
|
|
4
|
-
import { authenticateGithub } from './github-auth-command.js';
|
|
5
|
-
import { readGalaCredential } from './gala-credential-store.js';
|
|
6
|
-
import { readGithubCredential } from './github-credential-store.js';
|
|
7
|
-
import { resolveGithubLogin } from './github-identity.js';
|
|
8
|
-
import { galaCredentialAccepted } from './gala-credential-health.js';
|
|
9
|
-
import { openInBrowser } from './open-browser.js';
|
|
10
|
-
import { forgetGalaCredential } from './gala-credential-store.js';
|
|
11
|
-
|
|
12
|
-
export const GITHUB_APP_INSTALL_URL = 'https://github.com/apps/gala67-app/installations/new';
|
|
13
|
-
|
|
14
|
-
const DEFAULT_API_BASE_URL = 'https://api.gala67.com';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Everything `scaffold` needs, worked out rather than demanded.
|
|
18
|
-
*
|
|
19
|
-
* `scaffold` used to require four values up front — `--owner`, `--repository`, `--target` and
|
|
20
|
-
* `--installation-id` — and it failed outright if `auth` or `auth github` had not been run first,
|
|
21
|
-
* telling the writer to go and run them. Three of those four are derivable and the two sign-ins
|
|
22
|
-
* can simply happen. Every one of them is still accepted as an explicit override; nothing that
|
|
23
|
-
* worked before stops working.
|
|
24
|
-
*
|
|
25
|
-
* The steps are ordered so nothing is created until everything is known: the App installation is
|
|
26
|
-
* confirmed before a repository exists, rather than after, so an interrupted run leaves no
|
|
27
|
-
* half-connected repository behind.
|
|
28
|
-
*/
|
|
29
|
-
export async function prepareScaffold({
|
|
30
|
-
owner,
|
|
31
|
-
repository,
|
|
32
|
-
target,
|
|
33
|
-
githubInstallationId,
|
|
34
|
-
siteName,
|
|
35
|
-
cwd = process.cwd(),
|
|
36
|
-
notify = () => {},
|
|
37
|
-
ask,
|
|
38
|
-
openUrl = openInBrowser,
|
|
39
|
-
readGala = readGalaCredential,
|
|
40
|
-
readGithub = readGithubCredential,
|
|
41
|
-
credentialAccepted = galaCredentialAccepted,
|
|
42
|
-
forgetGala = forgetGalaCredential,
|
|
43
|
-
signInGala = authenticateGala,
|
|
44
|
-
signInGithub = authenticateGithub,
|
|
45
|
-
resolveLogin = resolveGithubLogin,
|
|
46
|
-
apiBaseUrl = DEFAULT_API_BASE_URL
|
|
47
|
-
} = {}) {
|
|
48
|
-
const gala = await ensureGala({
|
|
49
|
-
apiBaseUrl, notify, readGala, signInGala, credentialAccepted, forgetGala, openUrl
|
|
50
|
-
});
|
|
51
|
-
const github = await ensureGithub({ notify, readGithub, signInGithub, openUrl });
|
|
52
|
-
|
|
53
|
-
const resolvedOwner = owner ?? await resolveLogin({ accessToken: github.accessToken });
|
|
54
|
-
|
|
55
|
-
const resolvedRepository = repository
|
|
56
|
-
?? (target == null ? null : path.basename(path.resolve(cwd, target)))
|
|
57
|
-
?? repositoryNameFrom(siteName)
|
|
58
|
-
?? await askForRepository(ask);
|
|
59
|
-
|
|
60
|
-
// `--target ./` is the common case and means "here", so the repository takes its name from the
|
|
61
|
-
// directory the writer is standing in. Everywhere else the repository names its own folder.
|
|
62
|
-
const resolvedTarget = target ?? `./${resolvedRepository}`;
|
|
63
|
-
|
|
64
|
-
/*
|
|
65
|
-
* No installation lookup. The id is an internal GitHub identifier for an App the server owns, and
|
|
66
|
-
* nothing here can discover it reliably: a GitHub App token could list installations and the CLI
|
|
67
|
-
* cannot hold one, while the repository inventory only carries the id once a repository exists —
|
|
68
|
-
* which, in the flow that creates the first repository, is never. The server resolves it during
|
|
69
|
-
* registration. `--installation-id` still overrides, for an account with several.
|
|
70
|
-
*/
|
|
71
|
-
return Object.freeze({
|
|
72
|
-
owner: resolvedOwner,
|
|
73
|
-
repository: resolvedRepository,
|
|
74
|
-
target: resolvedTarget,
|
|
75
|
-
githubInstallationId: githubInstallationId ?? null
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* A missing, expired or refused credential is a step to take, not an error to report.
|
|
81
|
-
*
|
|
82
|
-
* The stored file is checked against the server before anything depends on it, because a
|
|
83
|
-
* credential that parses and has not expired can still be one the API refuses — and finding that
|
|
84
|
-
* out four calls later, as an opaque 401 from whichever endpoint got there first, is how a
|
|
85
|
-
* "sign in again" turned into a stack trace.
|
|
86
|
-
*/
|
|
87
|
-
async function ensureGala({
|
|
88
|
-
apiBaseUrl, notify, readGala, signInGala, credentialAccepted, forgetGala, openUrl
|
|
89
|
-
}) {
|
|
90
|
-
let stored = null;
|
|
91
|
-
try {
|
|
92
|
-
stored = await readGala();
|
|
93
|
-
} catch {
|
|
94
|
-
stored = null;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (stored != null) {
|
|
98
|
-
const base = stored.apiBaseUrl ?? apiBaseUrl;
|
|
99
|
-
if (await credentialAccepted({ apiBaseUrl: base, accessToken: stored.accessToken })) {
|
|
100
|
-
return stored;
|
|
101
|
-
}
|
|
102
|
-
// Leaving it on disk would make every later command repeat this discovery.
|
|
103
|
-
await forgetGala();
|
|
104
|
-
notify('Your Gala sign-in is no longer valid.');
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
notify('Signing in to Gala.');
|
|
108
|
-
await signInGala({
|
|
109
|
-
apiBaseUrl,
|
|
110
|
-
showInstructions: ({ verificationUri, userCode }) =>
|
|
111
|
-
notify(`${openUrl(verificationUri) ? 'Opened' : 'Open'} ${verificationUri}\nEnter code: ${userCode}`)
|
|
112
|
-
});
|
|
113
|
-
return readGala();
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async function ensureGithub({ notify, readGithub, signInGithub, openUrl }) {
|
|
117
|
-
try {
|
|
118
|
-
return await readGithub();
|
|
119
|
-
} catch {
|
|
120
|
-
notify('Signing in to GitHub.');
|
|
121
|
-
await signInGithub({
|
|
122
|
-
showScopeWarning: ({ explanation }) => notify(`GitHub authorization: ${explanation}`),
|
|
123
|
-
showInstructions: ({ verificationUri, userCode }) =>
|
|
124
|
-
notify(`${openUrl(verificationUri) ? 'Opened' : 'Open'} ${verificationUri}\nEnter code: ${userCode}`)
|
|
125
|
-
});
|
|
126
|
-
return readGithub();
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** GitHub repository names allow letters, digits, dot, underscore and hyphen, and nothing else. */
|
|
131
|
-
export function repositoryNameFrom(siteName) {
|
|
132
|
-
if (typeof siteName !== 'string') return null;
|
|
133
|
-
const slug = siteName
|
|
134
|
-
.trim().toLowerCase()
|
|
135
|
-
.replace(/[^a-z0-9._-]+/g, '-')
|
|
136
|
-
.replace(/^-+|-+$/g, '');
|
|
137
|
-
return slug === '' ? null : slug;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function askForRepository(ask) {
|
|
141
|
-
if (typeof ask !== 'function') {
|
|
142
|
-
throw new TypeError('repository is required; pass --repository or --site-name');
|
|
143
|
-
}
|
|
144
|
-
const answer = repositoryNameFrom(await ask('What should the publication repository be called? '));
|
|
145
|
-
if (answer == null) throw new TypeError('A repository name is required');
|
|
146
|
-
return answer;
|
|
147
|
-
}
|