Files
stack/agents/sage/work/vikunja-setup/before-19.mjs
T
jason.woltjeandClaude Opus 5.5 c8a7389615 vikunja: Q61a #19 install, bus on #19, SetSpark #35 and Launchpad #36; shares read (sage)
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]>
2026-10-11 13:22:50 -05:00

56 lines
3.1 KiB
JavaScript

// Sage, 2026-10-11: read-only, Q61a. With the sync key, saves project #19's state before the agents
// install the five buckets: the project, its views, each kanban view's buckets, the tasks in each
// bucket, and every task's done flag. GETs only; refuses to overwrite OUT; prints no secret.
// Usage: node before-19.mjs https://tasks.woltje.com SYNC_TOKEN_FILE OUT_JSON
import { readFileSync, writeFileSync, existsSync } from "node:fs";
const [ORIGIN, KEY, OUT] = process.argv.slice(2);
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
const BASE = ORIGIN + "/api/v2", P = 19;
const key = readFileSync(KEY, "utf8").trim();
const get = async (path) => {
const r = await fetch(BASE + path, { headers: { Authorization: `Bearer ${key}` }, redirect: "error" });
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
if (r.status !== 200) throw new Error(`GET ${path}: ${r.status} ${json?.code ?? ""} ${json?.message ?? ""}`);
return json;
};
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
async function all(path) {
const out = [];
for (let page = 1; page < 100; page++) {
const items = list(await get(`${path}${path.includes("?") ? "&" : "?"}per_page=50&page=${page}`));
out.push(...items);
if (items.length < 50) return out;
}
throw new Error(`${path}: more than 99 pages`);
}
const p = await get(`/projects/${P}`);
const rec = {
at: new Date().toISOString(),
project: { id: p.id, title: p.title, parent: p.parent_project_id, archived: p.is_archived, done_bucket_id: p.done_bucket_id ?? null, default_bucket_id: p.default_bucket_id ?? null },
views: [], tasks: [],
};
for (const v of await all(`/projects/${P}/views`)) {
const view = { id: v.id, title: v.title, kind: v.view_kind, position: v.position, mode: v.bucket_configuration_mode, done_bucket_id: v.done_bucket_id, default_bucket_id: v.default_bucket_id, filter: v.filter ?? null };
if (v.view_kind === "kanban") {
view.buckets = (await all(`/projects/${P}/views/${v.id}/buckets`)).map((b) => ({ id: b.id, title: b.title, position: b.position, limit: b.limit, count: b.count, tasks: [] }));
// The whole board, done tasks included, as the adapter reads it (sync.mjs board()).
const byId = new Map(view.buckets.map((b) => [b.id, b]));
for (let page = 1; page < 100; page++) {
let more = false;
for (const b of list(await get(`/projects/${P}/views/${v.id}/buckets/tasks?per_page=50&page=${page}`))) {
const tasks = b.tasks ?? [];
byId.get(b.id)?.tasks.push(...tasks.map((t) => t.id));
if (Number.isSafeInteger(b.count) ? b.count > page * 50 : tasks.length >= 50) more = true;
}
if (!more) break;
}
}
rec.views.push(view);
}
rec.tasks = (await all(`/projects/${P}/tasks`)).map((t) => ({ id: t.id, title: t.title, done: t.done, bucket_id: t.bucket_id ?? null, project_id: t.project_id }));
const out = JSON.stringify(rec, null, 1) + "\n";
if (out.includes(key)) throw new Error("secret in output; not written");
writeFileSync(OUT, out, { flag: "wx" });
process.stdout.write(out);