@sdsrs/code-graph 0.5.27 → 0.5.28

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
@@ -118,7 +118,7 @@ What you get:
118
118
  - **Code Explorer Agent** — Deep code understanding expert via `code-explorer`
119
119
  - **Auto-indexing Hook** — Incremental index on every file edit (PostToolUse)
120
120
  - **StatusLine** — Real-time health display (nodes, files, watch status) — compatible with other plugins' StatusLine via composite multiplexer
121
- - **Auto-update** — Checks for new versions every 24h, updates silently
121
+ - **Auto-update** — Checks for new versions every 6h, updates silently
122
122
 
123
123
  #### Manual Update
124
124
 
package/bin/cli.js CHANGED
@@ -1,62 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { execFileSync, spawn } = require("child_process");
3
+ const { spawn } = require("child_process");
4
4
  const path = require("path");
5
- const fs = require("fs");
6
- const os = require("os");
7
5
 
8
- function getBinaryName() {
9
- return os.platform() === "win32" ? "code-graph-mcp.exe" : "code-graph-mcp";
10
- }
11
-
12
- function findBinary() {
13
- const binaryName = getBinaryName();
14
-
15
- // 1. Check platform-specific npm package (code-graph-<os>-<arch>)
16
- const platformPkg = `@sdsrs/code-graph-${os.platform()}-${os.arch()}`;
17
- try {
18
- const pkgPath = require.resolve(`${platformPkg}/package.json`);
19
- const pkgDir = path.dirname(pkgPath);
20
- const platBinary = path.join(pkgDir, binaryName);
21
- if (fs.existsSync(platBinary)) {
22
- return platBinary;
23
- }
24
- } catch {
25
- // Platform package not installed
26
- }
27
-
28
- // 2. Check bundled binary in the same directory
29
- const bundled = path.join(__dirname, binaryName);
30
- if (fs.existsSync(bundled)) {
31
- return bundled;
32
- }
6
+ // Tell find-binary.js our package root so it can locate bundled binaries
7
+ // and detect dev mode from bin/ → repo root (one level up)
8
+ process.env._FIND_BINARY_ROOT = path.resolve(__dirname, "..");
33
9
 
34
- // 3. Check cargo build output (for development)
35
- const cargoRelease = path.join(__dirname, "..", "target", "release", binaryName);
36
- if (fs.existsSync(cargoRelease)) {
37
- return cargoRelease;
38
- }
39
-
40
- // 4. Check if available in PATH
41
- try {
42
- const which = os.platform() === "win32" ? "where" : "which";
43
- const result = execFileSync(which, [binaryName], { encoding: "utf8" }).trim();
44
- if (result) return result;
45
- } catch {
46
- // not in PATH
47
- }
48
-
49
- return null;
50
- }
10
+ const { findBinary } = require("../claude-plugin/scripts/find-binary");
51
11
 
52
12
  const binary = findBinary();
53
13
 
54
14
  if (!binary) {
55
15
  console.error(
56
16
  "Error: code-graph-mcp binary not found.\n\n" +
17
+ "To install:\n" +
18
+ " npm install -g @sdsrs/code-graph\n\n" +
57
19
  "To build from source:\n" +
58
- " cargo build --release --no-default-features\n\n" +
59
- "Or install the platform-specific binary."
20
+ " cargo build --release\n"
60
21
  );
61
22
  process.exit(1);
62
23
  }
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "sdsrs"
6
6
  },
7
- "version": "0.5.27",
7
+ "version": "0.5.28",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -66,10 +66,10 @@
66
66
  {
67
67
  "type": "command",
68
68
  "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-init.js\"",
69
- "timeout": 20
69
+ "timeout": 5
70
70
  }
71
71
  ],
72
- "description": "Health check, StatusLine registration, and update check at session start"
72
+ "description": "StatusLine self-heal, lifecycle sync, and background update launch at session start"
73
73
  }
74
74
  ]
75
75
  }
@@ -2,18 +2,39 @@
2
2
  'use strict';
3
3
  const { execFileSync } = require('child_process');
4
4
  const fs = require('fs');
5
+ const https = require('https');
5
6
  const path = require('path');
6
7
  const os = require('os');
7
8
  const { CACHE_DIR, PLUGIN_ID, MARKETPLACE_NAME, readManifest, readJson, writeJsonAtomic } = require('./lifecycle');
9
+ const { clearCache: clearBinaryCache } = require('./find-binary');
8
10
 
9
11
  // ── Configuration ──────────────────────────────────────────
10
12
  const GITHUB_REPO = 'sdsrss/code-graph-mcp';
11
- const NPM_PACKAGE = '@sdsrs/code-graph';
12
13
  const STATE_FILE = path.join(CACHE_DIR, 'update-state.json');
13
- const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h (GitHub allows 60 req/h unauthenticated)
14
+ const BINARY_CACHE_DIR = path.join(CACHE_DIR, 'bin');
15
+ const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h
14
16
  const RATE_LIMIT_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h if rate-limited
15
- const POST_UPDATE_INTERVAL_MS = 1 * 60 * 60 * 1000; // 1h after update (verify success)
16
17
  const FETCH_TIMEOUT_MS = 3000;
18
+ const VERSION_OUTPUT_RE = /^code-graph-mcp\s+(\d+\.\d+\.\d+)$/;
19
+
20
+ function isSilentMode(argv = process.argv.slice(2), env = process.env) {
21
+ return argv.includes('--silent') || env.CODE_GRAPH_AUTO_UPDATE_SILENT === '1';
22
+ }
23
+
24
+ // ── Platform → GitHub release asset name mapping ──────────
25
+ function getPlatformAssetName() {
26
+ const platform = os.platform();
27
+ const arch = os.arch();
28
+ const key = `${platform}-${arch}`;
29
+ const map = {
30
+ 'linux-x64': 'code-graph-mcp-linux-x64',
31
+ 'linux-arm64': 'code-graph-mcp-linux-arm64',
32
+ 'darwin-x64': 'code-graph-mcp-darwin-x64',
33
+ 'darwin-arm64': 'code-graph-mcp-darwin-arm64',
34
+ 'win32-x64': 'code-graph-mcp-win32-x64.exe',
35
+ };
36
+ return map[key] || null;
37
+ }
17
38
 
18
39
  // ── State Persistence ──────────────────────────────────────
19
40
 
@@ -43,9 +64,7 @@ function isDevMode() {
43
64
  function shouldCheck(state) {
44
65
  if (!state.lastCheck) return true;
45
66
  const elapsed = Date.now() - new Date(state.lastCheck).getTime();
46
- let interval = CHECK_INTERVAL_MS;
47
- if (state.rateLimited) interval = RATE_LIMIT_INTERVAL_MS;
48
- else if (state.pendingBinaryUpdate) interval = POST_UPDATE_INTERVAL_MS;
67
+ const interval = state.rateLimited ? RATE_LIMIT_INTERVAL_MS : CHECK_INTERVAL_MS;
49
68
  return elapsed >= interval;
50
69
  }
51
70
 
@@ -63,30 +82,67 @@ function compareVersions(a, b) {
63
82
 
64
83
  // ── GitHub API ─────────────────────────────────────────────
65
84
 
66
- async function fetchLatestRelease() {
67
- const url = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`;
68
- try {
69
- const res = await fetch(url, {
70
- signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
85
+ function requestJson(url, timeoutMs = FETCH_TIMEOUT_MS) {
86
+ return new Promise((resolve, reject) => {
87
+ const req = https.request(url, {
88
+ method: 'GET',
71
89
  headers: {
72
90
  'Accept': 'application/vnd.github+json',
73
91
  'User-Agent': 'code-graph-auto-update/1.0',
74
92
  },
93
+ }, (res) => {
94
+ let body = '';
95
+ res.setEncoding('utf8');
96
+ res.on('data', (chunk) => { body += chunk; });
97
+ res.on('end', () => {
98
+ if (!res.statusCode) {
99
+ reject(new Error('missing status code'));
100
+ return;
101
+ }
102
+ resolve({ statusCode: res.statusCode, body });
103
+ });
75
104
  });
76
105
 
77
- if (res.status === 403) {
78
- // Rate limited
106
+ req.setTimeout(timeoutMs, () => req.destroy(new Error('request timeout')));
107
+ req.on('error', reject);
108
+ req.end();
109
+ });
110
+ }
111
+
112
+ function parseLatestRelease(data, assetName = getPlatformAssetName()) {
113
+ if (!data || typeof data.tag_name !== 'string' || typeof data.tarball_url !== 'string') {
114
+ return null;
115
+ }
116
+
117
+ let binaryUrl = null;
118
+ if (assetName && Array.isArray(data.assets)) {
119
+ const asset = data.assets.find((entry) => entry && entry.name === assetName);
120
+ if (asset && typeof asset.browser_download_url === 'string') {
121
+ binaryUrl = asset.browser_download_url;
122
+ }
123
+ }
124
+
125
+ return {
126
+ version: data.tag_name.replace(/^v/, ''),
127
+ tarballUrl: data.tarball_url,
128
+ binaryUrl,
129
+ };
130
+ }
131
+
132
+ async function fetchLatestRelease(requestJsonFn = requestJson) {
133
+ const url = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`;
134
+ try {
135
+ const res = await requestJsonFn(url, FETCH_TIMEOUT_MS);
136
+
137
+ if (res.statusCode === 403) {
79
138
  const state = readState();
80
139
  saveState({ ...state, rateLimited: true });
81
140
  return null;
82
141
  }
83
- if (!res.ok) return null;
142
+ if (res.statusCode < 200 || res.statusCode >= 300) return null;
84
143
 
85
- const data = await res.json();
86
- return {
87
- version: data.tag_name.replace(/^v/, ''),
88
- tarballUrl: data.tarball_url,
89
- };
144
+ const data = JSON.parse(res.body);
145
+ return parseLatestRelease(data);
90
146
  } catch { return null; }
91
147
  }
92
148
 
@@ -105,14 +161,60 @@ function copyDirSync(src, dst) {
105
161
  }
106
162
  }
107
163
 
164
+ function getExtractedPluginVersion(pluginSrc) {
165
+ const manifest = readJson(path.join(pluginSrc, '.claude-plugin', 'plugin.json'));
166
+ return manifest && typeof manifest.version === 'string' ? manifest.version : null;
167
+ }
168
+
169
+ function readBinaryVersion(binaryPath) {
170
+ try {
171
+ const out = execFileSync(binaryPath, ['--version'], {
172
+ timeout: 2000,
173
+ stdio: ['pipe', 'pipe', 'pipe'],
174
+ }).toString().trim();
175
+ const match = out.match(VERSION_OUTPUT_RE);
176
+ return match ? match[1] : null;
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+
182
+ function promoteVerifiedBinary(binaryTmp, binaryDst, expectedVersion) {
183
+ try {
184
+ const stat = fs.statSync(binaryTmp);
185
+ if (stat.size <= 1_000_000) return false;
186
+
187
+ const actualVersion = readBinaryVersion(binaryTmp);
188
+ if (!actualVersion || (expectedVersion && actualVersion !== expectedVersion)) {
189
+ return false;
190
+ }
191
+
192
+ fs.renameSync(binaryTmp, binaryDst);
193
+ if (os.platform() !== 'win32') {
194
+ fs.chmodSync(binaryDst, 0o755);
195
+ }
196
+ clearBinaryCache();
197
+ return true;
198
+ } catch {
199
+ return false;
200
+ } finally {
201
+ try {
202
+ if (fs.existsSync(binaryTmp)) fs.unlinkSync(binaryTmp);
203
+ } catch { /* ok */ }
204
+ }
205
+ }
206
+
108
207
  // ── Download & Install ─────────────────────────────────────
109
208
 
110
209
  async function downloadAndInstall(latest) {
111
210
  const tmpDir = path.join(os.tmpdir(), `code-graph-update-${Date.now()}`);
211
+ let pluginUpdated = false;
212
+ let binaryUpdated = false;
213
+
112
214
  try {
113
215
  fs.mkdirSync(tmpDir, { recursive: true });
114
216
 
115
- // 1. Download tarball (safe: no shell interpolation)
217
+ // ── Step 1: Download and install plugin files from tarball ──
116
218
  const tarballPath = path.join(tmpDir, 'release.tar.gz');
117
219
  execFileSync('curl', [
118
220
  '-sL', '-o', tarballPath,
@@ -120,23 +222,22 @@ async function downloadAndInstall(latest) {
120
222
  latest.tarballUrl,
121
223
  ], { timeout: 30000, stdio: 'pipe' });
122
224
 
123
- // 2. Extract tarball
124
225
  execFileSync('tar', [
125
226
  'xzf', tarballPath, '-C', tmpDir, '--strip-components=1',
126
227
  ], { timeout: 15000, stdio: 'pipe' });
127
228
 
128
- // 3. Copy plugin files to cache (cross-platform)
129
229
  const pluginSrc = path.join(tmpDir, 'claude-plugin');
130
230
  const pluginDst = path.join(
131
231
  os.homedir(), '.claude', 'plugins', 'cache', MARKETPLACE_NAME, 'code-graph-mcp', latest.version
132
232
  );
133
233
 
134
- if (fs.existsSync(pluginSrc)) {
234
+ if (fs.existsSync(pluginSrc) && getExtractedPluginVersion(pluginSrc) === latest.version) {
135
235
  fs.mkdirSync(pluginDst, { recursive: true });
136
236
  copyDirSync(pluginSrc, pluginDst);
237
+ pluginUpdated = true;
137
238
  }
138
239
 
139
- // 4. Update installed_plugins.json to point to new version
240
+ // Update installed_plugins.json to point to new version
140
241
  const installedPath = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
141
242
  try {
142
243
  const installed = readJson(installedPath);
@@ -146,34 +247,41 @@ async function downloadAndInstall(latest) {
146
247
  installed.plugins[PLUGIN_ID][0].lastUpdated = new Date().toISOString();
147
248
  writeJsonAtomic(installedPath, installed);
148
249
  }
149
- } catch { /* installed_plugins update failed — not fatal */ }
250
+ } catch { /* not fatal */ }
150
251
 
151
- // 5. Update install manifest with tag version
252
+ // Update install manifest
152
253
  try {
153
254
  const manifest = readManifest();
154
255
  manifest.version = latest.version;
155
256
  manifest.updatedAt = new Date().toISOString();
156
257
  writeJsonAtomic(path.join(CACHE_DIR, 'install-manifest.json'), manifest);
157
- } catch { /* manifest update failed — not fatal */ }
158
-
159
- // 6. Update npm binary (non-blocking, best-effort)
160
- let binaryUpdated = false;
161
- try {
162
- execFileSync('npm', ['install', '-g', `${NPM_PACKAGE}@${latest.version}`], {
163
- timeout: 60000,
164
- stdio: 'pipe',
165
- });
166
- binaryUpdated = true;
167
- // Clear pending flag on success
168
- try { const s = readState(); delete s.pendingBinaryUpdate; saveState(s); } catch { /* ok */ }
169
- } catch {
170
- // npm package may not be published yet (race with CI). Record for retry.
171
- try { const s = readState(); s.pendingBinaryUpdate = latest.version; saveState(s); } catch { /* ok */ }
258
+ } catch { /* not fatal */ }
259
+
260
+ // ── Step 2: Download platform binary directly from GitHub release ──
261
+ if (latest.binaryUrl) {
262
+ try {
263
+ const binaryName = os.platform() === 'win32' ? 'code-graph-mcp.exe' : 'code-graph-mcp';
264
+ const binaryDst = path.join(BINARY_CACHE_DIR, binaryName);
265
+ const binaryTmp = binaryDst + '.tmp.' + process.pid;
266
+
267
+ fs.mkdirSync(BINARY_CACHE_DIR, { recursive: true });
268
+ execFileSync('curl', [
269
+ '-sL', '-o', binaryTmp,
270
+ latest.binaryUrl,
271
+ ], { timeout: 60000, stdio: 'pipe' });
272
+
273
+ if (promoteVerifiedBinary(binaryTmp, binaryDst, latest.version)) {
274
+ binaryUpdated = true;
275
+ }
276
+ } catch {
277
+ // Binary download failed — plugin update still counts as success
278
+ }
172
279
  }
173
280
 
174
- return true;
175
- } catch { return false; }
176
- finally {
281
+ return { pluginUpdated, binaryUpdated };
282
+ } catch {
283
+ return { pluginUpdated: false, binaryUpdated: false };
284
+ } finally {
177
285
  try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ok */ }
178
286
  }
179
287
  }
@@ -189,7 +297,6 @@ async function checkForUpdate() {
189
297
 
190
298
  // Time-based throttle
191
299
  if (!shouldCheck(state)) {
192
- // Report pending update from previous check
193
300
  if (state.updateAvailable && state.latestVersion) {
194
301
  return { updateAvailable: true, from: state.installedVersion, to: state.latestVersion };
195
302
  }
@@ -209,8 +316,8 @@ async function checkForUpdate() {
209
316
  const hasUpdate = compareVersions(latest.version, currentVersion) > 0;
210
317
 
211
318
  if (hasUpdate) {
212
- // Auto-update
213
- const success = await downloadAndInstall(latest);
319
+ const result = await downloadAndInstall(latest);
320
+ const success = result.pluginUpdated;
214
321
  const newState = {
215
322
  lastCheck: new Date().toISOString(),
216
323
  installedVersion: success ? latest.version : currentVersion,
@@ -218,12 +325,14 @@ async function checkForUpdate() {
218
325
  updateAvailable: !success,
219
326
  lastUpdate: success ? new Date().toISOString() : state.lastUpdate,
220
327
  rateLimited: false,
328
+ binaryUpdated: result.binaryUpdated,
221
329
  };
222
330
  saveState(newState);
223
331
 
224
332
  return {
225
333
  updateAvailable: !success,
226
334
  updated: success,
335
+ binaryUpdated: result.binaryUpdated,
227
336
  from: currentVersion,
228
337
  to: latest.version,
229
338
  };
@@ -244,20 +353,27 @@ async function checkForUpdate() {
244
353
  }
245
354
  }
246
355
 
247
- module.exports = { checkForUpdate, isDevMode, readState };
356
+ module.exports = {
357
+ checkForUpdate, isDevMode, readState, compareVersions,
358
+ getExtractedPluginVersion, readBinaryVersion, promoteVerifiedBinary, isSilentMode,
359
+ requestJson, parseLatestRelease, fetchLatestRelease,
360
+ };
248
361
 
249
362
  // CLI: node auto-update.js [check|status]
250
363
  if (require.main === module) {
251
364
  (async () => {
252
- const cmd = process.argv[2] || 'check';
365
+ const argv = process.argv.slice(2);
366
+ const cmd = argv.find(arg => !arg.startsWith('--')) || 'check';
367
+ const silent = isSilentMode(argv);
253
368
  if (cmd === 'status') {
254
369
  const state = readState();
255
370
  console.log(JSON.stringify(state, null, 2));
256
371
  } else {
257
- console.log('Checking for updates...');
372
+ if (!silent) console.log('Checking for updates...');
258
373
  const result = await checkForUpdate();
374
+ if (silent) return;
259
375
  if (result && result.updated) {
260
- console.log(`Updated: v${result.from} → v${result.to}`);
376
+ console.log(`Updated: v${result.from} → v${result.to} (binary: ${result.binaryUpdated ? 'yes' : 'no'})`);
261
377
  } else if (result && result.updateAvailable) {
262
378
  console.log(`Update available: v${result.to} (auto-install failed)`);
263
379
  } else if (isDevMode()) {
@@ -0,0 +1,97 @@
1
+ 'use strict';
2
+ const test = require('node:test');
3
+ const assert = require('node:assert/strict');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+
8
+ const {
9
+ fetchLatestRelease,
10
+ getExtractedPluginVersion,
11
+ parseLatestRelease,
12
+ readBinaryVersion,
13
+ promoteVerifiedBinary,
14
+ } = require('./auto-update');
15
+
16
+ function mkDir(prefix) {
17
+ return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
18
+ }
19
+
20
+ test('getExtractedPluginVersion reads extracted plugin manifest version', () => {
21
+ const root = mkDir('code-graph-plugin-');
22
+ const manifest = path.join(root, '.claude-plugin', 'plugin.json');
23
+ fs.mkdirSync(path.dirname(manifest), { recursive: true });
24
+ fs.writeFileSync(manifest, JSON.stringify({ version: '1.2.3' }, null, 2));
25
+ assert.equal(getExtractedPluginVersion(root), '1.2.3');
26
+ });
27
+
28
+ function writeFakeBinary(filePath, version) {
29
+ const script = [
30
+ '#!/usr/bin/env bash',
31
+ 'if [ "$1" = "--version" ]; then',
32
+ ` echo "code-graph-mcp ${version}"`,
33
+ ' exit 0',
34
+ 'fi',
35
+ 'exit 0',
36
+ `# ${'x'.repeat(1_100_000)}`,
37
+ '',
38
+ ].join('\n');
39
+ fs.writeFileSync(filePath, script);
40
+ fs.chmodSync(filePath, 0o755);
41
+ }
42
+
43
+ test('promoteVerifiedBinary accepts a runnable binary with the expected version', () => {
44
+ const dir = mkDir('code-graph-bin-');
45
+ const tmp = path.join(dir, 'code-graph-mcp.tmp');
46
+ const dst = path.join(dir, 'code-graph-mcp');
47
+ writeFakeBinary(tmp, '1.2.3');
48
+
49
+ assert.equal(readBinaryVersion(tmp), '1.2.3');
50
+ assert.equal(promoteVerifiedBinary(tmp, dst, '1.2.3'), true);
51
+ assert.equal(fs.existsSync(tmp), false);
52
+ assert.equal(fs.existsSync(dst), true);
53
+ });
54
+
55
+ test('promoteVerifiedBinary rejects binaries with mismatched version', () => {
56
+ const dir = mkDir('code-graph-bin-');
57
+ const tmp = path.join(dir, 'code-graph-mcp.tmp');
58
+ const dst = path.join(dir, 'code-graph-mcp');
59
+ writeFakeBinary(tmp, '1.2.2');
60
+
61
+ assert.equal(promoteVerifiedBinary(tmp, dst, '1.2.3'), false);
62
+ assert.equal(fs.existsSync(tmp), false);
63
+ assert.equal(fs.existsSync(dst), false);
64
+ });
65
+
66
+ test('parseLatestRelease selects the matching platform asset', () => {
67
+ const latest = parseLatestRelease({
68
+ tag_name: 'v1.2.3',
69
+ tarball_url: 'https://example.com/tarball.tgz',
70
+ assets: [
71
+ { name: 'code-graph-mcp-linux-x64', browser_download_url: 'https://example.com/linux-x64' },
72
+ { name: 'other', browser_download_url: 'https://example.com/other' },
73
+ ],
74
+ }, 'code-graph-mcp-linux-x64');
75
+
76
+ assert.deepEqual(latest, {
77
+ version: '1.2.3',
78
+ tarballUrl: 'https://example.com/tarball.tgz',
79
+ binaryUrl: 'https://example.com/linux-x64',
80
+ });
81
+ });
82
+
83
+ test('fetchLatestRelease parses JSON without relying on global fetch', async () => {
84
+ const latest = await fetchLatestRelease(async () => ({
85
+ statusCode: 200,
86
+ body: JSON.stringify({
87
+ tag_name: 'v2.0.0',
88
+ tarball_url: 'https://example.com/release.tgz',
89
+ assets: [
90
+ { name: 'code-graph-mcp-linux-x64', browser_download_url: 'https://example.com/bin' },
91
+ ],
92
+ }),
93
+ }));
94
+
95
+ assert.equal(latest.version, '2.0.0');
96
+ assert.equal(latest.tarballUrl, 'https://example.com/release.tgz');
97
+ });