@rathnasgala/cli 1.1.15 → 1.1.17
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/package.json +1 -1
- package/src/auth/profiles.js +28 -3
- package/src/commands/doctor.js +2 -2
- package/src/commands/domain.js +2 -2
- package/src/commands/init.js +2 -2
- package/src/commands/prism.js +2 -2
- package/src/commands/publish.js +2 -2
- package/src/git.js +75 -7
package/package.json
CHANGED
package/src/auth/profiles.js
CHANGED
|
@@ -122,6 +122,26 @@ export async function selectedProfile({ name, root = credentialDirectory() } = {
|
|
|
122
122
|
return readProfile(selected, { root });
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
export async function authenticatedProfile({
|
|
126
|
+
name,
|
|
127
|
+
terminal,
|
|
128
|
+
root = credentialDirectory(),
|
|
129
|
+
reauthenticate = addProfile,
|
|
130
|
+
} = {}) {
|
|
131
|
+
try {
|
|
132
|
+
return await selectedProfile({ name, root });
|
|
133
|
+
} catch (failure) {
|
|
134
|
+
if (!(failure instanceof ExpiredProfileError) || !terminal?.interactive) throw failure;
|
|
135
|
+
terminal.step(`Account profile ${failure.profileName} expired; signing in again`);
|
|
136
|
+
const refreshed = await reauthenticate({ terminal, root });
|
|
137
|
+
if (refreshed.metadata.name !== failure.profileName) {
|
|
138
|
+
throw new UsageError(`This repository uses account profile ${failure.profileName}, but you signed in as ${refreshed.metadata.name}. Sign in again with the matching account.`);
|
|
139
|
+
}
|
|
140
|
+
terminal.done(`Signed in again as ${failure.profileName}`);
|
|
141
|
+
return refreshed;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
125
145
|
export async function activeProfile({ root = credentialDirectory() } = {}) {
|
|
126
146
|
try {
|
|
127
147
|
const value = (await readFile(path.join(root, 'active-profile'), 'utf8')).trim();
|
|
@@ -145,13 +165,18 @@ async function readProfile(name, { root }) {
|
|
|
145
165
|
}
|
|
146
166
|
const [gala, github] = await Promise.all([readCredential(paths.gala), readCredential(paths.github)]);
|
|
147
167
|
if (gala == null || github == null) {
|
|
148
|
-
throw new
|
|
149
|
-
`Account profile ${name} has expired; run \`npx --yes @rathnasgala/cli@latest auth add\` to sign in again.`
|
|
150
|
-
);
|
|
168
|
+
throw new ExpiredProfileError(name);
|
|
151
169
|
}
|
|
152
170
|
return { metadata, gala, github };
|
|
153
171
|
}
|
|
154
172
|
|
|
173
|
+
class ExpiredProfileError extends UsageError {
|
|
174
|
+
constructor(profileName) {
|
|
175
|
+
super(`Account profile ${profileName} has expired; run \`npx --yes @rathnasgala/cli@latest auth add\` to sign in again.`);
|
|
176
|
+
this.profileName = profileName;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
155
180
|
async function setActiveProfile(name, { root }) {
|
|
156
181
|
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
157
182
|
await atomicText(path.join(root, 'active-profile'), `${requireProfileName(name)}\n`);
|
package/src/commands/doctor.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
import { galaApi } from '../api/gala.js';
|
|
5
5
|
import { accountForCommand } from '../auth/checkout-profile.js';
|
|
6
|
-
import {
|
|
6
|
+
import { authenticatedProfile } from '../auth/profiles.js';
|
|
7
7
|
import { cliCommand } from '../cli/invocation.js';
|
|
8
8
|
import { createGit } from '../git.js';
|
|
9
9
|
|
|
@@ -22,7 +22,7 @@ export async function doctor({ terminal, options, cwd = process.cwd() }) {
|
|
|
22
22
|
let selected;
|
|
23
23
|
checks.push(await checkCredential('Account profile', async () => {
|
|
24
24
|
const account = await accountForCommand(options, root, { terminal });
|
|
25
|
-
selected = await
|
|
25
|
+
selected = await authenticatedProfile({ name: account, terminal });
|
|
26
26
|
return ok(`${account}: Gala ${selected.metadata.gala.email} + GitHub @${selected.metadata.githubLogin}`);
|
|
27
27
|
}));
|
|
28
28
|
|
package/src/commands/domain.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
|
|
3
3
|
import { galaApi } from '../api/gala.js';
|
|
4
4
|
import { accountForCommand } from '../auth/checkout-profile.js';
|
|
5
|
-
import {
|
|
5
|
+
import { authenticatedProfile } from '../auth/profiles.js';
|
|
6
6
|
import { UsageError } from '../cli/args.js';
|
|
7
7
|
import { cliCommand } from '../cli/invocation.js';
|
|
8
8
|
import { customDomain } from '../domain.js';
|
|
@@ -24,7 +24,7 @@ export async function domain({ terminal, options, cwd = process.cwd() }) {
|
|
|
24
24
|
if (action !== 'set' && value != null) throw new UsageError(`domain ${action} takes no hostname`);
|
|
25
25
|
|
|
26
26
|
const account = await accountForCommand(options, root, { terminal });
|
|
27
|
-
const credential = (await
|
|
27
|
+
const credential = (await authenticatedProfile({ name: account, terminal })).gala;
|
|
28
28
|
const api = galaApi({ baseUrl: credential.apiBaseUrl, token: credential.accessToken });
|
|
29
29
|
|
|
30
30
|
if (action === 'status') {
|
package/src/commands/init.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { galaApi } from '../api/gala.js';
|
|
5
5
|
import { githubApi } from '../api/github.js';
|
|
6
6
|
import { accountForCommand, bindCheckoutProfile } from '../auth/checkout-profile.js';
|
|
7
|
-
import {
|
|
7
|
+
import { authenticatedProfile } from '../auth/profiles.js';
|
|
8
8
|
import { cloneRepository, createGit, populateEmptyRepository } from '../git.js';
|
|
9
9
|
import { UsageError } from '../cli/args.js';
|
|
10
10
|
import { CLI_INVOCATION, shellArgument } from '../cli/invocation.js';
|
|
@@ -45,7 +45,7 @@ export async function init({ terminal, options, cwd = process.cwd() }) {
|
|
|
45
45
|
|
|
46
46
|
const destination = await inspectDestination(directory);
|
|
47
47
|
|
|
48
|
-
const profile = await
|
|
48
|
+
const profile = await authenticatedProfile({ name: options.value('account'), terminal });
|
|
49
49
|
const { gala, github, metadata } = profile;
|
|
50
50
|
const account = metadata.name;
|
|
51
51
|
terminal.step(`Account ${account}: Gala ${metadata.gala.email} + GitHub @${metadata.githubLogin}`);
|
package/src/commands/prism.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
import { galaApi } from '../api/gala.js';
|
|
5
5
|
import { accountForCommand } from '../auth/checkout-profile.js';
|
|
6
|
-
import {
|
|
6
|
+
import { authenticatedProfile } from '../auth/profiles.js';
|
|
7
7
|
import { UsageError } from '../cli/args.js';
|
|
8
8
|
import { cliCommand } from '../cli/invocation.js';
|
|
9
9
|
import { createGit } from '../git.js';
|
|
@@ -23,7 +23,7 @@ export async function prism({ terminal, options, cwd = process.cwd() }) {
|
|
|
23
23
|
throw new UsageError('Run this inside a registered Gala publication, or pass --root.');
|
|
24
24
|
}
|
|
25
25
|
const account = await accountForCommand(options, root, { terminal });
|
|
26
|
-
const credential = (await
|
|
26
|
+
const credential = (await authenticatedProfile({ name: account, terminal })).gala;
|
|
27
27
|
const api = galaApi({ baseUrl: credential.apiBaseUrl, token: credential.accessToken });
|
|
28
28
|
const [action = 'status', ...args] = options.positional;
|
|
29
29
|
|
package/src/commands/publish.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { checkContent } from '../content.js';
|
|
4
4
|
import { createGit } from '../git.js';
|
|
5
5
|
import { accountForCommand } from '../auth/checkout-profile.js';
|
|
6
|
-
import {
|
|
6
|
+
import { authenticatedProfile } from '../auth/profiles.js';
|
|
7
7
|
import { readPublication } from '../publication.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -27,7 +27,7 @@ export async function publish({ terminal, options, cwd = process.cwd(), regenera
|
|
|
27
27
|
const today = options.value('today');
|
|
28
28
|
|
|
29
29
|
const account = await accountForCommand(options, root, { terminal });
|
|
30
|
-
const github = (await
|
|
30
|
+
const github = (await authenticatedProfile({ name: account, terminal })).github;
|
|
31
31
|
const git = createGit({ root, token: github.accessToken });
|
|
32
32
|
|
|
33
33
|
terminal.step('Catching up with GitHub');
|
package/src/git.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
4
|
import path from 'node:path';
|
|
5
|
+
import { parse } from 'yaml';
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* Git, authenticated as the writer's Gala credential rather than as the machine.
|
|
@@ -41,21 +44,21 @@ export function createGit({ root, token, spawnProcess = spawn } = {}) {
|
|
|
41
44
|
* them. On failure every captured line is emitted, because that is exactly when git's text is
|
|
42
45
|
* the most useful thing on screen.
|
|
43
46
|
*/
|
|
44
|
-
const run = (args, { allow = [0], capture = false } = {}) => new Promise((resolve, reject) => {
|
|
47
|
+
const run = (args, { allow = [0], capture = false, binary = false } = {}) => new Promise((resolve, reject) => {
|
|
45
48
|
const child = spawnProcess('git', ['-C', cwd, ...credentialArguments(token), ...args], {
|
|
46
49
|
cwd,
|
|
47
50
|
shell: false,
|
|
48
51
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
49
52
|
env: environmentFor(token)
|
|
50
53
|
});
|
|
51
|
-
|
|
54
|
+
const stdout = [];
|
|
52
55
|
let stderr = '';
|
|
53
|
-
child.stdout?.on('data', (chunk) => { stdout
|
|
56
|
+
child.stdout?.on('data', (chunk) => { stdout.push(Buffer.from(chunk)); });
|
|
54
57
|
child.stderr?.on('data', (chunk) => { stderr += chunk; });
|
|
55
58
|
child.once('error', reject);
|
|
56
59
|
child.once('exit', (code, signal) => {
|
|
57
60
|
if (signal || !allow.includes(code)) {
|
|
58
|
-
const said = `${stdout}${stderr}`.trim();
|
|
61
|
+
const said = `${Buffer.concat(stdout).toString('utf8')}${stderr}`.trim();
|
|
59
62
|
const failure = new Error(signal
|
|
60
63
|
? `git ${args[0]} stopped by ${signal}`
|
|
61
64
|
: `git ${args[0]} exited with ${code}`);
|
|
@@ -63,7 +66,8 @@ export function createGit({ root, token, spawnProcess = spawn } = {}) {
|
|
|
63
66
|
reject(failure);
|
|
64
67
|
return;
|
|
65
68
|
}
|
|
66
|
-
|
|
69
|
+
const output = Buffer.concat(stdout);
|
|
70
|
+
resolve(capture ? (binary ? output : output.toString('utf8').trim()) : code);
|
|
67
71
|
});
|
|
68
72
|
});
|
|
69
73
|
|
|
@@ -105,9 +109,11 @@ export function createGit({ root, token, spawnProcess = spawn } = {}) {
|
|
|
105
109
|
* makes the post-publish validation pass the sole local writer of any missing content ID.
|
|
106
110
|
*/
|
|
107
111
|
async takeRemote() {
|
|
108
|
-
const
|
|
109
|
-
const conflictedPaths = unmerged.split('\0').filter(Boolean);
|
|
112
|
+
const conflictedPaths = await unmergedPaths(run);
|
|
110
113
|
if (conflictedPaths.length > 0) {
|
|
114
|
+
if (await reconcileManagedThemeConflict(run, cwd, conflictedPaths).catch(() => false)) {
|
|
115
|
+
return git.takeRemote();
|
|
116
|
+
}
|
|
111
117
|
const failure = new Error('Git has unresolved conflicts. Gala left them untouched. Run git '
|
|
112
118
|
+ 'status, resolve or abort the operation it reports, then publish again.');
|
|
113
119
|
failure.detail = `Conflicted files:\n${conflictedPaths.join('\n')}`;
|
|
@@ -116,6 +122,17 @@ export function createGit({ root, token, spawnProcess = spawn } = {}) {
|
|
|
116
122
|
const branch = await git.branch();
|
|
117
123
|
await run(['fetch', 'origin', branch]);
|
|
118
124
|
await run(['rebase', '--autostash', `origin/${branch}`]);
|
|
125
|
+
const reappliedConflicts = await unmergedPaths(run);
|
|
126
|
+
if (reappliedConflicts.length > 0) {
|
|
127
|
+
if (await reconcileManagedThemeConflict(run, cwd, reappliedConflicts).catch(() => false)) {
|
|
128
|
+
return git.head();
|
|
129
|
+
}
|
|
130
|
+
const failure = new Error('Git updated from GitHub, but could not reapply your local work '
|
|
131
|
+
+ 'without conflicts. Gala left every file untouched. Run git status, resolve the named '
|
|
132
|
+
+ 'files, git add them, then publish again.');
|
|
133
|
+
failure.detail = `Conflicted files:\n${reappliedConflicts.join('\n')}`;
|
|
134
|
+
throw failure;
|
|
135
|
+
}
|
|
119
136
|
return git.head();
|
|
120
137
|
}
|
|
121
138
|
};
|
|
@@ -123,6 +140,57 @@ export function createGit({ root, token, spawnProcess = spawn } = {}) {
|
|
|
123
140
|
return git;
|
|
124
141
|
}
|
|
125
142
|
|
|
143
|
+
async function reconcileManagedThemeConflict(run, root, conflictedPaths) {
|
|
144
|
+
const manifestPath = '.gala/managed-files.json';
|
|
145
|
+
if (!conflictedPaths.includes(manifestPath)) return false;
|
|
146
|
+
let manifest;
|
|
147
|
+
try {
|
|
148
|
+
manifest = JSON.parse((await run(
|
|
149
|
+
['show', `:3:${manifestPath}`], { capture: true, binary: true }
|
|
150
|
+
)).toString('utf8'));
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
if (manifest?.schemaVersion !== 1 || typeof manifest.files !== 'object') return false;
|
|
155
|
+
const managedConflicts = conflictedPaths.filter((entry) => entry !== manifestPath
|
|
156
|
+
&& entry !== 'site.config.yml');
|
|
157
|
+
if (managedConflicts.length + 2 !== conflictedPaths.length) return false;
|
|
158
|
+
for (const managed of managedConflicts) {
|
|
159
|
+
const expected = manifest.files[managed];
|
|
160
|
+
if (!/^[0-9a-f]{64}$/.test(expected ?? '')) return false;
|
|
161
|
+
const local = await run(['show', `:3:${managed}`], { capture: true, binary: true });
|
|
162
|
+
if (createHash('sha256').update(local).digest('hex') !== expected) return false;
|
|
163
|
+
}
|
|
164
|
+
const worktreeConfig = await readFile(path.join(root, 'site.config.yml'), 'utf8');
|
|
165
|
+
const resolvedConfig = selectStashedConflictSections(worktreeConfig);
|
|
166
|
+
let configuration;
|
|
167
|
+
try {
|
|
168
|
+
configuration = parse(resolvedConfig);
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
if (configuration?.framework?.themePackage?.name !== manifest.themePackage?.name
|
|
173
|
+
|| configuration?.framework?.themePackage?.version !== manifest.themePackage?.version) return false;
|
|
174
|
+
await run(['checkout', '--theirs', '--', manifestPath, ...managedConflicts]);
|
|
175
|
+
await writeFile(path.join(root, 'site.config.yml'), resolvedConfig);
|
|
176
|
+
await run(['add', '--', ...conflictedPaths]);
|
|
177
|
+
return (await unmergedPaths(run)).length === 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function selectStashedConflictSections(value) {
|
|
181
|
+
return value.replace(
|
|
182
|
+
/^<<<<<<< Updated upstream\n[\s\S]*?^=======\n([\s\S]*?)^>>>>>>> Stashed changes\n/gm,
|
|
183
|
+
'$1'
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function unmergedPaths(run) {
|
|
188
|
+
const unmerged = await run(
|
|
189
|
+
['diff', '--name-only', '--diff-filter=U', '-z'], { capture: true }
|
|
190
|
+
);
|
|
191
|
+
return unmerged.split('\0').filter(Boolean);
|
|
192
|
+
}
|
|
193
|
+
|
|
126
194
|
export function cloneRepository({ url, target, token, spawnProcess = spawn }) {
|
|
127
195
|
const resolved = path.resolve(target);
|
|
128
196
|
return new Promise((resolve, reject) => {
|