import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { createHash, randomUUID } from 'node:crypto'; import { fileURLToPath } from 'node:url'; export { fs, path, os }; export const HERE=path.dirname(fileURLToPath(import.meta.url)), ROOT=path.resolve(HERE, '../..'); export class DecodeError extends Error { } export const fail=m=>{ throw new DecodeError(m); }; export const sha256=b=>createHash('sha256').update(b).digest('hex'); export function sha256_file(p){ const fd=fs.openSync(p, 'r'), h=createHash('sha256'), b=Buffer.alloc(1024*1024); try{ let n; while((n=fs.readSync(fd, b, 0, b.length, null)))h.update(b.subarray(0, n)); return h.digest('hex'); } finally{ fs.closeSync(fd); } } export const readText=p=>fs.readFileSync(p, 'utf8').replace(/^\uFEFF/, ''); export const escapeRE=s=>s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); export const matches=(s, re)=>[...s.matchAll(re)]; export const small=v=>typeof v==='bigint'&&v<=BigInt(Number.MAX_SAFE_INTEGER)&&v>=BigInt(Number.MIN_SAFE_INTEGER)?Number(v):v; export const zigzag=v=>small((BigInt(v)>>1n)^-(BigInt(v)&1n)); export const cat=(...parts)=>Buffer.concat(parts.flat()); export function dict(entries=[]){ const keys=[], target={ }; const out=new Proxy(target, { ownKeys:()=>keys, set(t, k, v){ if(!Object.hasOwn(t, k))keys.push(k); Reflect.defineProperty(t,k,{value:v,writable:true,enumerable:true,configurable:true}); return true; }, deleteProperty(t, k){ const i=keys.indexOf(k); if(i>=0)keys.splice(i, 1); return delete t[k]; } }); for(const [k, v]of entries)out[String(k)]=v; return out; } export class Float { constructor(value){ this.value=value; } } export function json(value, indent=2, ascii=false){ const quote=s=>{ let q=JSON.stringify(s); return ascii?q.replace(/[\u007f-\uffff]/g, c=>'\\u'+c.charCodeAt(0).toString(16).padStart(4, '0')):q; }; const walk=(v, d)=>{ if(v===null)return 'null'; if(v instanceof Float){ let s=String(v.value); return s.includes('.')||s.includes('e')?s:s+'.0'; } if(typeof v==='bigint')return String(v); if(typeof v==='string')return quote(v); if(typeof v==='number'){ if(!Number.isFinite(v))fail('Non-finite JSON number'); return String(v); } if(typeof v==='boolean')return String(v); if(typeof v!=='object')fail('Unsupported JSON value'); const arr=Array.isArray(v), entries=arr?v.map(x=>walk(x, d+1)):Object.keys(v).map(k=>quote(k)+': '+walk(v[k], d+1)); if(!entries.length)return arr?'[]':'{}'; const pad=' '.repeat(indent*(d+1)); return (arr?'[':'{')+'\n'+pad+entries.join(',\n'+pad)+'\n'+' '.repeat(indent*d)+(arr?']':'}'); }; return walk(value, 0)+'\n'; } // Parse integers losslessly and retain dictionary insertion order, including numeric keys. export function parseJSON(text){ let i=0; const ws=()=>{ while(/\s/.test(text[i]??'')&&iparseJSON(readText(p)); export function replace(p, data){ fs.mkdirSync(path.dirname(p), { recursive:true }); const tmp=path.join(path.dirname(p), '.tmp-'+randomUUID()); let fd; try{ fd=fs.openSync(tmp, 'wx', 0o600); fs.writeFileSync(fd, data); fs.fsyncSync(fd); fs.closeSync(fd); fd=undefined; fs.chmodSync(tmp, 0o644); fs.renameSync(tmp, p); } finally{ if(fd!==undefined)fs.closeSync(fd); fs.rmSync(tmp, { force:true }); } } export const atomic_json=(p, v, indent=2, ascii=false)=>replace(p, json(v, indent, ascii)); export const globBytes=dir=>fs.existsSync(dir)?fs.readdirSync(dir).filter(n=>n.endsWith('.bytes')).sort().map(n=>path.join(dir, n)):[]; export function cli(required=[], defaults={ }, bools=[], multi=[]){ const a={ ...defaults }; for(let i=2; i'--'+k+' VALUE').join(' ')}\nOptions: ${[...Object.keys(defaults),...bools,...multi].map(k=>'--'+k).join(', ')}`); process.exit(0); } const m=flag.match(/^--([\w-]+)(?:=(.*))?$/); if(!m)fail('Unknown argument '+flag); const k=m[1]; if(![...required, ...Object.keys(defaults), ...bools, ...multi].includes(k))fail('Unknown argument '+flag); if(bools.includes(k)){ if(m[2]!==undefined)fail('Flag takes no value: --'+k); a[k]=true; continue; } const v=m[2]??process.argv[++i]; if(v===undefined||v.startsWith('--'))fail('Missing value '+flag); if(multi.includes(k))(a[k]??=[]).push(v); else if(typeof defaults[k]==='number'||k==='season-id'){ if(!/^[+-]?\d+$/.test(v)||!Number.isSafeInteger(Number(v)))fail('Expected integer for --'+k); a[k]=Number(v); } else a[k]=v; } for(const k of required)if(a[k]===undefined)fail('Required --'+k); return a; } export function runMain(url, fn){ if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(url)){ try{ const result=fn(); if(result?.catch)result.catch(e=>{ console.error('Extraction stopped: '+e.message); process.exitCode=1; }); } catch(e){ console.error('Extraction stopped: '+e.message); process.exitCode=1; } } } export const subset=(a, b)=>[...a].every(v=>b.has(v)); export const uniqueValues=(rows, key)=>new Set(rows.map(key)).size===rows.length; export function lookup(rows, key, label='native key'){ const out=new Map(); for(const r of rows){ const k=key(r); if(out.has(k))fail('Duplicate '+label); out.set(k, r); } return out; } export const need=(map, key)=>map.get(key)??fail('Missing native reference '+key); export const resource=name=>path.join(ROOT, 'resources/grid_fight', name); export const catalogDefault=path.join(ROOT, 'protocol/evidence/cw-mechanics/catalog.json');