nukebase 0.0.0-stage → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -2
- package/bin/nukebase.js +97 -0
- package/lib/account-commands.js +369 -0
- package/lib/args.js +47 -0
- package/lib/credentials.js +118 -0
- package/lib/login.js +167 -0
- package/lib/project-commands.js +2323 -0
- package/lib/project.js +125 -0
- package/lib/sdk.js +94 -0
- package/lib/ui.js +91 -0
- package/lib/unzip.js +97 -0
- package/package.json +20 -4
package/lib/project.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// A project folder: finding it, its sys/config.json, and moving sign-ins left
|
|
2
|
+
// there by the old per-project CLI into ~/.nukebase.
|
|
3
|
+
//
|
|
4
|
+
// sys/config.json now holds only non-secret settings: API, uid (which account
|
|
5
|
+
// owns the project), project, account (that account's email or username, for
|
|
6
|
+
// messages), and the exclude/backup/updateExclude/streams settings.
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const creds = require('./credentials');
|
|
11
|
+
const login = require('./login');
|
|
12
|
+
|
|
13
|
+
const DEFAULT_EXCLUDE = ['sys', '.env', 'server/data', 'server/backup', 'server/files'];
|
|
14
|
+
const DEFAULT_BACKUP = ['server/data', 'server/files'];
|
|
15
|
+
const SECRET_FIELDS = ['token', 'password', 'username', 'email', 'privateKey'];
|
|
16
|
+
|
|
17
|
+
function findProject(dir) {
|
|
18
|
+
while (dir !== path.dirname(dir)) {
|
|
19
|
+
if (fs.existsSync(path.join(dir, 'sys', 'config.json'))) return dir;
|
|
20
|
+
dir = path.dirname(dir);
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function configPathOf(root) {
|
|
26
|
+
return path.join(root, 'sys', 'config.json');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readConfig(root) {
|
|
30
|
+
return JSON.parse(fs.readFileSync(configPathOf(root), 'utf8'));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeConfig(root, config) {
|
|
34
|
+
fs.mkdirSync(path.join(root, 'sys'), { recursive: true });
|
|
35
|
+
fs.writeFileSync(configPathOf(root), JSON.stringify(config, null, 2) + '\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The settings a freshly linked or created project gets.
|
|
39
|
+
function newConfig({ api, uid, project, account }, base = {}) {
|
|
40
|
+
const out = {
|
|
41
|
+
API: api, uid, project, account,
|
|
42
|
+
exclude: base.exclude || DEFAULT_EXCLUDE,
|
|
43
|
+
backup: base.backup || DEFAULT_BACKUP,
|
|
44
|
+
updateExclude: base.updateExclude || [],
|
|
45
|
+
streams: base.streams || 4
|
|
46
|
+
};
|
|
47
|
+
for (const [k, v] of Object.entries(base)) if (!(k in out) && !SECRET_FIELDS.includes(k)) out[k] = v;
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sessionFiles(root) {
|
|
52
|
+
const dir = path.join(root, 'sys');
|
|
53
|
+
try {
|
|
54
|
+
return fs.readdirSync(dir).filter(f => f.startsWith('.session-') && f.endsWith('.json')).map(f => path.join(dir, f));
|
|
55
|
+
} catch { return []; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Errors that mean "try again later" rather than "this sign-in is dead": a
|
|
59
|
+
// server without CLI sign-in yet, or no connection. The old sign-in is kept
|
|
60
|
+
// and used as before until the move can happen.
|
|
61
|
+
const isOldServer = (e) => /doesn't support CLI sign-in|Could not reach/.test(e.message);
|
|
62
|
+
|
|
63
|
+
// Move a project's old sign-in into ~/.nukebase, then strip it from
|
|
64
|
+
// sys/config.json. Returns the config to use.
|
|
65
|
+
// On a server without CLI sign-in yet, nothing changes and `legacy` is true:
|
|
66
|
+
// the caller then uses the token from sys/config.json as before.
|
|
67
|
+
async function migrateLegacy(root, config) {
|
|
68
|
+
const hasSecrets = SECRET_FIELDS.some(f => config[f]);
|
|
69
|
+
const sessions = sessionFiles(root);
|
|
70
|
+
if (!hasSecrets && !sessions.length) return { config, legacy: false };
|
|
71
|
+
|
|
72
|
+
const api = config.API;
|
|
73
|
+
const label = config.email || config.username || null;
|
|
74
|
+
let legacy = false;
|
|
75
|
+
|
|
76
|
+
if (config.uid && !creds.get(config.uid) && (config.token || (config.username && config.password))) {
|
|
77
|
+
let out = null;
|
|
78
|
+
if (config.token) {
|
|
79
|
+
try { out = await login.exchange(api, config.uid, config.token); }
|
|
80
|
+
catch (e) { if (isOldServer(e)) legacy = true; }
|
|
81
|
+
}
|
|
82
|
+
if (!out && !legacy && config.username && config.password) {
|
|
83
|
+
try { out = await login.password(api, config.username, config.password); }
|
|
84
|
+
catch (e) { if (isOldServer(e)) legacy = true; }
|
|
85
|
+
}
|
|
86
|
+
if (out) {
|
|
87
|
+
creds.put(out.uid, { label: out.label || label, api, token: out.token, method: 'migrated', created: Date.now() });
|
|
88
|
+
console.log(`🔑 Moved this project's sign-in (${out.label || label}) to ${creds.FILE}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (legacy) return { config, legacy: true };
|
|
92
|
+
|
|
93
|
+
for (const file of sessions) {
|
|
94
|
+
try {
|
|
95
|
+
const s = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
96
|
+
if (s.uid && s.token && !creds.get(s.uid)) {
|
|
97
|
+
const host = path.basename(file).replace(/^\.session-/, '').split('-')[0];
|
|
98
|
+
const sApi = host ? `https://${host}` : api;
|
|
99
|
+
const out = await login.exchange(sApi, s.uid, s.token);
|
|
100
|
+
creds.put(out.uid, { label: out.label || s.email, api: sApi, token: out.token, method: 'migrated', created: Date.now() });
|
|
101
|
+
console.log(`🔑 Moved the saved sign-in for ${out.label || s.email} to ${creds.FILE}`);
|
|
102
|
+
}
|
|
103
|
+
fs.unlinkSync(file);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
// Keep the file to try again later; a dead session is worth nothing.
|
|
106
|
+
if (isOldServer(e)) continue;
|
|
107
|
+
try { fs.unlinkSync(file); } catch {}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (hasSecrets) {
|
|
112
|
+
// Only the sign-in fields go; every other setting is kept as it was.
|
|
113
|
+
const clean = { ...config, account: config.account || label || undefined };
|
|
114
|
+
for (const f of SECRET_FIELDS) delete clean[f];
|
|
115
|
+
writeConfig(root, clean);
|
|
116
|
+
console.log('🔒 Removed the sign-in (token, password) from sys/config.json. Projects now hold only which account owns them.');
|
|
117
|
+
if (config.uid && !creds.get(config.uid)) {
|
|
118
|
+
console.log(` This project's account isn't signed in on this machine. Run: nukebase login${label && label.includes('@') ? ` --email ${label}` : ''}`);
|
|
119
|
+
}
|
|
120
|
+
return { config: clean, legacy: false };
|
|
121
|
+
}
|
|
122
|
+
return { config, legacy: false };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = { findProject, configPathOf, readConfig, writeConfig, newConfig, migrateLegacy, DEFAULT_EXCLUDE, DEFAULT_BACKUP };
|
package/lib/sdk.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// WebSocket client for the CLI: the log tail and the few reads the commands
|
|
2
|
+
// make (custom domain, project node). Signs in with the account's token from
|
|
3
|
+
// the credentials file; it never reads or writes any file itself.
|
|
4
|
+
|
|
5
|
+
const WebSocket = require('ws');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
const REQUEST_TIMEOUT_MS = 30 * 1000;
|
|
9
|
+
|
|
10
|
+
function createClient(url, { uid, token } = {}) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const pending = new Map();
|
|
13
|
+
const subscriptions = new Map(); // key -> { msg, handler }
|
|
14
|
+
let socket;
|
|
15
|
+
let first = true;
|
|
16
|
+
let closed = false;
|
|
17
|
+
|
|
18
|
+
const cookie = uid && token ? `uid=${uid}; token=${token}` : '';
|
|
19
|
+
|
|
20
|
+
// Same keying as the browser SDK: path segments stringified, so [users, 1]
|
|
21
|
+
// and [users, '1'] are one subscription, matching the server's tree.
|
|
22
|
+
const encodePath = (p) => JSON.stringify(Array.isArray(p) ? p.map(String) : (p ?? null));
|
|
23
|
+
const routeKey = (action, event, path) => JSON.stringify([action ?? null, event ?? null, encodePath(path)]);
|
|
24
|
+
|
|
25
|
+
function sendRequest(msg) {
|
|
26
|
+
return new Promise((res, rej) => {
|
|
27
|
+
if (closed) return rej(new Error('connection closed'));
|
|
28
|
+
const requestId = crypto.randomBytes(8).toString('hex');
|
|
29
|
+
msg.requestId = requestId;
|
|
30
|
+
const timer = setTimeout(() => {
|
|
31
|
+
pending.delete(requestId);
|
|
32
|
+
rej(new Error('request timed out'));
|
|
33
|
+
}, REQUEST_TIMEOUT_MS);
|
|
34
|
+
pending.set(requestId, { res, rej, timer });
|
|
35
|
+
try { socket.send(JSON.stringify(msg)); } catch (e) { clearTimeout(timer); pending.delete(requestId); rej(e); }
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function failPending(err) {
|
|
40
|
+
for (const [id, p] of pending) { clearTimeout(p.timer); p.rej(err); pending.delete(id); }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function connect() {
|
|
44
|
+
socket = new WebSocket(url, cookie ? { headers: { Cookie: cookie } } : {});
|
|
45
|
+
|
|
46
|
+
socket.on('message', (data) => {
|
|
47
|
+
let msg;
|
|
48
|
+
try { msg = JSON.parse(data); } catch { return; }
|
|
49
|
+
const p = msg.requestId && pending.get(msg.requestId);
|
|
50
|
+
if (p) { clearTimeout(p.timer); pending.delete(msg.requestId); p.res(msg); return; }
|
|
51
|
+
const sub = subscriptions.get(routeKey(msg.action, msg.event, msg.path));
|
|
52
|
+
if (sub) sub.handler(msg);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
socket.on('open', () => {
|
|
56
|
+
if (first) { first = false; resolve(client); return; }
|
|
57
|
+
for (const { msg } of subscriptions.values()) socket.send(JSON.stringify(msg));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
socket.on('close', () => {
|
|
61
|
+
failPending(new Error('disconnected'));
|
|
62
|
+
if (closed) return;
|
|
63
|
+
if (!first) setTimeout(() => { if (!closed) connect(); }, 5000);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
socket.on('error', (e) => { if (first) { closed = true; reject(e); } });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const client = {
|
|
70
|
+
get: (path) => sendRequest({ action: 'get', path }),
|
|
71
|
+
|
|
72
|
+
getSub({ event = 'value@', path }, handler) {
|
|
73
|
+
const msg = { action: 'getSub', event, path };
|
|
74
|
+
const key = routeKey(msg.action, event, path);
|
|
75
|
+
subscriptions.set(key, { msg, handler });
|
|
76
|
+
socket.send(JSON.stringify(msg));
|
|
77
|
+
return () => {
|
|
78
|
+
subscriptions.delete(key);
|
|
79
|
+
try { socket.send(JSON.stringify({ action: 'getSubStop', event, path })); } catch {}
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
close() {
|
|
84
|
+
closed = true;
|
|
85
|
+
failPending(new Error('connection closed'));
|
|
86
|
+
try { socket.close(); } catch {}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
connect();
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = createClient;
|
package/lib/ui.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Terminal prompts and small helpers shared by the commands.
|
|
2
|
+
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
const { spawn } = require('child_process');
|
|
5
|
+
|
|
6
|
+
// Projects are served from the master's region host; this is where `login`
|
|
7
|
+
// and `create` go when no project or option says otherwise.
|
|
8
|
+
const DEFAULT_API = (process.env.NUKEBASE_API || 'https://usw4.nukebase.com').replace(/\/$/, '');
|
|
9
|
+
|
|
10
|
+
function hostOf(api) {
|
|
11
|
+
try { return new URL(api).host; } catch { return api; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// `--region usw3` swaps the first host label: usw4.nukebase.com -> usw3.nukebase.com.
|
|
15
|
+
function regionApi(base, region) {
|
|
16
|
+
const u = new URL(base);
|
|
17
|
+
const labels = u.hostname.split('.');
|
|
18
|
+
labels[0] = region;
|
|
19
|
+
u.hostname = labels.join('.');
|
|
20
|
+
return u.origin;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function apiFor(args, base) {
|
|
24
|
+
if (args.get('api')) return args.get('api').replace(/\/$/, '');
|
|
25
|
+
if (args.get('region')) return regionApi(base || DEFAULT_API, args.get('region'));
|
|
26
|
+
return (base || DEFAULT_API).replace(/\/$/, '');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeConfirm(autoYes) {
|
|
30
|
+
return function confirm(question) {
|
|
31
|
+
if (autoYes) {
|
|
32
|
+
console.log(question + 'y ▶ auto-confirmed (--yes)');
|
|
33
|
+
return Promise.resolve(true);
|
|
34
|
+
}
|
|
35
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
36
|
+
return new Promise(resolve => {
|
|
37
|
+
let answered = false;
|
|
38
|
+
rl.question(question, answer => { answered = true; rl.close(); resolve(answer.trim().toLowerCase() === 'y'); });
|
|
39
|
+
// EOF with no answer (piped, no terminal): abort rather than hang.
|
|
40
|
+
rl.on('close', () => { if (!answered) resolve(false); });
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// One line of free text. Empty under --yes or without a terminal, so callers
|
|
46
|
+
// fall back to their default.
|
|
47
|
+
function makeAskLine(autoYes) {
|
|
48
|
+
return function askLine(question) {
|
|
49
|
+
if (autoYes || !process.stdin.isTTY) return Promise.resolve('');
|
|
50
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
51
|
+
return new Promise(resolve => {
|
|
52
|
+
let answered = false;
|
|
53
|
+
rl.question(question, answer => { answered = true; rl.close(); resolve(answer); });
|
|
54
|
+
rl.on('close', () => { if (!answered) resolve(''); });
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// A password prompt that doesn't echo what is typed.
|
|
60
|
+
function askHidden(question) {
|
|
61
|
+
if (!process.stdin.isTTY) return Promise.resolve('');
|
|
62
|
+
return new Promise(resolve => {
|
|
63
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
64
|
+
let muted = false;
|
|
65
|
+
rl._writeToOutput = (s) => { if (!muted) rl.output.write(s); };
|
|
66
|
+
rl.question(question, answer => { rl.output.write('\n'); rl.close(); resolve(answer); });
|
|
67
|
+
muted = true;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Open a URL in the default browser. Best effort: the URL is always printed
|
|
72
|
+
// too, for SSH sessions and machines without one.
|
|
73
|
+
function openBrowser(url) {
|
|
74
|
+
if (process.env.NUKEBASE_NO_BROWSER) return;
|
|
75
|
+
try {
|
|
76
|
+
let child;
|
|
77
|
+
if (process.platform === 'win32') {
|
|
78
|
+
// rundll32 takes the URL as one argument, so & in the query survives
|
|
79
|
+
// (cmd /c start would split on it).
|
|
80
|
+
child = spawn('rundll32', ['url.dll,FileProtocolHandler', url], { stdio: 'ignore', detached: true });
|
|
81
|
+
} else if (process.platform === 'darwin') {
|
|
82
|
+
child = spawn('open', [url], { stdio: 'ignore', detached: true });
|
|
83
|
+
} else {
|
|
84
|
+
child = spawn('xdg-open', [url], { stdio: 'ignore', detached: true });
|
|
85
|
+
}
|
|
86
|
+
child.on('error', () => {});
|
|
87
|
+
child.unref();
|
|
88
|
+
} catch {}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { DEFAULT_API, hostOf, regionApi, apiFor, makeConfirm, makeAskLine, askHidden, openBrowser };
|
package/lib/unzip.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Extract the project ZIP that /project streams back, without a dependency.
|
|
2
|
+
//
|
|
3
|
+
// Reads the central directory (always at the end, with real sizes even when
|
|
4
|
+
// the archive was written as a stream) and supports the two methods Info-ZIP
|
|
5
|
+
// writes: stored (0) and deflate (8). Zip64 sizes are read from the extra
|
|
6
|
+
// field. Every entry must land inside `dest`; links are not created.
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const zlib = require('zlib');
|
|
11
|
+
|
|
12
|
+
function findEndOfCentralDir(buf) {
|
|
13
|
+
for (let i = buf.length - 22; i >= Math.max(0, buf.length - 65557); i--) {
|
|
14
|
+
if (buf.readUInt32LE(i) === 0x06054b50) return i;
|
|
15
|
+
}
|
|
16
|
+
throw new Error('Not a ZIP file (no end-of-central-directory record)');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function zip64Values(extra, wanted) {
|
|
20
|
+
// wanted: which of [uncompressed, compressed, localOffset] were 0xFFFFFFFF
|
|
21
|
+
let p = 0;
|
|
22
|
+
while (p + 4 <= extra.length) {
|
|
23
|
+
const id = extra.readUInt16LE(p);
|
|
24
|
+
const size = extra.readUInt16LE(p + 2);
|
|
25
|
+
if (id === 0x0001) {
|
|
26
|
+
const out = {};
|
|
27
|
+
let q = p + 4;
|
|
28
|
+
for (const name of wanted) { out[name] = Number(extra.readBigUInt64LE(q)); q += 8; }
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
p += 4 + size;
|
|
32
|
+
}
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function extract(buf, dest) {
|
|
37
|
+
const root = path.resolve(dest);
|
|
38
|
+
const eocd = findEndOfCentralDir(buf);
|
|
39
|
+
let count = buf.readUInt16LE(eocd + 10);
|
|
40
|
+
let cdOffset = buf.readUInt32LE(eocd + 16);
|
|
41
|
+
if (cdOffset === 0xFFFFFFFF || count === 0xFFFF) {
|
|
42
|
+
// Zip64 end-of-central-directory locator sits just before the EOCD.
|
|
43
|
+
const loc = eocd - 20;
|
|
44
|
+
if (loc >= 0 && buf.readUInt32LE(loc) === 0x07064b50) {
|
|
45
|
+
const z64 = Number(buf.readBigUInt64LE(loc + 8));
|
|
46
|
+
count = Number(buf.readBigUInt64LE(z64 + 32));
|
|
47
|
+
cdOffset = Number(buf.readBigUInt64LE(z64 + 48));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let p = cdOffset;
|
|
52
|
+
let files = 0;
|
|
53
|
+
for (let n = 0; n < count; n++) {
|
|
54
|
+
if (buf.readUInt32LE(p) !== 0x02014b50) throw new Error('Corrupt ZIP (central directory)');
|
|
55
|
+
const method = buf.readUInt16LE(p + 10);
|
|
56
|
+
let compSize = buf.readUInt32LE(p + 20);
|
|
57
|
+
let size = buf.readUInt32LE(p + 24);
|
|
58
|
+
const nameLen = buf.readUInt16LE(p + 28);
|
|
59
|
+
const extraLen = buf.readUInt16LE(p + 30);
|
|
60
|
+
const commentLen = buf.readUInt16LE(p + 32);
|
|
61
|
+
const externalAttr = buf.readUInt32LE(p + 38);
|
|
62
|
+
let localOffset = buf.readUInt32LE(p + 42);
|
|
63
|
+
const name = buf.toString('utf8', p + 46, p + 46 + nameLen);
|
|
64
|
+
const extra = buf.subarray(p + 46 + nameLen, p + 46 + nameLen + extraLen);
|
|
65
|
+
const wanted = [];
|
|
66
|
+
if (size === 0xFFFFFFFF) wanted.push('size');
|
|
67
|
+
if (compSize === 0xFFFFFFFF) wanted.push('compSize');
|
|
68
|
+
if (localOffset === 0xFFFFFFFF) wanted.push('localOffset');
|
|
69
|
+
if (wanted.length) ({ size = size, compSize = compSize, localOffset = localOffset } = zip64Values(extra, wanted));
|
|
70
|
+
p += 46 + nameLen + extraLen + commentLen;
|
|
71
|
+
|
|
72
|
+
const target = path.resolve(root, name);
|
|
73
|
+
if (target !== root && !target.startsWith(root + path.sep)) throw new Error(`ZIP entry outside the project: ${name}`);
|
|
74
|
+
const unixMode = externalAttr >>> 16;
|
|
75
|
+
if ((unixMode & 0o170000) === 0o120000) continue; // symlink: skipped
|
|
76
|
+
if (name.endsWith('/')) { fs.mkdirSync(target, { recursive: true }); continue; }
|
|
77
|
+
|
|
78
|
+
if (buf.readUInt32LE(localOffset) !== 0x04034b50) throw new Error(`Corrupt ZIP (local header for ${name})`);
|
|
79
|
+
const dataStart = localOffset + 30 + buf.readUInt16LE(localOffset + 26) + buf.readUInt16LE(localOffset + 28);
|
|
80
|
+
const raw = buf.subarray(dataStart, dataStart + compSize);
|
|
81
|
+
let data;
|
|
82
|
+
if (method === 0) data = raw;
|
|
83
|
+
else if (method === 8) data = zlib.inflateRawSync(raw);
|
|
84
|
+
else throw new Error(`Unsupported ZIP compression (method ${method}) for ${name}`);
|
|
85
|
+
if (data.length !== size) throw new Error(`Corrupt ZIP (size mismatch for ${name})`);
|
|
86
|
+
|
|
87
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
88
|
+
fs.writeFileSync(target, data);
|
|
89
|
+
if (process.platform !== 'win32' && (unixMode & 0o111)) {
|
|
90
|
+
try { fs.chmodSync(target, unixMode & 0o777); } catch {}
|
|
91
|
+
}
|
|
92
|
+
files++;
|
|
93
|
+
}
|
|
94
|
+
return files;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = { extract };
|
package/package.json
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nukebase",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "NukeBase CLI: create, deploy and manage NukeBase projects",
|
|
5
|
+
"bin": {
|
|
6
|
+
"nukebase": "bin/nukebase.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin",
|
|
10
|
+
"lib"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"tar": "^7.5.15",
|
|
17
|
+
"ws": "^8.20.0"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://nukebase.com",
|
|
20
|
+
"keywords": ["nukebase", "database", "realtime", "hosting", "cli", "deploy"],
|
|
21
|
+
"license": "MIT"
|
|
22
|
+
}
|