Files the ENGINEERING_REVIEW_SCORECARD.md P0-P3 action plan as tracker items (one per affected product) via the platform-service POST /api/items API. Dependency-free Node seeder mints an HS256 token from $JWT_SECRET, dedupes by title, and supports --dry-run. No live writes performed (stack is down); run the script once the platform stack is up. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
139 lines
5.0 KiB
JavaScript
139 lines
5.0 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Seed ByteLyst tracker items from a JSON payload file.
|
|
*
|
|
* Creates feature/bug/task items via the platform-service tracker API
|
|
* (POST /api/items — the same endpoint tracker-web proxies to). Items are
|
|
* scoped per `productId`. Use this to file the ENGINEERING_REVIEW_SCORECARD.md
|
|
* P0-P3 work once the platform stack is running.
|
|
*
|
|
* Auth: mints a short-lived HS256 access token signed with $JWT_SECRET
|
|
* (same secret platform-service verifies with). No external deps — uses
|
|
* node:crypto only.
|
|
*
|
|
* Env:
|
|
* PLATFORM_API_URL Base URL of platform-service (default http://localhost:4003)
|
|
* JWT_SECRET Shared JWT secret (required unless --dry-run)
|
|
* SEED_SUB Token subject / reportedBy (default "eng-review-bot")
|
|
* SEED_EMAIL Token email claim (default "eng-review-bot@bytelyst.local")
|
|
* SEED_ROLE Token role claim (default "admin")
|
|
* ITEMS_FILE Payload file (default ./engineering-review-items.json)
|
|
*
|
|
* Flags:
|
|
* --dry-run Print what would be created; no token, no network calls.
|
|
* --force Skip the dedupe-by-title check.
|
|
*
|
|
* Examples:
|
|
* node seed-tracker-items.mjs --dry-run
|
|
* JWT_SECRET=... PLATFORM_API_URL=http://localhost:4003 node seed-tracker-items.mjs
|
|
*/
|
|
|
|
import { readFileSync } from 'node:fs';
|
|
import { createHmac } from 'node:crypto';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, resolve } from 'node:path';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
|
const args = new Set(process.argv.slice(2));
|
|
const DRY_RUN = args.has('--dry-run');
|
|
const FORCE = args.has('--force');
|
|
|
|
const API = (process.env.PLATFORM_API_URL || 'http://localhost:4003').replace(/\/$/, '');
|
|
const SECRET = process.env.JWT_SECRET || '';
|
|
const SUB = process.env.SEED_SUB || 'eng-review-bot';
|
|
const EMAIL = process.env.SEED_EMAIL || 'eng-review-bot@bytelyst.local';
|
|
const ROLE = process.env.SEED_ROLE || 'admin';
|
|
const ITEMS_FILE = resolve(__dirname, process.env.ITEMS_FILE || 'engineering-review-items.json');
|
|
|
|
function b64url(buf) {
|
|
return Buffer.from(buf).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
}
|
|
|
|
/** Mint an HS256 access token compatible with @bytelyst/auth extractAuth(). */
|
|
function mintToken() {
|
|
if (!SECRET) throw new Error('JWT_SECRET must be set (or use --dry-run)');
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const header = { alg: 'HS256', typ: 'JWT' };
|
|
const payload = { sub: SUB, email: EMAIL, role: ROLE, type: 'access', iat: now, exp: now + 3600 };
|
|
const head = b64url(JSON.stringify(header));
|
|
const body = b64url(JSON.stringify(payload));
|
|
const sig = b64url(createHmac('sha256', SECRET).update(`${head}.${body}`).digest());
|
|
return `${head}.${body}.${sig}`;
|
|
}
|
|
|
|
async function listTitles(token, productId) {
|
|
const url = `${API}/api/items?productId=${encodeURIComponent(productId)}&limit=500`;
|
|
const res = await fetch(url, {
|
|
headers: { authorization: `Bearer ${token}`, 'x-product-id': productId },
|
|
});
|
|
if (!res.ok) throw new Error(`list failed (${res.status})`);
|
|
const data = await res.json();
|
|
return new Set((data.items || []).map((i) => i.title));
|
|
}
|
|
|
|
async function createItem(token, item) {
|
|
const res = await fetch(`${API}/api/items`, {
|
|
method: 'POST',
|
|
headers: {
|
|
authorization: `Bearer ${token}`,
|
|
'content-type': 'application/json',
|
|
'x-product-id': item.productId,
|
|
},
|
|
body: JSON.stringify(item),
|
|
});
|
|
const text = await res.text();
|
|
if (!res.ok) throw new Error(`${res.status} ${text.slice(0, 300)}`);
|
|
return JSON.parse(text);
|
|
}
|
|
|
|
async function main() {
|
|
const raw = JSON.parse(readFileSync(ITEMS_FILE, 'utf8'));
|
|
const items = raw.items || raw;
|
|
console.log(`Loaded ${items.length} item(s) from ${ITEMS_FILE}`);
|
|
console.log(`Target: ${API}/api/items (dry-run=${DRY_RUN}, force=${FORCE})\n`);
|
|
|
|
if (DRY_RUN) {
|
|
for (const it of items) {
|
|
console.log(` [DRY] ${it.productId.padEnd(16)} ${it.type}/${it.priority} ${it.title}`);
|
|
}
|
|
console.log(`\nDry run only — nothing sent. ${items.length} item(s) would be created.`);
|
|
return;
|
|
}
|
|
|
|
const token = mintToken();
|
|
const titleCache = new Map();
|
|
let created = 0;
|
|
let skipped = 0;
|
|
let failed = 0;
|
|
|
|
for (const it of items) {
|
|
try {
|
|
if (!FORCE) {
|
|
if (!titleCache.has(it.productId)) {
|
|
titleCache.set(it.productId, await listTitles(token, it.productId).catch(() => new Set()));
|
|
}
|
|
if (titleCache.get(it.productId).has(it.title)) {
|
|
console.log(` SKIP ${it.productId.padEnd(16)} ${it.title} (already exists)`);
|
|
skipped++;
|
|
continue;
|
|
}
|
|
}
|
|
const res = await createItem(token, it);
|
|
console.log(` OK ${it.productId.padEnd(16)} ${res.id} ${it.title}`);
|
|
created++;
|
|
} catch (err) {
|
|
console.error(` FAIL ${it.productId.padEnd(16)} ${it.title}\n ${err.message}`);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
console.log(`\nDone. created=${created} skipped=${skipped} failed=${failed}`);
|
|
if (failed > 0) process.exit(1);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(`Fatal: ${err.message}`);
|
|
process.exit(1);
|
|
});
|