@catalyst-cloud/cli 0.14.5 → 0.14.7
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 +9 -0
- package/README.md +5 -3
- package/dist/args.js +51 -13
- package/dist/cli.js +58 -10
- package/dist/config.js +16 -3
- 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 +243 -0
- package/dist/env/onboard-draft.js +33 -0
- package/dist/env/selected-import-writer.js +163 -0
- package/dist/env/settings-authority.js +203 -0
- package/dist/events-cloud-tail.js +134 -0
- package/dist/events-cloud.js +500 -0
- package/dist/events.js +13 -4
- package/dist/legacy.js +145 -32
- package/dist/oauth.js +125 -33
- 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-login-candidate.js +261 -0
- package/dist/onboard-projects.js +350 -0
- package/dist/onboard-repositories.js +147 -37
- package/dist/onboard-runtime.js +104 -9
- 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 +99 -11
- package/dist/onboard-workflow.js +316 -0
- package/dist/onboard.js +359 -27
- package/dist/owned-fetch.js +182 -0
- package/dist/query.js +77 -27
- package/dist/ready.js +143 -35
- package/dist/replica.js +11 -4
- package/dist/sdk.js +33 -0
- package/dist/watch.js +2 -2
- package/package.json +3 -2
- package/skills/catalyst-github/SKILL.md +2 -2
- package/skills/catalyst-github/references/is-it-mergeable.md +1 -1
- 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 +3 -3
- package/skills/catalyst-linear/references/reading-a-ticket.md +3 -3
- package/skills/catalyst-linear/scripts/read-ticket.mjs +37 -12
- package/skills/catalyst-onboard/SKILL.md +5 -4
- package/skills/catalyst-onboard/references/local-sync.md +3 -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 +2 -2
- 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
|
@@ -1,44 +1,105 @@
|
|
|
1
|
-
import { loadConfig, normalizeBaseUrl, readManifest } from "./config.js";
|
|
1
|
+
import { loadConfig, normalizeBaseUrl, readManifest, } from "./config.js";
|
|
2
2
|
import { contractVersionInRange } from "./contract.js";
|
|
3
|
-
import { readExistingOnboardJson, selectedOnboardTeam } from "./onboard-existing.js";
|
|
3
|
+
import { readExistingOnboardJson, selectedOnboardTeam, } from "./onboard-existing.js";
|
|
4
4
|
const part = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$/;
|
|
5
5
|
const id = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
6
|
-
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
6
|
+
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
7
|
+
? value
|
|
8
|
+
: null;
|
|
7
9
|
const key = (row) => `${row.owner}/${row.name}`.toLowerCase();
|
|
8
|
-
const waiting = (reason) => ({
|
|
10
|
+
const waiting = (reason) => ({
|
|
11
|
+
state: "waiting",
|
|
12
|
+
reason,
|
|
13
|
+
});
|
|
9
14
|
const valid = (value) => {
|
|
10
15
|
const row = object(value);
|
|
11
|
-
return !!row &&
|
|
12
|
-
typeof row.
|
|
16
|
+
return (!!row &&
|
|
17
|
+
typeof row.teamId === "string" &&
|
|
18
|
+
id.test(row.teamId) &&
|
|
19
|
+
typeof row.repoId === "string" &&
|
|
20
|
+
id.test(row.repoId) &&
|
|
21
|
+
typeof row.owner === "string" &&
|
|
22
|
+
part.test(row.owner) &&
|
|
23
|
+
typeof row.name === "string" &&
|
|
24
|
+
part.test(row.name));
|
|
13
25
|
};
|
|
14
26
|
/** Names/IDs only. A saved selection never proves current access or repository readiness. */
|
|
15
27
|
export function selectedOnboardRepositories(journal) {
|
|
16
|
-
const
|
|
17
|
-
const
|
|
28
|
+
const selection = journal?.steps.find((row) => row.id === "github.repos" && row.state === "done");
|
|
29
|
+
const registered = journal?.steps.find((row) => row.id === "projects" && row.state === "done");
|
|
30
|
+
const receipt = selection?.evidence?.repository;
|
|
18
31
|
if (typeof receipt !== "string" || receipt.length > 64_000)
|
|
19
32
|
return [];
|
|
20
33
|
try {
|
|
21
34
|
const rows = JSON.parse(receipt);
|
|
22
|
-
if (!Array.isArray(rows) || !rows.length || rows.length > 100
|
|
35
|
+
if (!Array.isArray(rows) || !rows.length || rows.length > 100)
|
|
23
36
|
return [];
|
|
24
|
-
if (
|
|
37
|
+
if (rows.every(valid)) {
|
|
38
|
+
if (new Set(rows.map(key)).size !== rows.length)
|
|
39
|
+
return [];
|
|
40
|
+
return rows.map((row) => ({
|
|
41
|
+
teamId: row.teamId,
|
|
42
|
+
owner: row.owner,
|
|
43
|
+
name: row.name,
|
|
44
|
+
repoId: row.repoId,
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
// A fresh Q3 choice has names only. It may use later verified registration evidence only
|
|
48
|
+
// when the exact selected team/repository matches, never an earlier project's selection.
|
|
49
|
+
if (rows.length !== 1)
|
|
50
|
+
return [];
|
|
51
|
+
const selected = object(rows[0]);
|
|
52
|
+
if (!selected ||
|
|
53
|
+
selected.repoId !== undefined ||
|
|
54
|
+
typeof selected.teamId !== "string" ||
|
|
55
|
+
!id.test(selected.teamId) ||
|
|
56
|
+
typeof selected.owner !== "string" ||
|
|
57
|
+
!part.test(selected.owner) ||
|
|
58
|
+
typeof selected.name !== "string" ||
|
|
59
|
+
!part.test(selected.name))
|
|
60
|
+
return [];
|
|
61
|
+
const evidence = registered?.evidence?.repository;
|
|
62
|
+
if (typeof evidence !== "string" || evidence.length > 64_000)
|
|
25
63
|
return [];
|
|
26
|
-
|
|
64
|
+
const registeredRows = JSON.parse(evidence);
|
|
65
|
+
if (!Array.isArray(registeredRows) ||
|
|
66
|
+
registeredRows.length !== 1 ||
|
|
67
|
+
!registeredRows.every(valid))
|
|
68
|
+
return [];
|
|
69
|
+
const live = registeredRows[0];
|
|
70
|
+
return live.teamId === selected.teamId &&
|
|
71
|
+
key(live) === key({ owner: selected.owner, name: selected.name })
|
|
72
|
+
? [{ ...live }]
|
|
73
|
+
: [];
|
|
27
74
|
}
|
|
28
75
|
catch {
|
|
29
76
|
return [];
|
|
30
77
|
}
|
|
31
78
|
}
|
|
32
79
|
/** The personal ACL inventory chooses bindings; a fresh contract supplies IDs only. No cache read. */
|
|
33
|
-
async function
|
|
80
|
+
export async function readOnboardRepositoryInventory(ctx, journal, signal) {
|
|
34
81
|
const cfg = loadConfig(ctx.home);
|
|
35
|
-
if (!cfg?.user ||
|
|
36
|
-
!journal.
|
|
82
|
+
if (!cfg?.user ||
|
|
83
|
+
!journal.account ||
|
|
84
|
+
cfg.account !== journal.account ||
|
|
85
|
+
journal.membershipId !== cfg.user.id ||
|
|
86
|
+
!journal.baseUrl ||
|
|
87
|
+
normalizeBaseUrl(journal.baseUrl) !== normalizeBaseUrl(cfg.baseUrl))
|
|
37
88
|
return { reason: "repository_identity_unverified" };
|
|
89
|
+
const personId = cfg.user.id;
|
|
90
|
+
const sameIdentity = () => {
|
|
91
|
+
const current = loadConfig(ctx.home);
|
|
92
|
+
return (current?.user &&
|
|
93
|
+
current.account === cfg.account &&
|
|
94
|
+
current.user.id === personId &&
|
|
95
|
+
normalizeBaseUrl(current.baseUrl) === normalizeBaseUrl(cfg.baseUrl));
|
|
96
|
+
};
|
|
38
97
|
const teamId = selectedOnboardTeam(journal);
|
|
39
98
|
if (!teamId)
|
|
40
99
|
return { reason: "team_selection_unverified" };
|
|
41
100
|
const read = await readExistingOnboardJson(ctx, "/api/v1/repos", signal);
|
|
101
|
+
if (!sameIdentity())
|
|
102
|
+
return { reason: "repository_identity_unverified" };
|
|
42
103
|
if ("reason" in read)
|
|
43
104
|
return { reason: "repository_read_unavailable" };
|
|
44
105
|
const body = object(read.body);
|
|
@@ -48,8 +109,13 @@ async function inventory(ctx, journal, signal) {
|
|
|
48
109
|
const seen = new Set();
|
|
49
110
|
for (const value of body.repos) {
|
|
50
111
|
const row = object(value);
|
|
51
|
-
if (!row ||
|
|
52
|
-
typeof row.
|
|
112
|
+
if (!row ||
|
|
113
|
+
typeof row.teamId !== "string" ||
|
|
114
|
+
!id.test(row.teamId) ||
|
|
115
|
+
typeof row.owner !== "string" ||
|
|
116
|
+
!part.test(row.owner) ||
|
|
117
|
+
typeof row.name !== "string" ||
|
|
118
|
+
!part.test(row.name))
|
|
53
119
|
return { reason: "repository_inventory_shape" };
|
|
54
120
|
if (row.teamId !== teamId)
|
|
55
121
|
continue;
|
|
@@ -59,24 +125,37 @@ async function inventory(ctx, journal, signal) {
|
|
|
59
125
|
seen.add(name);
|
|
60
126
|
bindings.push({ owner: row.owner, name: row.name });
|
|
61
127
|
}
|
|
128
|
+
if (!sameIdentity())
|
|
129
|
+
return { reason: "repository_identity_unverified" };
|
|
62
130
|
if (!bindings.length)
|
|
63
131
|
return { repositories: [] };
|
|
64
132
|
const contractRead = await readExistingOnboardJson(ctx, "/api/v1/agent/contract", signal);
|
|
133
|
+
if (!sameIdentity())
|
|
134
|
+
return { reason: "repository_identity_unverified" };
|
|
65
135
|
if ("reason" in contractRead)
|
|
66
136
|
return { reason: "repository_contract_unavailable" };
|
|
67
137
|
const doc = object(contractRead.body);
|
|
68
138
|
const account = object(doc?.account);
|
|
69
139
|
const merge = object(doc?.merge);
|
|
70
|
-
if (!doc ||
|
|
140
|
+
if (!doc ||
|
|
141
|
+
account?.id !== journal.account ||
|
|
142
|
+
typeof doc.contractVersion !== "string" ||
|
|
71
143
|
contractVersionInRange(doc.contractVersion, readManifest().tenantContractRange) !== true ||
|
|
72
|
-
!merge ||
|
|
144
|
+
!merge ||
|
|
145
|
+
!Array.isArray(merge.repositories) ||
|
|
146
|
+
merge.repositories.length > 10_000)
|
|
73
147
|
return { reason: "repository_contract_unverified" };
|
|
74
148
|
const lookup = new Map();
|
|
75
149
|
const idNames = new Map();
|
|
76
150
|
for (const value of merge.repositories) {
|
|
77
151
|
const row = object(value);
|
|
78
|
-
if (!row ||
|
|
79
|
-
typeof row.
|
|
152
|
+
if (!row ||
|
|
153
|
+
typeof row.owner !== "string" ||
|
|
154
|
+
!part.test(row.owner) ||
|
|
155
|
+
typeof row.name !== "string" ||
|
|
156
|
+
!part.test(row.name) ||
|
|
157
|
+
typeof row.repoId !== "string" ||
|
|
158
|
+
!id.test(row.repoId))
|
|
80
159
|
return { reason: "repository_contract_unverified" };
|
|
81
160
|
const name = key({ owner: row.owner, name: row.name });
|
|
82
161
|
if (idNames.has(row.repoId) && idNames.get(row.repoId) !== name)
|
|
@@ -99,34 +178,57 @@ export function existingRepositoryAdapter(args, choose) {
|
|
|
99
178
|
const explicit = args.flags.repo;
|
|
100
179
|
if (explicit !== undefined) {
|
|
101
180
|
const names = Array.isArray(explicit) ? explicit : [explicit];
|
|
102
|
-
if (!names.length ||
|
|
181
|
+
if (!names.length ||
|
|
182
|
+
names.length > 100 ||
|
|
183
|
+
!names.every((name) => typeof name === "string" &&
|
|
184
|
+
/^[A-Za-z0-9][A-Za-z0-9_.-]{0,99}\/[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$/.test(name)))
|
|
103
185
|
return null;
|
|
104
|
-
const wanted = names.map(name => String(name).toLowerCase());
|
|
186
|
+
const wanted = names.map((name) => String(name).toLowerCase());
|
|
105
187
|
if (new Set(wanted).size !== wanted.length)
|
|
106
188
|
return null;
|
|
107
|
-
const selected = wanted.map(name => repositories.find(row => key(row) === name));
|
|
108
|
-
return selected.every((row) => !!row)
|
|
189
|
+
const selected = wanted.map((name) => repositories.find((row) => key(row) === name));
|
|
190
|
+
return selected.every((row) => !!row)
|
|
191
|
+
? selected
|
|
192
|
+
: null;
|
|
109
193
|
}
|
|
110
194
|
const saved = chosen ?? selectedOnboardRepositories(journal);
|
|
111
195
|
if (!saved.length)
|
|
112
196
|
return null;
|
|
113
|
-
const selected = saved.map(row => repositories.find(live => key(live) === key(row) &&
|
|
114
|
-
|
|
197
|
+
const selected = saved.map((row) => repositories.find((live) => key(live) === key(row) &&
|
|
198
|
+
live.teamId === row.teamId &&
|
|
199
|
+
live.repoId === row.repoId));
|
|
200
|
+
return selected.every((row) => !!row)
|
|
201
|
+
? selected
|
|
202
|
+
: null;
|
|
115
203
|
};
|
|
116
204
|
const check = async (ctx, journal, signal) => {
|
|
117
|
-
const list = await
|
|
205
|
+
const list = await readOnboardRepositoryInventory(ctx, journal, signal);
|
|
118
206
|
if ("reason" in list)
|
|
119
207
|
return waiting(list.reason);
|
|
120
208
|
if (!list.repositories.length)
|
|
121
209
|
return waiting("repository_inventory_empty");
|
|
122
210
|
const rows = requested(list.repositories, journal);
|
|
123
211
|
if (!rows)
|
|
124
|
-
return args.flags.repo !== undefined ||
|
|
125
|
-
|
|
126
|
-
|
|
212
|
+
return args.flags.repo !== undefined ||
|
|
213
|
+
chosen ||
|
|
214
|
+
selectedOnboardRepositories(journal).length
|
|
215
|
+
? waiting("repository_selection_unverified")
|
|
216
|
+
: choose
|
|
217
|
+
? { state: "pending" }
|
|
218
|
+
: waiting("repository_choice_required");
|
|
219
|
+
return {
|
|
220
|
+
state: "done",
|
|
221
|
+
evidence: {
|
|
222
|
+
repository: JSON.stringify(rows),
|
|
223
|
+
count: rows.length,
|
|
224
|
+
checkedAt: ctx.now().getTime(),
|
|
225
|
+
},
|
|
226
|
+
};
|
|
127
227
|
};
|
|
128
|
-
return {
|
|
129
|
-
|
|
228
|
+
return {
|
|
229
|
+
check,
|
|
230
|
+
act: async (ctx, journal, signal) => {
|
|
231
|
+
const list = await readOnboardRepositoryInventory(ctx, journal, signal);
|
|
130
232
|
if ("reason" in list)
|
|
131
233
|
return waiting(list.reason);
|
|
132
234
|
if (!list.repositories.length)
|
|
@@ -142,19 +244,27 @@ export function existingRepositoryAdapter(args, choose) {
|
|
|
142
244
|
}
|
|
143
245
|
signal?.addEventListener("abort", stopped, { once: true });
|
|
144
246
|
remove = () => signal?.removeEventListener("abort", stopped);
|
|
145
|
-
Promise.resolve()
|
|
247
|
+
Promise.resolve()
|
|
248
|
+
.then(() => signal?.aborted
|
|
249
|
+
? null
|
|
250
|
+
: choose(list.repositories.map((row) => ({ ...row }))))
|
|
251
|
+
.then(resolve, reject);
|
|
146
252
|
}).finally(() => remove());
|
|
147
253
|
if (!answer || signal?.aborted)
|
|
148
254
|
return waiting("interrupted");
|
|
149
|
-
if (!Array.isArray(answer) ||
|
|
255
|
+
if (!Array.isArray(answer) ||
|
|
256
|
+
!answer.length ||
|
|
257
|
+
answer.length > 100 ||
|
|
258
|
+
!answer.every((name) => typeof name === "string"))
|
|
150
259
|
return waiting("repository_selection_unverified");
|
|
151
|
-
const names = answer.map(name => name.toLowerCase());
|
|
260
|
+
const names = answer.map((name) => name.toLowerCase());
|
|
152
261
|
if (new Set(names).size !== names.length)
|
|
153
262
|
return waiting("repository_selection_unverified");
|
|
154
|
-
const rows = names.map(name => list.repositories.find(row => key(row) === name));
|
|
263
|
+
const rows = names.map((name) => list.repositories.find((row) => key(row) === name));
|
|
155
264
|
if (!rows.every((row) => !!row))
|
|
156
265
|
return waiting("repository_selection_unverified");
|
|
157
266
|
chosen = rows;
|
|
158
267
|
return check(ctx, journal, signal);
|
|
159
|
-
}
|
|
268
|
+
},
|
|
269
|
+
};
|
|
160
270
|
}
|
package/dist/onboard-runtime.js
CHANGED
|
@@ -5,14 +5,18 @@ import { openBrowser } from "./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) => {
|
|
@@ -160,9 +219,36 @@ export function createOnboardRuntime(args, ctx, hooks) {
|
|
|
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
|
+
}
|