install-19.mjs renames 57 done, 159 todo, 161 in-progress, 162 blocked, adds 248 in-review and moves the default to 159; no task moved. Writes go to /api/v1 (v2.7.0 405s bucket writes under /api/v2). create-33.mjs adds #35 and #36 under #33. Records hold no secret; shares-read titles removed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
50 lines
3.0 KiB
JavaScript
50 lines
3.0 KiB
JavaScript
// Sage, 2026-10-11: read-only, Q61a step 4. After ops-01's team shares on #3, #4 and #33, each bot
|
|
// key lists the projects it sees (id, parent, archived, max_permission). The sync key then reads one
|
|
// child of #33 and its first task page. GETs only; prints no secret.
|
|
// Usage: node shares-read.mjs https://tasks.woltje.com TOKEN_DIR
|
|
import { readFileSync } from "node:fs";
|
|
const [ORIGIN, DIR] = process.argv.slice(2);
|
|
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
|
const BASE = ORIGIN + "/api/v2";
|
|
const ROLES = ["pm", "cto", "coder", "reviewer", "sync"];
|
|
const keys = Object.fromEntries(ROLES.map((r) => [r, readFileSync(`${DIR}/${r}-vikunja.token`, "utf8").trim()]));
|
|
const get = async (path, auth) => {
|
|
const r = await fetch(BASE + path, { headers: { Authorization: `Bearer ${auth}` }, redirect: "error" });
|
|
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
|
return { status: r.status, json };
|
|
};
|
|
async function projects(auth) {
|
|
const out = [];
|
|
for (let page = 1; page < 20; page++) {
|
|
const r = await get(`/projects?per_page=50&page=${page}&is_archived=true`, auth);
|
|
if (r.status !== 200) return { status: r.status, items: out };
|
|
const items = r.json?.items ?? (Array.isArray(r.json) ? r.json : []);
|
|
out.push(...items);
|
|
if (items.length < 50) break;
|
|
}
|
|
return { status: 200, items: out };
|
|
}
|
|
const rec = { at: new Date().toISOString(), seen: {}, problems: [] };
|
|
for (const role of ROLES) {
|
|
const l = await projects(keys[role]);
|
|
rec.seen[role] = { status: l.status, projects: l.items.map((p) => ({ id: p.id, title: p.title, parent: p.parent_project_id, archived: p.is_archived, max: p.max_permission ?? p.max_right ?? null })) };
|
|
}
|
|
const sync = rec.seen.sync.projects;
|
|
const byId = new Map(sync.map((p) => [p.id, p]));
|
|
const under = (p, root) => { for (let x = p, n = 0; x && n < 50; x = byId.get(x.parent), n++) if (x.id === root) return true; return false; };
|
|
for (const root of [3, 4, 33]) if (!byId.has(root)) rec.problems.push(`sync does not see #${root}`);
|
|
rec.descendants = Object.fromEntries([3, 4, 33].map((root) => [root, sync.filter((p) => p.id !== root && under(p, root)).map((p) => p.id)]));
|
|
for (const role of ROLES.filter((r) => r !== "sync")) {
|
|
const ids = rec.seen[role].projects.map((p) => p.id).sort((a, b) => a - b).join();
|
|
if (ids !== sync.map((p) => p.id).sort((a, b) => a - b).join()) rec.problems.push(`${role} sees a different project set than sync`);
|
|
}
|
|
const child = rec.descendants[33]?.[0];
|
|
if (child) {
|
|
const p = await get(`/projects/${child}`, keys.sync);
|
|
const t = await get(`/projects/${child}/tasks?per_page=1`, keys.sync);
|
|
rec.childOf33 = { id: child, project: p.status, title: p.json?.title, tasks: t.status, firstPage: (t.json?.items ?? (Array.isArray(t.json) ? t.json : [])).length };
|
|
} else rec.problems.push("no child of #33 visible");
|
|
const out = JSON.stringify(rec, null, 1) + "\n";
|
|
if (Object.values(keys).some((s) => out.includes(s))) throw new Error("secret in output; not printed");
|
|
process.stdout.write(out);
|