@rathnasgala/cli 0.0.14 → 0.0.15
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/index.js +12 -2
- package/src/open-browser.js +40 -0
- package/src/scaffold-preflight.js +26 -12
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import { createInterface } from 'node:readline/promises';
|
|
|
20
20
|
import { upgradeTheme } from './upgrade-command.js';
|
|
21
21
|
import { authenticateGithub } from './github-auth-command.js';
|
|
22
22
|
import { scaffoldSite } from './scaffold-site.js';
|
|
23
|
+
import { openInBrowser } from './open-browser.js';
|
|
23
24
|
import { prepareScaffold } from './scaffold-preflight.js';
|
|
24
25
|
import { refreshEngagementSnapshot } from './refresh-command.js';
|
|
25
26
|
import { switchTopology } from './topology-command.js';
|
|
@@ -44,6 +45,15 @@ function reportAndExit(failure) {
|
|
|
44
45
|
process.exit(1);
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Opens the page and says so, or falls back to asking for it to be opened by hand. The URL is
|
|
50
|
+
* printed either way — it is the thing the writer may need to move to another device.
|
|
51
|
+
*/
|
|
52
|
+
function announce(verificationUri, userCode) {
|
|
53
|
+
const opened = openInBrowser(verificationUri);
|
|
54
|
+
return `${opened ? 'Opened' : 'Open'} ${verificationUri}\nEnter code: ${userCode}\n`;
|
|
55
|
+
}
|
|
56
|
+
|
|
47
57
|
const [command, ...args] = process.argv.slice(2);
|
|
48
58
|
const usage = 'Usage: gala <auth|configure|entitlement|scaffold|topology|validate|new|doctor|hook|preview|publish|record-deployment|refresh|upgrade|workflow> [options]';
|
|
49
59
|
|
|
@@ -77,7 +87,7 @@ if (command === 'auth') {
|
|
|
77
87
|
const result = await authenticateGithub({
|
|
78
88
|
showScopeWarning: ({ explanation }) => process.stdout.write(`GitHub authorization: ${explanation}\n`),
|
|
79
89
|
showInstructions: ({ verificationUri, userCode }) => {
|
|
80
|
-
process.stdout.write(
|
|
90
|
+
process.stdout.write(announce(verificationUri, userCode));
|
|
81
91
|
}
|
|
82
92
|
});
|
|
83
93
|
process.stdout.write(`GitHub authentication stored securely with scopes: ${result.scopes.join(', ')}.\n`);
|
|
@@ -87,7 +97,7 @@ if (command === 'auth') {
|
|
|
87
97
|
const result = await authenticateGala({
|
|
88
98
|
apiBaseUrl,
|
|
89
99
|
showInstructions: ({ verificationUri, userCode }) => {
|
|
90
|
-
process.stdout.write(
|
|
100
|
+
process.stdout.write(announce(verificationUri, userCode));
|
|
91
101
|
}
|
|
92
102
|
});
|
|
93
103
|
process.stdout.write(`Gala authentication stored securely until ${result.expiresAt.toISOString()}.\n`);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Opens a URL in the writer's browser, best effort.
|
|
5
|
+
*
|
|
6
|
+
* Printing "Open https://…" and a code asks someone to copy a URL out of a terminal by hand, three
|
|
7
|
+
* times over in one scaffold. The URL is still always printed — this only saves the copying, and it
|
|
8
|
+
* has to keep working when it cannot: over SSH, in a container, in CI, on a machine with no browser
|
|
9
|
+
* at all. So nothing here is allowed to fail the command.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately not attempted when there is no terminal. A CI job that silently spawns a browser
|
|
12
|
+
* process is a hang waiting to happen, and there is nobody there to look at it.
|
|
13
|
+
*/
|
|
14
|
+
export function openInBrowser(url, {
|
|
15
|
+
platform = process.platform,
|
|
16
|
+
environment = process.env,
|
|
17
|
+
interactive = process.stdin.isTTY === true,
|
|
18
|
+
spawnProcess = spawn
|
|
19
|
+
} = {}) {
|
|
20
|
+
if (!interactive) return false;
|
|
21
|
+
// Respected by convention across CLI tooling, and the escape hatch for anyone who does not want
|
|
22
|
+
// their browser taken over.
|
|
23
|
+
if (environment.GALA_NO_BROWSER || environment.CI || environment.NO_BROWSER) return false;
|
|
24
|
+
if (!/^https:\/\//.test(url)) return false;
|
|
25
|
+
|
|
26
|
+
const [command, args] = platform === 'darwin' ? ['open', [url]]
|
|
27
|
+
: platform === 'win32' ? ['cmd', ['/c', 'start', '', url]]
|
|
28
|
+
: ['xdg-open', [url]];
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const child = spawnProcess(command, args, { stdio: 'ignore', detached: true, shell: false });
|
|
32
|
+
// Without this the CLI waits for the browser to exit before it can finish.
|
|
33
|
+
child.unref?.();
|
|
34
|
+
// A missing opener is an ordinary outcome on a headless box, not something to report.
|
|
35
|
+
child.on?.('error', () => {});
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -7,6 +7,7 @@ import { readGithubCredential } from './github-credential-store.js';
|
|
|
7
7
|
import { resolveGithubLogin } from './github-identity.js';
|
|
8
8
|
import { resolveInstallationId } from './gala-installation-client.js';
|
|
9
9
|
import { galaCredentialAccepted } from './gala-credential-health.js';
|
|
10
|
+
import { openInBrowser } from './open-browser.js';
|
|
10
11
|
import { forgetGalaCredential } from './gala-credential-store.js';
|
|
11
12
|
|
|
12
13
|
export const GITHUB_APP_INSTALL_URL = 'https://github.com/apps/gala67-app/installations/new';
|
|
@@ -37,6 +38,7 @@ export async function prepareScaffold({
|
|
|
37
38
|
ask,
|
|
38
39
|
installUrl = GITHUB_APP_INSTALL_URL,
|
|
39
40
|
installAttempts = 3,
|
|
41
|
+
openUrl = openInBrowser,
|
|
40
42
|
readGala = readGalaCredential,
|
|
41
43
|
readGithub = readGithubCredential,
|
|
42
44
|
credentialAccepted = galaCredentialAccepted,
|
|
@@ -48,9 +50,9 @@ export async function prepareScaffold({
|
|
|
48
50
|
apiBaseUrl = DEFAULT_API_BASE_URL
|
|
49
51
|
} = {}) {
|
|
50
52
|
const gala = await ensureGala({
|
|
51
|
-
apiBaseUrl, notify, readGala, signInGala, credentialAccepted, forgetGala
|
|
53
|
+
apiBaseUrl, notify, readGala, signInGala, credentialAccepted, forgetGala, openUrl
|
|
52
54
|
});
|
|
53
|
-
const github = await ensureGithub({ notify, readGithub, signInGithub });
|
|
55
|
+
const github = await ensureGithub({ notify, readGithub, signInGithub, openUrl });
|
|
54
56
|
|
|
55
57
|
const resolvedOwner = owner ?? await resolveLogin({ accessToken: github.accessToken });
|
|
56
58
|
|
|
@@ -69,7 +71,7 @@ export async function prepareScaffold({
|
|
|
69
71
|
galaAccessToken: gala.accessToken,
|
|
70
72
|
githubAccessToken: github.accessToken,
|
|
71
73
|
owner: resolvedOwner,
|
|
72
|
-
notify, ask, installUrl, installAttempts, resolveInstallation
|
|
74
|
+
notify, ask, installUrl, installAttempts, resolveInstallation, openUrl
|
|
73
75
|
});
|
|
74
76
|
|
|
75
77
|
return Object.freeze({
|
|
@@ -88,7 +90,9 @@ export async function prepareScaffold({
|
|
|
88
90
|
* out four calls later, as an opaque 401 from whichever endpoint got there first, is how a
|
|
89
91
|
* "sign in again" turned into a stack trace.
|
|
90
92
|
*/
|
|
91
|
-
async function ensureGala({
|
|
93
|
+
async function ensureGala({
|
|
94
|
+
apiBaseUrl, notify, readGala, signInGala, credentialAccepted, forgetGala, openUrl
|
|
95
|
+
}) {
|
|
92
96
|
let stored = null;
|
|
93
97
|
try {
|
|
94
98
|
stored = await readGala();
|
|
@@ -110,12 +114,12 @@ async function ensureGala({ apiBaseUrl, notify, readGala, signInGala, credential
|
|
|
110
114
|
await signInGala({
|
|
111
115
|
apiBaseUrl,
|
|
112
116
|
showInstructions: ({ verificationUri, userCode }) =>
|
|
113
|
-
notify(
|
|
117
|
+
notify(`${openUrl(verificationUri) ? 'Opened' : 'Open'} ${verificationUri}\nEnter code: ${userCode}`)
|
|
114
118
|
});
|
|
115
119
|
return readGala();
|
|
116
120
|
}
|
|
117
121
|
|
|
118
|
-
async function ensureGithub({ notify, readGithub, signInGithub }) {
|
|
122
|
+
async function ensureGithub({ notify, readGithub, signInGithub, openUrl }) {
|
|
119
123
|
try {
|
|
120
124
|
return await readGithub();
|
|
121
125
|
} catch {
|
|
@@ -123,7 +127,7 @@ async function ensureGithub({ notify, readGithub, signInGithub }) {
|
|
|
123
127
|
await signInGithub({
|
|
124
128
|
showScopeWarning: ({ explanation }) => notify(`GitHub authorization: ${explanation}`),
|
|
125
129
|
showInstructions: ({ verificationUri, userCode }) =>
|
|
126
|
-
notify(
|
|
130
|
+
notify(`${openUrl(verificationUri) ? 'Opened' : 'Open'} ${verificationUri}\nEnter code: ${userCode}`)
|
|
127
131
|
});
|
|
128
132
|
return readGithub();
|
|
129
133
|
}
|
|
@@ -138,7 +142,7 @@ async function ensureGithub({ notify, readGithub, signInGithub }) {
|
|
|
138
142
|
*/
|
|
139
143
|
async function ensureInstallation({
|
|
140
144
|
apiBaseUrl, galaAccessToken, githubAccessToken, owner,
|
|
141
|
-
notify, ask, installUrl, installAttempts, resolveInstallation
|
|
145
|
+
notify, ask, installUrl, installAttempts, resolveInstallation, openUrl
|
|
142
146
|
}) {
|
|
143
147
|
for (let attempt = 0; attempt < Math.max(1, installAttempts); attempt += 1) {
|
|
144
148
|
const installationId = await resolveInstallation({
|
|
@@ -148,15 +152,25 @@ async function ensureInstallation({
|
|
|
148
152
|
|
|
149
153
|
if (typeof ask !== 'function') {
|
|
150
154
|
throw new Error(
|
|
151
|
-
`The Gala GitHub App is not installed on ${owner}. Install it at ${installUrl} and run
|
|
152
|
-
+ 'or pass --installation-id explicitly.'
|
|
155
|
+
`The Gala GitHub App is not installed on ${owner}. Install it at ${installUrl} and run `
|
|
156
|
+
+ 'scaffold again, or pass --installation-id explicitly.'
|
|
153
157
|
);
|
|
154
158
|
}
|
|
155
|
-
|
|
159
|
+
|
|
160
|
+
// An installation belongs to one account. Installing it on a personal account when the
|
|
161
|
+
// publication is meant for an organisation looks like it worked and changes nothing here, so
|
|
162
|
+
// the account being checked is named every time rather than assumed.
|
|
163
|
+
notify(attempt === 0
|
|
164
|
+
? `The Gala GitHub App is not installed on ${owner} yet.`
|
|
165
|
+
: `Still not seeing the App on ${owner}. Check that you installed it on ${owner} itself `
|
|
166
|
+
+ 'and not on another account or organisation you belong to.');
|
|
167
|
+
notify(`${openUrl(installUrl) ? 'Opened' : 'Open'} ${installUrl}`);
|
|
156
168
|
await ask('Press enter once the App is installed. ');
|
|
157
169
|
}
|
|
158
170
|
throw new Error(
|
|
159
|
-
`The Gala GitHub App still does not cover ${owner}. Install it at ${installUrl}
|
|
171
|
+
`The Gala GitHub App still does not cover ${owner}. Install it at ${installUrl} for ${owner} `
|
|
172
|
+
+ 'specifically, then run scaffold again. If the App is installed under a different account, '
|
|
173
|
+
+ 'pass --owner for that account, or --installation-id to name the installation directly.'
|
|
160
174
|
);
|
|
161
175
|
}
|
|
162
176
|
|