85 lines
4.8 KiB
JavaScript
85 lines
4.8 KiB
JavaScript
#!/usr/bin/env node
|
|
// tools/packs.mjs — the content packs of a checkout (docs/PACKS.md; server/packs.js is the registry the server runs,
|
|
// shared/packs.js the format):
|
|
//
|
|
// node tools/packs.mjs list [--json] the packs by type (folder packs in packs/, the language packs of
|
|
// public/i18n/), what was skipped and why, the warnings
|
|
// node tools/packs.mjs index [--out <file>] write the pack index — what GET /packs/index.json answers — to
|
|
// packs/index.json (git-ignored), for a static host that cannot run the
|
|
// server's live list; tools/package.mjs writes it into the release zips
|
|
// node tools/packs.mjs check [--strict] list, and exit 1 with --strict when a pack is skipped or warned about
|
|
// --root <dir> another checkout or an unpacked release (default: this checkout)
|
|
//
|
|
// The strings of a language pack are checked by node tools/i18n.mjs check <code> (docs/I18N.md).
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
import { scanPacks, packIndexOf } from '../server/packs.js';
|
|
import { PACK_TYPES, PACK_INDEX_FILE } from '../shared/packs.js';
|
|
|
|
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
const USAGE = 'usage: node tools/packs.mjs list [--json] | index [--out <file>] | check [--strict] [--root <dir>]';
|
|
|
|
/** The pack folders of a checkout. @param {string} root */
|
|
export const packDirs = (root) => ({ publicDir: path.join(root, 'public'), dataDir: path.join(root, 'data'), packsDir: path.join(root, 'packs') });
|
|
|
|
/**
|
|
* Write the pack index of a checkout (default <root>/packs/index.json); returns its body.
|
|
* @param {string} root
|
|
* @param {string} [out]
|
|
*/
|
|
export function writePackIndex(root, out = path.join(root, 'packs', PACK_INDEX_FILE)) {
|
|
const { packs } = scanPacks(packDirs(root));
|
|
const body = packIndexOf(packs);
|
|
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
fs.writeFileSync(out, `${JSON.stringify(body, null, 2)}\n`);
|
|
return body;
|
|
}
|
|
|
|
/**
|
|
* The CLI (exported for tools/i18n.mjs index).
|
|
* @param {string[]} argv
|
|
* @returns {Promise<number>} the exit code
|
|
*/
|
|
export async function main(argv) {
|
|
const [cmd, ...rest] = argv;
|
|
let root = REPO;
|
|
let out = null;
|
|
const flags = new Set();
|
|
for (let i = 0; i < rest.length; i++) {
|
|
const a = rest[i];
|
|
if (a === '--root' || a === '--out') {
|
|
const v = rest[++i];
|
|
if (!v || v.startsWith('--')) { console.error(`packs: ${a} needs a path\n${USAGE}`); return 2; }
|
|
if (a === '--root') root = path.resolve(v); else out = path.resolve(v);
|
|
} else if (a.startsWith('--')) flags.add(a.slice(2));
|
|
else { console.error(`packs: unexpected ${a}\n${USAGE}`); return 2; }
|
|
}
|
|
if (cmd === 'index') {
|
|
const file = out || path.join(root, 'packs', PACK_INDEX_FILE);
|
|
const body = writePackIndex(root, file);
|
|
console.log(`wrote ${path.relative(process.cwd(), file) || file}: ${body.packs.length} pack(s) — ${body.packs.map((p) => `${p.type} ${p.id}`).join(', ') || 'none'}`);
|
|
return 0;
|
|
}
|
|
if (cmd !== 'list' && cmd !== 'check') { console.log(USAGE); return cmd ? 2 : 0; }
|
|
const scan = scanPacks(packDirs(root));
|
|
if (flags.has('json')) { console.log(JSON.stringify({ index: packIndexOf(scan.packs), skipped: scan.skipped, warnings: scan.warnings }, null, 2)); return 0; }
|
|
for (const type of Object.keys(PACK_TYPES)) {
|
|
const list = scan.packs.filter((p) => p.type === type);
|
|
console.log(`${type} (${PACK_TYPES[type].status}: ${PACK_TYPES[type].summary}): ${list.length ? '' : 'none'}`);
|
|
for (const p of list) {
|
|
const m = p.manifest;
|
|
const lang = type === 'lang' ? ` ${m.lang}${m.fallback?.length ? ` → ${m.fallback.join(' → ')}` : ''}` : '';
|
|
const extra = [m.version && `v${m.version}`, m.app && `app ${m.app}${m.compatible ? '' : ' (not this version)'}`, p.strings !== undefined && `${p.strings} strings`, p.files.data && 'game texts'].filter(Boolean).join(', ');
|
|
console.log(` ${p.id.padEnd(16)} ${m.name}${m.englishName !== m.name ? ` (${m.englishName})` : ''}${lang} — ${p.where}${extra ? `; ${extra}` : ''}`);
|
|
}
|
|
}
|
|
for (const k of scan.skipped) console.log(`skipped ${k.where}: ${k.problems.join('; ')}`);
|
|
for (const w of scan.warnings) console.log(`warning ${w.where}: ${w.warning}`);
|
|
return cmd === 'check' && flags.has('strict') && (scan.skipped.length || scan.warnings.length) ? 1 : 0;
|
|
}
|
|
|
|
const invoked = (() => { try { return pathToFileURL(fs.realpathSync(process.argv[1] || '')).href; } catch { return null; } })();
|
|
if (invoked === import.meta.url) main(process.argv.slice(2)).then((code) => { process.exitCode = code; }, (e) => { console.error(`packs: ${e.message}`); process.exitCode = 2; });
|