@catalyst-cloud/cli 0.14.6 → 0.14.8
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/CHANGELOG.md +14 -0
- package/README.md +7 -5
- package/dist/args.js +68 -18
- package/dist/browser.js +37 -3
- package/dist/cli.js +58 -12
- package/dist/config.js +17 -5
- package/dist/consent-browser.js +58 -0
- package/dist/daemon-authority-lifetime.js +171 -0
- package/dist/daemon-authority.js +216 -0
- package/dist/daemon-cache.js +348 -0
- package/dist/daemon-store.js +370 -0
- package/dist/daemon-supervisor.js +460 -0
- package/dist/env/onboard-draft-store.js +250 -0
- package/dist/env/onboard-draft.js +33 -0
- package/dist/env/selected-import-writer.js +163 -0
- package/dist/env/settings-authority.js +205 -0
- package/dist/events-cloud-tail.js +146 -0
- package/dist/events-cloud.js +619 -0
- package/dist/events.js +186 -31
- package/dist/oauth.js +129 -34
- package/dist/onboard-accounts.js +259 -0
- package/dist/onboard-automation-management.js +15 -0
- package/dist/onboard-bootstrap.js +162 -0
- package/dist/onboard-capabilities.js +15 -7
- package/dist/onboard-capacity.js +226 -0
- package/dist/onboard-existing.js +106 -32
- package/dist/onboard-file-snapshot.js +45 -0
- package/dist/onboard-github.js +14 -3
- package/dist/onboard-login-candidate.js +261 -0
- package/dist/onboard-next.js +139 -0
- package/dist/onboard-personal.js +14 -3
- package/dist/onboard-projects.js +350 -0
- package/dist/onboard-repositories.js +146 -37
- package/dist/onboard-runtime.js +108 -13
- package/dist/onboard-settings-context.js +141 -0
- package/dist/onboard-settings-import.js +127 -0
- package/dist/onboard-settings.js +234 -0
- package/dist/onboard-ui.js +85 -59
- package/dist/onboard-workflow.js +316 -0
- package/dist/onboard-workspace.js +14 -3
- package/dist/onboard.js +422 -39
- package/dist/owned-fetch.js +182 -0
- package/dist/pagination.js +13 -1
- package/dist/query.js +104 -31
- package/dist/ready.js +143 -35
- package/dist/replica.js +11 -4
- package/dist/repository-id.js +5 -0
- package/dist/sdk.js +33 -0
- package/dist/watch.js +2 -2
- package/package.json +3 -2
- package/skills/catalyst-github/SKILL.md +4 -2
- package/skills/catalyst-github/references/is-it-mergeable.md +1 -1
- package/skills/catalyst-github/references/local-replica.md +78 -0
- package/skills/catalyst-github/references/reading-from-the-cloud.md +24 -0
- package/skills/catalyst-github/references/what-a-pr-accumulates.md +1 -1
- package/skills/catalyst-github/scripts/is-it-mergeable.mjs +210 -45
- package/skills/catalyst-github/scripts/read-pr.mjs +51 -15
- package/skills/catalyst-linear/SKILL.md +5 -3
- package/skills/catalyst-linear/references/local-replica.md +91 -0
- package/skills/catalyst-linear/references/reading-a-ticket.md +3 -3
- package/skills/catalyst-linear/references/reading-from-the-cloud.md +38 -0
- package/skills/catalyst-linear/scripts/read-ticket.mjs +37 -12
- package/skills/catalyst-onboard/SKILL.md +6 -4
- package/skills/catalyst-onboard/references/local-replica.md +40 -0
- package/skills/catalyst-onboard/references/local-sync.md +8 -4
- package/skills/catalyst-onboard/scripts/local-sync.mjs +2 -2
- package/skills/catalyst-onboard/scripts/where-am-i.mjs +1 -1
- package/skills/run-this-project/SKILL.md +1 -1
- package/skills/unstick/SKILL.md +1 -1
- package/skills/what-needs-me/SKILL.md +1 -1
- package/skills/what-this-repo-needs/SKILL.md +1 -1
- package/skills/whats-happening/SKILL.md +4 -2
- package/skills/whats-happening/references/local-replica.md +51 -0
- package/skills/whats-happening/references/reading-from-the-cloud.md +21 -0
- package/skills/whats-happening/references/status-reply.md +1 -1
- package/skills/whats-happening/scripts/snapshot.mjs +104 -26
- package/vendor/settings/index.d.ts +4 -0
- package/vendor/settings/index.js +2937 -0
- package/vendor/settings/provenance.json +148 -0
package/dist/onboard-runtime.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { existsSync, realpathSync } from "node:fs";
|
|
2
2
|
import { userInfo } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { openConsentBrowser } from "./consent-browser.js";
|
|
5
5
|
import { defaultSkillsDirFor, loadConfig, normalizeBaseUrl, readManifest, } from "./config.js";
|
|
6
6
|
import { CliError } from "./errors.js";
|
|
7
7
|
import { cmdLegacy, findLegacy } from "./legacy.js";
|
|
8
|
-
import { bearerFor } from "./oauth.js";
|
|
9
8
|
import { personalConsentAdapter } from "./onboard-personal.js";
|
|
10
9
|
import { boundedOnboardSignin } from "./onboard-signin.js";
|
|
11
10
|
import { existingLinearAdapters } from "./onboard-existing.js";
|
|
12
11
|
import { linearWorkspaceAdapter } from "./onboard-workspace.js";
|
|
13
|
-
import {
|
|
12
|
+
import { firstProjectAdapters } from "./onboard-projects.js";
|
|
13
|
+
import { onboardCapacityAdapter } from "./onboard-capacity.js";
|
|
14
|
+
import { onboardAutomationManagementAdapter } from "./onboard-automation-management.js";
|
|
15
|
+
import { onboardWorkflowVerificationAdapter } from "./onboard-workflow.js";
|
|
16
|
+
import { onboardAccountsAdapter } from "./onboard-accounts.js";
|
|
14
17
|
import { githubInstallationAdapter } from "./onboard-github.js";
|
|
15
18
|
import { guardOnboardCapabilities, } from "./onboard-capabilities.js";
|
|
19
|
+
import { onboardSettingsAdapter, } from "./onboard-settings.js";
|
|
16
20
|
import { fetchMe } from "./transport.js";
|
|
17
21
|
const waiting = (reason) => ({
|
|
18
22
|
state: "waiting",
|
|
@@ -45,7 +49,56 @@ export function createOnboardRuntime(args, ctx, hooks) {
|
|
|
45
49
|
(journal.baseUrl &&
|
|
46
50
|
normalizeBaseUrl(journal.baseUrl) !== normalizeBaseUrl(cfg.baseUrl))))
|
|
47
51
|
throw new CliError("the saved setup belongs to another workspace or member", "onboard-identity-mismatch", 12);
|
|
48
|
-
|
|
52
|
+
// The Q1 preview reads the current token directly; stopping setup must not rotate saved credentials.
|
|
53
|
+
const expiry = cfg.auth
|
|
54
|
+
? Date.parse(cfg.auth.expiresAt) - stepCtx.now().getTime()
|
|
55
|
+
: 0;
|
|
56
|
+
const bearer = cfg.key ||
|
|
57
|
+
(cfg.auth && Number.isFinite(expiry) && expiry > 30_000
|
|
58
|
+
? cfg.auth.accessToken
|
|
59
|
+
: undefined);
|
|
60
|
+
if (!bearer)
|
|
61
|
+
throw new CliError("Renew your login with catalyst login, then run catalyst onboard.", "onboard-login-refresh-required", 11);
|
|
62
|
+
const owned = new AbortController();
|
|
63
|
+
const signal = hooks.ui
|
|
64
|
+
? AbortSignal.any([owned.signal, hooks.ui.signal])
|
|
65
|
+
: owned.signal;
|
|
66
|
+
const timer = setTimeout(() => owned.abort(), 30_000);
|
|
67
|
+
let remove = () => { };
|
|
68
|
+
let me;
|
|
69
|
+
try {
|
|
70
|
+
me = await new Promise((resolve, reject) => {
|
|
71
|
+
const stopped = () => reject(new CliError("Your membership could not be checked. Run catalyst onboard to retry.", "onboard-membership-unavailable", 11));
|
|
72
|
+
if (signal.aborted) {
|
|
73
|
+
stopped();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
signal.addEventListener("abort", stopped, { once: true });
|
|
77
|
+
remove = () => signal.removeEventListener("abort", stopped);
|
|
78
|
+
const boundedFetch = ((url, init) => {
|
|
79
|
+
if (signal.aborted)
|
|
80
|
+
return Promise.reject(new Error("membership_read_stopped"));
|
|
81
|
+
return stepCtx.fetch(url, {
|
|
82
|
+
...init,
|
|
83
|
+
redirect: "error",
|
|
84
|
+
signal: init?.signal
|
|
85
|
+
? AbortSignal.any([signal, init.signal])
|
|
86
|
+
: signal,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
fetchMe(cfg.baseUrl, bearer, boundedFetch).then((value) => (signal.aborted ? stopped() : resolve(value)), reject);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
remove();
|
|
95
|
+
}
|
|
96
|
+
const current = loadConfig(stepCtx.home);
|
|
97
|
+
if (!current?.user ||
|
|
98
|
+
current.account !== cfg.account ||
|
|
99
|
+
current.user.id !== cfg.user.id ||
|
|
100
|
+
normalizeBaseUrl(current.baseUrl) !== normalizeBaseUrl(cfg.baseUrl))
|
|
101
|
+
throw new CliError("Your saved login changed while membership was checked. Run catalyst onboard again.", "onboard-identity-mismatch", 12);
|
|
49
102
|
if (!me.user || me.account !== cfg.account || me.user.id !== cfg.user.id)
|
|
50
103
|
throw new CliError("the saved login and live member do not match", "onboard-identity-mismatch", 12);
|
|
51
104
|
// /me names a person within the account, not an independently exposed membership-row ID.
|
|
@@ -54,6 +107,12 @@ export function createOnboardRuntime(args, ctx, hooks) {
|
|
|
54
107
|
membershipId: me.user.id,
|
|
55
108
|
baseUrl: normalizeBaseUrl(cfg.baseUrl),
|
|
56
109
|
role: me.user.role,
|
|
110
|
+
display: {
|
|
111
|
+
personLabel: me.user.label,
|
|
112
|
+
email: me.user.email,
|
|
113
|
+
workspaceName: me.name,
|
|
114
|
+
workspaceSlug: me.slug,
|
|
115
|
+
},
|
|
57
116
|
};
|
|
58
117
|
};
|
|
59
118
|
const legacyCheck = async (stepCtx) => {
|
|
@@ -71,7 +130,7 @@ export function createOnboardRuntime(args, ctx, hooks) {
|
|
|
71
130
|
};
|
|
72
131
|
const personalAdapter = (provider) => personalConsentAdapter({
|
|
73
132
|
provider,
|
|
74
|
-
openBrowser: hooks.openBrowser ??
|
|
133
|
+
openBrowser: hooks.openBrowser ?? openConsentBrowser,
|
|
75
134
|
sleep: hooks.sleep,
|
|
76
135
|
wait: hooks.ui
|
|
77
136
|
? (message, run) => hooks.ui.wait(message, run)
|
|
@@ -147,22 +206,49 @@ export function createOnboardRuntime(args, ctx, hooks) {
|
|
|
147
206
|
...linear,
|
|
148
207
|
"linear.workspace": linearWorkspaceAdapter({
|
|
149
208
|
fallback: linear["linear.workspace"],
|
|
150
|
-
openBrowser: hooks.openBrowser ??
|
|
209
|
+
openBrowser: hooks.openBrowser ?? openConsentBrowser,
|
|
151
210
|
wait: hooks.ui
|
|
152
211
|
? (message, work) => hooks.ui.wait(message, work)
|
|
153
212
|
: undefined,
|
|
154
213
|
sleep: hooks.sleep,
|
|
155
214
|
}),
|
|
156
215
|
"github.install": githubInstallationAdapter({
|
|
157
|
-
openBrowser: hooks.openBrowser ??
|
|
216
|
+
openBrowser: hooks.openBrowser ?? openConsentBrowser,
|
|
158
217
|
wait: hooks.ui
|
|
159
218
|
? (message, work) => hooks.ui.wait(message, work)
|
|
160
219
|
: undefined,
|
|
161
220
|
sleep: hooks.sleep,
|
|
162
221
|
}),
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
:
|
|
222
|
+
settings: onboardSettingsAdapter({
|
|
223
|
+
...hooks.settings,
|
|
224
|
+
review: hooks.ui?.reviewSettings
|
|
225
|
+
? (summaries) => hooks.ui.reviewSettings(summaries)
|
|
226
|
+
: hooks.settings?.review,
|
|
227
|
+
message: hooks.ui
|
|
228
|
+
? (text) => hooks.ui.message(text)
|
|
229
|
+
: (text) => ctx.stderr(text),
|
|
230
|
+
}),
|
|
231
|
+
...firstProjectAdapters(args, {
|
|
232
|
+
chooseExisting: hooks.ui?.chooseRepositories
|
|
233
|
+
? (rows) => hooks.ui.chooseRepositories(rows.map((row) => ({ ...row })))
|
|
234
|
+
: undefined,
|
|
235
|
+
chooseFirst: hooks.ui?.chooseFirstRepository
|
|
236
|
+
? (rows) => hooks.ui.chooseFirstRepository(rows.map((row) => ({ ...row })))
|
|
237
|
+
: undefined,
|
|
238
|
+
message: (text) => (hooks.ui ? hooks.ui.message(text) : ctx.stderr(text)),
|
|
239
|
+
}),
|
|
240
|
+
accounts: onboardAccountsAdapter({
|
|
241
|
+
message: (text) => (hooks.ui ? hooks.ui.message(text) : ctx.stderr(text)),
|
|
242
|
+
}),
|
|
243
|
+
capacity: onboardCapacityAdapter({
|
|
244
|
+
message: (text) => (hooks.ui ? hooks.ui.message(text) : ctx.stderr(text)),
|
|
245
|
+
}),
|
|
246
|
+
"linear.adopt": onboardWorkflowVerificationAdapter({
|
|
247
|
+
message: (text) => (hooks.ui ? hooks.ui.message(text) : ctx.stderr(text)),
|
|
248
|
+
}),
|
|
249
|
+
"linear.automations": onboardAutomationManagementAdapter({
|
|
250
|
+
message: (text) => (hooks.ui ? hooks.ui.message(text) : ctx.stderr(text)),
|
|
251
|
+
}),
|
|
166
252
|
"linear.personal": personalAdapter("linear"),
|
|
167
253
|
"github.personal": personalAdapter("github"),
|
|
168
254
|
daemon: {
|
|
@@ -220,11 +306,19 @@ export function createOnboardRuntime(args, ctx, hooks) {
|
|
|
220
306
|
],
|
|
221
307
|
fallback: "connections",
|
|
222
308
|
},
|
|
223
|
-
"
|
|
224
|
-
check: [get("/api/v1/agent/
|
|
309
|
+
"github.repos": {
|
|
310
|
+
check: [get("/api/v1/repos"), get("/api/v1/agent/contract")],
|
|
225
311
|
fallback: "connections",
|
|
226
312
|
},
|
|
227
|
-
|
|
313
|
+
settings: {
|
|
314
|
+
check: [
|
|
315
|
+
get("/api/v1/agent/teams"),
|
|
316
|
+
get("/api/v1/repos"),
|
|
317
|
+
get("/api/v1/agent/contract"),
|
|
318
|
+
],
|
|
319
|
+
fallback: "connections",
|
|
320
|
+
},
|
|
321
|
+
projects: {
|
|
228
322
|
check: [get("/api/v1/repos"), get("/api/v1/agent/contract")],
|
|
229
323
|
fallback: "connections",
|
|
230
324
|
},
|
|
@@ -246,6 +340,7 @@ export function createOnboardRuntime(args, ctx, hooks) {
|
|
|
246
340
|
// tenant-bound step guards can run. Unsupported/local-only steps make no endpoint requests.
|
|
247
341
|
return {
|
|
248
342
|
identity: (journal) => identity(ctx, journal),
|
|
343
|
+
...(hooks.stageSignin ? { stageSignin: hooks.stageSignin } : {}),
|
|
249
344
|
bindSignals: true,
|
|
250
345
|
ui: hooks.ui,
|
|
251
346
|
adapters,
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { verifyOnboardRoutes } from "./onboard-capabilities.js";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { loadConfig, normalizeBaseUrl } from "./config.js";
|
|
5
|
+
import { readExistingOnboardJson, readOnboardTeamInventory, selectedOnboardTeam, } from "./onboard-existing.js";
|
|
6
|
+
import { readOnboardRepositoryInventory, selectedOnboardRepositories, } from "./onboard-repositories.js";
|
|
7
|
+
import { readLocalSettings, } from "./env/settings-authority.js";
|
|
8
|
+
const run = promisify(execFile);
|
|
9
|
+
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
10
|
+
? value
|
|
11
|
+
: null;
|
|
12
|
+
const repoKey = (repo) => `${repo.owner}/${repo.name}`.toLowerCase();
|
|
13
|
+
/** Read only Git's local origin; no credential helper, provider call, hook or repo command runs. */
|
|
14
|
+
export async function readLocalRepositoryName(repoRoot, signal) {
|
|
15
|
+
try {
|
|
16
|
+
if (signal?.aborted)
|
|
17
|
+
return null;
|
|
18
|
+
const result = await run("git", ["config", "--local", "--get-all", "remote.origin.url"], {
|
|
19
|
+
cwd: repoRoot,
|
|
20
|
+
timeout: 3_000,
|
|
21
|
+
maxBuffer: 16_384,
|
|
22
|
+
signal,
|
|
23
|
+
env: {
|
|
24
|
+
PATH: process.env.PATH,
|
|
25
|
+
GIT_CONFIG_GLOBAL: "/dev/null",
|
|
26
|
+
GIT_CONFIG_SYSTEM: "/dev/null",
|
|
27
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
28
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
const urls = result.stdout.trim().split(/\r?\n/);
|
|
32
|
+
if (urls.length !== 1 || signal?.aborted)
|
|
33
|
+
return null;
|
|
34
|
+
const raw = urls[0];
|
|
35
|
+
if (/[\u0000-\u0020\u007f]/.test(raw))
|
|
36
|
+
return null;
|
|
37
|
+
const ssh = /^git@github\.com:([A-Za-z0-9][A-Za-z0-9-]{0,38})\/([A-Za-z0-9._-]{1,100}?)(?:\.git)?$/.exec(raw);
|
|
38
|
+
if (ssh)
|
|
39
|
+
return `${ssh[1]}/${ssh[2]}`.toLowerCase();
|
|
40
|
+
const url = new URL(raw);
|
|
41
|
+
if (url.protocol !== "https:" ||
|
|
42
|
+
url.hostname !== "github.com" ||
|
|
43
|
+
url.port ||
|
|
44
|
+
url.username ||
|
|
45
|
+
url.password ||
|
|
46
|
+
url.search ||
|
|
47
|
+
url.hash)
|
|
48
|
+
return null;
|
|
49
|
+
const path = url.pathname.replace(/^\//, "").replace(/\.git$/, "");
|
|
50
|
+
return /^[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9._-]{1,100}$/.test(path)
|
|
51
|
+
? path.toLowerCase()
|
|
52
|
+
: null;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Fresh account/member, team inventory and personal repository ACL establish authority. The
|
|
59
|
+
* journal chooses identifiers only; neither its done state nor the disk contract is proof. */
|
|
60
|
+
export function onboardSettingsAuthorityPorts(input) {
|
|
61
|
+
const selected = { ...input.repository };
|
|
62
|
+
const root = input.repoRoot;
|
|
63
|
+
return {
|
|
64
|
+
readSettings: readLocalSettings,
|
|
65
|
+
readContext: async (signal) => {
|
|
66
|
+
try {
|
|
67
|
+
const { ctx, journal } = input;
|
|
68
|
+
const cfg = loadConfig(ctx.home);
|
|
69
|
+
const account = journal.account ?? journal.tenant;
|
|
70
|
+
if (signal?.aborted ||
|
|
71
|
+
!cfg?.user ||
|
|
72
|
+
!account ||
|
|
73
|
+
account !== cfg.account ||
|
|
74
|
+
journal.membershipId !== cfg.user.id ||
|
|
75
|
+
!journal.baseUrl ||
|
|
76
|
+
normalizeBaseUrl(journal.baseUrl) !== normalizeBaseUrl(cfg.baseUrl))
|
|
77
|
+
return null;
|
|
78
|
+
const teamId = selectedOnboardTeam(journal);
|
|
79
|
+
if (!teamId ||
|
|
80
|
+
teamId !== selected.teamId ||
|
|
81
|
+
!selectedOnboardRepositories(journal).some((repo) => repoKey(repo) === repoKey(selected) &&
|
|
82
|
+
repo.repoId === selected.repoId &&
|
|
83
|
+
repo.teamId === teamId))
|
|
84
|
+
return null;
|
|
85
|
+
const supported = await verifyOnboardRoutes(ctx, journal, [
|
|
86
|
+
{ method: "GET", path: "/api/v1/agent/teams" },
|
|
87
|
+
{ method: "GET", path: "/api/v1/repos" },
|
|
88
|
+
{ method: "GET", path: "/api/v1/agent/contract" },
|
|
89
|
+
], signal);
|
|
90
|
+
if ("reason" in supported || signal?.aborted)
|
|
91
|
+
return null;
|
|
92
|
+
const meRead = await readExistingOnboardJson(ctx, "/api/v1/me", signal);
|
|
93
|
+
if ("reason" in meRead)
|
|
94
|
+
return null;
|
|
95
|
+
const me = object(meRead.body);
|
|
96
|
+
const user = object(me?.user);
|
|
97
|
+
if (me?.account !== account ||
|
|
98
|
+
user?.id !== cfg.user.id ||
|
|
99
|
+
typeof user.role !== "string" ||
|
|
100
|
+
!["owner", "admin"].includes(user.role))
|
|
101
|
+
return null;
|
|
102
|
+
const teams = await readOnboardTeamInventory(ctx, signal);
|
|
103
|
+
if ("reason" in teams)
|
|
104
|
+
return null;
|
|
105
|
+
const team = teams.teams.find((row) => row.id === teamId);
|
|
106
|
+
if (!team || !/^[A-Z][A-Z0-9]{0,15}$/.test(team.key))
|
|
107
|
+
return null;
|
|
108
|
+
const inventory = await readOnboardRepositoryInventory(ctx, journal, signal);
|
|
109
|
+
if ("reason" in inventory ||
|
|
110
|
+
!inventory.repositories.some((repo) => repoKey(repo) === repoKey(selected) &&
|
|
111
|
+
repo.repoId === selected.repoId &&
|
|
112
|
+
repo.teamId === selected.teamId))
|
|
113
|
+
return null;
|
|
114
|
+
const local = await (input.readRepositoryName ?? readLocalRepositoryName)(root, signal);
|
|
115
|
+
if (local !== repoKey(selected) || signal?.aborted)
|
|
116
|
+
return null;
|
|
117
|
+
const current = loadConfig(ctx.home);
|
|
118
|
+
if (!current?.user ||
|
|
119
|
+
current.account !== cfg.account ||
|
|
120
|
+
current.user.id !== cfg.user.id ||
|
|
121
|
+
normalizeBaseUrl(current.baseUrl) !== normalizeBaseUrl(cfg.baseUrl) ||
|
|
122
|
+
!["owner", "admin"].includes(current.user.role))
|
|
123
|
+
return null;
|
|
124
|
+
return {
|
|
125
|
+
accountId: account,
|
|
126
|
+
personId: cfg.user.id,
|
|
127
|
+
baseUrl: normalizeBaseUrl(cfg.baseUrl),
|
|
128
|
+
role: user.role,
|
|
129
|
+
teamId,
|
|
130
|
+
teamKey: team.key,
|
|
131
|
+
repoId: selected.repoId,
|
|
132
|
+
repoName: repoKey(selected),
|
|
133
|
+
repoRoot: root,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { verifyOnboardRoutes } from "./onboard-capabilities.js";
|
|
2
|
+
import { loadConfig, normalizeBaseUrl } from "./config.js";
|
|
3
|
+
import { createSelectedImportWriter, } from "./env/selected-import-writer.js";
|
|
4
|
+
const uncertain = () => new Error("selected import outcome uncertain");
|
|
5
|
+
const bind = (context) => JSON.stringify([
|
|
6
|
+
context.accountId,
|
|
7
|
+
context.personId,
|
|
8
|
+
context.baseUrl,
|
|
9
|
+
context.role,
|
|
10
|
+
context.teamId,
|
|
11
|
+
context.teamKey,
|
|
12
|
+
context.repoId,
|
|
13
|
+
context.repoName,
|
|
14
|
+
context.repoRoot,
|
|
15
|
+
]);
|
|
16
|
+
/** The review boundary owns declaration authority. Each actual write additionally rechecks
|
|
17
|
+
* current access and identity; neither the disk contract nor the receipt supplies a scope. */
|
|
18
|
+
export function createOnboardSettingsWriter(input) {
|
|
19
|
+
const expected = { ...input.context };
|
|
20
|
+
const binding = bind(expected);
|
|
21
|
+
try {
|
|
22
|
+
const url = new URL(expected.baseUrl);
|
|
23
|
+
if (url.protocol !== "https:" ||
|
|
24
|
+
url.origin !== expected.baseUrl ||
|
|
25
|
+
!["owner", "admin"].includes(expected.role))
|
|
26
|
+
throw uncertain();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw uncertain();
|
|
30
|
+
}
|
|
31
|
+
const post = async (path, body) => {
|
|
32
|
+
const owned = new AbortController();
|
|
33
|
+
const signal = input.signal
|
|
34
|
+
? AbortSignal.any([input.signal, owned.signal])
|
|
35
|
+
: owned.signal;
|
|
36
|
+
let timer;
|
|
37
|
+
let remove = () => { };
|
|
38
|
+
try {
|
|
39
|
+
return await new Promise((resolve, reject) => {
|
|
40
|
+
const stopped = () => reject(uncertain());
|
|
41
|
+
if (signal.aborted) {
|
|
42
|
+
stopped();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
signal.addEventListener("abort", stopped, { once: true });
|
|
46
|
+
remove = () => signal.removeEventListener("abort", stopped);
|
|
47
|
+
timer = setTimeout(() => owned.abort(), 30_000);
|
|
48
|
+
Promise.resolve()
|
|
49
|
+
.then(async () => {
|
|
50
|
+
const current = await input.ports.readContext(signal);
|
|
51
|
+
if (!current || bind(current) !== binding || signal.aborted)
|
|
52
|
+
throw uncertain();
|
|
53
|
+
const cfg = loadConfig(input.ctx.home);
|
|
54
|
+
if (!cfg?.user ||
|
|
55
|
+
cfg.account !== expected.accountId ||
|
|
56
|
+
cfg.user.id !== expected.personId ||
|
|
57
|
+
normalizeBaseUrl(cfg.baseUrl) !== expected.baseUrl ||
|
|
58
|
+
!["owner", "admin"].includes(cfg.user.role))
|
|
59
|
+
throw uncertain();
|
|
60
|
+
const expiry = cfg.auth
|
|
61
|
+
? Date.parse(cfg.auth.expiresAt) - input.ctx.now().getTime()
|
|
62
|
+
: 0;
|
|
63
|
+
const bearer = cfg.key ||
|
|
64
|
+
(cfg.auth && Number.isFinite(expiry) && expiry > 30_000
|
|
65
|
+
? cfg.auth.accessToken
|
|
66
|
+
: undefined);
|
|
67
|
+
if (!bearer || signal.aborted)
|
|
68
|
+
throw uncertain();
|
|
69
|
+
const supported = await verifyOnboardRoutes(input.ctx, {
|
|
70
|
+
account: expected.accountId,
|
|
71
|
+
tenant: expected.accountId,
|
|
72
|
+
membershipId: expected.personId,
|
|
73
|
+
baseUrl: expected.baseUrl,
|
|
74
|
+
}, [{ method: "POST", path }], signal);
|
|
75
|
+
if ("reason" in supported || signal.aborted)
|
|
76
|
+
throw uncertain();
|
|
77
|
+
const response = await input.ctx.fetch(`${expected.baseUrl}${path}`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
redirect: "error",
|
|
80
|
+
signal,
|
|
81
|
+
headers: {
|
|
82
|
+
authorization: `Bearer ${bearer}`,
|
|
83
|
+
accept: "application/json",
|
|
84
|
+
"content-type": "application/json",
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify(body),
|
|
87
|
+
});
|
|
88
|
+
// Refusal bodies may contain unsafe text. The adapter needs their status only.
|
|
89
|
+
const result = {
|
|
90
|
+
status: response.status,
|
|
91
|
+
body: response.ok
|
|
92
|
+
? (await response.json())
|
|
93
|
+
: undefined,
|
|
94
|
+
};
|
|
95
|
+
if (signal.aborted)
|
|
96
|
+
throw uncertain();
|
|
97
|
+
return result;
|
|
98
|
+
})
|
|
99
|
+
.then((value) => (signal.aborted ? stopped() : resolve(value)), () => reject(uncertain()));
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
if (timer)
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
remove();
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const writer = createSelectedImportWriter({
|
|
109
|
+
kind: input.kind,
|
|
110
|
+
writeVariable: (name, value) => post("/me/env-vars", {
|
|
111
|
+
scope: "repo",
|
|
112
|
+
repoId: expected.repoId,
|
|
113
|
+
name,
|
|
114
|
+
value,
|
|
115
|
+
}),
|
|
116
|
+
importSecrets: (text) => post("/me/secrets/import", {
|
|
117
|
+
repo: expected.repoName,
|
|
118
|
+
text,
|
|
119
|
+
rotateExisting: [],
|
|
120
|
+
source: "onboarding selected import",
|
|
121
|
+
}),
|
|
122
|
+
});
|
|
123
|
+
return async (text, names) => {
|
|
124
|
+
const result = await writer(text, names);
|
|
125
|
+
return { writtenNames: result.written, failedNames: result.failed };
|
|
126
|
+
};
|
|
127
|
+
}
|