@finch.app/minitools 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/bin/minitools.mjs +186 -99
- package/package.json +1 -1
package/bin/minitools.mjs
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* @finch.app/minitools — install Finch extensions to the correct location.
|
|
4
4
|
*
|
|
5
|
-
* Zero npm dependencies. npm sources are
|
|
6
|
-
* so third-party install scripts never run
|
|
5
|
+
* Zero npm dependencies. npm sources are installed by downloading the registry
|
|
6
|
+
* dist.tarball (.tgz) directly, so third-party install scripts never run.
|
|
7
7
|
*/
|
|
8
8
|
import {
|
|
9
9
|
existsSync, mkdirSync, readdirSync, cpSync, rmSync,
|
|
10
|
-
readFileSync, writeFileSync,
|
|
10
|
+
readFileSync, writeFileSync,
|
|
11
11
|
} from 'node:fs';
|
|
12
12
|
import { join, resolve, basename } from 'node:path';
|
|
13
13
|
import { homedir, tmpdir } from 'node:os';
|
|
@@ -147,63 +147,8 @@ function installExtensionDir(srcDir, destRoot, lockSource) {
|
|
|
147
147
|
return info.id;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
/**
|
|
151
|
-
* Look for an executable on PATH (via `which`/`where`), falling back to a
|
|
152
|
-
* handful of common install locations that don't always make it onto the
|
|
153
|
-
* PATH inherited by a GUI-launched app (Homebrew, nvm, Volta, Windows
|
|
154
|
-
* installer). Returns the resolved path, or null if not found anywhere.
|
|
155
|
-
*/
|
|
156
|
-
function findExecutable(name) {
|
|
157
|
-
const finder = process.platform === 'win32' ? 'where' : 'which';
|
|
158
|
-
const found = spawnSync(finder, [name], { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf-8' });
|
|
159
|
-
if (found.status === 0) {
|
|
160
|
-
const first = found.stdout.split(/\r?\n/).map((s) => s.trim()).find(Boolean);
|
|
161
|
-
if (first) return first;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const candidateDirs = process.platform === 'win32'
|
|
165
|
-
? [
|
|
166
|
-
join(process.env.ProgramFiles ?? 'C:\\Program Files', 'nodejs'),
|
|
167
|
-
join(process.env.APPDATA ?? '', 'npm'),
|
|
168
|
-
]
|
|
169
|
-
: (() => {
|
|
170
|
-
const dirs = ['/opt/homebrew/bin', '/usr/local/bin', join(homedir(), '.volta', 'bin')];
|
|
171
|
-
const nvmRoot = join(homedir(), '.nvm', 'versions', 'node');
|
|
172
|
-
try {
|
|
173
|
-
for (const version of readdirSync(nvmRoot)) dirs.push(join(nvmRoot, version, 'bin'));
|
|
174
|
-
} catch { /* nvm not installed */ }
|
|
175
|
-
return dirs;
|
|
176
|
-
})();
|
|
177
|
-
const exeName = process.platform === 'win32' ? `${name}.cmd` : name;
|
|
178
|
-
for (const dir of candidateDirs) {
|
|
179
|
-
const candidate = join(dir, exeName);
|
|
180
|
-
if (existsSync(candidate)) return candidate;
|
|
181
|
-
}
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const NODEJS_INSTALL_HINT = 'Install Node.js (which bundles npm) from https://nodejs.org, then try again.';
|
|
186
|
-
|
|
187
|
-
function npmInstallToTemp(spec, tmp) {
|
|
188
|
-
const npmPath = findExecutable('npm');
|
|
189
|
-
if (!npmPath) {
|
|
190
|
-
throw new Error(`This extension is an npm package, but no "npm" executable was found on this machine.\n${NODEJS_INSTALL_HINT}`);
|
|
191
|
-
}
|
|
192
|
-
mkdirSync(tmp, { recursive: true });
|
|
193
|
-
const r = spawnSync(npmPath, ['install', '--ignore-scripts', '--omit=dev', '--prefix', tmp, spec], {
|
|
194
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
195
|
-
encoding: 'utf-8',
|
|
196
|
-
});
|
|
197
|
-
if (r.error) {
|
|
198
|
-
throw new Error(`Failed to run npm (${npmPath}): ${r.error.message}\n${NODEJS_INSTALL_HINT}`);
|
|
199
|
-
}
|
|
200
|
-
if (r.status !== 0) {
|
|
201
|
-
throw new Error(`npm install failed:\n${r.stderr || r.stdout || `exit code ${r.status}`}`);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
150
|
function isLocalSource(src) {
|
|
206
|
-
return src.startsWith('./') || src.startsWith('../') || src.startsWith('/') || src.startsWith('~');
|
|
151
|
+
return src.startsWith('./') || src.startsWith('../') || src.startsWith('/') || src.startsWith('~') || /^[a-zA-Z]:[\\/]/.test(src);
|
|
207
152
|
}
|
|
208
153
|
function isZipUrl(src) {
|
|
209
154
|
return /^https?:\/\/.+\.zip(\?.*)?$/i.test(src);
|
|
@@ -211,6 +156,13 @@ function isZipUrl(src) {
|
|
|
211
156
|
function isZipFile(src) {
|
|
212
157
|
return src.toLowerCase().endsWith('.zip');
|
|
213
158
|
}
|
|
159
|
+
function isTgzUrl(src) {
|
|
160
|
+
return /^https?:\/\/.+\.(?:tgz|tar\.gz)(\?.*)?$/i.test(src);
|
|
161
|
+
}
|
|
162
|
+
function isTgzFile(src) {
|
|
163
|
+
const lower = src.toLowerCase();
|
|
164
|
+
return lower.endsWith('.tgz') || lower.endsWith('.tar.gz');
|
|
165
|
+
}
|
|
214
166
|
function expandHome(path) {
|
|
215
167
|
return path.replace(/^~(?=\/|$)/, homedir());
|
|
216
168
|
}
|
|
@@ -219,16 +171,32 @@ function expandHome(path) {
|
|
|
219
171
|
* Download a URL to a local file path using Node's built-in fetch (Node 18+).
|
|
220
172
|
* Falls back to curl/wget if fetch is unavailable.
|
|
221
173
|
*/
|
|
174
|
+
function downloadWithSystemTool(url, destPath, fetchError) {
|
|
175
|
+
const curlExe = process.platform === 'win32' ? 'curl.exe' : 'curl';
|
|
176
|
+
const curl = spawnSync(curlExe, ['-fL', '--retry', '2', '--connect-timeout', '20', '-o', destPath, url], {
|
|
177
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
178
|
+
encoding: 'utf-8',
|
|
179
|
+
});
|
|
180
|
+
if (curl.status === 0 && !curl.error) return;
|
|
181
|
+
if (process.platform === 'win32') {
|
|
182
|
+
const ps = spawnSync('powershell.exe', ['-NoProfile', '-Command', 'Invoke-WebRequest -Uri $args[0] -OutFile $args[1]', url, destPath], {
|
|
183
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
184
|
+
encoding: 'utf-8',
|
|
185
|
+
});
|
|
186
|
+
if (ps.status === 0 && !ps.error) return;
|
|
187
|
+
throw new Error(`Download failed: ${fetchError?.message ?? fetchError}\n${curl.stderr || curl.stdout || curl.error?.message || 'curl failed'}\n${ps.stderr || ps.stdout || ps.error?.message || 'powershell failed'}`);
|
|
188
|
+
}
|
|
189
|
+
throw new Error(`Download failed: ${fetchError?.message ?? fetchError}\n${curl.stderr || curl.stdout || curl.error?.message || 'curl failed'}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
222
192
|
async function downloadFile(url, destPath) {
|
|
223
|
-
|
|
193
|
+
try {
|
|
224
194
|
const res = await fetch(url, { redirect: 'follow' });
|
|
225
|
-
if (!res.ok) throw new Error(
|
|
195
|
+
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
|
226
196
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
227
197
|
writeFileSync(destPath, buf);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const r = spawnSync('curl', ['-fsSL', '-o', destPath, url], { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf-8' });
|
|
231
|
-
if (r.status !== 0) throw new Error(`curl failed:\n${r.stderr || r.stdout}`);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
downloadWithSystemTool(url, destPath, err);
|
|
232
200
|
}
|
|
233
201
|
}
|
|
234
202
|
|
|
@@ -237,6 +205,21 @@ async function downloadFile(url, destPath) {
|
|
|
237
205
|
*/
|
|
238
206
|
function extractZip(zipPath, destDir) {
|
|
239
207
|
mkdirSync(destDir, { recursive: true });
|
|
208
|
+
if (process.platform === 'win32') {
|
|
209
|
+
const r = spawnSync('powershell.exe', ['-NoProfile', '-Command', 'Expand-Archive -LiteralPath $args[0] -DestinationPath $args[1] -Force', zipPath, destDir], {
|
|
210
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
211
|
+
encoding: 'utf-8',
|
|
212
|
+
});
|
|
213
|
+
if (r.status === 0 && !r.error) return;
|
|
214
|
+
const tar = spawnSync('tar.exe', ['-xf', zipPath, '-C', destDir], {
|
|
215
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
216
|
+
encoding: 'utf-8',
|
|
217
|
+
});
|
|
218
|
+
if (tar.status !== 0 || tar.error) {
|
|
219
|
+
throw new Error(`zip extract failed:\n${r.stderr || tar.stderr || r.stdout || tar.stdout || tar.error?.message || 'unknown error'}`);
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
240
223
|
const r = spawnSync('unzip', ['-q', '-o', zipPath, '-d', destDir], {
|
|
241
224
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
242
225
|
encoding: 'utf-8',
|
|
@@ -247,6 +230,75 @@ function extractZip(zipPath, destDir) {
|
|
|
247
230
|
if (r.status !== 0) throw new Error(`unzip failed:\n${r.stderr || r.stdout || `exit code ${r.status}`}`);
|
|
248
231
|
}
|
|
249
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Extract a .tgz/.tar.gz archive using the system tar command. macOS/Linux ship
|
|
235
|
+
* it by default; modern Windows includes bsdtar as tar.exe.
|
|
236
|
+
*/
|
|
237
|
+
function extractTgz(tgzPath, destDir) {
|
|
238
|
+
mkdirSync(destDir, { recursive: true });
|
|
239
|
+
const tarExe = process.platform === 'win32' ? 'tar.exe' : 'tar';
|
|
240
|
+
const r = spawnSync(tarExe, ['-xzf', tgzPath, '-C', destDir], {
|
|
241
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
242
|
+
encoding: 'utf-8',
|
|
243
|
+
});
|
|
244
|
+
if (r.error) {
|
|
245
|
+
throw new Error(`No "tar" command found on this machine: ${r.error.message}`);
|
|
246
|
+
}
|
|
247
|
+
if (r.status !== 0) throw new Error(`tar extract failed:\n${r.stderr || r.stdout || `exit code ${r.status}`}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function parseNpmSpec(spec) {
|
|
251
|
+
const trimmed = String(spec ?? '').trim();
|
|
252
|
+
if (!trimmed) throw new Error('missing npm package name');
|
|
253
|
+
if (trimmed.startsWith('@')) {
|
|
254
|
+
const versionSep = trimmed.indexOf('@', 1);
|
|
255
|
+
return versionSep > 0
|
|
256
|
+
? { name: trimmed.slice(0, versionSep), version: trimmed.slice(versionSep + 1) || 'latest' }
|
|
257
|
+
: { name: trimmed, version: 'latest' };
|
|
258
|
+
}
|
|
259
|
+
const versionSep = trimmed.lastIndexOf('@');
|
|
260
|
+
return versionSep > 0
|
|
261
|
+
? { name: trimmed.slice(0, versionSep), version: trimmed.slice(versionSep + 1) || 'latest' }
|
|
262
|
+
: { name: trimmed, version: 'latest' };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let registryOverride = null;
|
|
266
|
+
|
|
267
|
+
function configuredRegistryUrl() {
|
|
268
|
+
return (registryOverride || process.env.npm_config_registry || 'https://registry.npmjs.org').replace(/\/+$/, '');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function npmMetadataUrl(packageName, version) {
|
|
272
|
+
return `${configuredRegistryUrl()}/${encodeURIComponent(packageName)}/${encodeURIComponent(version || 'latest')}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function officialMiniToolDownloadUrl(packageName, version) {
|
|
276
|
+
return `https://community.finchwork.app/download/minitool/${packageName}/${encodeURIComponent(version)}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function resolveNpmTarball(spec) {
|
|
280
|
+
const { name, version } = parseNpmSpec(spec);
|
|
281
|
+
const url = npmMetadataUrl(name, version);
|
|
282
|
+
let meta;
|
|
283
|
+
try {
|
|
284
|
+
const res = await fetch(url, { redirect: 'follow' });
|
|
285
|
+
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
|
286
|
+
meta = await res.json();
|
|
287
|
+
} catch (err) {
|
|
288
|
+
throw new Error(`npm metadata fetch failed: ${err instanceof Error ? err.message : String(err)} — ${url}`);
|
|
289
|
+
}
|
|
290
|
+
const resolvedVersion = String(meta.version ?? version);
|
|
291
|
+
const tarball = meta?.dist?.tarball;
|
|
292
|
+
if (typeof tarball !== 'string' || !tarball) {
|
|
293
|
+
throw new Error(`No dist.tarball found for ${name}@${version}`);
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
package: name,
|
|
297
|
+
version: resolvedVersion,
|
|
298
|
+
tarball: officialMiniToolDownloadUrl(name, resolvedVersion),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
250
302
|
/**
|
|
251
303
|
* Install extensions from a zip file (local path or remote URL).
|
|
252
304
|
* The zip may contain one or more extensions at any nesting level.
|
|
@@ -279,25 +331,60 @@ async function installFromZip(src, dest, isUrl) {
|
|
|
279
331
|
}
|
|
280
332
|
}
|
|
281
333
|
|
|
334
|
+
async function installFromTgz(src, dest, { isUrl, lockSource }) {
|
|
335
|
+
const tmp = join(tmpdir(), `finch-ext-${randomUUID()}`);
|
|
336
|
+
const tgzPath = join(tmp, 'extension.tgz');
|
|
337
|
+
const extractDir = join(tmp, 'extracted');
|
|
338
|
+
mkdirSync(tmp, { recursive: true });
|
|
339
|
+
try {
|
|
340
|
+
if (isUrl) {
|
|
341
|
+
console.log(` Downloading ${src} …`);
|
|
342
|
+
await downloadFile(src, tgzPath);
|
|
343
|
+
} else {
|
|
344
|
+
const abs = resolve(expandHome(src));
|
|
345
|
+
if (!existsSync(abs)) throw new Error(`file not found: ${abs}`);
|
|
346
|
+
cpSync(abs, tgzPath);
|
|
347
|
+
}
|
|
348
|
+
console.log(' Extracting …');
|
|
349
|
+
extractTgz(tgzPath, extractDir);
|
|
350
|
+
const found = findPluginDirs(extractDir, 5);
|
|
351
|
+
if (found.length === 0) throw new Error('No Finch extension found inside the tgz archive.');
|
|
352
|
+
const source = lockSource ?? (isUrl
|
|
353
|
+
? { type: 'tgz', url: src }
|
|
354
|
+
: { type: 'tgz', localPath: resolve(expandHome(src)) });
|
|
355
|
+
for (const dir of found) installExtensionDir(dir, dest, source);
|
|
356
|
+
} finally {
|
|
357
|
+
try { rmSync(tmp, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
282
361
|
async function cmdAdd(src, opts) {
|
|
283
362
|
const dest = targetDir(opts);
|
|
284
363
|
|
|
285
|
-
// ---
|
|
364
|
+
// --- archive URL (e.g. https://github.com/.../archive/main.zip or npm tarball .tgz) ---
|
|
286
365
|
if (isZipUrl(src)) {
|
|
287
366
|
await installFromZip(src, dest, true);
|
|
288
367
|
return;
|
|
289
368
|
}
|
|
369
|
+
if (isTgzUrl(src)) {
|
|
370
|
+
await installFromTgz(src, dest, { isUrl: true });
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
290
373
|
|
|
291
374
|
// --- local path ---
|
|
292
375
|
if (isLocalSource(src)) {
|
|
293
376
|
const abs = resolve(expandHome(src));
|
|
294
377
|
if (!existsSync(abs)) throw new Error(`path not found: ${abs}`);
|
|
295
378
|
|
|
296
|
-
// Local
|
|
379
|
+
// Local archive file
|
|
297
380
|
if (isZipFile(src)) {
|
|
298
381
|
await installFromZip(src, dest, false);
|
|
299
382
|
return;
|
|
300
383
|
}
|
|
384
|
+
if (isTgzFile(src)) {
|
|
385
|
+
await installFromTgz(src, dest, { isUrl: false });
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
301
388
|
|
|
302
389
|
// Local directory
|
|
303
390
|
const direct = pluginInfo(abs);
|
|
@@ -311,18 +398,12 @@ async function cmdAdd(src, opts) {
|
|
|
311
398
|
return;
|
|
312
399
|
}
|
|
313
400
|
|
|
314
|
-
// --- npm package ---
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
// Prefer the top-level package matching the requested spec when possible.
|
|
321
|
-
const first = found[0];
|
|
322
|
-
installExtensionDir(first, dest, { type: 'npm', package: src });
|
|
323
|
-
} finally {
|
|
324
|
-
try { rmSync(tmp, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
325
|
-
}
|
|
401
|
+
// --- npm package: fetch registry metadata, download dist.tarball, then extract locally. ---
|
|
402
|
+
const resolved = await resolveNpmTarball(src);
|
|
403
|
+
await installFromTgz(resolved.tarball, dest, {
|
|
404
|
+
isUrl: true,
|
|
405
|
+
lockSource: { type: 'npm', package: resolved.package, version: resolved.version },
|
|
406
|
+
});
|
|
326
407
|
}
|
|
327
408
|
|
|
328
409
|
function listInstalled(dir) {
|
|
@@ -517,7 +598,7 @@ async function cmdUpdate(id, opts) {
|
|
|
517
598
|
return;
|
|
518
599
|
}
|
|
519
600
|
|
|
520
|
-
// zip source: re-download / re-extract from the recorded URL or local path.
|
|
601
|
+
// zip/tgz source: re-download / re-extract from the recorded URL or local path.
|
|
521
602
|
if (source.type === 'zip') {
|
|
522
603
|
if (source.url) {
|
|
523
604
|
await installFromZip(source.url, dir, true);
|
|
@@ -528,34 +609,39 @@ async function cmdUpdate(id, opts) {
|
|
|
528
609
|
}
|
|
529
610
|
return;
|
|
530
611
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
const info = pluginInfo(fresh);
|
|
541
|
-
rmSync(target, { recursive: true, force: true });
|
|
542
|
-
cpSync(fresh, target, { recursive: true, force: true, dereference: false });
|
|
543
|
-
recordInstall(dir, id, source);
|
|
544
|
-
console.log(`✓ Updated "${info.displayName}" (${id}) → v${info.version}`);
|
|
545
|
-
} finally {
|
|
546
|
-
try { rmSync(tmp, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
612
|
+
if (source.type === 'tgz') {
|
|
613
|
+
if (source.url) {
|
|
614
|
+
await installFromTgz(source.url, dir, { isUrl: true, lockSource: source });
|
|
615
|
+
} else if (source.localPath) {
|
|
616
|
+
await installFromTgz(source.localPath, dir, { isUrl: false, lockSource: source });
|
|
617
|
+
} else {
|
|
618
|
+
throw new Error(`tgz install record for "${id}" has no url or localPath; reinstall with \`add\`.`);
|
|
619
|
+
}
|
|
620
|
+
return;
|
|
547
621
|
}
|
|
622
|
+
|
|
623
|
+
// npm source: reinstall the latest published version by downloading dist.tarball directly.
|
|
624
|
+
const resolved = await resolveNpmTarball(source.package ?? id);
|
|
625
|
+
await installFromTgz(resolved.tarball, dir, {
|
|
626
|
+
isUrl: true,
|
|
627
|
+
lockSource: { type: 'npm', package: resolved.package, version: resolved.version },
|
|
628
|
+
});
|
|
548
629
|
}
|
|
549
630
|
|
|
550
631
|
function parseArgs(argv) {
|
|
551
632
|
const args = [...argv];
|
|
552
633
|
const cmd = args.shift();
|
|
553
|
-
const opts = { global: false };
|
|
634
|
+
const opts = { global: false, registry: null };
|
|
554
635
|
const rest = [];
|
|
555
636
|
for (let i = 0; i < args.length; i += 1) {
|
|
556
637
|
const a = args[i];
|
|
557
638
|
if (a === '--global' || a === '-g') opts.global = true;
|
|
558
|
-
else if (a === '--
|
|
639
|
+
else if (a === '--registry') {
|
|
640
|
+
const value = args[i + 1];
|
|
641
|
+
if (!value || value.startsWith('--')) throw new Error('--registry requires a URL');
|
|
642
|
+
opts.registry = value;
|
|
643
|
+
i += 1;
|
|
644
|
+
} else if (a === '--cwd') {
|
|
559
645
|
// Removed: extensions no longer support project/--cwd scope. Consume any
|
|
560
646
|
// following path argument so old invocations fail loudly instead of
|
|
561
647
|
// silently being parsed as the install source.
|
|
@@ -566,12 +652,13 @@ function parseArgs(argv) {
|
|
|
566
652
|
}
|
|
567
653
|
|
|
568
654
|
function help() {
|
|
569
|
-
console.log(`npx @finch.app/minitools\n\nUsage:\n add <npm-package|local-path|url.zip> [--global]\n update <id> [--global]\n list [--global]\n remove <id> [--global]\n enable <id>\n disable <id>\n where\n doctor [path]\n\nInstall locations:\n default workspace.json#finchHomeDir/.finch/extensions/ (personal — default)\n --global ~/.finch/extensions/ (global)\n\nThere is no project/--cwd scope — extensions only install to personal or global.\n`);
|
|
655
|
+
console.log(`npx @finch.app/minitools\n\nUsage:\n add <npm-package|local-path|url.zip|url.tgz> [--global] [--registry <url>]\n update <id> [--global] [--registry <url>]\n list [--global]\n remove <id> [--global]\n enable <id>\n disable <id>\n where\n doctor [path]\n\nInstall locations:\n default workspace.json#finchHomeDir/.finch/extensions/ (personal — default)\n --global ~/.finch/extensions/ (global)\n\nRegistry:\n --registry <url> overrides npm registry for npm package metadata/tarball downloads.\n If omitted, npm_config_registry is used, then https://registry.npmjs.org.\n\nThere is no project/--cwd scope — extensions only install to personal or global.\n`);
|
|
570
656
|
}
|
|
571
657
|
|
|
572
658
|
(async () => {
|
|
573
659
|
try {
|
|
574
660
|
const { cmd, rest, opts } = parseArgs(process.argv.slice(2));
|
|
661
|
+
registryOverride = opts.registry;
|
|
575
662
|
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') return help();
|
|
576
663
|
if (cmd === 'add') {
|
|
577
664
|
if (!rest[0]) throw new Error('missing source');
|