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]>
59 lines
3.8 KiB
JavaScript
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);
|