build_bundle.js

161 lines · 4.9 KB

Open raw

Copy & run

wget https://perlytiara.github.io/turtles.tips/raw/programs/perlytiara/mastermine/scripts/build_bundle.js
1// Simple bundler to regenerate _deploy_pastebin/mastermine.lua
2// from the contents of programs/perlytiara/mastermine
3// Usage: node scripts/build_bundle.js [--check]
4// If --check is passed, the script only validates that the output is up-to-date.
5
6const fs = require('fs');
7const path = require('path');
8
9const repoRoot = path.resolve(__dirname, '..');
10const outputDir = path.join(repoRoot, '_deploy_pastebin');
11const outputFile = path.join(outputDir, 'mastermine.lua');
12
13const INCLUDE_TOP_LEVEL = new Set([
14 'LICENSE',
15 'README.md',
16 'hub.lua',
17 'pocket.lua',
18 'turtle.lua',
19]);
20
21const INCLUDE_DIRS = new Set([
22 'hub_files',
23 'pocket_files',
24 'turtle_files',
25]);
26
27function listFilesRecursive(baseDir, rel = '') {
28 const full = path.join(baseDir, rel);
29 const entries = fs.readdirSync(full, { withFileTypes: true });
30 const files = [];
31 for (const e of entries) {
32 if (e.name === '.git' || e.name === '.DS_Store') continue;
33 if (rel === '' && e.isDirectory()) {
34 // Only include whitelisted directories at the top-level
35 if (!INCLUDE_DIRS.has(e.name)) continue;
36 }
37 if (rel === '' && e.isFile()) {
38 // Only include whitelisted top-level files
39 if (!INCLUDE_TOP_LEVEL.has(e.name)) continue;
40 }
41 const nextRel = path.posix.join(rel.replace(/\\/g, '/'), e.name);
42 const nextFull = path.join(baseDir, nextRel);
43 if (e.isDirectory()) {
44 for (const f of listFilesRecursive(baseDir, nextRel)) files.push(f);
45 } else if (e.isFile()) {
46 files.push({ relPath: nextRel.replace(/\\/g, '/'), absPath: nextFull });
47 }
48 }
49 return files;
50}
51
52function chooseEquals(content) {
53 // Find a bracket level that doesn't collide with content
54 // We'll try up to 10 '=' signs which should be plenty
55 for (let n = 3; n <= 10; n++) {
56 const marker = ']'+ '='.repeat(n) +']';
57 if (!content.includes(marker)) return '='.repeat(n);
58 }
59 // Fallback: escape closing brackets by splitting
60 return '='.repeat(12);
61}
62
63function buildFilesTable(baseDir) {
64 const files = listFilesRecursive(baseDir);
65 // Stable order: top-level files first, then dirs alphabetically, then files in them
66 files.sort((a, b) => a.relPath.localeCompare(b.relPath));
67 const lines = [];
68 for (let i = 0; i < files.length; i++) {
69 const { relPath, absPath } = files[i];
70 const content = fs.readFileSync(absPath, 'utf8');
71 const eq = chooseEquals(content);
72 const open = `[${eq}[`;
73 const close = `]${eq}]`;
74 const comma = i === files.length - 1 ? '' : ',';
75 // Preserve exact file contents, do not normalize newlines
76 lines.push(` ["${relPath}"] = ${open}${content}${close}${comma}`);
77 }
78 return lines.join('\n');
79}
80
81function generateBundle(baseDir) {
82 const header = [
83 'output_dir = ...',
84 'local function find_disk_mount()',
85 ' for i = 1, 16 do',
86 " local name = (i == 1) and 'disk' or ('disk' .. i)",
87 ' if fs.isDir(name) then return name end',
88 ' end',
89 ' for _, n in ipairs(fs.list("/")) do',
90 ' if string.match(n, "^disk%d*$") and fs.isDir(n) then return n end',
91 ' end',
92 ' return nil',
93 'end',
94 'local function ensure_dir(p)',
95 ' if not fs.isDir(p) then fs.makeDir(p) end',
96 'end',
97 'if not output_dir or output_dir == "" then',
98 ' output_dir = find_disk_mount() or "disk"',
99 'end',
100 'path = shell.resolve(output_dir)',
101 'if not fs.isDir(path) then',
102 ' local alt = find_disk_mount()',
103 ' if alt and fs.isDir(alt) then',
104 ' path = shell.resolve(alt)',
105 ' else',
106 ' path = shell.resolve("files")',
107 ' ensure_dir(path)',
108 ' print("No disk mount found; writing files to ./files")',
109 ' end',
110 'end',
111 '',
112 'files = {',
113 ].join('\n');
114
115 const tableBody = buildFilesTable(baseDir);
116
117 const footer = [
118 '}',
119 '',
120 'local function ensure_dir_for(file_path)',
121 ' local parent = fs.getDir(file_path)',
122 ' if parent ~= "" and not fs.exists(parent) then',
123 ' fs.makeDir(parent)',
124 ' end',
125 'end',
126 '',
127 'for k, v in pairs(files) do',
128 ' local target = fs.combine(path, k)',
129 ' ensure_dir_for(target)',
130 ' local file = fs.open(target, \"w\")',
131 ' file.write(v)',
132 ' file.close()',
133 'end',
134 '',
135 ].join('\n');
136
137 return `${header}\n${tableBody}\n${footer}`;
138}
139
140function ensureDir(p) {
141 if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
142}
143
144function run() {
145 const checkOnly = process.argv.includes('--check');
146 const bundle = generateBundle(repoRoot);
147 ensureDir(outputDir);
148 if (checkOnly && fs.existsSync(outputFile)) {
149 const current = fs.readFileSync(outputFile, 'utf8');
150 const upToDate = current === bundle;
151 process.stdout.write(upToDate ? 'Bundle is up-to-date.\n' : 'Bundle is stale.\n');
152 process.exit(upToDate ? 0 : 1);
153 }
154 fs.writeFileSync(outputFile, bundle, 'utf8');
155 process.stdout.write(`Wrote ${path.relative(repoRoot, outputFile)} (size ${bundle.length} bytes)\n`);
156}
157
158run();
159
160
161