@maravilla-labs/cli 0.1.6 → 0.1.8

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 CHANGED
@@ -16,6 +16,11 @@ pnpm add -g @maravilla-labs/cli
16
16
  yarn global add @maravilla-labs/cli
17
17
  ```
18
18
 
19
+ **Note:** To see installation progress with npm 7+, use the `--foreground-scripts` flag:
20
+ ```bash
21
+ npm install -g @maravilla-labs/cli --foreground-scripts
22
+ ```
23
+
19
24
  Verify:
20
25
 
21
26
  ```bash
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.6",
3
+ "version": "0.1.8",
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,14 +11,24 @@ 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) {
17
18
  console.log('✿ Skipping Maravilla CLI binary install (CI or skip flag set)');
18
19
  process.exit(0);
19
20
  }
21
+
20
22
  import { spawn } from 'child_process';
21
23
 
24
+ // Detect if output is being suppressed (npm 7+ behavior)
25
+ // Write to stderr as a fallback since it's more likely to be shown
26
+ const isOutputSuppressed = !process.stdout.isTTY && !process.env.npm_config_foreground_scripts;
27
+ if (isOutputSuppressed) {
28
+ console.error('✿ Installing Maravilla CLI binary...');
29
+ console.error('✿ To see detailed progress, run: npm install --foreground-scripts');
30
+ }
31
+
22
32
  const __filename = fileURLToPath(import.meta.url);
23
33
  const __dirname = path.dirname(__filename);
24
34
 
@@ -28,6 +38,10 @@ const VERSION = process.env.MARAVILLA_VERSION || 'latest'; // cli-vX.Y.Z or 'lat
28
38
  const BIN_DIR = path.join(__dirname, '..', 'bin');
29
39
  const EXECUTABLE = process.platform === 'win32' ? `${BIN_NAME}.exe` : BIN_NAME;
30
40
  const INSTALL_PATH = path.join(BIN_DIR, EXECUTABLE);
41
+ const GH_TOKEN = process.env.MARAVILLA_GH_TOKEN || process.env.GITHUB_TOKEN || '';
42
+ const PROXY = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || '';
43
+ const HTTP_TIMEOUT_MS = Number(process.env.MARAVILLA_HTTP_TIMEOUT_MS || 15000);
44
+ const HTTP_RETRIES = Math.max(1, Number(process.env.MARAVILLA_HTTP_RETRIES || 3));
31
45
 
32
46
  function flower() {
33
47
  return chalk.magenta('✿');
@@ -43,13 +57,13 @@ async function abortableFetch(url, opts = {}, timeoutMs = 30000) {
43
57
  const controller = new AbortController();
44
58
  const t = setTimeout(() => controller.abort(), timeoutMs);
45
59
  try {
46
- return await fetch(url, { ...opts, signal: controller.signal });
60
+ return await fetch(url, { ...opts, signal: controller.signal });
47
61
  } finally {
48
62
  clearTimeout(t);
49
63
  }
50
64
  }
51
65
 
52
- async function withRetries(taskFn, tries = 3, baseDelay = 1000) {
66
+ async function withRetries(taskFn, tries = HTTP_RETRIES, baseDelay = 1000) {
53
67
  let attempt = 0;
54
68
  let lastErr;
55
69
  while (attempt < tries) {
@@ -67,13 +81,37 @@ async function withRetries(taskFn, tries = 3, baseDelay = 1000) {
67
81
  throw lastErr;
68
82
  }
69
83
 
84
+ function makeFetchOpts() {
85
+ const headers = { 'User-Agent': 'maravilla-cli-installer' };
86
+ if (GH_TOKEN) headers['Authorization'] = `Bearer ${GH_TOKEN}`;
87
+ const opts = { headers };
88
+ if (PROXY) {
89
+ try { opts.agent = new HttpsProxyAgent(PROXY); } catch {}
90
+ }
91
+ return opts;
92
+ }
93
+
70
94
  async function getReleaseTag() {
71
95
  if (VERSION && VERSION !== 'latest') return VERSION;
72
96
  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;
97
+ const opts = makeFetchOpts();
98
+ try {
99
+ console.log(chalk.gray(`${flower()} fetching latest release tag (${Math.round(HTTP_TIMEOUT_MS/100)/10}s timeout, ${HTTP_RETRIES} retries)`));
100
+ const res = await withRetries(() => abortableFetch(api, opts, HTTP_TIMEOUT_MS));
101
+ if (!res.ok) throw new Error(`Failed to fetch latest release: ${res.status} ${res.statusText}`);
102
+ const json = await res.json();
103
+ if (!json.tag_name) throw new Error('No tag_name in API response');
104
+ return json.tag_name;
105
+ } catch (e) {
106
+ console.log(chalk.yellow(`${flower()} API lookup failed (${e.message}). Falling back to HTML redirect...`));
107
+ // Fallback: use HTML redirect to /releases/tag/<tag>
108
+ const html = `https://github.com/${REPO}/releases/latest`;
109
+ const res = await abortableFetch(html, { ...opts, redirect: 'manual' }, Math.min(HTTP_TIMEOUT_MS, 10000));
110
+ const loc = res.headers.get('location') || '';
111
+ const m = loc.match(/\/releases\/tag\/([^/]+)$/);
112
+ if (m) return m[1];
113
+ throw new Error('Could not resolve latest tag via fallback');
114
+ }
77
115
  }
78
116
 
79
117
  async function downloadWithProgress(url, dest) {
@@ -139,6 +177,19 @@ function cmpSemver(a, b) {
139
177
 
140
178
  async function install() {
141
179
  const { target, ext } = resolveTarget();
180
+
181
+ // Detect current installed version first to enable true offline fast-path
182
+ let cur = null;
183
+ try { cur = await currentVersion(); } catch {}
184
+
185
+ if (process.env.MARAVILLA_OFFLINE === '1') {
186
+ if (cur) {
187
+ log(chalk.yellow(`Offline mode: keeping existing v${cur}`));
188
+ return;
189
+ }
190
+ throw new Error('Offline mode: no existing binary; set MARAVILLA_VERSION to a specific tag or retry online');
191
+ }
192
+
142
193
  log(`Resolving latest release tag from ${chalk.cyan(REPO)}`);
143
194
  const tag = await getReleaseTag();
144
195
  const art = artifactName(BIN_NAME, tag, target);
@@ -155,21 +206,8 @@ async function install() {
155
206
  let requested = null;
156
207
  const m = tag.match(/v(\d+\.\d+\.\d+)/);
157
208
  if (m) requested = m[1];
158
- let cur = null;
159
- try { cur = await currentVersion(); } catch {}
160
209
 
161
210
  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
211
  if (cur && requested && cmpSemver(cur, requested) >= 0) {
174
212
  log(chalk.green(`Already up to date (v${cur})`));
175
213
  return;
@@ -183,7 +221,7 @@ async function install() {
183
221
  // Verify checksum if SHA256SUMS is available
184
222
  try {
185
223
  log('Verifying checksum');
186
- const sumsRes = await abortableFetch(sumsUrl, { headers: { 'User-Agent': 'maravilla-cli-installer' } }, 15000);
224
+ const sumsRes = await abortableFetch(sumsUrl, makeFetchOpts(), 15000);
187
225
  if (sumsRes.ok) {
188
226
  const sumsText = await sumsRes.text();
189
227
  const expected = sumsText
@@ -244,7 +282,13 @@ async function sha256File(filePath) {
244
282
  });
245
283
  }
246
284
 
247
- install().catch(err => {
248
- console.error(chalk.red(`${flower()} install failed: ${err.message}`));
249
- process.exitCode = 1;
250
- });
285
+ install()
286
+ .then(() => {
287
+ if (isOutputSuppressed) {
288
+ console.error('✿ Maravilla CLI binary installed successfully!');
289
+ }
290
+ })
291
+ .catch(err => {
292
+ console.error(chalk.red(`${flower()} install failed: ${err.message}`));
293
+ process.exitCode = 1;
294
+ });
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) {