create-egregore 0.18.9 → 0.18.10
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/lib/local.js +110 -13
- package/package.json +1 -1
package/lib/local.js
CHANGED
|
@@ -125,6 +125,62 @@ async function getFileContent(token, owner, repo, filePath) {
|
|
|
125
125
|
return Buffer.from(data.content, "base64").toString("utf-8");
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// True if `owner` resolves to a real GitHub account (user OR org — both live
|
|
129
|
+
// under /users/{login}). Lets join distinguish "wrong owner" from "owner has
|
|
130
|
+
// no Egregore", which is the difference between a typo and a missing repo.
|
|
131
|
+
async function ownerExists(token, owner) {
|
|
132
|
+
const { status } = await ghApi("GET", `/users/${owner}`, token);
|
|
133
|
+
return status === 200;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Repo names under `owner` that the authenticated user can actually reach —
|
|
137
|
+
// org membership or a (already-accepted) personal-repo collaborator invite.
|
|
138
|
+
// /user/repos is the only endpoint that honors private + collaborator repos
|
|
139
|
+
// across arbitrary owners, so we list it and filter by owner.
|
|
140
|
+
async function listAccessibleRepos(token, owner) {
|
|
141
|
+
const ownerLc = owner.toLowerCase();
|
|
142
|
+
const names = [];
|
|
143
|
+
let page = 1;
|
|
144
|
+
while (page <= 4) { // cap at 400 accessible repos
|
|
145
|
+
const url = `/user/repos?per_page=100&sort=updated&affiliation=owner,collaborator,organization_member&page=${page}`;
|
|
146
|
+
const { status, data } = await ghApi("GET", url, token);
|
|
147
|
+
if (status !== 200 || !Array.isArray(data) || data.length === 0) break;
|
|
148
|
+
for (const r of data) {
|
|
149
|
+
if (r.owner?.login?.toLowerCase() === ownerLc) names.push(r.name);
|
|
150
|
+
}
|
|
151
|
+
if (data.length < 100) break;
|
|
152
|
+
page += 1;
|
|
153
|
+
}
|
|
154
|
+
return names;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Discover every repo under `owner` that carries an egregore.json on its
|
|
158
|
+
// default branch. Probes the conventional name first, then Egregore-looking
|
|
159
|
+
// names, then the rest — capped so a large account doesn't fan out forever.
|
|
160
|
+
// Returns [{ repo, config }] so the caller never re-fetches the config.
|
|
161
|
+
async function discoverEgregoreRepos(token, owner) {
|
|
162
|
+
const seen = new Set();
|
|
163
|
+
const candidates = [];
|
|
164
|
+
const add = (name) => {
|
|
165
|
+
const key = name.toLowerCase();
|
|
166
|
+
if (!seen.has(key)) { seen.add(key); candidates.push(name); }
|
|
167
|
+
};
|
|
168
|
+
add("egregore"); // conventional default — also covers public repos not in the list
|
|
169
|
+
const accessible = await listAccessibleRepos(token, owner);
|
|
170
|
+
for (const n of accessible.filter((n) => /egregore/i.test(n))) add(n);
|
|
171
|
+
for (const n of accessible.filter((n) => !/egregore/i.test(n))) add(n);
|
|
172
|
+
|
|
173
|
+
const MAX_PROBES = 20;
|
|
174
|
+
const found = [];
|
|
175
|
+
for (const repo of candidates.slice(0, MAX_PROBES)) {
|
|
176
|
+
let raw = null;
|
|
177
|
+
try { raw = await getFileContent(token, owner, repo, "egregore.json"); } catch { raw = null; }
|
|
178
|
+
if (!raw) continue;
|
|
179
|
+
try { found.push({ repo, config: JSON.parse(raw) }); } catch { /* malformed config — skip */ }
|
|
180
|
+
}
|
|
181
|
+
return found;
|
|
182
|
+
}
|
|
183
|
+
|
|
128
184
|
async function putFileContent(token, owner, repo, filePath, content, message) {
|
|
129
185
|
// Get SHA if file already exists
|
|
130
186
|
const { status: existStatus, data: existData } = await ghApi(
|
|
@@ -1253,22 +1309,63 @@ async function localJoinFlow(orgArg, ui, ctx = {}) {
|
|
|
1253
1309
|
await acceptPendingInvitations(githubToken, orgLogin, ui);
|
|
1254
1310
|
s1.stop("Invitations accepted");
|
|
1255
1311
|
|
|
1256
|
-
// 4. Resolve
|
|
1257
|
-
//
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1312
|
+
// 4. Resolve which repo under `orgLogin` holds egregore.json. An explicit
|
|
1313
|
+
// repo is read directly; a bare owner is discovered (the founder flow
|
|
1314
|
+
// names the repo after the org slug, NOT "egregore", so we can't assume).
|
|
1315
|
+
// The shared install() helper owns all local setup after this point.
|
|
1316
|
+
const s2 = ui.spinner(
|
|
1317
|
+
explicitRepo
|
|
1318
|
+
? `Reading ${orgLogin}/${explicitRepo}/egregore.json...`
|
|
1319
|
+
: `Finding the Egregore in ${orgLogin}...`,
|
|
1320
|
+
);
|
|
1321
|
+
let candidates = [];
|
|
1261
1322
|
try {
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1323
|
+
if (explicitRepo) {
|
|
1324
|
+
const raw = await getFileContent(githubToken, orgLogin, explicitRepo, "egregore.json");
|
|
1325
|
+
if (raw) candidates = [{ repo: explicitRepo, config: JSON.parse(raw) }];
|
|
1326
|
+
} else {
|
|
1327
|
+
candidates = await discoverEgregoreRepos(githubToken, orgLogin);
|
|
1328
|
+
}
|
|
1329
|
+
} catch {
|
|
1330
|
+
candidates = [];
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
if (candidates.length === 0) {
|
|
1334
|
+
s2.fail("Could not find an Egregore to join");
|
|
1335
|
+
const exists = await ownerExists(githubToken, orgLogin);
|
|
1336
|
+
if (!exists) {
|
|
1337
|
+
// The owner itself isn't a GitHub account — almost always a repo name
|
|
1338
|
+
// passed where owner/repo was meant (e.g. `join xenocracy` for
|
|
1339
|
+
// `fcdagdelen/xenocracy`).
|
|
1340
|
+
ui.error(`No GitHub user or organization named ${ui.bold(orgLogin)}.`);
|
|
1341
|
+
ui.info(`Join targets are ${ui.bold("owner/repo")} — e.g. ${ui.bold(joinCommand("acme", "acme"))}.`);
|
|
1342
|
+
} else if (explicitRepo) {
|
|
1343
|
+
ui.error(`${ui.bold(`${orgLogin}/${explicitRepo}`)} has no egregore.json on its default branch.`);
|
|
1344
|
+
ui.info("Confirm the repo name, that egregore.json is committed to the default branch, and that you accepted the GitHub invitation.");
|
|
1345
|
+
} else {
|
|
1346
|
+
ui.error(`No Egregore repo found under ${ui.bold(orgLogin)} that you can access.`);
|
|
1347
|
+
ui.info(`If you know it, pass the repo: ${ui.bold(joinCommand(orgLogin, "<repo>"))}. Otherwise ask the admin for the exact ${ui.bold("owner/repo")}.`);
|
|
1348
|
+
}
|
|
1270
1349
|
process.exit(1);
|
|
1271
1350
|
}
|
|
1351
|
+
|
|
1352
|
+
let repoName;
|
|
1353
|
+
let config;
|
|
1354
|
+
if (candidates.length === 1) {
|
|
1355
|
+
({ repo: repoName, config } = candidates[0]);
|
|
1356
|
+
s2.stop(`Found ${orgLogin}/${repoName}`);
|
|
1357
|
+
} else {
|
|
1358
|
+
s2.stop(`Found ${candidates.length} Egregores in ${orgLogin}`);
|
|
1359
|
+
const picked = await ui.choose(
|
|
1360
|
+
`Which Egregore in ${orgLogin}?`,
|
|
1361
|
+
candidates.map((c) => ({
|
|
1362
|
+
label: `${orgLogin}/${c.repo}`,
|
|
1363
|
+
description: c.config.org_name || "",
|
|
1364
|
+
_candidate: c,
|
|
1365
|
+
})),
|
|
1366
|
+
);
|
|
1367
|
+
({ repo: repoName, config } = picked._candidate);
|
|
1368
|
+
}
|
|
1272
1369
|
ui.success(`Found ${ui.bold(config.org_name || orgLogin)}`);
|
|
1273
1370
|
|
|
1274
1371
|
const orgName = config.org_name || orgLogin;
|
package/package.json
CHANGED