@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.
@@ -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.0",
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": {
@@ -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 fetch(api, { headers: { 'User-Agent': 'maravilla-cli-installer' } });
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 fetch(url);
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) { if (bar) bar.tick(chunk.length); super.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
- const sumsRes = await fetch(sumsUrl, { headers: { 'User-Agent': 'maravilla-cli-installer' } });
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
- // Simply rerun the postinstall script to fetch latest/specified version
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 child = spawn(process.execPath, [postinstall], { stdio: 'inherit' });
12
- child.on('exit', (code) => process.exit(code ?? 0));
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
+ });