insta 0.0.72 → 0.0.73
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/dist/commands/github.js +56 -12
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/commands/github.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ApiClient, ApiError, requireProject } from '../api.js';
|
|
2
2
|
import { agentMode } from '../agent.js';
|
|
3
|
-
import { info, printJson } from '../util.js';
|
|
3
|
+
import { info, printJson, openUrl } from '../util.js';
|
|
4
4
|
import { resolveSoleService, parsePort, q } from './services.js';
|
|
5
5
|
export function parseRepoRef(raw) {
|
|
6
6
|
const s = raw.trim().replace(/^https?:\/\//i, '').replace(/^(www\.)?github\.com\//i, '').replace(/\/+$/, '').replace(/\.git$/i, '');
|
|
@@ -81,7 +81,7 @@ const sleepSeconds = (s) => new Promise((r) => setTimeout(r, s * 1000));
|
|
|
81
81
|
const pollDelay = (s) => Math.min(Math.max(s, 1), 60);
|
|
82
82
|
// GitHub shows the person a code to type; the platform holds the device code and finishes the exchange,
|
|
83
83
|
// so nothing secret passes through the CLI.
|
|
84
|
-
export async function authorizeTerminal(api, wait = sleepSeconds) {
|
|
84
|
+
export async function authorizeTerminal(api, wait = sleepSeconds, open = openUrl) {
|
|
85
85
|
const start = await api.request('POST', '/me/github/device', {});
|
|
86
86
|
const deadline = Date.parse(start.expiresAt);
|
|
87
87
|
// A NaN deadline makes every comparison false, which reads as an instant expiry — or, inverted, as a
|
|
@@ -92,6 +92,7 @@ export async function authorizeTerminal(api, wait = sleepSeconds) {
|
|
|
92
92
|
const say = (line) => process.stderr.write(line + '\n');
|
|
93
93
|
say('this terminal is not authorized with GitHub yet — authorize it once:');
|
|
94
94
|
say(` open ${start.verificationUri} and enter the code ${start.userCode}`);
|
|
95
|
+
open(start.verificationUri);
|
|
95
96
|
say('waiting for you to confirm… (ctrl-c to abort)');
|
|
96
97
|
const stopAt = Math.min(deadline, Date.now() + 3600_000); // no device code sensibly outlives an hour
|
|
97
98
|
const asked = Number(start.interval);
|
|
@@ -126,21 +127,64 @@ export function canAuthorizeHere(opts = {}) {
|
|
|
126
127
|
return false;
|
|
127
128
|
return !!agentMode() || !!process.stderr.isTTY;
|
|
128
129
|
}
|
|
129
|
-
export async function findCallerRepo(api, ref, authorize = authorizeTerminal, canAuthorize = canAuthorizeHere()) {
|
|
130
|
-
|
|
130
|
+
export async function findCallerRepo(api, ref, authorize = authorizeTerminal, canAuthorize = canAuthorizeHere(), wait = sleepSeconds, open = openUrl) {
|
|
131
|
+
let mine = await api.request('GET', '/me/github/repos');
|
|
131
132
|
if (!mine.linked && !canAuthorize) {
|
|
132
133
|
throw new Error('this GitHub account is not authorized for InstaCloud yet, and nothing here can read the code GitHub shows — run `insta compute connect-repo` from a terminal, connect the repository from the console, or pass --public for a public repository');
|
|
133
134
|
}
|
|
134
|
-
const repos = mine.linked ? mine.repos : await authorize(api);
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
const repos = mine.linked ? mine.repos : await authorize(api, wait, open);
|
|
136
|
+
const lookup = (rows) => {
|
|
137
|
+
const hit = rows.find((r) => r.owner.toLowerCase() === ref.owner.toLowerCase() && r.repo.toLowerCase() === ref.repo.toLowerCase());
|
|
138
|
+
if (!hit)
|
|
139
|
+
return;
|
|
140
|
+
const whole = (n) => n !== null && n !== '' && Number.isInteger(Number(n)) && Number(n) > 0;
|
|
141
|
+
if (!whole(hit.installationId) || !whole(hit.id))
|
|
142
|
+
throw new Error(`${ref.owner}/${ref.repo} came back without an installation to build it through — reconnect GitHub in the console, or pass --public for a public repository`);
|
|
138
143
|
return { installationId: Number(hit.installationId), repoId: Number(hit.id) };
|
|
144
|
+
};
|
|
145
|
+
const hit = lookup(repos);
|
|
139
146
|
if (hit)
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
throw new Error(
|
|
143
|
-
|
|
147
|
+
return hit;
|
|
148
|
+
if (!canAuthorize)
|
|
149
|
+
throw new Error(`${ref.owner}/${ref.repo} is not one your GitHub account can reach through the App — run \`insta compute connect-repo\` without --json from a terminal to install or configure the App, or pass --public for a public repository`);
|
|
150
|
+
if (!mine.linked)
|
|
151
|
+
mine = await api.request('GET', '/me/github/repos');
|
|
152
|
+
const installation = mine.installations.find((i) => i.accountLogin.toLowerCase() === ref.owner.toLowerCase());
|
|
153
|
+
let url;
|
|
154
|
+
if (installation) {
|
|
155
|
+
const settings = installation.accountType === 'Organization' ? `/organizations/${encodeURIComponent(installation.accountLogin)}/settings` : '/settings';
|
|
156
|
+
url = `https://github.com${settings}/installations/${installation.installationId}`;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
const setup = await api.request('POST', '/me/github/setup', {});
|
|
160
|
+
const install = new URL(setup.installUrl);
|
|
161
|
+
// The CLI verifies access by polling; this marker only selects the browser's return-to-terminal page.
|
|
162
|
+
install.searchParams.set('state', 'cli');
|
|
163
|
+
url = install.toString();
|
|
164
|
+
}
|
|
165
|
+
process.stderr.write(`${installation ? 'configure' : 'install'} the InstaCloud GitHub App on ${ref.owner} and grant access to ${ref.owner}/${ref.repo}:\n ${url}\nIf you cannot change access, ask the account owner or organization admin.\nWaiting for repository access… (ctrl-c to abort)\n`);
|
|
166
|
+
open(url);
|
|
167
|
+
const stopAt = Date.now() + 900_000;
|
|
168
|
+
let interval = 5;
|
|
169
|
+
while (Date.now() < stopAt) {
|
|
170
|
+
await wait(Math.min(interval, (stopAt - Date.now()) / 1000));
|
|
171
|
+
const remaining = stopAt - Date.now();
|
|
172
|
+
if (remaining <= 0)
|
|
173
|
+
break;
|
|
174
|
+
try {
|
|
175
|
+
mine = await api.request('GET', '/me/github/repos', undefined, { signal: AbortSignal.timeout(remaining) });
|
|
176
|
+
}
|
|
177
|
+
catch (e) {
|
|
178
|
+
if (e instanceof ApiError && e.status !== 429)
|
|
179
|
+
throw e;
|
|
180
|
+
interval = pollDelay(interval + 5);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const ready = lookup(mine.repos);
|
|
184
|
+
if (ready)
|
|
185
|
+
return ready;
|
|
186
|
+
}
|
|
187
|
+
throw new Error(`timed out waiting for access to ${ref.owner}/${ref.repo} — save the App's repository access on GitHub, then run the command again`);
|
|
144
188
|
}
|
|
145
189
|
async function targetService(api, projectId, branch, serviceName) {
|
|
146
190
|
const { services } = await api.request('GET', `/projects/${projectId}/services${q(branch)}`);
|
package/dist/index.js
CHANGED
|
@@ -255,7 +255,7 @@ for (const [flags, description] of computeCmd.EXEC_OPTIONS)
|
|
|
255
255
|
execCmd.option(flags, description);
|
|
256
256
|
compute.command('repo [service]').description('Show what a compute service deploys from: the image it runs, or the GitHub repository — owner/repo, the branch it builds, root directory, which paths a push must change to redeploy it, and whether pushes redeploy it at all')
|
|
257
257
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => githubCmd.computeRepo(service, o)));
|
|
258
|
-
compute.command('connect-repo <owner/repo> [service]').description("Connect a GitHub repository to an EXISTING compute service: the repo is built (its Dockerfile, or nixpacks when there is none) and deployed into that service, and every later push to the tracked repository branch redeploys it. The repo must be one your own GitHub account can reach through the InstaCloud App, or be public; the
|
|
258
|
+
compute.command('connect-repo <owner/repo> [service]').description("Connect a GitHub repository to an EXISTING compute service: the repo is built (its Dockerfile, or nixpacks when there is none) and deployed into that service, and every later push to the tracked repository branch redeploys it. The repo must be one your own GitHub account can reach through the InstaCloud App, or be public; the CLI opens GitHub for device-code authorization and App installation or configuration when needed, then waits for repository access and continues. URLs are also printed for remote terminals; --json requires access to be ready. Build and start commands come from detection and cannot be set. Connecting again replaces the service's current source")
|
|
259
259
|
.option('--public', 'the repo is public and no GitHub App installation is needed (deploys are manual; pushes cannot redeploy)')
|
|
260
260
|
.option('--root-dir <dir>', 'the directory of the repo to build (a monorepo with several deployable directories lists them and exits 1 without it)')
|
|
261
261
|
.option('--repo-branch <name>', "the repository branch to build (default: the repo's default branch)")
|