@rathnasgala/cli 1.1.12 → 1.1.14

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 CHANGED
@@ -12,6 +12,16 @@ Nothing to install. Every command runs through `npx`.
12
12
 
13
13
  ## Start a publication
14
14
 
15
+ Sign in once. The CLI binds the verified Gala and GitHub identities together, names the account
16
+ profile after the GitHub username, and makes it active.
17
+
18
+ ```console
19
+ npx --yes @rathnasgala/cli@latest auth add
20
+ ```
21
+
22
+ Use `auth list` to inspect every pair, `auth use <github-login>` to change the active account, and
23
+ `auth remove <github-login>` to forget one. Tokens from different profiles are never combined.
24
+
15
25
  Create it in the current empty folder:
16
26
 
17
27
  ```console
@@ -30,7 +40,7 @@ accepted and its `.git` directory is preserved. To reserve a custom domain durin
30
40
  `--domain blog.example.com`; Gala still requires GitHub ownership verification and healthy DNS
31
41
  before activating it.
32
42
 
33
- It signs you in to Gala and to GitHub if you are not already, creates the repository, registers the
43
+ It uses the selected profile, creates the repository, registers the
34
44
  publication, and leaves a working checkout in the folder. GitHub starts the first deployment in the
35
45
  background. The command links to that deployment rather than presenting the public address as live
36
46
  before GitHub has finished.
@@ -126,18 +136,20 @@ Run any command with `--help`.
126
136
 
127
137
  | Command | What it does | Options |
128
138
  | --- | --- | --- |
129
- | `init` | Create a publication in the current or named empty directory | `--name`, `--domain` |
130
- | `domain` | Inspect or change the custom domain | `--root` |
139
+ | `init` | Create a publication in the current or named empty directory | `--name`, `--domain`, `--account` |
140
+ | `domain` | Inspect or change the custom domain | `--root`, `--account` |
131
141
  | `new` | Start a post | `--language`, `--root`, `--today` |
132
142
  | `preview` | Build and serve the publication locally | `--root`, `--today` |
133
- | `publish` | Check, record and send your work to GitHub | `--root`, `--today`, `--skip-checks` |
134
- | `prism` | Manage author-approved reading configurations | `--root`, `--language`, `--depth`, `--intent`, `--modality`, `--file`, `--reason`, `--yes` |
143
+ | `publish` | Check, record and send your work to GitHub | `--root`, `--today`, `--account`, `--skip-checks` |
144
+ | `prism` | Manage author-approved reading configurations | `--root`, `--account`, `--language`, `--depth`, `--intent`, `--modality`, `--file`, `--reason`, `--yes` |
135
145
  | `upgrade` | Inspect and apply a verified managed-theme update | `--root`, `--channel`, `--yes` |
136
- | `doctor` | Check a publication and say what is wrong | `--root` |
137
- | `auth` | Sign in to Gala and GitHub | - |
146
+ | `doctor` | Check a publication and say what is wrong | `--root`, `--account` |
147
+ | `auth` | Add, list, select or remove GitHub-named account profiles | `--api-base-url` |
138
148
 
139
- `auth` is never a prerequisite you have to remember: any command that needs a credential obtains
140
- one. It exists for when you want to do it deliberately - a new machine, or a different account.
149
+ `init` shows both identities and uses the active account automatically. Registration stores that
150
+ profile binding inside the checkout's private `.git` metadata. Later authenticated commands use the
151
+ binding even if another profile becomes active. `--account` remains available for an explicit CI or
152
+ unbound-checkout selection, but it can never override a different bound owner.
141
153
 
142
154
  Every command prompts for what it needs when run in a terminal, and every prompt has an option that
143
155
  supplies it instead. With no terminal attached - in CI - nothing is ever prompted for: a value that
@@ -164,7 +176,7 @@ configured - so publishing works on a machine where those differ, or where none
164
176
  Sign in again:
165
177
 
166
178
  ```console
167
- npx --yes @rathnasgala/cli@latest auth
179
+ npx --yes @rathnasgala/cli@latest auth add
168
180
  ```
169
181
 
170
182
  ### Gala cannot reach the repository
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rathnasgala/cli",
3
- "version": "1.1.12",
3
+ "version": "1.1.14",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src"
package/src/api/gala.js CHANGED
@@ -38,6 +38,15 @@ export function galaApi({ baseUrl = DEFAULT_API_BASE_URL, token } = {}) {
38
38
  }
39
39
  },
40
40
 
41
+ async profile() {
42
+ const body = await requestJson(`${root}/v1/me`, authorized('Gala account lookup'));
43
+ if (typeof body?.userId !== 'string' || typeof body?.email !== 'string'
44
+ || typeof body?.displayName !== 'string') {
45
+ throw new TypeError('Gala returned an unusable account identity');
46
+ }
47
+ return { userId: body.userId, email: body.email, displayName: body.displayName };
48
+ },
49
+
41
50
  /** Exchanges the GitHub token for the short-lived capability the GitHub-scoped routes require. */
42
51
  async githubCapability(githubToken) {
43
52
  const body = await requestJson(`${root}/v1/auth/github/device-authorizations`,
@@ -0,0 +1,51 @@
1
+ import { readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { UsageError } from '../cli/args.js';
5
+ import { requireProfileName } from './profiles.js';
6
+
7
+ const FILENAME = 'gala-account-profile';
8
+
9
+ export async function bindCheckoutProfile(root, name) {
10
+ const gitDirectory = path.join(path.resolve(root), '.git');
11
+ const metadata = await stat(gitDirectory).catch(() => null);
12
+ if (!metadata?.isDirectory()) {
13
+ throw new Error('Cannot bind the account profile because this is not a standard Git checkout.');
14
+ }
15
+ const target = path.join(gitDirectory, FILENAME);
16
+ const temporary = `${target}.${process.pid}.tmp`;
17
+ try {
18
+ await writeFile(temporary, `${requireProfileName(name)}\n`, { flag: 'wx', mode: 0o600 });
19
+ await rename(temporary, target);
20
+ } catch (failure) {
21
+ await rm(temporary, { force: true });
22
+ throw failure;
23
+ }
24
+ }
25
+
26
+ export async function checkoutProfile(root) {
27
+ try {
28
+ return requireProfileName((await readFile(
29
+ path.join(path.resolve(root), '.git', FILENAME), 'utf8'
30
+ )).trim());
31
+ } catch (failure) {
32
+ if (failure?.code === 'ENOENT') return null;
33
+ throw failure;
34
+ }
35
+ }
36
+
37
+ export async function accountForCommand(options, root) {
38
+ const explicit = options.value('account');
39
+ const bound = await checkoutProfile(root);
40
+ if (explicit != null) {
41
+ const selected = requireProfileName(explicit);
42
+ if (bound != null && bound !== selected) {
43
+ throw new UsageError(
44
+ `This checkout belongs to account profile ${bound}; --account ${selected} cannot override it.`
45
+ );
46
+ }
47
+ return selected;
48
+ }
49
+ if (bound != null) return bound;
50
+ throw new UsageError('This checkout has no account profile binding; pass --account <github-login>.');
51
+ }
@@ -0,0 +1,208 @@
1
+ import { chmod, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { galaApi } from '../api/gala.js';
5
+ import { githubApi } from '../api/github.js';
6
+ import { UsageError } from '../cli/args.js';
7
+ import { galaCredential } from './gala.js';
8
+ import { githubCredential } from './github.js';
9
+ import { credentialDirectory, readCredential } from './store.js';
10
+
11
+ const PROFILE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
12
+ const ULID = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/;
13
+ const STORE_VERSION = '2';
14
+
15
+ export function requireProfileName(value) {
16
+ if (typeof value !== 'string' || !PROFILE.test(value)) {
17
+ throw new UsageError('Account profile names use lowercase letters, numbers and single hyphens.');
18
+ }
19
+ return value;
20
+ }
21
+
22
+ export function profilePaths(name, { root = credentialDirectory() } = {}) {
23
+ const safe = requireProfileName(name);
24
+ const directory = path.join(root, 'profiles', safe);
25
+ return {
26
+ directory,
27
+ metadata: path.join(directory, 'profile.json'),
28
+ gala: path.join(directory, 'gala.json'),
29
+ github: path.join(directory, 'github.json'),
30
+ active: path.join(root, 'active-profile'),
31
+ };
32
+ }
33
+
34
+ export async function addProfile({
35
+ terminal,
36
+ apiBaseUrl,
37
+ root = credentialDirectory(),
38
+ galaSignIn = galaCredential,
39
+ githubSignIn = githubCredential,
40
+ galaLookup = async (credential) => galaApi({
41
+ baseUrl: credential.apiBaseUrl,
42
+ token: credential.accessToken,
43
+ }).profile(),
44
+ githubLookup = async (credential) => githubApi(credential.accessToken).viewer(),
45
+ }) {
46
+ await mkdir(root, { recursive: true, mode: 0o700 });
47
+ const pending = await mkdtemp(path.join(root, '.pending-profile-'));
48
+ await chmod(pending, 0o700);
49
+ const pendingPaths = {
50
+ gala: path.join(pending, 'gala.json'),
51
+ github: path.join(pending, 'github.json'),
52
+ };
53
+ try {
54
+ const gala = await galaSignIn({ terminal, apiBaseUrl, target: pendingPaths.gala });
55
+ const galaIdentity = await galaLookup(gala);
56
+ const github = await githubSignIn({ terminal, target: pendingPaths.github });
57
+ const githubLogin = await githubLookup(github);
58
+ if (typeof githubLogin !== 'string') {
59
+ throw new UsageError('GitHub did not return an account username.');
60
+ }
61
+ const name = requireProfileName(githubLogin.toLowerCase());
62
+ const metadata = normalizeMetadata({ schemaVersion: 2, name, gala: galaIdentity, githubLogin });
63
+ await atomicJson(path.join(pending, 'profile.json'), metadata);
64
+ await prepareStore(root);
65
+ const paths = profilePaths(name, { root });
66
+ await rm(paths.directory, { recursive: true, force: true });
67
+ await rename(pending, paths.directory);
68
+ await setActiveProfile(name, { root });
69
+ return { metadata, gala, github };
70
+ } catch (failure) {
71
+ await rm(pending, { recursive: true, force: true });
72
+ throw failure;
73
+ }
74
+ }
75
+
76
+ export async function listProfiles({ root = credentialDirectory() } = {}) {
77
+ const directory = path.join(root, 'profiles');
78
+ let names;
79
+ try {
80
+ names = await readdir(directory);
81
+ } catch (failure) {
82
+ if (failure?.code === 'ENOENT') return [];
83
+ throw failure;
84
+ }
85
+ const active = await activeProfile({ root });
86
+ const profiles = [];
87
+ for (const name of names.sort()) {
88
+ try {
89
+ const metadata = JSON.parse(await readFile(profilePaths(name, { root }).metadata, 'utf8'));
90
+ profiles.push({ ...normalizeMetadata(metadata), active: name === active });
91
+ } catch (failure) {
92
+ if (failure?.code !== 'ENOENT' && !(failure instanceof SyntaxError)) throw failure;
93
+ }
94
+ }
95
+ return profiles;
96
+ }
97
+
98
+ export async function useProfile(name, { root = credentialDirectory() } = {}) {
99
+ const selected = await readProfile(name, { root });
100
+ await setActiveProfile(selected.metadata.name, { root });
101
+ return selected.metadata;
102
+ }
103
+
104
+ export async function removeProfile(name, { root = credentialDirectory() } = {}) {
105
+ const paths = profilePaths(name, { root });
106
+ try {
107
+ const metadata = await stat(paths.directory);
108
+ if (!metadata.isDirectory()) throw new UsageError(`Account profile ${name} is unavailable.`);
109
+ } catch (failure) {
110
+ if (failure?.code === 'ENOENT') throw new UsageError(`Account profile ${name} does not exist.`);
111
+ throw failure;
112
+ }
113
+ await rm(paths.directory, { recursive: true });
114
+ if (await activeProfile({ root }) === name) await rm(paths.active, { force: true });
115
+ }
116
+
117
+ export async function selectedProfile({ name, root = credentialDirectory() } = {}) {
118
+ const selected = name == null ? await activeProfile({ root }) : requireProfileName(name);
119
+ if (selected == null) {
120
+ throw new UsageError('No account profile is active. Run `npx --yes @rathnasgala/cli@latest auth add`.');
121
+ }
122
+ return readProfile(selected, { root });
123
+ }
124
+
125
+ export async function activeProfile({ root = credentialDirectory() } = {}) {
126
+ try {
127
+ const value = (await readFile(path.join(root, 'active-profile'), 'utf8')).trim();
128
+ return requireProfileName(value);
129
+ } catch (failure) {
130
+ if (failure?.code === 'ENOENT') return null;
131
+ throw failure;
132
+ }
133
+ }
134
+
135
+ async function readProfile(name, { root }) {
136
+ const paths = profilePaths(name, { root });
137
+ let metadata;
138
+ try {
139
+ metadata = normalizeMetadata(JSON.parse(await readFile(paths.metadata, 'utf8')));
140
+ } catch (failure) {
141
+ if (failure?.code === 'ENOENT' || failure instanceof SyntaxError) {
142
+ throw new UsageError(`Account profile ${name} is incomplete; remove it and add it again.`);
143
+ }
144
+ throw failure;
145
+ }
146
+ const [gala, github] = await Promise.all([readCredential(paths.gala), readCredential(paths.github)]);
147
+ if (gala == null || github == null) {
148
+ throw new UsageError(`Account profile ${name} has expired; remove it and add it again.`);
149
+ }
150
+ return { metadata, gala, github };
151
+ }
152
+
153
+ async function setActiveProfile(name, { root }) {
154
+ await mkdir(root, { recursive: true, mode: 0o700 });
155
+ await atomicText(path.join(root, 'active-profile'), `${requireProfileName(name)}\n`);
156
+ }
157
+
158
+ function normalizeMetadata(value) {
159
+ const name = requireProfileName(value?.name);
160
+ if (value?.schemaVersion !== 2) {
161
+ throw new UsageError(`Account profile ${name} has an unsupported format.`);
162
+ }
163
+ const userId = value?.gala?.userId;
164
+ const email = value?.gala?.email;
165
+ const displayName = value?.gala?.displayName;
166
+ const githubLogin = value?.githubLogin;
167
+ if (typeof userId !== 'string' || !ULID.test(userId)
168
+ || typeof email !== 'string' || email === ''
169
+ || typeof displayName !== 'string' || displayName === ''
170
+ || typeof githubLogin !== 'string' || githubLogin === '') {
171
+ throw new UsageError(`Account profile ${name} has invalid identity metadata.`);
172
+ }
173
+ if (githubLogin.toLowerCase() !== name) {
174
+ throw new UsageError(`Account profile ${name} does not match its GitHub identity.`);
175
+ }
176
+ return { schemaVersion: 2, name, gala: { userId, email, displayName }, githubLogin };
177
+ }
178
+
179
+ async function prepareStore(root) {
180
+ const version = path.join(root, 'profile-store-version');
181
+ const current = await readFile(version, 'utf8').catch((failure) => {
182
+ if (failure?.code === 'ENOENT') return null;
183
+ throw failure;
184
+ });
185
+ if (current?.trim() !== STORE_VERSION) {
186
+ await rm(path.join(root, 'profiles'), { recursive: true, force: true });
187
+ await rm(path.join(root, 'active-profile'), { force: true });
188
+ await rm(path.join(root, 'credentials.json'), { force: true });
189
+ await rm(path.join(root, 'github-credentials.json'), { force: true });
190
+ await atomicText(version, `${STORE_VERSION}\n`);
191
+ }
192
+ await mkdir(path.join(root, 'profiles'), { recursive: true, mode: 0o700 });
193
+ }
194
+
195
+ async function atomicJson(target, value) {
196
+ return atomicText(target, `${JSON.stringify(value)}\n`);
197
+ }
198
+
199
+ async function atomicText(target, value) {
200
+ const temporary = `${target}.${process.pid}.tmp`;
201
+ try {
202
+ await writeFile(temporary, value, { flag: 'wx', mode: 0o600 });
203
+ await rename(temporary, target);
204
+ } catch (failure) {
205
+ await rm(temporary, { force: true });
206
+ throw failure;
207
+ }
208
+ }
package/src/auth/store.js CHANGED
@@ -31,6 +31,10 @@ export function credentialPath(name, { platform = process.platform, environment
31
31
  return path.join(environment.XDG_CONFIG_HOME || path.join(home, '.config'), 'gala', `${name}.json`);
32
32
  }
33
33
 
34
+ export function credentialDirectory(options = {}) {
35
+ return path.dirname(credentialPath('credentials', options));
36
+ }
37
+
34
38
  export async function writeCredential(target, record) {
35
39
  const directory = path.dirname(path.resolve(target));
36
40
  await mkdir(directory, { recursive: true, mode: 0o700 });
@@ -1,20 +1,42 @@
1
- import { galaCredential } from '../auth/gala.js';
2
- import { githubCredential } from '../auth/github.js';
1
+ import { addProfile, listProfiles, removeProfile, useProfile } from '../auth/profiles.js';
2
+ import { UsageError } from '../cli/args.js';
3
3
 
4
4
  /**
5
5
  * Signs in to both, and says so.
6
6
  *
7
- * Not a prerequisite the writer has to remember: every command that needs a credential obtains one.
8
- * This exists for the times they want to do it deliberately - a new machine, a different account,
9
- * or after a token has expired.
7
+ * One login creates and activates the GitHub-named pair. A writer returns here only for a new
8
+ * machine, a different account, or an expired token.
10
9
  */
11
10
  export async function auth({ terminal, options }) {
12
- const gala = await galaCredential({ terminal, apiBaseUrl: options.value('api-base-url') });
13
- const github = await githubCredential({ terminal });
14
-
15
- terminal.blank();
16
- terminal.done(`Gala - valid until ${new Date(gala.expiresAt).toLocaleString()}`);
17
- terminal.done(github.expiresAt
18
- ? `GitHub - valid until ${new Date(github.expiresAt).toLocaleString()}`
19
- : 'GitHub - signed in');
11
+ const [action = 'list', name, ...extra] = options.positional;
12
+ if (extra.length > 0 || !['list', 'add', 'use', 'remove'].includes(action)) {
13
+ throw new UsageError('Use: auth [list|add|use <github-login>|remove <github-login>]');
14
+ }
15
+ if (action === 'list') {
16
+ if (name != null) throw new UsageError('auth list takes no profile name');
17
+ const profiles = await listProfiles();
18
+ if (profiles.length === 0) {
19
+ terminal.note('No account profiles. Run auth add.');
20
+ return [];
21
+ }
22
+ for (const profile of profiles) {
23
+ terminal.done(`${profile.active ? '*' : ' '} ${profile.name}: Gala ${profile.gala.email} + GitHub @${profile.githubLogin}`);
24
+ }
25
+ return profiles;
26
+ }
27
+ if (action === 'add') {
28
+ if (name != null) throw new UsageError('auth add takes no profile name; it uses your GitHub username');
29
+ const profile = await addProfile({ terminal, apiBaseUrl: options.value('api-base-url') });
30
+ terminal.done(`Using ${profile.metadata.name}: Gala ${profile.metadata.gala.email} + GitHub @${profile.metadata.githubLogin}`);
31
+ return profile.metadata;
32
+ }
33
+ if (name == null) throw new UsageError(`auth ${action} needs a GitHub username`);
34
+ if (action === 'use') {
35
+ const profile = await useProfile(name);
36
+ terminal.done(`Using ${name}: Gala ${profile.gala.email} + GitHub @${profile.githubLogin}`);
37
+ return profile;
38
+ }
39
+ await removeProfile(name);
40
+ terminal.done(`Removed account profile ${name}`);
41
+ return null;
20
42
  }
@@ -2,8 +2,8 @@ import { readFile, readdir, stat } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
 
4
4
  import { galaApi } from '../api/gala.js';
5
- import { galaCredential } from '../auth/gala.js';
6
- import { githubCredential } from '../auth/github.js';
5
+ import { accountForCommand } from '../auth/checkout-profile.js';
6
+ import { selectedProfile } from '../auth/profiles.js';
7
7
  import { cliCommand } from '../cli/invocation.js';
8
8
  import { createGit } from '../git.js';
9
9
 
@@ -19,8 +19,16 @@ export async function doctor({ terminal, options, cwd = process.cwd() }) {
19
19
  const root = path.resolve(options.value('root') ?? cwd);
20
20
  const checks = [];
21
21
 
22
+ let selected;
23
+ checks.push(await checkCredential('Account profile', async () => {
24
+ const account = await accountForCommand(options, root);
25
+ selected = await selectedProfile({ name: account });
26
+ return ok(`${account}: Gala ${selected.metadata.gala.email} + GitHub @${selected.metadata.githubLogin}`);
27
+ }));
28
+
22
29
  checks.push(await checkCredential('Gala sign-in', async () => {
23
- const gala = await galaCredential({ terminal, apiBaseUrl: options.value('api-base-url') });
30
+ if (selected == null) throw new Error('account profile is unavailable');
31
+ const gala = selected.gala;
24
32
  const accepted = await galaApi({ baseUrl: gala.apiBaseUrl, token: gala.accessToken }).accepted();
25
33
  return accepted
26
34
  ? ok(`valid until ${new Date(gala.expiresAt).toLocaleString()}`)
@@ -28,7 +36,8 @@ export async function doctor({ terminal, options, cwd = process.cwd() }) {
28
36
  }));
29
37
 
30
38
  checks.push(await checkCredential('GitHub sign-in', async () => {
31
- const github = await githubCredential({ terminal });
39
+ if (selected == null) throw new Error('account profile is unavailable');
40
+ const github = selected.github;
32
41
  return ok(github.expiresAt
33
42
  ? `valid until ${new Date(github.expiresAt).toLocaleString()}`
34
43
  : 'signed in');
@@ -1,7 +1,8 @@
1
1
  import path from 'node:path';
2
2
 
3
3
  import { galaApi } from '../api/gala.js';
4
- import { galaCredential } from '../auth/gala.js';
4
+ import { accountForCommand } from '../auth/checkout-profile.js';
5
+ import { selectedProfile } from '../auth/profiles.js';
5
6
  import { UsageError } from '../cli/args.js';
6
7
  import { cliCommand } from '../cli/invocation.js';
7
8
  import { customDomain } from '../domain.js';
@@ -22,9 +23,8 @@ export async function domain({ terminal, options, cwd = process.cwd() }) {
22
23
  if (action === 'set' && value == null) throw new UsageError('domain set needs a hostname');
23
24
  if (action !== 'set' && value != null) throw new UsageError(`domain ${action} takes no hostname`);
24
25
 
25
- const credential = await galaCredential({
26
- terminal, apiBaseUrl: options.value('api-base-url'),
27
- });
26
+ const account = await accountForCommand(options, root);
27
+ const credential = (await selectedProfile({ name: account })).gala;
28
28
  const api = galaApi({ baseUrl: credential.apiBaseUrl, token: credential.accessToken });
29
29
 
30
30
  if (action === 'status') {
@@ -3,8 +3,8 @@ import path from 'node:path';
3
3
 
4
4
  import { galaApi } from '../api/gala.js';
5
5
  import { githubApi } from '../api/github.js';
6
- import { galaCredential } from '../auth/gala.js';
7
- import { githubCredential } from '../auth/github.js';
6
+ import { accountForCommand, bindCheckoutProfile } from '../auth/checkout-profile.js';
7
+ import { selectedProfile } 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,8 +45,10 @@ export async function init({ terminal, options, cwd = process.cwd() }) {
45
45
 
46
46
  const destination = await inspectDestination(directory);
47
47
 
48
- const gala = await galaCredential({ terminal, apiBaseUrl: options.value('api-base-url') });
49
- const github = await githubCredential({ terminal });
48
+ const profile = await selectedProfile({ name: options.value('account') });
49
+ const { gala, github, metadata } = profile;
50
+ const account = metadata.name;
51
+ terminal.step(`Account ${account}: Gala ${metadata.gala.email} + GitHub @${metadata.githubLogin}`);
50
52
 
51
53
  const name = await publicationName({ terminal, explicitName, directory });
52
54
 
@@ -71,6 +73,7 @@ export async function init({ terminal, options, cwd = process.cwd() }) {
71
73
  if (destination === 'empty-git') await populateEmptyRepository(checkout);
72
74
  else await cloneRepository(checkout);
73
75
 
76
+ await bindCheckoutProfile(directory, account);
74
77
  terminal.step('Registering the publication');
75
78
  const git = createGit({ root: directory, token: github.accessToken });
76
79
  const registration = await api.registerSite({
@@ -2,7 +2,8 @@ import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
 
4
4
  import { galaApi } from '../api/gala.js';
5
- import { galaCredential } from '../auth/gala.js';
5
+ import { accountForCommand } from '../auth/checkout-profile.js';
6
+ import { selectedProfile } from '../auth/profiles.js';
6
7
  import { UsageError } from '../cli/args.js';
7
8
  import { cliCommand } from '../cli/invocation.js';
8
9
  import { createGit } from '../git.js';
@@ -21,7 +22,8 @@ export async function prism({ terminal, options, cwd = process.cwd() }) {
21
22
  if (!publication || !ULID.test(publication.siteId ?? '')) {
22
23
  throw new UsageError('Run this inside a registered Gala publication, or pass --root.');
23
24
  }
24
- const credential = await galaCredential({ terminal, apiBaseUrl: options.value('api-base-url') });
25
+ const account = await accountForCommand(options, root);
26
+ const credential = (await selectedProfile({ name: account })).gala;
25
27
  const api = galaApi({ baseUrl: credential.apiBaseUrl, token: credential.accessToken });
26
28
  const [action = 'status', ...args] = options.positional;
27
29
 
@@ -2,7 +2,8 @@ import path from 'node:path';
2
2
 
3
3
  import { checkContent } from '../content.js';
4
4
  import { createGit } from '../git.js';
5
- import { githubCredential } from '../auth/github.js';
5
+ import { accountForCommand } from '../auth/checkout-profile.js';
6
+ import { selectedProfile } from '../auth/profiles.js';
6
7
  import { readPublication } from '../publication.js';
7
8
 
8
9
  /**
@@ -25,7 +26,8 @@ export async function publish({ terminal, options, cwd = process.cwd(), regenera
25
26
  const root = path.resolve(options.value('root') ?? cwd);
26
27
  const today = options.value('today');
27
28
 
28
- const github = await githubCredential({ terminal });
29
+ const account = await accountForCommand(options, root);
30
+ const github = (await selectedProfile({ name: account })).github;
29
31
  const git = createGit({ root, token: github.accessToken });
30
32
 
31
33
  terminal.step('Catching up with GitHub');
@@ -26,20 +26,21 @@ import { cliCommand } from './cli/invocation.js';
26
26
  */
27
27
  export const COMMANDS = {
28
28
  auth: {
29
- summary: 'Sign in to Gala and GitHub',
29
+ summary: 'Add, inspect, select or remove account profiles',
30
+ usage: cliCommand('auth [list|add|use <github-login>|remove <github-login>] [--api-base-url URL]'),
30
31
  flags: ['api-base-url'],
31
32
  run: auth
32
33
  },
33
34
  init: {
34
35
  summary: 'Create a publication in an empty directory',
35
- usage: cliCommand('init [directory] [--name my-notes] [--domain blog.example.com]'),
36
- flags: ['name', 'domain', 'api-base-url'],
36
+ usage: cliCommand('init [directory] [--name my-notes] [--domain blog.example.com] [--account github-login]'),
37
+ flags: ['name', 'domain', 'account'],
37
38
  run: init
38
39
  },
39
40
  domain: {
40
41
  summary: 'Inspect or change this publication’s custom domain',
41
- usage: cliCommand('domain [status|set <hostname>|check|cancel|remove] [--root path]'),
42
- flags: ['root', 'api-base-url'],
42
+ usage: cliCommand('domain [status|set <hostname>|check|cancel|remove] [--root path] [--account github-login]'),
43
+ flags: ['root', 'account'],
43
44
  run: domain
44
45
  },
45
46
  new: {
@@ -55,14 +56,15 @@ export const COMMANDS = {
55
56
  },
56
57
  publish: {
57
58
  summary: 'Check, record and send your work to GitHub',
58
- flags: ['root', 'today'],
59
+ usage: cliCommand('publish [--root path] [--today YYYY-MM-DD] [--account github-login] [--skip-checks]'),
60
+ flags: ['root', 'today', 'account'],
59
61
  switches: ['skip-checks'],
60
62
  run: publish
61
63
  },
62
64
  prism: {
63
65
  summary: 'Manage author-approved Prism configurations',
64
- usage: cliCommand('prism <status|mode|link-policy|list|create|edit|generate|submit|approve|reject|revoke> [arguments]'),
65
- flags: ['root', 'api-base-url', 'language', 'depth', 'intent', 'modality', 'file', 'reason'],
66
+ usage: cliCommand('prism <status|mode|link-policy|list|create|edit|generate|submit|approve|reject|revoke> [arguments] [--account github-login]'),
67
+ flags: ['root', 'account', 'language', 'depth', 'intent', 'modality', 'file', 'reason'],
66
68
  switches: ['yes'],
67
69
  run: prism
68
70
  },
@@ -75,7 +77,8 @@ export const COMMANDS = {
75
77
  },
76
78
  doctor: {
77
79
  summary: 'Check a publication and say what is wrong',
78
- flags: ['root', 'api-base-url'],
80
+ usage: cliCommand('doctor [--root path] [--account github-login]'),
81
+ flags: ['root', 'account'],
79
82
  run: doctor
80
83
  }
81
84
  };
package/src/git.js CHANGED
@@ -105,6 +105,14 @@ export function createGit({ root, token, spawnProcess = spawn } = {}) {
105
105
  * makes the post-publish validation pass the sole local writer of any missing content ID.
106
106
  */
107
107
  async takeRemote() {
108
+ const unmerged = await run(['diff', '--name-only', '--diff-filter=U', '-z'], { capture: true });
109
+ const conflictedPaths = unmerged.split('\0').filter(Boolean);
110
+ if (conflictedPaths.length > 0) {
111
+ const failure = new Error('Git has unresolved conflicts. Gala left them untouched. Run git '
112
+ + 'status, resolve or abort the operation it reports, then publish again.');
113
+ failure.detail = `Conflicted files:\n${conflictedPaths.join('\n')}`;
114
+ throw failure;
115
+ }
108
116
  const branch = await git.branch();
109
117
  await run(['fetch', 'origin', branch]);
110
118
  await run(['rebase', '--autostash', `origin/${branch}`]);