@nanocollective/roster 0.1.0-alpha.24 → 0.1.0-alpha.26
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
CHANGED
|
@@ -140,7 +140,7 @@ async function createApp(spec2, opts = {}) {
|
|
|
140
140
|
res.end(
|
|
141
141
|
page(
|
|
142
142
|
`${app.slug} created`,
|
|
143
|
-
"
|
|
143
|
+
"Its keys are saved in the repository's secrets. You can close this tab; the terminal has the next step."
|
|
144
144
|
)
|
|
145
145
|
);
|
|
146
146
|
finish(() => resolve6(app));
|
|
@@ -210,7 +210,8 @@ async function open(url) {
|
|
|
210
210
|
// src/lib/install.ts
|
|
211
211
|
import { existsSync, readFileSync } from "fs";
|
|
212
212
|
import { join } from "path";
|
|
213
|
-
function installTargets(ws, org, parseYaml, handle, spec2) {
|
|
213
|
+
function installTargets(ws, org, parseYaml, handle, spec2, scope = "private") {
|
|
214
|
+
if (scope === "public") return [...new Set(spec2.worksIn)];
|
|
214
215
|
const peers = [];
|
|
215
216
|
for (const s of org.staff ?? []) {
|
|
216
217
|
if (s.handle === handle) continue;
|
|
@@ -222,7 +223,8 @@ function installTargets(ws, org, parseYaml, handle, spec2) {
|
|
|
222
223
|
} catch {
|
|
223
224
|
}
|
|
224
225
|
}
|
|
225
|
-
|
|
226
|
+
const ops = org.org ? [`${org.org}/${ws.opsName}`] : [];
|
|
227
|
+
return [.../* @__PURE__ */ new Set([spec2.brain, ...ops, ...spec2.worksIn, ...peers])];
|
|
226
228
|
}
|
|
227
229
|
function installUrl(slug, targetId, repoIds = []) {
|
|
228
230
|
const base2 = `https://github.com/apps/${encodeURIComponent(slug)}/installations/new`;
|
|
@@ -538,7 +540,14 @@ async function appCommand(argv) {
|
|
|
538
540
|
`);
|
|
539
541
|
return 2;
|
|
540
542
|
}
|
|
541
|
-
const targets = installTargets(
|
|
543
|
+
const targets = installTargets(
|
|
544
|
+
ws,
|
|
545
|
+
org,
|
|
546
|
+
parseYaml,
|
|
547
|
+
handle,
|
|
548
|
+
spec2,
|
|
549
|
+
opts.public ? "public" : "private"
|
|
550
|
+
);
|
|
542
551
|
const existing = await api(`/apps/${name}`);
|
|
543
552
|
if (existing.ok) {
|
|
544
553
|
const install2 = await preselectedInstall(existing.data?.slug ?? name, org.org, targets);
|
|
@@ -6480,7 +6489,14 @@ async function portalCommand(argv) {
|
|
|
6480
6489
|
res.end(JSON.stringify({ error: `staff.yaml declares no ${payload.scope} app` }));
|
|
6481
6490
|
return;
|
|
6482
6491
|
}
|
|
6483
|
-
const targets = installTargets(
|
|
6492
|
+
const targets = installTargets(
|
|
6493
|
+
ws,
|
|
6494
|
+
org,
|
|
6495
|
+
parseYaml,
|
|
6496
|
+
entry.handle,
|
|
6497
|
+
spec2,
|
|
6498
|
+
isPublic ? "public" : "private"
|
|
6499
|
+
);
|
|
6484
6500
|
const existing = await api(`/apps/${name}`);
|
|
6485
6501
|
if (existing.ok) {
|
|
6486
6502
|
const install = await preselectedInstall(existing.data?.slug ?? name, org.org, targets);
|
|
@@ -6817,7 +6833,7 @@ async function portalCommand(argv) {
|
|
|
6817
6833
|
});
|
|
6818
6834
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
6819
6835
|
res.end(
|
|
6820
|
-
`<body style="font:15px/1.6 system-ui;max-width:34rem;margin:14vh auto;padding:0 1.4rem"><h1 style="font-size:1.3rem">${app.slug} created</h1><p>Its
|
|
6836
|
+
`<body style="font:15px/1.6 system-ui;max-width:34rem;margin:14vh auto;padding:0 1.4rem"><h1 style="font-size:1.3rem">${app.slug} created</h1><p>Its keys are saved in ${pending.brain}'s secrets.</p><p><b>Next: install it.</b> Go back to the portal and press Install.</p><p>You can close this tab.</p></body>`
|
|
6821
6837
|
);
|
|
6822
6838
|
}).catch((err) => {
|
|
6823
6839
|
appResults.set(state, { ok: false, error: err.message });
|
package/docs/manual-steps.md
CHANGED
|
@@ -45,8 +45,9 @@ hand. roster says so if it happens.
|
|
|
45
45
|
## 3. Confirm the App's install
|
|
46
46
|
|
|
47
47
|
**Do:** press **Install it** in the portal, or open the link `roster app` prints. The page opens
|
|
48
|
-
with the organisation and the repos this staff member needs already selected: its brain,
|
|
49
|
-
peer's tracker, and the product repos. Check the
|
|
48
|
+
with the organisation and the repos this staff member needs already selected: its brain, the
|
|
49
|
+
ops repo (every run checks it out first), each peer's tracker, and the product repos. Check the
|
|
50
|
+
list and confirm. Choosing **All repositories** instead also works.
|
|
50
51
|
|
|
51
52
|
**Why not automated:** installing is a grant of access to specific repositories, and GitHub asks
|
|
52
53
|
a person to confirm it. That is correct and should not be worked around.
|
package/package.json
CHANGED
|
@@ -276,3 +276,12 @@ body[data-lightbox] {overflow:hidden}
|
|
|
276
276
|
|
|
277
277
|
/* And the affordance on the document side: an image you can open should say so. */
|
|
278
278
|
.md img,img.zoomable{cursor:zoom-in}
|
|
279
|
+
|
|
280
|
+
/* ---------- a dialog for a whole task (sheet() in dialog.js) ---------- */
|
|
281
|
+
dialog.sheet{border:1px solid var(--border-strong);border-radius:var(--r-lg);background:var(--bg);
|
|
282
|
+
color:var(--ink);padding:0;width:min(860px,calc(100vw - 32px));max-height:calc(100vh - 48px);
|
|
283
|
+
box-shadow:var(--shadow-lg)}
|
|
284
|
+
dialog.sheet[open]{animation:dlgin .18s var(--ease)}
|
|
285
|
+
dialog.sheet::backdrop{background:rgba(0,0,0,.35);backdrop-filter:blur(8px)}
|
|
286
|
+
.sheetbody{padding:22px 24px 26px;overflow:auto;max-height:calc(100vh - 50px)}
|
|
287
|
+
.sheettitle{margin:0 0 14px;font:600 18px var(--sans);letter-spacing:-.02em}
|
|
@@ -158,3 +158,38 @@ export function askYes({ title, hint, confirm = "Continue" }) {
|
|
|
158
158
|
cancel.focus();
|
|
159
159
|
});
|
|
160
160
|
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* A larger dialog for a whole task: hiring someone, or finishing their setup. Native <dialog>
|
|
164
|
+
* again, appended to <body> rather than to the screen, so a background refresh repainting the
|
|
165
|
+
* screen leaves it open with whatever was typed into it.
|
|
166
|
+
*
|
|
167
|
+
* @returns {{set: (node: Node) => void, close: () => void, isOpen: () => boolean} | null}
|
|
168
|
+
* null where there is no dialog element (the test shim), and the caller draws inline instead.
|
|
169
|
+
*/
|
|
170
|
+
export function sheet({ node, onClose }) {
|
|
171
|
+
const box = document.createElement("dialog");
|
|
172
|
+
if (typeof box.showModal !== "function") return null;
|
|
173
|
+
box.className = "sheet";
|
|
174
|
+
const body = el("div", { className: "sheetbody" });
|
|
175
|
+
body.append(node);
|
|
176
|
+
box.append(body);
|
|
177
|
+
let open = true;
|
|
178
|
+
const close = () => {
|
|
179
|
+
if (!open) return;
|
|
180
|
+
open = false;
|
|
181
|
+
box.close();
|
|
182
|
+
box.remove();
|
|
183
|
+
onClose?.();
|
|
184
|
+
};
|
|
185
|
+
box.addEventListener("cancel", (e) => {
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
close();
|
|
188
|
+
});
|
|
189
|
+
box.addEventListener("click", (e) => {
|
|
190
|
+
if (onBackdrop(e, box.getBoundingClientRect())) close();
|
|
191
|
+
});
|
|
192
|
+
document.body.append(box);
|
|
193
|
+
box.showModal();
|
|
194
|
+
return { set: (n) => body.replaceChildren(n), close, isOpen: () => open };
|
|
195
|
+
}
|
|
@@ -27,8 +27,7 @@ export function appPanel(opts) {
|
|
|
27
27
|
}),
|
|
28
28
|
el("p", {
|
|
29
29
|
textContent:
|
|
30
|
-
"A tab opens
|
|
31
|
-
"into the repo's secrets. The key is held in memory and never written to disk.",
|
|
30
|
+
"A GitHub tab opens. Confirm there, then come back here to install it.",
|
|
32
31
|
}),
|
|
33
32
|
);
|
|
34
33
|
|
|
@@ -6,15 +6,15 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { getStaffProgress, post } from "../api.js";
|
|
9
|
-
import { askYes } from "../dialog.js";
|
|
9
|
+
import { askYes, sheet } from "../dialog.js";
|
|
10
10
|
import { ago, el, esc } from "../dom.js";
|
|
11
11
|
import { refreshAll } from "../refresh.js";
|
|
12
12
|
import { S } from "../state.js";
|
|
13
|
-
import { appPanel } from "./app.js";
|
|
14
|
-
import { credentialPanel } from "./credential.js";
|
|
15
13
|
import { hireFlow, line, rolePicker } from "./hire.js";
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
|
|
15
|
+
/* The open dialog, if any. It lives on <body>, so it outlives a repaint of this screen, and a
|
|
16
|
+
background refresh leaves whatever was typed into it alone. */
|
|
17
|
+
let current = null;
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* @param {{open?: string, result?: object}} [o] `open` keeps one staff member's steps on
|
|
@@ -27,24 +27,52 @@ export function viewStaff(m, o = {}) {
|
|
|
27
27
|
|
|
28
28
|
const pane = el("div", { className: "hirepane" });
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const cancel = el("button", { className: "ghbtn", textContent: "Cancel" });
|
|
37
|
-
cancel.onclick = () => pane.replaceChildren();
|
|
38
|
-
pane.append(el("div", { className: "row" }, [cancel]));
|
|
30
|
+
/* Tasks open in a dialog. Where there is no dialog element (the test shim) they draw into
|
|
31
|
+
the pane instead, which is how this screen always drew them. */
|
|
32
|
+
function present(node) {
|
|
33
|
+
if (current?.isOpen()) {
|
|
34
|
+
current.set(node);
|
|
35
|
+
return;
|
|
39
36
|
}
|
|
40
|
-
|
|
37
|
+
current = sheet({
|
|
38
|
+
node,
|
|
39
|
+
onClose: () => {
|
|
40
|
+
current = null;
|
|
41
|
+
S.staffOpen = null;
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
if (!current) pane.replaceChildren(node);
|
|
45
|
+
}
|
|
46
|
+
function dismiss() {
|
|
47
|
+
if (current) current.close();
|
|
48
|
+
else pane.replaceChildren();
|
|
49
|
+
S.staffOpen = null;
|
|
50
|
+
if (empty) pick();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// With nobody hired the roles are the page. After that they are a dialog behind Hire someone.
|
|
54
|
+
function pick() {
|
|
55
|
+
if (empty) {
|
|
56
|
+
pane.replaceChildren(rolePicker({ onPick: (role) => open(role) }));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const cancel = el("button", { className: "ghbtn", textContent: "Cancel" });
|
|
60
|
+
cancel.onclick = dismiss;
|
|
61
|
+
present(
|
|
62
|
+
el("div", {}, [
|
|
63
|
+
el("h2", { className: "sheettitle", textContent: "Pick a role" }),
|
|
64
|
+
rolePicker({ onPick: (role) => open(role) }),
|
|
65
|
+
el("div", { className: "row", style: "margin-top:14px" }, [cancel]),
|
|
66
|
+
]),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
41
69
|
|
|
42
70
|
/* One staff member's steps, hired or not yet. A hire repaints the whole screen, so the new
|
|
43
71
|
card is in the list and the steps reopen on the same person. */
|
|
44
72
|
function open(role, result) {
|
|
45
73
|
// Remembered, so a refresh after a trip to GitHub (creating the App) reopens it.
|
|
46
74
|
S.staffOpen = role;
|
|
47
|
-
|
|
75
|
+
present(
|
|
48
76
|
hireFlow({
|
|
49
77
|
role,
|
|
50
78
|
result,
|
|
@@ -52,14 +80,9 @@ export function viewStaff(m, o = {}) {
|
|
|
52
80
|
m.replaceChildren();
|
|
53
81
|
viewStaff(m, { open: handle, result: r });
|
|
54
82
|
},
|
|
55
|
-
onClose:
|
|
56
|
-
S.staffOpen = null;
|
|
57
|
-
if (empty) pick();
|
|
58
|
-
else pane.replaceChildren();
|
|
59
|
-
},
|
|
83
|
+
onClose: dismiss,
|
|
60
84
|
}),
|
|
61
85
|
);
|
|
62
|
-
pane.scrollIntoView?.({ behavior: "smooth", block: "start" });
|
|
63
86
|
}
|
|
64
87
|
|
|
65
88
|
if (!empty) {
|
|
@@ -75,9 +98,10 @@ export function viewStaff(m, o = {}) {
|
|
|
75
98
|
const opened = o.open && S.data.staff.find((s) => s.handle === o.open);
|
|
76
99
|
const kept = !opened && S.staffOpen;
|
|
77
100
|
const keptStaff = kept && S.data.staff.find((s) => s.handle === kept.handle);
|
|
101
|
+
if (empty) pick();
|
|
78
102
|
if (opened) open(roleOf(opened), o.result);
|
|
79
|
-
|
|
80
|
-
else if (
|
|
103
|
+
// An open dialog is still open; only the inline fallback has to be drawn again.
|
|
104
|
+
else if (kept && !current?.isOpen()) open(keptStaff ? roleOf(keptStaff) : kept);
|
|
81
105
|
|
|
82
106
|
/* --------------------------- one staff member --------------------------- */
|
|
83
107
|
|
|
@@ -93,73 +117,32 @@ export function viewStaff(m, o = {}) {
|
|
|
93
117
|
|
|
94
118
|
const status = el("span", { className: "meta" });
|
|
95
119
|
const go = el("button", { className: "ghbtn", textContent: "Retire" });
|
|
96
|
-
go.onclick = () =>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
it refers to already inside it and somewhere to put the answer. */
|
|
101
|
-
const write = el("button", { className: "ghbtn", textContent: "Write the charter" });
|
|
102
|
-
write.onclick = () => {
|
|
103
|
-
pane.replaceChildren(
|
|
104
|
-
el("div", { className: "card" }, [
|
|
105
|
-
paste({
|
|
106
|
-
kind: "charter",
|
|
107
|
-
staff: s.handle,
|
|
108
|
-
title: "Write " + s.name + "'s charter with your own AI",
|
|
109
|
-
onSaved: () => refreshAll(false),
|
|
110
|
-
}),
|
|
111
|
-
]),
|
|
112
|
-
);
|
|
113
|
-
pane.scrollIntoView?.({ behavior: "smooth", block: "start" });
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
/* The step `hire` has always had to hand back: there is no API that creates a GitHub App,
|
|
117
|
-
so it is a browser hand-off either way. It may as well be this browser. */
|
|
118
|
-
const app = el("button", { className: "ghbtn", textContent: "GitHub App" });
|
|
119
|
-
app.onclick = () => {
|
|
120
|
-
pane.replaceChildren(
|
|
121
|
-
el("div", { className: "card" }, [
|
|
122
|
-
el("h3", { textContent: s.name + "'s identity" }),
|
|
123
|
-
appPanel({ staff: s.handle, name: s.name, scope: "private" }),
|
|
124
|
-
appPanel({ staff: s.handle, name: s.name, scope: "public" }),
|
|
125
|
-
]),
|
|
126
|
-
);
|
|
127
|
-
pane.scrollIntoView?.({ behavior: "smooth", block: "start" });
|
|
120
|
+
go.onclick = () => {
|
|
121
|
+
const host = el("div");
|
|
122
|
+
present(host);
|
|
123
|
+
retireForm(host, s, status);
|
|
128
124
|
};
|
|
129
125
|
|
|
130
|
-
/*
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
pane.replaceChildren(el("div", { className: "card" }, [credentialPanel()]));
|
|
135
|
-
pane.scrollIntoView?.({ behavior: "smooth", block: "start" });
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const run = el("button", { className: "ghbtn", textContent: "Run once now" });
|
|
139
|
-
run.onclick = () => {
|
|
140
|
-
pane.replaceChildren(
|
|
141
|
-
el("div", { className: "card" }, [
|
|
142
|
-
runOnce({ staff: s.handle, name: s.name, onDone: (ok) => ok && refreshAll(true) }),
|
|
143
|
-
]),
|
|
144
|
-
);
|
|
145
|
-
pane.scrollIntoView?.({ behavior: "smooth", block: "start" });
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
/* Whether setup is finished, as far as can be told cheaply: the charter from disk at once,
|
|
149
|
-
and the App's secrets and a first run from GitHub when it answers. */
|
|
150
|
-
const finish = el("button", { className: "ghbtn primary", textContent: "Finish setting up", hidden: true });
|
|
126
|
+
/* Every setup step (the App, the charter, the credential, a run) is in the one list, so the
|
|
127
|
+
card has one way in. It says Finish setting up until setup looks finished: the charter
|
|
128
|
+
from disk at once, and the App's secrets and a first run from GitHub when it answers. */
|
|
129
|
+
const finish = el("button", { className: "ghbtn", textContent: "Set up" });
|
|
151
130
|
finish.onclick = () => open(roleOf(s));
|
|
152
|
-
|
|
131
|
+
const unfinished = () => {
|
|
132
|
+
finish.textContent = "Finish setting up";
|
|
133
|
+
finish.className = "ghbtn primary";
|
|
134
|
+
};
|
|
135
|
+
if (s.rig?.charterStub) unfinished();
|
|
153
136
|
else {
|
|
154
137
|
getStaffProgress(s.handle)
|
|
155
138
|
.then((p) => {
|
|
156
|
-
if (p.app === false || p.ran === false)
|
|
139
|
+
if (p.app === false || p.ran === false) unfinished();
|
|
157
140
|
})
|
|
158
141
|
.catch(() => {});
|
|
159
142
|
}
|
|
160
143
|
|
|
161
144
|
d.append(
|
|
162
|
-
el("div", { className: "row", style: "margin-top:10px" }, [finish,
|
|
145
|
+
el("div", { className: "row", style: "margin-top:10px" }, [finish, go, status]),
|
|
163
146
|
);
|
|
164
147
|
return d;
|
|
165
148
|
}
|
|
@@ -206,7 +189,7 @@ export function viewStaff(m, o = {}) {
|
|
|
206
189
|
const status = el("span", { className: "meta" });
|
|
207
190
|
const go = el("button", { className: "ghbtn", textContent: "Retire " + plan.handle });
|
|
208
191
|
const cancel = el("button", { className: "ghbtn", textContent: "Cancel" });
|
|
209
|
-
cancel.onclick =
|
|
192
|
+
cancel.onclick = dismiss;
|
|
210
193
|
go.onclick = () =>
|
|
211
194
|
apply(
|
|
212
195
|
{ action: "retire", handle: plan.handle },
|