Files
stack/agents/sage/work/vikunja-setup/create-33.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

59 lines
3.8 KiB
JavaScript

// Sage, 2026-10-11: Q61a, the first real work. The pm key creates "SetSpark" and "Launchpad" under
// #33 "Business", with no tasks. Refuses if #33 already has a child by either title. The sync key
// reads each new project back. Rollback is an archive with the pm key; deletion is Jason's or
// ops-01's. Prints no secret.
// Usage: node create-33.mjs https://tasks.woltje.com PM_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
import { readFileSync, writeFileSync, existsSync } from "node:fs";
const [ORIGIN, KEY, SYNC, 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");
// v2.7.0 serves reads under /api/v2; project create is PUT /api/v1/projects (GET /api/v2/routes).
const PARENT = 33, TITLES = ["SetSpark", "Launchpad"];
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
async function api(method, path, body, auth) {
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
return { status: r.status, json };
}
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
async function projects() {
const out = [];
for (let page = 1; page < 20; page++) {
const r = await api("GET", `/api/v2/projects?per_page=50&page=${page}&is_archived=true`, undefined, sync);
if (r.status !== 200) throw new Error(`list projects: ${r.status}`);
out.push(...list(r.json));
if (list(r.json).length < 50) return out;
}
throw new Error("more than 19 pages");
}
const rec = { at: new Date().toISOString(), parent: PARENT, created: [], problems: [] };
const save = () => {
const out = JSON.stringify(rec, null, 1) + "\n";
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
writeFileSync(OUT, out);
};
const before = await projects();
if (!before.some((p) => p.id === PARENT)) throw new Error(`#${PARENT} not visible`);
const clash = before.filter((p) => TITLES.includes(p.title));
if (clash.length) throw new Error(`a project titled ${clash.map((p) => `${p.title} (#${p.id}, parent ${p.parent_project_id})`).join(", ")} exists; refusing`);
writeFileSync(OUT, "", { flag: "wx" });
for (const title of TITLES) {
rec.created.push({ title });
save();
const r = await api("PUT", "/api/v1/projects", { title, parent_project_id: PARENT }, key);
if (r.status >= 300 || !Number.isSafeInteger(r.json?.id)) {
rec.problems.push(`create ${title}: ${r.status} ${r.json?.code ?? ""} ${r.json?.message ?? ""}`);
break;
}
Object.assign(rec.created.at(-1), { id: r.json.id, parent: r.json.parent_project_id, inverse: `archive: POST /api/v1/projects/${r.json.id} is_archived true (pm); delete is Jason's or ops-01's` });
const back = await api("GET", `/api/v2/projects/${r.json.id}`, undefined, sync);
const tasks = await api("GET", `/api/v2/projects/${r.json.id}/tasks?per_page=1`, undefined, sync);
Object.assign(rec.created.at(-1), { readBack: back.status, title_back: back.json?.title, parent_back: back.json?.parent_project_id, archived: back.json?.is_archived, tasks: tasks.status === 200 ? list(tasks.json).length : `status ${tasks.status}` });
if (back.status !== 200 || back.json?.title !== title || back.json?.parent_project_id !== PARENT || back.json?.is_archived) rec.problems.push(`${title} read-back differs`);
if (tasks.status !== 200 || list(tasks.json).length !== 0) rec.problems.push(`${title} task read not empty or failed`);
}
save();
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
if (rec.problems.length) process.exit(2);