@nanocollective/roster 0.1.0-alpha.1 → 0.1.0-alpha.11
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/README.md +65 -84
- package/dist/cli.js +4687 -2495
- package/docs/README.md +19 -11
- package/docs/agents.md +328 -13
- package/docs/architecture.md +13 -5
- package/docs/charters/cmo.md +69 -0
- package/docs/charters/cto.md +71 -0
- package/docs/charters/support.md +60 -0
- package/docs/commands.md +90 -11
- package/docs/concepts.md +64 -12
- package/docs/cost.md +39 -3
- package/docs/developing.md +16 -21
- package/docs/doctor-codes.md +21 -6
- package/docs/export.md +2 -1
- package/docs/extending.md +13 -4
- package/docs/getting-started.md +118 -80
- package/docs/images/brain.jpg +0 -0
- package/docs/images/org.jpg +0 -0
- package/docs/images/prompt.jpg +0 -0
- package/docs/images/setup-org.jpg +0 -0
- package/docs/images/setup-plan.jpg +0 -0
- package/docs/images/staff.jpg +0 -0
- package/docs/manual-steps.md +94 -101
- package/docs/memory.md +29 -8
- package/docs/org-yaml.md +74 -11
- package/docs/portal.md +261 -47
- package/docs/prompts.md +71 -11
- package/docs/security.md +43 -7
- package/docs/session-workflow.md +51 -21
- package/docs/staff-yaml.md +16 -7
- package/docs/troubleshooting.md +23 -20
- package/docs/upgrading.md +6 -0
- package/docs/writing-a-charter.md +33 -17
- package/package.json +1 -1
- package/templates/brain/.github/workflows/%%STAFF%%-daily.yaml +7 -0
- package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +16 -4
- package/templates/brain/CHARTER.md +3 -3
- package/templates/brain/README.md +1 -0
- package/templates/brain/log/decisions.md +3 -0
- package/templates/brain/staff.yaml +0 -1
- package/templates/brain/strategy/ideas.md +7 -0
- package/templates/ops/.github/workflows/session.yaml +117 -40
- package/templates/ops/agents.mjs +127 -8
- package/templates/ops/compose.mjs +77 -7
- package/templates/ops/inflight.mjs +157 -0
- package/templates/ops/org/operating.md +21 -7
- package/templates/ops/org/voice.md +9 -0
- package/templates/ops/prompts/_identity.md +8 -1
- package/templates/ops/prompts/_inflight.md +14 -0
- package/templates/ops/prompts/_paths.md +2 -1
- package/templates/ops/prompts/daily.md +16 -7
- package/templates/ops/prompts/mention.md +18 -2
- package/templates/ops/run-record.mjs +144 -0
- package/templates/portal/css/base.css +238 -64
- package/templates/portal/css/brain.css +30 -20
- package/templates/portal/css/diff.css +15 -10
- package/templates/portal/css/graph.css +12 -7
- package/templates/portal/css/health.css +32 -11
- package/templates/portal/css/inbox.css +117 -14
- package/templates/portal/css/layout.css +92 -41
- package/templates/portal/css/markdown.css +38 -15
- package/templates/portal/css/runs.css +13 -0
- package/templates/portal/css/setup.css +52 -39
- package/templates/portal/index.html +24 -3
- package/templates/portal/js/api.js +65 -4
- package/templates/portal/js/app.js +112 -12
- package/templates/portal/js/dialog.js +94 -4
- package/templates/portal/js/dom.js +25 -0
- package/templates/portal/js/icons.js +8 -1
- package/templates/portal/js/lightbox.js +273 -0
- package/templates/portal/js/md.js +23 -6
- package/templates/portal/js/mention.js +264 -0
- package/templates/portal/js/refresh.js +136 -6
- package/templates/portal/js/state.js +55 -8
- package/templates/portal/js/views/app.js +23 -5
- package/templates/portal/js/views/checklist.js +20 -7
- package/templates/portal/js/views/credential.js +93 -0
- package/templates/portal/js/views/docs.js +94 -4
- package/templates/portal/js/views/files.js +58 -14
- package/templates/portal/js/views/graph.js +1 -1
- package/templates/portal/js/views/health.js +178 -37
- package/templates/portal/js/views/inbox.js +938 -98
- package/templates/portal/js/views/memory.js +16 -1
- package/templates/portal/js/views/org.js +161 -61
- package/templates/portal/js/views/paste.js +29 -0
- package/templates/portal/js/views/prompt.js +57 -66
- package/templates/portal/js/views/repos.js +12 -7
- package/templates/portal/js/views/runonce.js +94 -0
- package/templates/portal/js/views/runs.js +165 -0
- package/templates/portal/js/views/setup.js +302 -56
- package/templates/portal/js/views/staff.js +143 -22
- package/templates/portal/js/yaml.js +134 -0
- package/templates/brain/.github/workflows/%%STAFF%%-pr-mention.yaml +0 -50
- package/templates/ops/prompts/pr-mention.md +0 -57
|
@@ -5,28 +5,124 @@
|
|
|
5
5
|
* is rather than as it was left. Setup cannot be finished in one sitting, so it must survive
|
|
6
6
|
* being abandoned halfway. */
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
8
|
+
import {
|
|
9
|
+
checkOrg,
|
|
10
|
+
createTenant,
|
|
11
|
+
fileUrl,
|
|
12
|
+
getAccess,
|
|
13
|
+
getOrg,
|
|
14
|
+
getSetup,
|
|
15
|
+
joinOrg,
|
|
16
|
+
planTenant,
|
|
17
|
+
saveFile,
|
|
18
|
+
setAccess,
|
|
19
|
+
} from "../api.js";
|
|
20
|
+
import { $, el, esc, grow, toClipboard } from "../dom.js";
|
|
21
|
+
import { go } from "../router.js";
|
|
22
|
+
import { S as App } from "../state.js";
|
|
10
23
|
import { checklist } from "./checklist.js";
|
|
24
|
+
import { credentialPanel } from "./credential.js";
|
|
11
25
|
import { repoPicker } from "./repos.js";
|
|
12
26
|
import { paste } from "./paste.js";
|
|
13
27
|
|
|
14
28
|
let S = null; // /api/setup/status, refreshed after anything that changes the world
|
|
15
29
|
|
|
30
|
+
/* Where the doctor checklist is drawn on this screen, so a save can re-run it. Left stale, it
|
|
31
|
+
went on saying business.stub after business.md had been saved, until "Check again". */
|
|
32
|
+
let healthHost = null;
|
|
33
|
+
|
|
34
|
+
/* Each "what is left" step that is a file, by id, so a save can mark it done in place rather
|
|
35
|
+
than repainting the page and throwing away the diff somebody is still reading. */
|
|
36
|
+
const fileSteps = new Map();
|
|
37
|
+
|
|
16
38
|
export async function viewSetup(main) {
|
|
17
39
|
S = await getSetup();
|
|
18
40
|
render(main);
|
|
19
41
|
}
|
|
20
42
|
|
|
43
|
+
/**
|
|
44
|
+
* The same steps once a tenant exists, as their own screen in the portal.
|
|
45
|
+
*
|
|
46
|
+
* Setup used to be reachable only before there was a tenant, so the reload that followed
|
|
47
|
+
* creating one landed on an empty Inbox with the repo picker, the credential and both files
|
|
48
|
+
* gone. The sidebar offers this while anything is unfinished, and an org nobody has been hired
|
|
49
|
+
* into opens on it.
|
|
50
|
+
*/
|
|
51
|
+
export async function viewGettingStarted(main) {
|
|
52
|
+
main.append(el("h1", { textContent: "Getting started" }));
|
|
53
|
+
const sub = el("p", { className: "sub", textContent: "Reading what is left…" });
|
|
54
|
+
main.append(sub);
|
|
55
|
+
S = await getSetup();
|
|
56
|
+
// Somebody may have clicked elsewhere while GitHub was answering.
|
|
57
|
+
if (App.view !== "setup") return;
|
|
58
|
+
sub.textContent =
|
|
59
|
+
"What is still to do before " + (App.data?.name ?? "this org") + " runs well. Each step " +
|
|
60
|
+
"reads the world rather than remembering a click, so it is safe to leave and come back.";
|
|
61
|
+
|
|
62
|
+
if (!App.data?.staff?.length) {
|
|
63
|
+
const next = step(1, "Hire your first staff member", false);
|
|
64
|
+
const hire = el("button", { className: "btn primary", textContent: "Hire someone" });
|
|
65
|
+
hire.onclick = () => go({ view: "staff" });
|
|
66
|
+
next.append(
|
|
67
|
+
el("p", {
|
|
68
|
+
textContent:
|
|
69
|
+
"Nothing runs until somebody is hired. Hiring creates their repo, their workflows and " +
|
|
70
|
+
"their pinned issue; the GitHub App and the charter follow on their card.",
|
|
71
|
+
}),
|
|
72
|
+
el("div", { className: "row" }, [hire]),
|
|
73
|
+
);
|
|
74
|
+
main.append(next);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
main.append(afterCreate(App.data?.staff?.length ? 1 : 2));
|
|
78
|
+
|
|
79
|
+
const health = step(App.data?.staff?.length ? 2 : 3, "What doctor still says", false);
|
|
80
|
+
healthHost = el("div", { style: "margin-top:12px" });
|
|
81
|
+
health.append(healthHost);
|
|
82
|
+
main.append(health);
|
|
83
|
+
checklist(healthHost, {});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Whether the sidebar offers Getting started: while anything is left, or while it is open. */
|
|
87
|
+
export function paintSetupNav() {
|
|
88
|
+
const nav = $("#setupnav");
|
|
89
|
+
if (!nav) return;
|
|
90
|
+
nav.hidden = !App.data?.unfinished?.length && App.view !== "setup";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* After a save: the checklist and the step list are both derived from disk, so both are asked
|
|
94
|
+
again rather than updated by hand. The sidebar's list comes from /api/org, which is the one
|
|
95
|
+
the portal already reads. */
|
|
96
|
+
async function recheck() {
|
|
97
|
+
if (healthHost) checklist(healthHost, {});
|
|
98
|
+
try {
|
|
99
|
+
S = await getSetup();
|
|
100
|
+
for (const [id, mark] of fileSteps) mark(!(S.tenant.left ?? []).includes(id));
|
|
101
|
+
} catch {
|
|
102
|
+
// The saved file is saved either way; a stale tick is not worth an error on top of it.
|
|
103
|
+
}
|
|
104
|
+
if (App.data) {
|
|
105
|
+
const fresh = await getOrg().catch(() => null);
|
|
106
|
+
if (fresh?.unfinished) App.data.unfinished = fresh.unfinished;
|
|
107
|
+
paintSetupNav();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
21
111
|
function render(main) {
|
|
112
|
+
/* The sidebar is written by boot() before any of this is known. Once a tenant has been
|
|
113
|
+
adopted, leaving it on "no org yet" contradicts the card directly below it. */
|
|
114
|
+
const brand = $("#orgname");
|
|
115
|
+
if (brand) brand.textContent = S.tenant.found ? (S.tenant.org ?? "set up") : "no org yet";
|
|
116
|
+
|
|
22
117
|
main.replaceChildren();
|
|
23
118
|
main.append(el("h1", { textContent: "Set up your org" }));
|
|
24
119
|
main.append(
|
|
25
120
|
el("p", {
|
|
26
121
|
className: "sub",
|
|
27
122
|
textContent:
|
|
28
|
-
"
|
|
29
|
-
"you
|
|
123
|
+
"Sign in with gh, choose the organisation and the agent, then what is left: a setting " +
|
|
124
|
+
"roster tries for you, the agent's credential, the repos your staff work in, and the two " +
|
|
125
|
+
"files only you can write. Nothing is created until you have read the plan.",
|
|
30
126
|
}),
|
|
31
127
|
);
|
|
32
128
|
|
|
@@ -36,7 +132,7 @@ function render(main) {
|
|
|
36
132
|
|
|
37
133
|
/* Drawn whenever a tenant exists, not only in the seconds after creating one. Setup takes
|
|
38
134
|
days: the repo picker and the two manual steps have to still be here tomorrow. */
|
|
39
|
-
if (S.tenant.found) main.append(afterCreate());
|
|
135
|
+
if (S.tenant.found) main.append(afterCreate(3));
|
|
40
136
|
}
|
|
41
137
|
|
|
42
138
|
/* ---------------------------------- 1 · gh ---------------------------------- */
|
|
@@ -73,12 +169,14 @@ function stepOrg(main) {
|
|
|
73
169
|
}),
|
|
74
170
|
el("p", {
|
|
75
171
|
className: "sub",
|
|
76
|
-
textContent:
|
|
172
|
+
textContent:
|
|
173
|
+
"Reload the page to open the portal. Anything below that is still undone stays under " +
|
|
174
|
+
"Getting started in the sidebar until it is.",
|
|
77
175
|
}),
|
|
78
176
|
);
|
|
79
|
-
|
|
80
|
-
card.append(
|
|
81
|
-
checklist(
|
|
177
|
+
healthHost = el("div", { style: "margin-top:12px" });
|
|
178
|
+
card.append(healthHost);
|
|
179
|
+
checklist(healthHost, {});
|
|
82
180
|
return card;
|
|
83
181
|
}
|
|
84
182
|
|
|
@@ -156,28 +254,28 @@ function stepOrg(main) {
|
|
|
156
254
|
const human = field("human", "Your GitHub login", S.gh.login ?? "", "The person the agents answer to.");
|
|
157
255
|
|
|
158
256
|
card.append(form);
|
|
257
|
+
const agentHead = el("h3", { textContent: "Which coding agent runs a session" });
|
|
159
258
|
|
|
160
259
|
/* The agent. Its id decides the secret name later, so setup can name it exactly rather than
|
|
161
260
|
saying "it depends on the runner". */
|
|
162
|
-
card.append(
|
|
261
|
+
card.append(agentHead);
|
|
163
262
|
const agents = el("div", { className: "choices" });
|
|
164
263
|
S.agents.forEach((a, i) => {
|
|
165
264
|
const opt = el("label", { className: "choice" });
|
|
166
265
|
const radio = el("input", { name: "agent", value: a.id, checked: i === 0 });
|
|
167
266
|
radio.setAttribute("type", "radio");
|
|
168
|
-
opt.append(radio, el("b", { textContent: a.label })
|
|
267
|
+
opt.append(radio, el("b", { textContent: a.label }));
|
|
169
268
|
opt.append(el("code", { textContent: a.tokenEnv }));
|
|
170
269
|
agents.append(opt);
|
|
171
270
|
});
|
|
172
271
|
card.append(agents);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
);
|
|
272
|
+
const agentNote = el("p", {
|
|
273
|
+
className: "sub",
|
|
274
|
+
textContent:
|
|
275
|
+
"Anything else works too: a runner is an install command, a run command and the name of " +
|
|
276
|
+
"the secret holding its credential. Write those three into org.yaml afterwards.",
|
|
277
|
+
});
|
|
278
|
+
card.append(agentNote);
|
|
181
279
|
|
|
182
280
|
const params = () => ({
|
|
183
281
|
org: org.value === "__other" ? other.input.value.trim() : org.value,
|
|
@@ -200,11 +298,29 @@ function stepOrg(main) {
|
|
|
200
298
|
join.hidden = !joining;
|
|
201
299
|
plan.hidden = joining;
|
|
202
300
|
if (joining) go.hidden = true;
|
|
301
|
+
|
|
302
|
+
/* An org that already runs roster has answered all of these: its own org.yaml names the
|
|
303
|
+
business, the human and the agent. Leaving them on screen asks for values that will be
|
|
304
|
+
thrown away, and implies the checkout is going to use them. */
|
|
305
|
+
for (const bit of [name.wrap, human.wrap, agentHead, agents, agentNote]) bit.hidden = joining;
|
|
203
306
|
}
|
|
204
307
|
|
|
205
308
|
join.onclick = async () => {
|
|
206
309
|
join.disabled = true;
|
|
207
|
-
|
|
310
|
+
/* Cloning an ops repo and every brain took 38 seconds against a real org, behind a word
|
|
311
|
+
that never changed. A static "Cloning…" for that long reads as hung, so this says what
|
|
312
|
+
it is doing and keeps a clock running to show it is alive. */
|
|
313
|
+
const clock = el("p", { className: "sub" });
|
|
314
|
+
out.replaceChildren(clock);
|
|
315
|
+
const started = Date.now();
|
|
316
|
+
const tick = () => {
|
|
317
|
+
const s = Math.round((Date.now() - started) / 1000);
|
|
318
|
+
clock.textContent =
|
|
319
|
+
`Cloning ${existing}: the ops repo and one for each staff member. ${s}s. ` +
|
|
320
|
+
"A first checkout usually takes under a minute.";
|
|
321
|
+
};
|
|
322
|
+
tick();
|
|
323
|
+
const ticking = setInterval(tick, 1000);
|
|
208
324
|
try {
|
|
209
325
|
const result = await joinOrg(existing);
|
|
210
326
|
if (!result.ok) throw new Error(result.error);
|
|
@@ -219,6 +335,7 @@ function stepOrg(main) {
|
|
|
219
335
|
} catch (err) {
|
|
220
336
|
out.replaceChildren(el("p", { className: "err", textContent: String(err.message || err) }));
|
|
221
337
|
} finally {
|
|
338
|
+
clearInterval(ticking);
|
|
222
339
|
join.disabled = false;
|
|
223
340
|
}
|
|
224
341
|
};
|
|
@@ -235,7 +352,10 @@ function stepOrg(main) {
|
|
|
235
352
|
out.replaceChildren();
|
|
236
353
|
out.append(
|
|
237
354
|
el("p", {
|
|
238
|
-
textContent:
|
|
355
|
+
textContent:
|
|
356
|
+
`${result.files.length} files in ${result.dir}, and one private repo, ${p.org}/roster-ops, ` +
|
|
357
|
+
"with its Actions access set so every repo in the org can call its workflow. If GitHub " +
|
|
358
|
+
"refuses that, you get the reason and the page to click.",
|
|
239
359
|
}),
|
|
240
360
|
);
|
|
241
361
|
const list = el("div", { className: "filelist" });
|
|
@@ -278,56 +398,182 @@ function stepOrg(main) {
|
|
|
278
398
|
|
|
279
399
|
/* ------------------------- 5 · the two GitHub insists on ------------------------- */
|
|
280
400
|
|
|
281
|
-
function afterCreate() {
|
|
282
|
-
|
|
401
|
+
function afterCreate(n) {
|
|
402
|
+
fileSteps.clear();
|
|
403
|
+
const card = step(n, "What is left", false);
|
|
283
404
|
card.append(
|
|
284
405
|
el("p", {
|
|
285
406
|
textContent:
|
|
286
|
-
"
|
|
287
|
-
"you
|
|
407
|
+
"The first is a setting roster tries for you. The last two are the files that decide " +
|
|
408
|
+
"whether any of this is worth running, and only you can write them.",
|
|
288
409
|
}),
|
|
289
410
|
);
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
one.append(
|
|
293
|
-
el("b", { textContent: "Let the ops repo's workflow be called" }),
|
|
294
|
-
el("p", {
|
|
295
|
-
textContent:
|
|
296
|
-
"Settings → Actions → General → Access → “Accessible from repositories in the " +
|
|
297
|
-
"organisation”, on roster-ops.",
|
|
298
|
-
}),
|
|
299
|
-
el("small", {
|
|
300
|
-
textContent:
|
|
301
|
-
"Skip it and every workflow fails with “workflow not found”, which reads like a typo in " +
|
|
302
|
-
"a path and is not one. It is an org permission on a repo, so it is yours to click.",
|
|
303
|
-
}),
|
|
304
|
-
);
|
|
305
|
-
if (S.tenant.org) {
|
|
306
|
-
const url = `https://github.com/${S.tenant.org}/roster-ops/settings/actions`;
|
|
307
|
-
one.append(el("a", { className: "btn", href: url, target: "_blank", textContent: "Open that page" }));
|
|
308
|
-
}
|
|
309
|
-
card.append(one);
|
|
411
|
+
card.append(accessStep());
|
|
412
|
+
card.append(credentialPanel());
|
|
310
413
|
|
|
311
414
|
if (S.tenant.org) {
|
|
312
415
|
card.append(repoPicker({ org: S.tenant.org, known: S.tenant.repos ?? [] }));
|
|
313
416
|
}
|
|
314
417
|
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
"It ships as questions. It is composed into the top of every prompt, every run, and an " +
|
|
321
|
-
"agent that cannot answer them writes work that is plausible and generic.",
|
|
322
|
-
}),
|
|
418
|
+
const business = fileStep(
|
|
419
|
+
"business",
|
|
420
|
+
"Answer org/business.md",
|
|
421
|
+
"It ships as questions. It is composed into the top of every prompt, every run, and an " +
|
|
422
|
+
"agent that cannot answer them writes work that is plausible and generic.",
|
|
323
423
|
);
|
|
324
|
-
card.append(two);
|
|
325
|
-
|
|
326
424
|
// The copy-a-prompt loop, which is the whole answer to "how do I write this file".
|
|
327
|
-
|
|
425
|
+
business.append(paste({ kind: "discover", title: "Write it with your own AI", onSaved: recheck }));
|
|
426
|
+
card.append(business);
|
|
427
|
+
|
|
428
|
+
const priorities = fileStep(
|
|
429
|
+
"priorities",
|
|
430
|
+
"Rank org/priorities.md",
|
|
431
|
+
"What matters this month, at most three things in order, and what is out of scope. Every " +
|
|
432
|
+
"daily run reads it; without it each staff member picks its own direction from its charter.",
|
|
433
|
+
);
|
|
434
|
+
priorities.append(prioritiesEditor());
|
|
435
|
+
card.append(priorities);
|
|
328
436
|
return card;
|
|
329
437
|
}
|
|
330
438
|
|
|
439
|
+
/**
|
|
440
|
+
* One of the files only a person can write, with whether it is written yet.
|
|
441
|
+
*
|
|
442
|
+
* Read from the status rather than from the click that saved it, so a file written in an
|
|
443
|
+
* editor, or by a coding agent, shows as done here too.
|
|
444
|
+
*/
|
|
445
|
+
function fileStep(id, title, why) {
|
|
446
|
+
const box = el("div", { className: "manual" });
|
|
447
|
+
const tag = el("span", { className: "meta" });
|
|
448
|
+
const mark = (done) => {
|
|
449
|
+
tag.textContent = done ? "Written." : "Still the stub it shipped as.";
|
|
450
|
+
box.classList[done ? "add" : "remove"]("done");
|
|
451
|
+
};
|
|
452
|
+
mark(!(S.tenant.left ?? []).includes(id));
|
|
453
|
+
fileSteps.set(id, mark);
|
|
454
|
+
box.append(el("div", {}, [el("b", { textContent: title }), tag]), el("p", { textContent: why }));
|
|
455
|
+
return box;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* org/priorities.md, edited in place.
|
|
460
|
+
*
|
|
461
|
+
* There is no brief for it, and it does not want one: business.md needs an interview because
|
|
462
|
+
* nobody can write it off the top of their head, while a ranked list of three things is
|
|
463
|
+
* quicker to type than to explain to a model. So the file opens as it is on disk, stub and
|
|
464
|
+
* all, and saving it is the same commit-and-push as every other editor here.
|
|
465
|
+
*/
|
|
466
|
+
function prioritiesEditor() {
|
|
467
|
+
const box = el("div", { style: "margin-top:9px" });
|
|
468
|
+
const path = opsName() + "/org/priorities.md";
|
|
469
|
+
const ta = el("textarea", { className: "pastebox" });
|
|
470
|
+
ta.rows = 12;
|
|
471
|
+
const save = el("button", { className: "btn primary", textContent: "Save and commit" });
|
|
472
|
+
const out = el("div");
|
|
473
|
+
box.append(ta, el("div", { className: "row", style: "margin-top:9px" }, [save]), out);
|
|
474
|
+
|
|
475
|
+
let before = "";
|
|
476
|
+
save.disabled = true;
|
|
477
|
+
// Not getFile: an absent file answers 404 with a message, and that is not what to edit.
|
|
478
|
+
fetch(fileUrl(path), { cache: "no-store" })
|
|
479
|
+
.then((r) => (r.ok ? r.text() : ""))
|
|
480
|
+
.then((text) => {
|
|
481
|
+
before = text;
|
|
482
|
+
ta.value = text;
|
|
483
|
+
grow(ta);
|
|
484
|
+
save.disabled = false;
|
|
485
|
+
})
|
|
486
|
+
.catch((err) => out.replaceChildren(el("p", { className: "err", textContent: String(err.message || err) })));
|
|
487
|
+
ta.addEventListener("input", () => grow(ta));
|
|
488
|
+
|
|
489
|
+
save.onclick = async () => {
|
|
490
|
+
if (ta.value.trim() === before.trim()) {
|
|
491
|
+
out.replaceChildren(el("p", { className: "sub", textContent: "Nothing changed yet." }));
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
save.disabled = true;
|
|
495
|
+
save.textContent = "Saving…";
|
|
496
|
+
try {
|
|
497
|
+
const r = await saveFile(path, ta.value, "portal: write org/priorities.md");
|
|
498
|
+
before = ta.value;
|
|
499
|
+
out.replaceChildren(
|
|
500
|
+
el("p", {
|
|
501
|
+
className: r.pushed ? "sub" : "err",
|
|
502
|
+
textContent: r.pushed
|
|
503
|
+
? "Committed and pushed. Every daily run reads it from the next one."
|
|
504
|
+
: "Committed, but the push failed: " + (r.note ?? ""),
|
|
505
|
+
}),
|
|
506
|
+
);
|
|
507
|
+
recheck();
|
|
508
|
+
} catch (err) {
|
|
509
|
+
out.replaceChildren(el("p", { className: "err", textContent: String(err.message || err) }));
|
|
510
|
+
} finally {
|
|
511
|
+
save.disabled = false;
|
|
512
|
+
save.textContent = "Save and commit";
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
return box;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** The ops repo's directory name, which is what every workspace-relative path starts with. */
|
|
519
|
+
function opsName() {
|
|
520
|
+
return App.data?.opsName ?? String(S.tenant.opsDir ?? "roster-ops").split("/").pop();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* The ops repo's Actions access. Read on arrival, so a page reloaded after it was set says it
|
|
525
|
+
* is done without anybody pressing anything; the button asks the server to set it, and a
|
|
526
|
+
* refusal comes back with GitHub's reason and the page to click instead.
|
|
527
|
+
*
|
|
528
|
+
* Skipped, every workflow fails with "workflow not found", which reads like a typo in a path.
|
|
529
|
+
*/
|
|
530
|
+
function accessStep() {
|
|
531
|
+
const box = el("div", { className: "manual" });
|
|
532
|
+
box.append(el("b", { textContent: "Let every repo in the org call the ops repo's workflow" }));
|
|
533
|
+
const out = el("div");
|
|
534
|
+
box.append(out);
|
|
535
|
+
const say = (text, cls) => el("p", { className: cls ?? "sub", textContent: text });
|
|
536
|
+
|
|
537
|
+
const offer = (why) => {
|
|
538
|
+
const go = el("button", { className: "btn primary", textContent: "Set it for me" });
|
|
539
|
+
out.replaceChildren(
|
|
540
|
+
say(
|
|
541
|
+
(why ? why + " " : "") +
|
|
542
|
+
"Settings → Actions → General → Access on roster-ops. Skip it and every workflow fails " +
|
|
543
|
+
"with “workflow not found”.",
|
|
544
|
+
),
|
|
545
|
+
el("div", { className: "row" }, [go]),
|
|
546
|
+
);
|
|
547
|
+
go.onclick = async () => {
|
|
548
|
+
go.disabled = true;
|
|
549
|
+
try {
|
|
550
|
+
const r = await setAccess();
|
|
551
|
+
if (r.ok) {
|
|
552
|
+
out.replaceChildren(say("Set. Every repo in the org can call roster-ops."));
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
out.replaceChildren(
|
|
556
|
+
say("GitHub refused: " + r.error + ".", "err"),
|
|
557
|
+
el("a", { className: "btn", href: r.link, target: "_blank", textContent: "Set it by hand" }),
|
|
558
|
+
say("Access → “Accessible from repositories in the organisation”."),
|
|
559
|
+
);
|
|
560
|
+
} catch (err) {
|
|
561
|
+
out.replaceChildren(say(String(err.message || err), "err"));
|
|
562
|
+
go.disabled = false;
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
out.replaceChildren(say("Checking…"));
|
|
568
|
+
getAccess()
|
|
569
|
+
.then((r) => {
|
|
570
|
+
if (r.ok) out.replaceChildren(say("Done. Every repo in the org can call roster-ops."));
|
|
571
|
+
else offer(r.level ? "It is set to “" + r.level + "”." : "");
|
|
572
|
+
})
|
|
573
|
+
.catch(() => offer(""));
|
|
574
|
+
return box;
|
|
575
|
+
}
|
|
576
|
+
|
|
331
577
|
/* ---------------------------------- furniture ---------------------------------- */
|
|
332
578
|
|
|
333
579
|
function step(n, title, done) {
|