@maravilla-labs/cli 0.1.5 → 0.1.7

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 CHANGED
Binary file
@@ -2,11 +2,11 @@
2
2
  // Thin wrapper that runs the package updater
3
3
  import { fileURLToPath } from 'url';
4
4
  import path from 'path';
5
- import { spawn } from 'child_process';
5
+ import { createRequire } from 'module';
6
6
 
7
7
  const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = path.dirname(__filename);
9
- const updater = path.join(__dirname, '..', 'scripts', 'update.js');
10
9
 
11
- const child = spawn(process.execPath, [updater], { stdio: 'inherit' });
12
- child.on('exit', (code) => process.exit(code ?? 0));
10
+ // directly import the updater script
11
+ const require = createRequire(import.meta.url);
12
+ await import(path.join(__dirname, '..', 'scripts', 'update.js'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maravilla-labs/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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",
@@ -50,6 +50,7 @@
50
50
  "tar": "^6.2.1",
51
51
  "unzipper": "^0.10.14",
52
52
  "progress": "^2.0.3",
53
- "chalk": "^5.3.0"
53
+ "chalk": "^5.3.0",
54
+ "https-proxy-agent": "^7.0.4"
54
55
  }
55
56
  }
@@ -11,6 +11,7 @@ import ProgressBar from 'progress';
11
11
  import chalk from 'chalk';
12
12
  import { resolveTarget, artifactName } from './platform.js';
13
13
  import crypto from 'crypto';
14
+ import { HttpsProxyAgent } from 'https-proxy-agent';
14
15
 
15
16
  // Allow CI to skip downloading the binary
16
17
  if (process.env.MARAVILLA_CLI_SKIP_INSTALL === '1' || process.env.CI) {
@@ -28,6 +29,10 @@ const VERSION = process.env.MARAVILLA_VERSION || 'latest'; // cli-vX.Y.Z or 'lat
28
29
  const BIN_DIR = path.join(__dirname, '..', 'bin');
29
30
  const EXECUTABLE = process.platform === 'win32' ? `${BIN_NAME}.exe` : BIN_NAME;
30
31
  const INSTALL_PATH = path.join(BIN_DIR, EXECUTABLE);
32
+ const GH_TOKEN = process.env.MARAVILLA_GH_TOKEN || process.env.GITHUB_TOKEN || '';
33
+ const PROXY = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || '';
34
+ const HTTP_TIMEOUT_MS = Number(process.env.MARAVILLA_HTTP_TIMEOUT_MS || 15000);
35
+ const HTTP_RETRIES = Math.max(1, Number(process.env.MARAVILLA_HTTP_RETRIES || 3));
31
36
 
32
37
  function flower() {
33
38
  return chalk.magenta('✿');
@@ -43,13 +48,13 @@ async function abortableFetch(url, opts = {}, timeoutMs = 30000) {
43
48
  const controller = new AbortController();
44
49
  const t = setTimeout(() => controller.abort(), timeoutMs);
45
50
  try {
46
- return await fetch(url, { ...opts, signal: controller.signal });
51
+ return await fetch(url, { ...opts, signal: controller.signal });
47
52
  } finally {
48
53
  clearTimeout(t);
49
54
  }
50
55
  }
51
56
 
52
- async function withRetries(taskFn, tries = 3, baseDelay = 1000) {
57
+ async function withRetries(taskFn, tries = HTTP_RETRIES, baseDelay = 1000) {
53
58
  let attempt = 0;
54
59
  let lastErr;
55
60
  while (attempt < tries) {
@@ -67,13 +72,37 @@ async function withRetries(taskFn, tries = 3, baseDelay = 1000) {
67
72
  throw lastErr;
68
73
  }
69
74
 
75
+ function makeFetchOpts() {
76
+ const headers = { 'User-Agent': 'maravilla-cli-installer' };
77
+ if (GH_TOKEN) headers['Authorization'] = `Bearer ${GH_TOKEN}`;
78
+ const opts = { headers };
79
+ if (PROXY) {
80
+ try { opts.agent = new HttpsProxyAgent(PROXY); } catch {}
81
+ }
82
+ return opts;
83
+ }
84
+
70
85
  async function getReleaseTag() {
71
86
  if (VERSION && VERSION !== 'latest') return VERSION;
72
87
  const api = `https://api.github.com/repos/${REPO}/releases/latest`;
73
- const res = await withRetries(() => abortableFetch(api, { headers: { 'User-Agent': 'maravilla-cli-installer' } }, 20000));
74
- if (!res.ok) throw new Error(`Failed to fetch latest release: ${res.status} ${res.statusText}`);
75
- const json = await res.json();
76
- return json.tag_name;
88
+ const opts = makeFetchOpts();
89
+ try {
90
+ console.log(chalk.gray(`${flower()} fetching latest release tag (${Math.round(HTTP_TIMEOUT_MS/100)/10}s timeout, ${HTTP_RETRIES} retries)`));
91
+ const res = await withRetries(() => abortableFetch(api, opts, HTTP_TIMEOUT_MS));
92
+ if (!res.ok) throw new Error(`Failed to fetch latest release: ${res.status} ${res.statusText}`);
93
+ const json = await res.json();
94
+ if (!json.tag_name) throw new Error('No tag_name in API response');
95
+ return json.tag_name;
96
+ } catch (e) {
97
+ console.log(chalk.yellow(`${flower()} API lookup failed (${e.message}). Falling back to HTML redirect...`));
98
+ // Fallback: use HTML redirect to /releases/tag/<tag>
99
+ const html = `https://github.com/${REPO}/releases/latest`;
100
+ const res = await abortableFetch(html, { ...opts, redirect: 'manual' }, Math.min(HTTP_TIMEOUT_MS, 10000));
101
+ const loc = res.headers.get('location') || '';
102
+ const m = loc.match(/\/releases\/tag\/([^/]+)$/);
103
+ if (m) return m[1];
104
+ throw new Error('Could not resolve latest tag via fallback');
105
+ }
77
106
  }
78
107
 
79
108
  async function downloadWithProgress(url, dest) {
@@ -139,6 +168,19 @@ function cmpSemver(a, b) {
139
168
 
140
169
  async function install() {
141
170
  const { target, ext } = resolveTarget();
171
+
172
+ // Detect current installed version first to enable true offline fast-path
173
+ let cur = null;
174
+ try { cur = await currentVersion(); } catch {}
175
+
176
+ if (process.env.MARAVILLA_OFFLINE === '1') {
177
+ if (cur) {
178
+ log(chalk.yellow(`Offline mode: keeping existing v${cur}`));
179
+ return;
180
+ }
181
+ throw new Error('Offline mode: no existing binary; set MARAVILLA_VERSION to a specific tag or retry online');
182
+ }
183
+
142
184
  log(`Resolving latest release tag from ${chalk.cyan(REPO)}`);
143
185
  const tag = await getReleaseTag();
144
186
  const art = artifactName(BIN_NAME, tag, target);
@@ -155,21 +197,8 @@ async function install() {
155
197
  let requested = null;
156
198
  const m = tag.match(/v(\d+\.\d+\.\d+)/);
157
199
  if (m) requested = m[1];
158
- let cur = null;
159
- try { cur = await currentVersion(); } catch {}
160
200
 
161
201
  if (!force) {
162
- if (process.env.MARAVILLA_OFFLINE === '1') {
163
- if (cur) {
164
- if (requested && cmpSemver(cur, requested) < 0) {
165
- log(chalk.yellow(`Offline mode: cannot update to v${requested}; keeping existing v${cur}`));
166
- return;
167
- }
168
- log(chalk.yellow(`Offline mode: keeping existing v${cur}`));
169
- return;
170
- }
171
- throw new Error('Offline mode: no existing binary to keep');
172
- }
173
202
  if (cur && requested && cmpSemver(cur, requested) >= 0) {
174
203
  log(chalk.green(`Already up to date (v${cur})`));
175
204
  return;
@@ -183,7 +212,7 @@ async function install() {
183
212
  // Verify checksum if SHA256SUMS is available
184
213
  try {
185
214
  log('Verifying checksum');
186
- const sumsRes = await abortableFetch(sumsUrl, { headers: { 'User-Agent': 'maravilla-cli-installer' } }, 15000);
215
+ const sumsRes = await abortableFetch(sumsUrl, makeFetchOpts(), 15000);
187
216
  if (sumsRes.ok) {
188
217
  const sumsText = await sumsRes.text();
189
218
  const expected = sumsText
package/scripts/update.js CHANGED
@@ -3,6 +3,7 @@ import { fileURLToPath } from 'url';
3
3
  import path from 'path';
4
4
  import { spawn } from 'child_process';
5
5
  import fetch from 'node-fetch';
6
+ import { HttpsProxyAgent } from 'https-proxy-agent';
6
7
  import chalk from 'chalk';
7
8
 
8
9
  // Re-run postinstall only when an update is actually needed
@@ -15,6 +16,10 @@ const REPO = process.env.MARAVILLA_CLI_REPO || 'maravilla-labs/maravilla-cli';
15
16
  const BIN_DIR = path.join(__dirname, '..', 'bin');
16
17
  const EXECUTABLE = process.platform === 'win32' ? `${BIN_NAME}.exe` : BIN_NAME;
17
18
  const INSTALL_PATH = path.join(BIN_DIR, EXECUTABLE);
19
+ const GH_TOKEN = process.env.MARAVILLA_GH_TOKEN || process.env.GITHUB_TOKEN || '';
20
+ const PROXY = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || '';
21
+ const HTTP_TIMEOUT_MS = Number(process.env.MARAVILLA_HTTP_TIMEOUT_MS || 15000);
22
+ const HTTP_RETRIES = Math.max(1, Number(process.env.MARAVILLA_HTTP_RETRIES || 3));
18
23
 
19
24
  function flower() {
20
25
  return chalk.magenta('✿');
@@ -43,15 +48,39 @@ async function currentVersion() {
43
48
  });
44
49
  }
45
50
 
51
+ function makeFetchOpts() {
52
+ const headers = { 'User-Agent': 'maravilla-cli-updater' };
53
+ if (GH_TOKEN) headers['Authorization'] = `Bearer ${GH_TOKEN}`;
54
+ const opts = { headers };
55
+ if (PROXY) {
56
+ try { opts.agent = new HttpsProxyAgent(PROXY); } catch {}
57
+ }
58
+ return opts;
59
+ }
60
+
46
61
  async function latestVersion() {
47
62
  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 };
63
+ const opts = makeFetchOpts();
64
+ try {
65
+ const controller = new AbortController();
66
+ const t = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
67
+ const res = await fetch(api, { ...opts, signal: controller.signal });
68
+ clearTimeout(t);
69
+ if (!res.ok) throw new Error(`Failed to fetch latest release: ${res.status} ${res.statusText}`);
70
+ const json = await res.json();
71
+ const tag = json.tag_name || '';
72
+ const m = tag.match(/v(\d+\.\d+\.\d+)/);
73
+ return { tag, version: m ? m[1] : null };
74
+ } catch (e) {
75
+ // Fallback to HTML redirect
76
+ const html = `https://github.com/${REPO}/releases/latest`;
77
+ const res = await fetch(html, { ...opts, redirect: 'manual' });
78
+ const loc = res.headers.get('location') || '';
79
+ const m = loc.match(/\/releases\/tag\/([^/]+)$/);
80
+ const tag = m ? m[1] : '';
81
+ const mv = tag.match(/v(\d+\.\d+\.\d+)/);
82
+ return { tag, version: mv ? mv[1] : null };
83
+ }
55
84
  }
56
85
 
57
86
  function cmpSemver(a, b) {