@maravilla-labs/cli 0.1.0 → 0.1.3
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/bin/maravilla-update.js +12 -0
- package/package.json +3 -2
- package/scripts/postinstall.js +82 -4
- package/scripts/update.js +85 -3
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Thin wrapper that runs the package updater
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const updater = path.join(__dirname, '..', 'scripts', 'update.js');
|
|
10
|
+
|
|
11
|
+
const child = spawn(process.execPath, [updater], { stdio: 'inherit' });
|
|
12
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maravilla-labs/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "NPM wrapper for the Maravilla CLI binary; downloads the right release for your platform.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Maravilla Labs",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
},
|
|
15
15
|
"homepage": "https://github.com/solutas/maravilla-runtime#readme",
|
|
16
16
|
"bin": {
|
|
17
|
-
"maravilla": "bin/maravilla"
|
|
17
|
+
"maravilla": "bin/maravilla",
|
|
18
|
+
"maravilla-update": "bin/maravilla-update.js"
|
|
18
19
|
},
|
|
19
20
|
"type": "module",
|
|
20
21
|
"scripts": {
|
package/scripts/postinstall.js
CHANGED
|
@@ -37,17 +37,47 @@ function log(msg) {
|
|
|
37
37
|
console.log(`${flower()} ${msg}`);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
41
|
+
|
|
42
|
+
async function abortableFetch(url, opts = {}, timeoutMs = 30000) {
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const t = setTimeout(() => controller.abort(), timeoutMs);
|
|
45
|
+
try {
|
|
46
|
+
return await fetch(url, { ...opts, signal: controller.signal });
|
|
47
|
+
} finally {
|
|
48
|
+
clearTimeout(t);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function withRetries(taskFn, tries = 3, baseDelay = 1000) {
|
|
53
|
+
let attempt = 0;
|
|
54
|
+
let lastErr;
|
|
55
|
+
while (attempt < tries) {
|
|
56
|
+
try {
|
|
57
|
+
return await taskFn();
|
|
58
|
+
} catch (e) {
|
|
59
|
+
lastErr = e;
|
|
60
|
+
attempt++;
|
|
61
|
+
if (attempt >= tries) break;
|
|
62
|
+
const delay = baseDelay * Math.pow(2, attempt - 1);
|
|
63
|
+
console.log(chalk.yellow(`${flower()} retrying in ${Math.round(delay/100)/10}s...`));
|
|
64
|
+
await sleep(delay);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
throw lastErr;
|
|
68
|
+
}
|
|
69
|
+
|
|
40
70
|
async function getReleaseTag() {
|
|
41
71
|
if (VERSION && VERSION !== 'latest') return VERSION;
|
|
42
72
|
const api = `https://api.github.com/repos/${REPO}/releases/latest`;
|
|
43
|
-
const res = await
|
|
73
|
+
const res = await withRetries(() => abortableFetch(api, { headers: { 'User-Agent': 'maravilla-cli-installer' } }, 20000));
|
|
44
74
|
if (!res.ok) throw new Error(`Failed to fetch latest release: ${res.status} ${res.statusText}`);
|
|
45
75
|
const json = await res.json();
|
|
46
76
|
return json.tag_name;
|
|
47
77
|
}
|
|
48
78
|
|
|
49
79
|
async function downloadWithProgress(url, dest) {
|
|
50
|
-
const res = await
|
|
80
|
+
const res = await withRetries(() => abortableFetch(url, {}, 60000));
|
|
51
81
|
if (!res.ok) throw new Error(`Failed to download ${url}: ${res.status} ${res.statusText}`);
|
|
52
82
|
const total = Number(res.headers.get('content-length')) || 0;
|
|
53
83
|
const bar = total ? new ProgressBar(`${flower()} downloading [:bar] :percent :etas`, {
|
|
@@ -58,9 +88,14 @@ async function downloadWithProgress(url, dest) {
|
|
|
58
88
|
res.body,
|
|
59
89
|
new (class extends fs.WriteStream {
|
|
60
90
|
constructor(dest) { super(dest); }
|
|
61
|
-
write(chunk, enc, cb) {
|
|
91
|
+
write(chunk, enc, cb) {
|
|
92
|
+
if (bar) bar.tick(chunk.length);
|
|
93
|
+
else process.stdout.write('.');
|
|
94
|
+
super.write(chunk, enc, cb);
|
|
95
|
+
}
|
|
62
96
|
})(dest)
|
|
63
97
|
);
|
|
98
|
+
if (!bar) process.stdout.write('\n');
|
|
64
99
|
}
|
|
65
100
|
|
|
66
101
|
async function extractArchive(archivePath, targetDir, ext) {
|
|
@@ -73,8 +108,38 @@ async function extractArchive(archivePath, targetDir, ext) {
|
|
|
73
108
|
}
|
|
74
109
|
}
|
|
75
110
|
|
|
111
|
+
async function currentVersion() {
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
const child = spawn(INSTALL_PATH, ['--version'], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
114
|
+
let out = '';
|
|
115
|
+
child.stdout.on('data', (d) => (out += d.toString()));
|
|
116
|
+
child.on('error', () => resolve(null));
|
|
117
|
+
child.on('exit', (code) => {
|
|
118
|
+
if (code === 0) {
|
|
119
|
+
const v = (out || '').trim();
|
|
120
|
+
const m = v.match(/(\d+\.\d+\.\d+)/);
|
|
121
|
+
resolve(m ? m[1] : null);
|
|
122
|
+
} else {
|
|
123
|
+
resolve(null);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function cmpSemver(a, b) {
|
|
130
|
+
if (!a || !b) return 1;
|
|
131
|
+
const pa = a.split('.').map(Number);
|
|
132
|
+
const pb = b.split('.').map(Number);
|
|
133
|
+
for (let i = 0; i < 3; i++) {
|
|
134
|
+
if (pa[i] > pb[i]) return 1;
|
|
135
|
+
if (pa[i] < pb[i]) return -1;
|
|
136
|
+
}
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
76
140
|
async function install() {
|
|
77
141
|
const { target, ext } = resolveTarget();
|
|
142
|
+
log(`Resolving latest release tag from ${chalk.cyan(REPO)}`);
|
|
78
143
|
const tag = await getReleaseTag();
|
|
79
144
|
const art = artifactName(BIN_NAME, tag, target);
|
|
80
145
|
const assetFile = `${art}.${ext}`;
|
|
@@ -85,6 +150,19 @@ async function install() {
|
|
|
85
150
|
const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'maravilla-cli-'));
|
|
86
151
|
const archive = path.join(tmpDir, assetFile);
|
|
87
152
|
|
|
153
|
+
// Fast path: if installed and matches latest, skip
|
|
154
|
+
if (VERSION === 'latest') {
|
|
155
|
+
try {
|
|
156
|
+
const cur = await currentVersion();
|
|
157
|
+
const m = tag.match(/v(\d+\.\d+\.\d+)/);
|
|
158
|
+
const latest = m ? m[1] : null;
|
|
159
|
+
if (cur && latest && cmpSemver(cur, latest) >= 0) {
|
|
160
|
+
log(chalk.green(`Already up to date (v${cur})`));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
} catch {}
|
|
164
|
+
}
|
|
165
|
+
|
|
88
166
|
log(chalk.bold(`Installing ${BIN_NAME} ${chalk.cyan(tag)} for ${chalk.cyan(target)}`));
|
|
89
167
|
log(`Downloading from ${url}`);
|
|
90
168
|
await downloadWithProgress(url, archive);
|
|
@@ -92,7 +170,7 @@ async function install() {
|
|
|
92
170
|
// Verify checksum if SHA256SUMS is available
|
|
93
171
|
try {
|
|
94
172
|
log('Verifying checksum');
|
|
95
|
-
|
|
173
|
+
const sumsRes = await abortableFetch(sumsUrl, { headers: { 'User-Agent': 'maravilla-cli-installer' } }, 15000);
|
|
96
174
|
if (sumsRes.ok) {
|
|
97
175
|
const sumsText = await sumsRes.text();
|
|
98
176
|
const expected = sumsText
|
package/scripts/update.js
CHANGED
|
@@ -2,11 +2,93 @@
|
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { spawn } from 'child_process';
|
|
5
|
+
import fetch from 'node-fetch';
|
|
6
|
+
import chalk from 'chalk';
|
|
5
7
|
|
|
6
|
-
//
|
|
8
|
+
// Re-run postinstall only when an update is actually needed
|
|
7
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
10
|
const __dirname = path.dirname(__filename);
|
|
9
11
|
const postinstall = path.join(__dirname, 'postinstall.js');
|
|
10
12
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
+
const BIN_NAME = 'maravilla';
|
|
14
|
+
const REPO = process.env.MARAVILLA_CLI_REPO || 'maravilla-labs/maravilla-cli';
|
|
15
|
+
const BIN_DIR = path.join(__dirname, '..', 'bin');
|
|
16
|
+
const EXECUTABLE = process.platform === 'win32' ? `${BIN_NAME}.exe` : BIN_NAME;
|
|
17
|
+
const INSTALL_PATH = path.join(BIN_DIR, EXECUTABLE);
|
|
18
|
+
|
|
19
|
+
function flower() {
|
|
20
|
+
return chalk.magenta('✿');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function log(msg) {
|
|
24
|
+
console.log(`${flower()} ${msg}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function currentVersion() {
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
const child = spawn(INSTALL_PATH, ['--version'], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
30
|
+
let out = '';
|
|
31
|
+
child.stdout.on('data', (d) => (out += d.toString()));
|
|
32
|
+
child.on('error', () => resolve(null));
|
|
33
|
+
child.on('exit', (code) => {
|
|
34
|
+
if (code === 0) {
|
|
35
|
+
const v = (out || '').trim();
|
|
36
|
+
// clap default prints: "maravilla X.Y.Z" — extract semver
|
|
37
|
+
const match = v.match(/(\d+\.\d+\.\d+)/);
|
|
38
|
+
resolve(match ? match[1] : null);
|
|
39
|
+
} else {
|
|
40
|
+
resolve(null);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function latestVersion() {
|
|
47
|
+
const api = `https://api.github.com/repos/${REPO}/releases/latest`;
|
|
48
|
+
const res = await fetch(api, { headers: { 'User-Agent': 'maravilla-cli-updater' } });
|
|
49
|
+
if (!res.ok) throw new Error(`Failed to fetch latest release: ${res.status} ${res.statusText}`);
|
|
50
|
+
const json = await res.json();
|
|
51
|
+
const tag = json.tag_name || '';
|
|
52
|
+
// tags are like cli-vX.Y.Z
|
|
53
|
+
const m = tag.match(/v(\d+\.\d+\.\d+)/);
|
|
54
|
+
return { tag, version: m ? m[1] : null };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function cmpSemver(a, b) {
|
|
58
|
+
if (!a || !b) return 1; // treat unknown as needs update
|
|
59
|
+
const pa = a.split('.').map(Number);
|
|
60
|
+
const pb = b.split('.').map(Number);
|
|
61
|
+
for (let i = 0; i < 3; i++) {
|
|
62
|
+
if (pa[i] > pb[i]) return 1;
|
|
63
|
+
if (pa[i] < pb[i]) return -1;
|
|
64
|
+
}
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function main() {
|
|
69
|
+
// Allow forcing a version via env (MARAVILLA_VERSION)
|
|
70
|
+
const forced = process.env.MARAVILLA_VERSION?.replace(/^v/, '') || null;
|
|
71
|
+
const cur = await currentVersion();
|
|
72
|
+
const { tag, version: latest } = await latestVersion();
|
|
73
|
+
|
|
74
|
+
if (forced) {
|
|
75
|
+
log(chalk.cyan(`Forcing update to v${forced}`));
|
|
76
|
+
} else if (cur && latest && cmpSemver(cur, latest) >= 0) {
|
|
77
|
+
log(chalk.green(`Already up to date (v${cur})`));
|
|
78
|
+
return 0;
|
|
79
|
+
} else if (cur && latest) {
|
|
80
|
+
log(`Updating from v${cur} to ${chalk.cyan(tag)}`);
|
|
81
|
+
} else {
|
|
82
|
+
log(`Updating to ${chalk.cyan(tag)}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return await new Promise((resolve) => {
|
|
86
|
+
const child = spawn(process.execPath, [postinstall], { stdio: 'inherit' });
|
|
87
|
+
child.on('exit', (code) => resolve(code ?? 0));
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
main().then((code) => process.exit(code)).catch((e) => {
|
|
92
|
+
console.error(chalk.red(`${flower()} update failed: ${e.message}`));
|
|
93
|
+
process.exit(1);
|
|
94
|
+
});
|