@nanocollective/roster 0.1.0-alpha.43 → 0.1.0-alpha.44
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/cli.js +237 -21
- package/docs/README.md +1 -1
- package/docs/commands.md +3 -2
- package/docs/concepts.md +33 -20
- package/docs/doctor-codes.md +2 -0
- package/docs/getting-started.md +5 -4
- package/docs/org-yaml.md +1 -0
- package/docs/portal.md +39 -6
- package/docs/session-workflow.md +14 -0
- package/docs/staff-yaml.md +16 -1
- package/package.json +18 -20
- package/templates/brain/.github/workflows/%%STAFF%%-daily.yaml +20 -1
- package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +35 -7
- package/templates/brain/staff.yaml +4 -1
- package/templates/ops/.github/workflows/session.yaml +86 -3
- package/templates/ops/compose.mjs +15 -0
- package/templates/ops/org/operating.md +25 -10
- package/templates/ops/prompts/daily.md +23 -4
- package/templates/ops/prompts/mention.md +19 -0
- package/templates/portal/css/home.css +57 -0
- package/templates/portal/index.html +8 -8
- package/templates/portal/js/api.js +3 -0
- package/templates/portal/js/app.js +5 -3
- package/templates/portal/js/homesort.js +154 -0
- package/templates/portal/js/readiness.js +35 -0
- package/templates/portal/js/refresh.js +9 -2
- package/templates/portal/js/state.js +2 -6
- package/templates/portal/js/views/brain.js +1 -1
- package/templates/portal/js/views/hire.js +10 -0
- package/templates/portal/js/views/home.js +391 -0
- package/templates/portal/js/views/inbox.js +2 -2
- package/templates/portal/js/views/setup.js +6 -2
- package/templates/portal/js/views/staff.js +14 -2
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
<link rel="stylesheet" href="/assets/css/markdown.css">
|
|
14
14
|
<link rel="stylesheet" href="/assets/css/brain.css">
|
|
15
15
|
<link rel="stylesheet" href="/assets/css/inbox.css">
|
|
16
|
+
<link rel="stylesheet" href="/assets/css/home.css">
|
|
16
17
|
<link rel="stylesheet" href="/assets/css/graph.css">
|
|
17
18
|
<link rel="stylesheet" href="/assets/css/diff.css">
|
|
18
19
|
<link rel="stylesheet" href="/assets/css/health.css">
|
|
@@ -30,18 +31,17 @@
|
|
|
30
31
|
<!-- Only while something is left to set up; app.js decides. -->
|
|
31
32
|
<button class="nav top" id="setupnav" data-view="setup" hidden>
|
|
32
33
|
<span class="ic" data-icon="task-done"></span> Getting started
|
|
34
|
+
<span class="n" id="setupcount"></span>
|
|
33
35
|
</button>
|
|
34
|
-
<button class="nav top" id="
|
|
35
|
-
<span class="ic" data-icon="inbox"></span>
|
|
36
|
+
<button class="nav top" id="homenav" data-view="home" title="What needs you, and who is working">
|
|
37
|
+
<span class="ic" data-icon="inbox"></span> Home
|
|
38
|
+
<span class="n" id="homecount"></span>
|
|
39
|
+
</button>
|
|
40
|
+
<button class="nav" id="inboxnav" data-view="inbox" title="Every open issue on each staff member's tracker">
|
|
41
|
+
<span class="ic" data-icon="issue-open"></span> Trackers
|
|
36
42
|
<span class="n" id="inboxcount"></span>
|
|
37
43
|
</button>
|
|
38
44
|
<div class="staffviews" id="inboxsub" hidden></div>
|
|
39
|
-
<!-- "Pending work", not "Pull requests": what is behind it is finished work waiting on you,
|
|
40
|
-
and the word for how it is delivered is not the word for what it is. -->
|
|
41
|
-
<button class="nav" id="prsnav" data-view="prs" title="Finished work waiting on you to merge">
|
|
42
|
-
<span class="ic" data-icon="merged"></span> Pending work
|
|
43
|
-
<span class="n" id="prcount"></span>
|
|
44
|
-
</button>
|
|
45
45
|
<button class="nav" id="runsnav" data-view="runs" title="What ran, how it ended, and what it cost">
|
|
46
46
|
<span class="ic" data-icon="play"></span> Runs
|
|
47
47
|
</button>
|
|
@@ -10,6 +10,9 @@ export const getSync = () => json("/api/sync").catch(() => null);
|
|
|
10
10
|
/** Every staff member's recent runs and 30-day spend. Online only; offline it says so. */
|
|
11
11
|
export const getRuns = (force) => json("/api/runs" + (force ? "?refresh=1" : ""));
|
|
12
12
|
|
|
13
|
+
/** Who is running now, what started it, and what finished in the last few hours. Never cached. */
|
|
14
|
+
export const getLive = () => json("/api/live");
|
|
15
|
+
|
|
13
16
|
/** The repos org.yaml lists. Off disk, so the new-issue form does not wait on the inbox. */
|
|
14
17
|
export const getRepos = () => json("/api/repos");
|
|
15
18
|
|
|
@@ -20,6 +20,7 @@ import { viewChanged } from "./views/changed.js";
|
|
|
20
20
|
import { viewDocs } from "./views/docs.js";
|
|
21
21
|
import { viewGraph } from "./views/graph.js";
|
|
22
22
|
import { viewHealth } from "./views/health.js";
|
|
23
|
+
import { viewHome } from "./views/home.js";
|
|
23
24
|
import { viewInbox, viewPrs } from "./views/inbox.js";
|
|
24
25
|
import { viewOrg } from "./views/org.js";
|
|
25
26
|
import { viewPrompt } from "./views/prompt.js";
|
|
@@ -30,6 +31,7 @@ import { viewStaff } from "./views/staff.js";
|
|
|
30
31
|
|
|
31
32
|
const SCREEN = {
|
|
32
33
|
setup: viewGettingStarted,
|
|
34
|
+
home: viewHome,
|
|
33
35
|
inbox: viewInbox,
|
|
34
36
|
prs: viewPrs,
|
|
35
37
|
runs: viewRuns,
|
|
@@ -57,8 +59,8 @@ const NEEDS = ["org", "name", "opsName", "staff"];
|
|
|
57
59
|
/** The org-wide screens in the sidebar, by button. */
|
|
58
60
|
const NAV = [
|
|
59
61
|
["#setupnav", "setup"],
|
|
62
|
+
["#homenav", "home"],
|
|
60
63
|
["#inboxnav", "inbox"],
|
|
61
|
-
["#prsnav", "prs"],
|
|
62
64
|
["#runsnav", "runs"],
|
|
63
65
|
["#staffnav", "staff"],
|
|
64
66
|
["#orgnav", "org"],
|
|
@@ -100,7 +102,7 @@ export async function boot() {
|
|
|
100
102
|
/* Every one of these reads a tenant, so during setup they answer 409 and the sidebar is a
|
|
101
103
|
row of four dead ends beside an empty Staff heading. There is one thing to do on this
|
|
102
104
|
screen; the navigation comes back with the org. */
|
|
103
|
-
for (const el of document.querySelectorAll("#
|
|
105
|
+
for (const el of document.querySelectorAll("#homenav, #inboxnav, #runsnav, #orgnav, #staffnav, #docsnav, #refreshall, .sect, #stafflist")) {
|
|
104
106
|
el.hidden = true;
|
|
105
107
|
}
|
|
106
108
|
for (const slot of document.querySelectorAll("[data-icon]")) {
|
|
@@ -274,10 +276,10 @@ function paintSidebar() {
|
|
|
274
276
|
el("span", { className: "hh", textContent: s.handle }),
|
|
275
277
|
);
|
|
276
278
|
whatsLeft(s).then((left) => {
|
|
279
|
+
paintSetupNav();
|
|
277
280
|
if (!left.length) return;
|
|
278
281
|
dot.hidden = false;
|
|
279
282
|
head.title = "Not ready to run: " + sentence(left);
|
|
280
|
-
paintSetupNav();
|
|
281
283
|
});
|
|
282
284
|
|
|
283
285
|
const views = el("div", { className: "staffviews" });
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/* Where every issue and pull request goes on Home.
|
|
2
|
+
*
|
|
3
|
+
* Every open item lands in exactly one place, so nothing is missed and nothing shows twice:
|
|
4
|
+
*
|
|
5
|
+
* needs an ask on a person, or a pull request ready to merge
|
|
6
|
+
* requests something a person asked a staff member for
|
|
7
|
+
* staff a staff member's own work: their status issue, a peer's ask, their own issues
|
|
8
|
+
* elsewhere draft pull requests, and product or ops issues nobody has put on a person
|
|
9
|
+
*
|
|
10
|
+
* Closed items only matter when a staff member closed them today: that is "Closed today", with
|
|
11
|
+
* Reopen beside each, because staff now close things without asking. */
|
|
12
|
+
|
|
13
|
+
/** The kinds an ask on a person carries. Older trackers used other words for a chore. */
|
|
14
|
+
const KINDS = ["decision", "review", "chore"];
|
|
15
|
+
const CHORE_WORDS = ["setup", "submit", "data", "blocked"];
|
|
16
|
+
|
|
17
|
+
const lower = (s) => String(s ?? "").toLowerCase();
|
|
18
|
+
|
|
19
|
+
/** How old a run report can be and still be this morning's. */
|
|
20
|
+
const REPORT_HOURS = 20;
|
|
21
|
+
|
|
22
|
+
/** A run report as lines, without the @mention it opens with. */
|
|
23
|
+
function reportLines(body) {
|
|
24
|
+
return String(body ?? "")
|
|
25
|
+
.split("\n")
|
|
26
|
+
.map((l) => l.replace(/^\s*(@[\w-]+\s*)+/, "").replace(/^[-*]\s+/, "").trim())
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
.slice(0, 5);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param items the inbox list: open and recently closed issues and pull requests
|
|
33
|
+
* @param staff S.data.staff: handle, name, brain, statusIssue, mention
|
|
34
|
+
* @param humans [{ github, marker }]
|
|
35
|
+
* @param live /api/live's staff list, or [] before it has answered
|
|
36
|
+
* @param now for "today", in the viewer's own day
|
|
37
|
+
*/
|
|
38
|
+
export function sortHome(items, staff, humans, live = [], now = new Date()) {
|
|
39
|
+
const people = new Set(humans.map((h) => lower(h.github)).filter(Boolean));
|
|
40
|
+
const markers = new Set(humans.map((h) => lower(h.marker)).filter(Boolean));
|
|
41
|
+
const byBrain = new Map(staff.filter((s) => s.brain).map((s) => [s.brain, s]));
|
|
42
|
+
const working = new Set();
|
|
43
|
+
for (const l of live) {
|
|
44
|
+
const s = staff.find((x) => x.handle === l.handle);
|
|
45
|
+
for (const r of l.running ?? []) if (s && r.issue) working.add(`${s.brain}#${r.issue}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const out = { reports: [], needs: [], requests: [], staff: [], elsewhere: [], closedToday: [] };
|
|
49
|
+
const today = dayOf(now);
|
|
50
|
+
|
|
51
|
+
for (const item of items) {
|
|
52
|
+
const owner = byBrain.get(item.repo) ?? null;
|
|
53
|
+
|
|
54
|
+
if (item.state !== "OPEN") {
|
|
55
|
+
if (
|
|
56
|
+
owner &&
|
|
57
|
+
item.kind === "issue" &&
|
|
58
|
+
item.closedAt &&
|
|
59
|
+
dayOf(new Date(item.closedAt)) === today &&
|
|
60
|
+
!people.has(lower(item.closedBy))
|
|
61
|
+
) {
|
|
62
|
+
out.closedToday.push({ item, staff: owner, reason: reasonOf(item, people) });
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The run report: the last word on a status issue, from the staff member, in the last day.
|
|
68
|
+
const c = item.lastComment;
|
|
69
|
+
if (
|
|
70
|
+
owner &&
|
|
71
|
+
item.number === owner.statusIssue &&
|
|
72
|
+
c &&
|
|
73
|
+
!people.has(lower(c.author)) &&
|
|
74
|
+
now - new Date(c.createdAt) < REPORT_HOURS * 3_600_000
|
|
75
|
+
) {
|
|
76
|
+
out.reports.push({ staff: owner, item, at: c.createdAt, lines: reportLines(c.body) });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const place = placeOf(item, owner, people, markers);
|
|
80
|
+
const entry = { item, staff: owner, ...place };
|
|
81
|
+
if (place.place === "requests") {
|
|
82
|
+
entry.status = working.has(`${item.repo}#${item.number}`)
|
|
83
|
+
? "working"
|
|
84
|
+
: item.lastComment && !people.has(lower(item.lastComment.author))
|
|
85
|
+
? "answered"
|
|
86
|
+
: "waiting";
|
|
87
|
+
}
|
|
88
|
+
out[place.place].push(entry);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const oldest = (a, b) => a.item.createdAt.localeCompare(b.item.createdAt);
|
|
92
|
+
out.needs.sort(oldest);
|
|
93
|
+
out.requests.sort((a, b) => b.item.updatedAt.localeCompare(a.item.updatedAt));
|
|
94
|
+
out.closedToday.sort((a, b) => b.item.closedAt.localeCompare(a.item.closedAt));
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Which place an open item belongs in, and for an ask, which kind it is. */
|
|
99
|
+
export function placeOf(item, owner, people, markers) {
|
|
100
|
+
if (item.kind === "pr") {
|
|
101
|
+
return item.draft ? { place: "elsewhere", why: "draft" } : { place: "needs", kind: "merge" };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const labels = (item.labels ?? []).map(lower);
|
|
105
|
+
const onPerson =
|
|
106
|
+
(item.assignees ?? []).some((a) => people.has(lower(a))) ||
|
|
107
|
+
labels.some((l) => markers.has(l));
|
|
108
|
+
const byPerson = people.has(lower(item.author));
|
|
109
|
+
|
|
110
|
+
if (!owner) {
|
|
111
|
+
return onPerson && !byPerson
|
|
112
|
+
? { place: "needs", kind: kindOf(labels) }
|
|
113
|
+
: { place: "elsewhere", why: item.role === "product" ? "contributor" : "other" };
|
|
114
|
+
}
|
|
115
|
+
if (item.number === owner.statusIssue || labels.includes("keep-open")) {
|
|
116
|
+
return { place: "staff", why: "status" };
|
|
117
|
+
}
|
|
118
|
+
if (byPerson) return { place: "requests" };
|
|
119
|
+
if (onPerson) return { place: "needs", kind: kindOf(labels) };
|
|
120
|
+
return {
|
|
121
|
+
place: "staff",
|
|
122
|
+
why: labels.some((l) => l.startsWith("from-")) ? "peer" : "own",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function kindOf(labels) {
|
|
127
|
+
const found = KINDS.find((k) => labels.includes(k));
|
|
128
|
+
if (found) return found;
|
|
129
|
+
return labels.some((l) => CHORE_WORDS.includes(l)) ? "chore" : "ask";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The one line a staff member closed it with: their last comment's first line. */
|
|
133
|
+
function reasonOf(item, people) {
|
|
134
|
+
const c = item.lastComment;
|
|
135
|
+
if (!c || people.has(lower(c.author))) return "";
|
|
136
|
+
const line = String(c.body ?? "")
|
|
137
|
+
.split("\n")
|
|
138
|
+
.map((l) => l.trim())
|
|
139
|
+
.find(Boolean);
|
|
140
|
+
return line ? line.replace(/^[#>*\s-]+/, "").slice(0, 160) : "";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** A date as the viewer's own calendar day. */
|
|
144
|
+
export function dayOf(d) {
|
|
145
|
+
return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Whole days from today to a YYYY-MM-DD date: 0 today, negative once it has passed. */
|
|
149
|
+
export function daysUntil(date, now = new Date()) {
|
|
150
|
+
const [y, m, d] = String(date).split("-").map(Number);
|
|
151
|
+
const due = new Date(y, m - 1, d);
|
|
152
|
+
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
153
|
+
return Math.round((due - start) / 86_400_000);
|
|
154
|
+
}
|
|
@@ -33,3 +33,38 @@ export function sentence(left) {
|
|
|
33
33
|
if (left.length < 2) return left.join("");
|
|
34
34
|
return left.slice(0, -1).join(", ") + " and " + left.at(-1);
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
/** Per staff member: hire, the GitHub App, the charter, the agent credential, a first run. */
|
|
38
|
+
const STAFF_STEPS = 5;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Setup as a count, from the same answers as everything above: the org's three steps (create
|
|
42
|
+
* it, business.md, priorities.md), then five per staff member. With nobody hired, the first
|
|
43
|
+
* hire's five are counted as still to do.
|
|
44
|
+
*
|
|
45
|
+
* Null until every staff member's readiness has answered. A count that starts high and drops
|
|
46
|
+
* when GitHub replies reads as steps coming undone.
|
|
47
|
+
*/
|
|
48
|
+
export function progress(
|
|
49
|
+
unfinished = S.data?.unfinished ?? [],
|
|
50
|
+
staff = S.data?.staff ?? [],
|
|
51
|
+
readiness = S.readiness ?? {},
|
|
52
|
+
) {
|
|
53
|
+
const orgSteps = 3;
|
|
54
|
+
let done = orgSteps - unfinished.filter((id) => id !== "hire").length;
|
|
55
|
+
let total = orgSteps;
|
|
56
|
+
if (!staff.length) return { done, total: total + STAFF_STEPS };
|
|
57
|
+
for (const s of staff) {
|
|
58
|
+
const left = readiness[s.handle];
|
|
59
|
+
if (!left) return null;
|
|
60
|
+
total += STAFF_STEPS;
|
|
61
|
+
done += STAFF_STEPS - left.length;
|
|
62
|
+
}
|
|
63
|
+
return { done, total };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** One staff member's own count, or null before their readiness is known. */
|
|
67
|
+
export function staffProgress(handle, readiness = S.readiness ?? {}) {
|
|
68
|
+
const left = readiness[handle];
|
|
69
|
+
return left ? { done: STAFF_STEPS - left.length, total: STAFF_STEPS } : null;
|
|
70
|
+
}
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
import { getInbox, getOrg, getSync } from "./api.js";
|
|
11
11
|
import { $, ago, el, esc } from "./dom.js";
|
|
12
12
|
import { render } from "./router.js";
|
|
13
|
-
import {
|
|
13
|
+
import { sortHome } from "./homesort.js";
|
|
14
|
+
import { humansOf, openCount, S, unreadCount } from "./state.js";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* The inbox, fetched at most once at a time.
|
|
@@ -53,8 +54,8 @@ export function ensureInbox(force) {
|
|
|
53
54
|
export function stampCounts() {
|
|
54
55
|
const waiting = !S.inbox;
|
|
55
56
|
for (const [sel, n] of [
|
|
57
|
+
["#homecount", needsCount],
|
|
56
58
|
["#inboxcount", openCount],
|
|
57
|
-
["#prcount", openPrCount],
|
|
58
59
|
["#unreadcount", unreadCount],
|
|
59
60
|
]) {
|
|
60
61
|
const slot = document.querySelector(sel);
|
|
@@ -71,6 +72,12 @@ export function stampCounts() {
|
|
|
71
72
|
* which in practice meant "after you visit the Inbox". A sidebar that says nothing until you
|
|
72
73
|
* look at it is not a sidebar.
|
|
73
74
|
*/
|
|
75
|
+
/** What Home's "Needs you" holds, for the sidebar. */
|
|
76
|
+
function needsCount() {
|
|
77
|
+
if (!S.inbox || !S.data) return 0;
|
|
78
|
+
return sortHome(S.inbox.items ?? [], S.data.staff, humansOf(), S.live?.staff ?? []).needs.length;
|
|
79
|
+
}
|
|
80
|
+
|
|
74
81
|
export function countInBackground() {
|
|
75
82
|
stampCounts();
|
|
76
83
|
return ensureInbox(false)
|
|
@@ -22,7 +22,7 @@ export const VIEWS = [
|
|
|
22
22
|
export const VIEW_ALIAS = { memory: "brain" };
|
|
23
23
|
|
|
24
24
|
/** Screens that belong to the org rather than to one staff member. */
|
|
25
|
-
export const ORG_WIDE = new Set(["setup", "inbox", "prs", "runs", "org", "staff", "docs"]);
|
|
25
|
+
export const ORG_WIDE = new Set(["setup", "home", "inbox", "prs", "runs", "org", "staff", "docs"]);
|
|
26
26
|
|
|
27
27
|
/** The two screens that read the inbox, and so share its filters and its `?t=` thread. */
|
|
28
28
|
const INBOXY = new Set(["inbox", "prs"]);
|
|
@@ -44,7 +44,7 @@ export const S = {
|
|
|
44
44
|
staffHandle: null,
|
|
45
45
|
/* What is waiting on you, not whose brain you read last. The org-wide screens are where a
|
|
46
46
|
session starts, which is also why the Staff list below them opens folded. */
|
|
47
|
-
view: "
|
|
47
|
+
view: "home",
|
|
48
48
|
/** The inbox filter box. Brain and "what changed" keep their own, below. */
|
|
49
49
|
query: "",
|
|
50
50
|
|
|
@@ -90,10 +90,6 @@ export const openCount = () =>
|
|
|
90
90
|
export const unreadCount = () =>
|
|
91
91
|
(S.inbox?.items ?? []).filter((i) => i.unread && i.state === "OPEN" && i.kind !== "pr").length;
|
|
92
92
|
|
|
93
|
-
/** The same, for the screen that is only pull requests. */
|
|
94
|
-
export const openPrCount = () =>
|
|
95
|
-
(S.inbox?.items ?? []).filter((i) => i.state === "OPEN" && i.kind === "pr").length;
|
|
96
|
-
|
|
97
93
|
export const staff = () => S.data.staff.find((s) => s.handle === S.staffHandle) ?? S.data.staff[0];
|
|
98
94
|
|
|
99
95
|
/**
|
|
@@ -150,7 +150,7 @@ export function viewBrain(m) {
|
|
|
150
150
|
const fileHits = plain.filter((f) => !q || f.path.toLowerCase().includes(q));
|
|
151
151
|
|
|
152
152
|
/* ---- memory ---- */
|
|
153
|
-
const mem = group("
|
|
153
|
+
const mem = group("What they know", s.facts.length + " facts");
|
|
154
154
|
if (q) {
|
|
155
155
|
for (const f of hits.slice(0, 200)) mem.append(row("fact:" + f.slug, f.slug, "", "tmem"));
|
|
156
156
|
if (!hits.length) {
|
|
@@ -353,6 +353,16 @@ function hireStep(host, role, onHired) {
|
|
|
353
353
|
app = field("app", org + "-" + role.handle, "This staff member's GitHub App. App names are unique across GitHub.");
|
|
354
354
|
const pub = field("public app", org + "-robot", "The App all staff share for public product repos.");
|
|
355
355
|
fields.push(["app", app], ["publicApp", pub]);
|
|
356
|
+
/* Only shown when a product repo is public, which is the only time the shared App is
|
|
357
|
+
created. Hidden, it still sends the same default, so nothing changes underneath. */
|
|
358
|
+
pub.row.hidden = true;
|
|
359
|
+
getRepos()
|
|
360
|
+
.then((r) => {
|
|
361
|
+
pub.row.hidden = !(r.repos ?? []).some(
|
|
362
|
+
(x) => x.role === "product" && String(x.visibility ?? "").toLowerCase() !== "private",
|
|
363
|
+
);
|
|
364
|
+
})
|
|
365
|
+
.catch(() => {});
|
|
356
366
|
}
|
|
357
367
|
|
|
358
368
|
const advanced = el("div", { className: "hireadvanced", hidden: true }, [
|