@nanocollective/roster 0.1.0-alpha.52 → 0.1.0-alpha.53
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 +143 -67
- package/docs/portal.md +10 -3
- package/package.json +1 -1
- package/templates/portal/css/home.css +9 -4
- package/templates/portal/js/views/home.js +23 -9
- package/templates/portal/js/views/runs.js +14 -0
package/dist/cli.js
CHANGED
|
@@ -2892,30 +2892,36 @@ function overBudget(doc, dir, budgets) {
|
|
|
2892
2892
|
}
|
|
2893
2893
|
|
|
2894
2894
|
// src/lib/runs.ts
|
|
2895
|
-
import { existsSync as existsSync15, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, rmSync as rmSync2 } from "fs";
|
|
2896
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
2895
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
2896
|
+
import { homedir, tmpdir as tmpdir2 } from "os";
|
|
2897
2897
|
import { join as join16 } from "path";
|
|
2898
2898
|
var KINDS2 = ["daily", "mention"];
|
|
2899
2899
|
var records = /* @__PURE__ */ new Map();
|
|
2900
|
-
|
|
2900
|
+
var fetching = /* @__PURE__ */ new Set();
|
|
2901
|
+
function cacheDir() {
|
|
2902
|
+
return process.env.ROSTER_CACHE_DIR ?? join16(homedir(), ".cache", "roster", "runs");
|
|
2903
|
+
}
|
|
2904
|
+
function cacheFile(brain, id) {
|
|
2905
|
+
return join16(cacheDir(), brain.replace("/", "__"), `${id}.json`);
|
|
2906
|
+
}
|
|
2907
|
+
function knownRecord(brain, id) {
|
|
2908
|
+
const key = `${brain}#${id}`;
|
|
2909
|
+
if (records.has(key)) return records.get(key);
|
|
2910
|
+
const file = cacheFile(brain, id);
|
|
2911
|
+
if (!existsSync15(file)) return void 0;
|
|
2912
|
+
try {
|
|
2913
|
+
const record = JSON.parse(readFileSync14(file, "utf8")).record;
|
|
2914
|
+
records.set(key, record);
|
|
2915
|
+
return record;
|
|
2916
|
+
} catch {
|
|
2917
|
+
return void 0;
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
async function staffRuns(brain, handle, days = 30, opts = {}) {
|
|
2901
2921
|
const since = new Date(Date.now() - days * 864e5).toISOString().slice(0, 10);
|
|
2902
2922
|
const lists = await Promise.all(
|
|
2903
2923
|
KINDS2.map(
|
|
2904
|
-
(kind) =>
|
|
2905
|
-
"run",
|
|
2906
|
-
"list",
|
|
2907
|
-
"--repo",
|
|
2908
|
-
brain,
|
|
2909
|
-
"--workflow",
|
|
2910
|
-
`${handle}-${kind}.yaml`,
|
|
2911
|
-
"--created",
|
|
2912
|
-
`>=${since}`,
|
|
2913
|
-
"--limit",
|
|
2914
|
-
// Skipped mentions (the gate saying no) count towards the limit, and there are hundreds.
|
|
2915
|
-
kind === "mention" ? "500" : "100",
|
|
2916
|
-
"--json",
|
|
2917
|
-
"databaseId,conclusion,status,createdAt,updatedAt,url"
|
|
2918
|
-
]).then((res) => ({ kind, res }))
|
|
2924
|
+
(kind) => realRuns(brain, `${handle}-${kind}.yaml`, since).then((res) => ({ kind, res }))
|
|
2919
2925
|
)
|
|
2920
2926
|
);
|
|
2921
2927
|
const errors = [];
|
|
@@ -2943,14 +2949,71 @@ async function staffRuns(brain, handle, days = 30) {
|
|
|
2943
2949
|
}
|
|
2944
2950
|
}
|
|
2945
2951
|
rows.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2952
|
+
const missing = [];
|
|
2953
|
+
for (const row of rows.filter((r) => r.status === "completed")) {
|
|
2954
|
+
const known = knownRecord(brain, row.id);
|
|
2955
|
+
if (known === void 0) missing.push(row);
|
|
2956
|
+
else row.record = known;
|
|
2957
|
+
}
|
|
2958
|
+
if (opts.wait === false) {
|
|
2959
|
+
const todo = missing.filter((r) => !fetching.has(`${brain}#${r.id}`));
|
|
2960
|
+
for (const r of todo) fetching.add(`${brain}#${r.id}`);
|
|
2961
|
+
void pool(todo, 6, async (row) => {
|
|
2962
|
+
await recordFor(brain, row.id);
|
|
2963
|
+
fetching.delete(`${brain}#${row.id}`);
|
|
2964
|
+
});
|
|
2965
|
+
return { runs: rows, errors, pending: missing.length };
|
|
2966
|
+
}
|
|
2967
|
+
await pool(missing, 6, async (row) => {
|
|
2968
|
+
row.record = await recordFor(brain, row.id);
|
|
2969
|
+
});
|
|
2970
|
+
return { runs: rows, errors, pending: 0 };
|
|
2971
|
+
}
|
|
2972
|
+
var OUTCOMES = [
|
|
2973
|
+
"success",
|
|
2974
|
+
"failure",
|
|
2975
|
+
"cancelled",
|
|
2976
|
+
"timed_out",
|
|
2977
|
+
"action_required",
|
|
2978
|
+
"startup_failure",
|
|
2979
|
+
"in_progress",
|
|
2980
|
+
"queued"
|
|
2981
|
+
];
|
|
2982
|
+
async function realRuns(brain, workflow, since) {
|
|
2983
|
+
const one = async (status) => {
|
|
2984
|
+
const out = [];
|
|
2985
|
+
for (let page2 = 1; page2 <= 10; page2++) {
|
|
2986
|
+
const res = await ghJson([
|
|
2987
|
+
"api",
|
|
2988
|
+
`repos/${brain}/actions/workflows/${workflow}/runs?created=>=${since}&status=${status}&per_page=100&page=${page2}`
|
|
2989
|
+
]);
|
|
2990
|
+
if (!res.ok) return { ok: false, error: res.error };
|
|
2991
|
+
const runs = res.data?.workflow_runs ?? [];
|
|
2992
|
+
out.push(...runs);
|
|
2993
|
+
if (runs.length < 100) break;
|
|
2951
2994
|
}
|
|
2952
|
-
|
|
2953
|
-
|
|
2995
|
+
return { ok: true, runs: out };
|
|
2996
|
+
};
|
|
2997
|
+
const answers = await Promise.all(OUTCOMES.map(one));
|
|
2998
|
+
const failed = answers.find((a) => !a.ok);
|
|
2999
|
+
if (failed && !failed.ok) return { ok: false, error: failed.error };
|
|
3000
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3001
|
+
const data = [];
|
|
3002
|
+
for (const a of answers) {
|
|
3003
|
+
for (const r of a.ok ? a.runs : []) {
|
|
3004
|
+
if (seen.has(r.id)) continue;
|
|
3005
|
+
seen.add(r.id);
|
|
3006
|
+
data.push({
|
|
3007
|
+
databaseId: r.id,
|
|
3008
|
+
conclusion: r.conclusion,
|
|
3009
|
+
status: r.status,
|
|
3010
|
+
createdAt: r.created_at,
|
|
3011
|
+
updatedAt: r.updated_at,
|
|
3012
|
+
url: r.html_url
|
|
3013
|
+
});
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
return { ok: true, data };
|
|
2954
3017
|
}
|
|
2955
3018
|
async function recordFor(brain, id) {
|
|
2956
3019
|
const key = `${brain}#${id}`;
|
|
@@ -2971,6 +3034,13 @@ async function recordFor(brain, id) {
|
|
|
2971
3034
|
const file = join16(dir, "run.json");
|
|
2972
3035
|
const record = res.ok && existsSync15(file) ? JSON.parse(readFileSync14(file, "utf8")) : null;
|
|
2973
3036
|
records.set(key, record);
|
|
3037
|
+
if (res.ok || /no valid artifacts|not found|no artifact/i.test(res.error ?? "")) {
|
|
3038
|
+
try {
|
|
3039
|
+
mkdirSync3(join16(cacheDir(), brain.replace("/", "__")), { recursive: true });
|
|
3040
|
+
writeFileSync4(cacheFile(brain, id), JSON.stringify({ record }));
|
|
3041
|
+
} catch {
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
2974
3044
|
return record;
|
|
2975
3045
|
} catch {
|
|
2976
3046
|
records.set(key, null);
|
|
@@ -3005,19 +3075,19 @@ function budgetOf(value) {
|
|
|
3005
3075
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
3006
3076
|
import {
|
|
3007
3077
|
existsSync as existsSync16,
|
|
3008
|
-
mkdirSync as
|
|
3078
|
+
mkdirSync as mkdirSync4,
|
|
3009
3079
|
mkdtempSync as mkdtempSync4,
|
|
3010
3080
|
readdirSync as readdirSync5,
|
|
3011
3081
|
readFileSync as readFileSync15,
|
|
3012
3082
|
rmSync as rmSync4,
|
|
3013
|
-
writeFileSync as
|
|
3083
|
+
writeFileSync as writeFileSync6
|
|
3014
3084
|
} from "fs";
|
|
3015
3085
|
import { tmpdir as tmpdir4 } from "os";
|
|
3016
3086
|
import { dirname as dirname6, join as join18 } from "path";
|
|
3017
3087
|
|
|
3018
3088
|
// src/lib/merge.ts
|
|
3019
3089
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
3020
|
-
import { mkdtempSync as mkdtempSync3, rmSync as rmSync3, writeFileSync as
|
|
3090
|
+
import { mkdtempSync as mkdtempSync3, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
3021
3091
|
import { tmpdir as tmpdir3 } from "os";
|
|
3022
3092
|
import { join as join17 } from "path";
|
|
3023
3093
|
function merge3(current, base2, incoming) {
|
|
@@ -3026,9 +3096,9 @@ function merge3(current, base2, incoming) {
|
|
|
3026
3096
|
const c = join17(dir, "current");
|
|
3027
3097
|
const b = join17(dir, "base");
|
|
3028
3098
|
const i = join17(dir, "incoming");
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3099
|
+
writeFileSync5(c, current);
|
|
3100
|
+
writeFileSync5(b, base2);
|
|
3101
|
+
writeFileSync5(i, incoming);
|
|
3032
3102
|
try {
|
|
3033
3103
|
const text = execFileSync4(
|
|
3034
3104
|
"git",
|
|
@@ -3237,8 +3307,8 @@ function apply(opsDir, seedDir, tplDir, plans) {
|
|
|
3237
3307
|
if (p.verdict === "edited-managed") edited++;
|
|
3238
3308
|
if (p.verdict === "conflict") {
|
|
3239
3309
|
const out = join18(opsDir, p.rel + ".roster-merge");
|
|
3240
|
-
|
|
3241
|
-
|
|
3310
|
+
mkdirSync4(dirname6(out), { recursive: true });
|
|
3311
|
+
writeFileSync6(out, p.rejected);
|
|
3242
3312
|
process.stdout.write(` wrote ${p.rel}.roster-merge \u2014 resolve it, then re-run
|
|
3243
3313
|
`);
|
|
3244
3314
|
left++;
|
|
@@ -3253,8 +3323,8 @@ function apply(opsDir, seedDir, tplDir, plans) {
|
|
|
3253
3323
|
continue;
|
|
3254
3324
|
}
|
|
3255
3325
|
const dest = join18(opsDir, p.rel);
|
|
3256
|
-
|
|
3257
|
-
|
|
3326
|
+
mkdirSync4(dirname6(dest), { recursive: true });
|
|
3327
|
+
writeFileSync6(dest, p.next);
|
|
3258
3328
|
seed(seedDir, tplDir, p.rel);
|
|
3259
3329
|
wrote++;
|
|
3260
3330
|
}
|
|
@@ -3263,7 +3333,7 @@ function apply(opsDir, seedDir, tplDir, plans) {
|
|
|
3263
3333
|
process.stdout.write(left ? `, ${left} left for you
|
|
3264
3334
|
|
|
3265
3335
|
` : "\n\n");
|
|
3266
|
-
if (!left)
|
|
3336
|
+
if (!left) writeFileSync6(join18(opsDir, ".roster-version"), stamp() + "\n");
|
|
3267
3337
|
if (edited) {
|
|
3268
3338
|
process.stdout.write(
|
|
3269
3339
|
` ${edited} framework file${edited === 1 ? "" : "s"} still carr${edited === 1 ? "ies" : "y"} a local edit. Move it into the framework, or doctor keeps failing.
|
|
@@ -3275,8 +3345,8 @@ function apply(opsDir, seedDir, tplDir, plans) {
|
|
|
3275
3345
|
}
|
|
3276
3346
|
function seed(seedDir, tplDir, rel) {
|
|
3277
3347
|
const dest = join18(seedDir, rel);
|
|
3278
|
-
|
|
3279
|
-
|
|
3348
|
+
mkdirSync4(dirname6(dest), { recursive: true });
|
|
3349
|
+
writeFileSync6(dest, readFileSync15(join18(tplDir, rel), "utf8"));
|
|
3280
3350
|
}
|
|
3281
3351
|
function writeBaseline(opsDir, seedDir, tplDir, ref) {
|
|
3282
3352
|
const framework = join18(tplDir, "..", "..");
|
|
@@ -3301,10 +3371,10 @@ function writeBaseline(opsDir, seedDir, tplDir, ref) {
|
|
|
3301
3371
|
maxBuffer: 16 * 1024 * 1024
|
|
3302
3372
|
});
|
|
3303
3373
|
const dest = join18(seedDir, rel);
|
|
3304
|
-
|
|
3305
|
-
|
|
3374
|
+
mkdirSync4(dirname6(dest), { recursive: true });
|
|
3375
|
+
writeFileSync6(dest, text);
|
|
3306
3376
|
}
|
|
3307
|
-
|
|
3377
|
+
writeFileSync6(join18(opsDir, ".roster-version"), `${ref}
|
|
3308
3378
|
`);
|
|
3309
3379
|
process.stdout.write(
|
|
3310
3380
|
`
|
|
@@ -3408,8 +3478,8 @@ function planBrains(ws, parseYaml) {
|
|
|
3408
3478
|
function unified(before, after) {
|
|
3409
3479
|
const dir = mkdtempSync4(join18(tmpdir4(), "roster-diff-"));
|
|
3410
3480
|
try {
|
|
3411
|
-
|
|
3412
|
-
|
|
3481
|
+
writeFileSync6(join18(dir, "a"), before);
|
|
3482
|
+
writeFileSync6(join18(dir, "b"), after);
|
|
3413
3483
|
try {
|
|
3414
3484
|
execFileSync5(
|
|
3415
3485
|
"diff",
|
|
@@ -3481,8 +3551,8 @@ function applyBrains(brains) {
|
|
|
3481
3551
|
}
|
|
3482
3552
|
if (f.next === void 0) continue;
|
|
3483
3553
|
const dest = join18(b.root, f.rel);
|
|
3484
|
-
|
|
3485
|
-
|
|
3554
|
+
mkdirSync4(dirname6(dest), { recursive: true });
|
|
3555
|
+
writeFileSync6(dest, f.next);
|
|
3486
3556
|
wrote++;
|
|
3487
3557
|
}
|
|
3488
3558
|
}
|
|
@@ -4516,7 +4586,7 @@ function unkindedAsks(scope, repo, issues, humans) {
|
|
|
4516
4586
|
}
|
|
4517
4587
|
|
|
4518
4588
|
// src/commands/export.ts
|
|
4519
|
-
import { writeFileSync as
|
|
4589
|
+
import { writeFileSync as writeFileSync7 } from "fs";
|
|
4520
4590
|
|
|
4521
4591
|
// src/lib/export.ts
|
|
4522
4592
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
@@ -4707,7 +4777,7 @@ async function exportCommand(argv) {
|
|
|
4707
4777
|
const data = buildExport(ws, readOrg(ws.opsDir, parseYaml), parseYaml);
|
|
4708
4778
|
const json2 = JSON.stringify(data, null, 2);
|
|
4709
4779
|
if (out) {
|
|
4710
|
-
|
|
4780
|
+
writeFileSync7(out, json2 + "\n");
|
|
4711
4781
|
const facts = data.staff.reduce((n, s) => n + s.facts.length, 0);
|
|
4712
4782
|
process.stdout.write(
|
|
4713
4783
|
` ${out} ${data.staff.length} staff, ${facts} facts, ${Math.round(json2.length / 1024)}kb
|
|
@@ -5062,7 +5132,7 @@ function parseFlags7(argv) {
|
|
|
5062
5132
|
|
|
5063
5133
|
// src/commands/init.ts
|
|
5064
5134
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
5065
|
-
import { existsSync as existsSync20, mkdirSync as
|
|
5135
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync5, readFileSync as readFileSync19, writeFileSync as writeFileSync8 } from "fs";
|
|
5066
5136
|
import { dirname as dirname7, join as join22, resolve as resolve3 } from "path";
|
|
5067
5137
|
var initHelp = `
|
|
5068
5138
|
roster init --org <github-org> [--name "Acme"] [--human <login>] [--apply]
|
|
@@ -5155,16 +5225,16 @@ async function initCommand(argv) {
|
|
|
5155
5225
|
process.stdout.write(" Nothing was created. Re-run with --apply.\n\n");
|
|
5156
5226
|
return 0;
|
|
5157
5227
|
}
|
|
5158
|
-
|
|
5228
|
+
mkdirSync5(opsDir, { recursive: true });
|
|
5159
5229
|
for (const [rel, text] of files) {
|
|
5160
5230
|
const dest = join22(opsDir, rel);
|
|
5161
|
-
|
|
5162
|
-
|
|
5231
|
+
mkdirSync5(dirname7(dest), { recursive: true });
|
|
5232
|
+
writeFileSync8(dest, text);
|
|
5163
5233
|
}
|
|
5164
5234
|
for (const rel of templateFiles(opsTemplateDir())) {
|
|
5165
5235
|
const dest = join22(opsDir, ".roster", "seed", rel);
|
|
5166
|
-
|
|
5167
|
-
|
|
5236
|
+
mkdirSync5(dirname7(dest), { recursive: true });
|
|
5237
|
+
writeFileSync8(dest, readFileSync19(join22(opsTemplateDir(), rel), "utf8"));
|
|
5168
5238
|
}
|
|
5169
5239
|
process.stdout.write(` wrote ${files.size} files and the recorded base
|
|
5170
5240
|
`);
|
|
@@ -5776,7 +5846,7 @@ function execWithStdin(args, input) {
|
|
|
5776
5846
|
|
|
5777
5847
|
// src/lib/attach.ts
|
|
5778
5848
|
import { execFileSync as execFileSync8 } from "child_process";
|
|
5779
|
-
import { existsSync as existsSync21, mkdirSync as
|
|
5849
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync6, writeFileSync as writeFileSync9 } from "fs";
|
|
5780
5850
|
import { basename, extname, join as join24 } from "path";
|
|
5781
5851
|
var MAX_UPLOAD = 25 * 1024 * 1024;
|
|
5782
5852
|
var ATTACH_DIR = "attachments";
|
|
@@ -5791,8 +5861,8 @@ function attach(opts) {
|
|
|
5791
5861
|
}
|
|
5792
5862
|
const rel = free(repoDir, `${ATTACH_DIR}/${today}-${safeName(opts.name)}`);
|
|
5793
5863
|
const full = join24(repoDir, rel);
|
|
5794
|
-
|
|
5795
|
-
|
|
5864
|
+
mkdirSync6(join24(repoDir, ATTACH_DIR), { recursive: true });
|
|
5865
|
+
writeFileSync9(full, data);
|
|
5796
5866
|
const git4 = (args) => execFileSync8("git", ["-C", repoDir, ...args], {
|
|
5797
5867
|
encoding: "utf8",
|
|
5798
5868
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -6501,7 +6571,7 @@ function portalAsset(rel) {
|
|
|
6501
6571
|
}
|
|
6502
6572
|
|
|
6503
6573
|
// src/commands/retire.ts
|
|
6504
|
-
import { existsSync as existsSync24, readdirSync as readdirSync8, readFileSync as readFileSync21, writeFileSync as
|
|
6574
|
+
import { existsSync as existsSync24, readdirSync as readdirSync8, readFileSync as readFileSync21, writeFileSync as writeFileSync10 } from "fs";
|
|
6505
6575
|
import { join as join27 } from "path";
|
|
6506
6576
|
var retireHelp = `
|
|
6507
6577
|
roster retire <handle>
|
|
@@ -6637,7 +6707,7 @@ async function applyRetirePlan(ws, plan2) {
|
|
|
6637
6707
|
for (const p of plan2.peers) {
|
|
6638
6708
|
const path = join27(ws.root, p.dir, "staff.yaml");
|
|
6639
6709
|
const text = readFileSync21(path, "utf8");
|
|
6640
|
-
|
|
6710
|
+
writeFileSync10(path, removePeerLine(text, plan2.handle));
|
|
6641
6711
|
process.stdout.write(` removed from ${p.dir}/staff.yaml
|
|
6642
6712
|
`);
|
|
6643
6713
|
if (!p.brain) continue;
|
|
@@ -6665,7 +6735,7 @@ async function applyRetirePlan(ws, plan2) {
|
|
|
6665
6735
|
function removeFromOrgYaml(ws, plan2) {
|
|
6666
6736
|
const path = join27(ws.opsDir, "org.yaml");
|
|
6667
6737
|
const text = readFileSync21(path, "utf8");
|
|
6668
|
-
|
|
6738
|
+
writeFileSync10(path, removeOrgLines(text, plan2.handle, plan2.dir));
|
|
6669
6739
|
}
|
|
6670
6740
|
function removeOrgLines(text, handle, dir) {
|
|
6671
6741
|
return text.split("\n").filter((line) => {
|
|
@@ -7777,7 +7847,9 @@ async function portalCommand(argv) {
|
|
|
7777
7847
|
const staff = await Promise.all(
|
|
7778
7848
|
(org.staff ?? []).map(async (s) => {
|
|
7779
7849
|
const brain = `${org.org}/${s.dir ?? s.handle}`;
|
|
7780
|
-
const { runs, errors } = await staffRuns(brain, s.handle
|
|
7850
|
+
const { runs, errors, pending: pending2 } = await staffRuns(brain, s.handle, 30, {
|
|
7851
|
+
wait: false
|
|
7852
|
+
});
|
|
7781
7853
|
return {
|
|
7782
7854
|
handle: s.handle,
|
|
7783
7855
|
name: s.name ?? s.handle,
|
|
@@ -7785,7 +7857,8 @@ async function portalCommand(argv) {
|
|
|
7785
7857
|
budget: budgetOf(s.budget),
|
|
7786
7858
|
spend: spend(runs),
|
|
7787
7859
|
runs,
|
|
7788
|
-
errors
|
|
7860
|
+
errors,
|
|
7861
|
+
pending: pending2
|
|
7789
7862
|
};
|
|
7790
7863
|
})
|
|
7791
7864
|
);
|
|
@@ -7795,14 +7868,17 @@ async function portalCommand(argv) {
|
|
|
7795
7868
|
total.known += s.spend.known;
|
|
7796
7869
|
total.runs += s.spend.runs;
|
|
7797
7870
|
}
|
|
7871
|
+
const pending = staff.reduce((n, s) => n + s.pending, 0);
|
|
7798
7872
|
const body2 = {
|
|
7799
7873
|
online: true,
|
|
7800
7874
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7801
7875
|
budget: budgetOf(org.budget),
|
|
7802
7876
|
spend: total,
|
|
7803
|
-
staff
|
|
7877
|
+
staff,
|
|
7878
|
+
// Costs still being read. The screen asks again until this is 0.
|
|
7879
|
+
pending
|
|
7804
7880
|
};
|
|
7805
|
-
runsCache = { at: Date.now(), body: body2 };
|
|
7881
|
+
if (!pending) runsCache = { at: Date.now(), body: body2 };
|
|
7806
7882
|
return body2;
|
|
7807
7883
|
})().then((body2) => json(res, body2)).catch(
|
|
7808
7884
|
(err) => json(res, { online: false, error: String(err?.message ?? err), staff: [] })
|
|
@@ -8149,7 +8225,7 @@ function parseFlags11(argv) {
|
|
|
8149
8225
|
|
|
8150
8226
|
// src/commands/prompt.ts
|
|
8151
8227
|
import { execFileSync as execFileSync10 } from "child_process";
|
|
8152
|
-
import { existsSync as existsSync26, mkdtempSync as mkdtempSync5, readFileSync as readFileSync24, writeFileSync as
|
|
8228
|
+
import { existsSync as existsSync26, mkdtempSync as mkdtempSync5, readFileSync as readFileSync24, writeFileSync as writeFileSync11 } from "fs";
|
|
8153
8229
|
import { tmpdir as tmpdir5 } from "os";
|
|
8154
8230
|
import { join as join29 } from "path";
|
|
8155
8231
|
|
|
@@ -8220,8 +8296,8 @@ async function promptCommand(argv) {
|
|
|
8220
8296
|
const dir = mkdtempSync5(join29(tmpdir5(), "roster-diff-"));
|
|
8221
8297
|
const a = join29(dir, "live.md");
|
|
8222
8298
|
const b = join29(dir, "composed.md");
|
|
8223
|
-
|
|
8224
|
-
|
|
8299
|
+
writeFileSync11(a, normalise(live));
|
|
8300
|
+
writeFileSync11(b, normalise(composed));
|
|
8225
8301
|
const entry = org.staff?.find((s) => s.handle === handle);
|
|
8226
8302
|
process.stdout.write(`
|
|
8227
8303
|
${entry?.name ?? handle} \xB7 ${kind}
|
|
@@ -8257,7 +8333,7 @@ async function gatherInflight(opsDir, root, handle) {
|
|
|
8257
8333
|
const manifest = existsSync26(manifestPath) ? parseYaml(readFileSync24(manifestPath, "utf8"), "staff.yaml") : {};
|
|
8258
8334
|
const gh = (args) => execFileSync10("gh", args, { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
|
|
8259
8335
|
const dir = mkdtempSync5(join29(tmpdir5(), "roster-inflight-"));
|
|
8260
|
-
|
|
8336
|
+
writeFileSync11(join29(dir, "inflight.md"), describe2(gather2({ org, manifest, gh })));
|
|
8261
8337
|
return dir;
|
|
8262
8338
|
}
|
|
8263
8339
|
function normalise(text) {
|
package/docs/portal.md
CHANGED
|
@@ -87,7 +87,8 @@ Where the portal opens. An Ask box at the top, then these, in order.
|
|
|
87
87
|
- **Ask.** Pick a staff member, type what you want, **Send**. It opens an issue on their
|
|
88
88
|
tracker with their `@handle` in front, which wakes them in about a minute.
|
|
89
89
|
- **Latest reports.** Each staff member's run report from the last day: the three lines they
|
|
90
|
-
post on their status issue at the end of a daily run
|
|
90
|
+
post on their status issue at the end of a daily run, with the issues and pull requests it
|
|
91
|
+
names as links. Click a report to open their status issue. Nothing shows until there is one.
|
|
91
92
|
- **Needs you.** Every ask a staff member has put on you, and every pull request ready to
|
|
92
93
|
merge, oldest first. Each card has one action on its right:
|
|
93
94
|
- a `decision` has **Reply**, and **Go with the default** when it carries one. The default
|
|
@@ -262,8 +263,14 @@ than drawing an empty table that reads as "nothing ran".
|
|
|
262
263
|
|
|
263
264
|
Cost comes from the record each run leaves behind (see [cost](cost.md#what-each-run-cost)). A
|
|
264
265
|
run from before records existed, or from an agent that does not report cost, shows a dash, and
|
|
265
|
-
a total says how many runs it could price. Each record is downloaded once and kept
|
|
266
|
-
|
|
266
|
+
a total says how many runs it could price. Each record is downloaded once, ever, and kept in
|
|
267
|
+
`~/.cache/roster/runs` (or `ROSTER_CACHE_DIR`), since a finished run's record never changes.
|
|
268
|
+
The table shows at once with the costs already known; any not yet read are fetched in the
|
|
269
|
+
background, and the screen says how many and fills them in as they arrive.
|
|
270
|
+
|
|
271
|
+
The run list asks GitHub for each outcome separately (succeeded, failed, cancelled and so on)
|
|
272
|
+
rather than paging through every run, because most of a mention caller's runs are comments that
|
|
273
|
+
woke nobody and were skipped.
|
|
267
274
|
|
|
268
275
|
A skipped mention is not a run and is not listed. A `setup-failure` is a run that failed before
|
|
269
276
|
the agent started, usually a token or a checkout. Past a [`budget`](org-yaml.md#budget), the
|
package/package.json
CHANGED
|
@@ -76,10 +76,15 @@
|
|
|
76
76
|
background:var(--blue);margin:0 7px 1px 0;vertical-align:middle}
|
|
77
77
|
|
|
78
78
|
.homereports{display:flex;flex-direction:column;gap:14px;padding:14px 16px}
|
|
79
|
-
.homereport{display:grid;grid-template-columns:190px 1fr;gap:12px;font-size:13px;line-height:1.5
|
|
80
|
-
|
|
81
|
-
.homereport
|
|
82
|
-
.homereport
|
|
79
|
+
.homereport{display:grid;grid-template-columns:190px 1fr;gap:12px;font-size:13px;line-height:1.5;
|
|
80
|
+
cursor:pointer;border-radius:8px;padding:6px 8px;margin:-6px -8px}
|
|
81
|
+
.homereport:hover{background:var(--hover)}
|
|
82
|
+
.homereport:focus-visible{outline:2px solid var(--accent);outline-offset:0}
|
|
83
|
+
.homereportwho{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
84
|
+
.homereportwho .meta{padding-left:21px}
|
|
85
|
+
.homereportbody p{margin:0 0 4px;color:var(--ink-dim);font-size:13px;line-height:1.5}
|
|
86
|
+
.homereportbody p:first-child{color:var(--ink)}
|
|
87
|
+
.homereportbody a{color:var(--accent)}
|
|
83
88
|
@media (max-width:640px){.homereport{grid-template-columns:1fr;gap:4px}}
|
|
84
89
|
|
|
85
90
|
@media (max-width:640px){
|
|
@@ -8,6 +8,7 @@ import { getLive, post } from "../api.js";
|
|
|
8
8
|
import { sheet } from "../dialog.js";
|
|
9
9
|
import { ago, el, skeleton } from "../dom.js";
|
|
10
10
|
import { daysUntil, sortHome } from "../homesort.js";
|
|
11
|
+
import { mdlite } from "../md.js";
|
|
11
12
|
import { icon, iconHTML, staffIcon } from "../icons.js";
|
|
12
13
|
import { ensureInbox, stampCounts } from "../refresh.js";
|
|
13
14
|
import { go, render } from "../router.js";
|
|
@@ -58,16 +59,29 @@ export function viewHome(m) {
|
|
|
58
59
|
page.append(section("Latest reports"));
|
|
59
60
|
const box = el("div", { className: "card homereports" });
|
|
60
61
|
for (const r of h.reports) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
el("div", {}, r.lines.map((l) => el("p", { textContent: l }))),
|
|
62
|
+
/* The report is markdown the staff member wrote, with links to the work it names. The
|
|
63
|
+
whole report opens their status issue, where it was posted; a link inside it goes
|
|
64
|
+
where it points. */
|
|
65
|
+
const row = el("div", { className: "homereport homeclick", tabIndex: 0, role: "button" }, [
|
|
66
|
+
el("div", { className: "homereportwho" }, [
|
|
67
|
+
el("span", { className: "homewho2" }, [icon(staffIcon(r.staff), "ic"), el("b", { textContent: r.staff.name })]),
|
|
68
|
+
el("span", { className: "meta", textContent: ago(r.at) }),
|
|
69
69
|
]),
|
|
70
|
-
|
|
70
|
+
el("div", {
|
|
71
|
+
className: "md homereportbody",
|
|
72
|
+
innerHTML: mdlite(r.lines.join("\n\n"), { repo: r.staff.brain }),
|
|
73
|
+
}),
|
|
74
|
+
]);
|
|
75
|
+
row.onclick = (ev) => {
|
|
76
|
+
if (!ev.target.closest("a, button")) openThread(r.item);
|
|
77
|
+
};
|
|
78
|
+
row.onkeydown = (ev) => {
|
|
79
|
+
if (ev.target === row && (ev.key === "Enter" || ev.key === " ")) {
|
|
80
|
+
ev.preventDefault();
|
|
81
|
+
openThread(r.item);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
box.append(row);
|
|
71
85
|
}
|
|
72
86
|
page.append(box);
|
|
73
87
|
}
|
|
@@ -77,6 +77,20 @@ function paint(m, data) {
|
|
|
77
77
|
g.append(tile("Runs", String(data.spend?.runs ?? 0), "skipped mentions are not counted"));
|
|
78
78
|
m.append(g);
|
|
79
79
|
|
|
80
|
+
/* The table shows at once with the costs already known; the rest are read from GitHub in the
|
|
81
|
+
background, and the screen asks again until they are all in. */
|
|
82
|
+
if (data.pending) {
|
|
83
|
+
m.append(
|
|
84
|
+
el("p", {
|
|
85
|
+
className: "hnote",
|
|
86
|
+
textContent: "Reading the cost of " + data.pending + " run" + (data.pending === 1 ? "" : "s") + " from GitHub…",
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
setTimeout(() => {
|
|
90
|
+
if (S.view === "runs") load(true);
|
|
91
|
+
}, 3000);
|
|
92
|
+
}
|
|
93
|
+
|
|
80
94
|
for (const s of data.staff ?? []) m.append(...staffSection(s));
|
|
81
95
|
|
|
82
96
|
m.append(
|