@tankpkg/cli 0.15.8 → 0.16.1
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/dist/bin/tank.js +314 -24
- package/dist/bin/tank.js.map +1 -1
- package/dist/{debug-logger-BCwL85ni.js → debug-logger-BDCjX9M-.js} +2 -2
- package/dist/debug-logger-BDCjX9M-.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/package.json +1 -1
- package/package.json +1 -1
- package/dist/debug-logger-BCwL85ni.js.map +0 -1
package/dist/bin/tank.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as VERSION, i as USER_AGENT, l as setConfig, n as flushLogs, o as getConfig, s as getConfigDir, t as authFlowLog } from "../debug-logger-
|
|
2
|
+
import { a as VERSION, i as USER_AGENT, l as setConfig, n as flushLogs, o as getConfig, s as getConfigDir, t as authFlowLog } from "../debug-logger-BDCjX9M-.js";
|
|
3
3
|
import { t as logger } from "../logger-BhULz3Uz.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { Command } from "commander";
|
|
@@ -10,12 +10,12 @@ import path, { join } from "node:path";
|
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import semver from "semver";
|
|
12
12
|
import ora from "ora";
|
|
13
|
+
import crypto$1, { randomUUID } from "node:crypto";
|
|
14
|
+
import { createInterface } from "node:readline";
|
|
13
15
|
import { confirm, input } from "@inquirer/prompts";
|
|
14
16
|
import { execSync, spawn } from "node:child_process";
|
|
15
|
-
import crypto$1 from "node:crypto";
|
|
16
|
-
import { mkdir, mkdtemp, rm, stat } from "node:fs/promises";
|
|
17
17
|
import { create, extract } from "tar";
|
|
18
|
-
import {
|
|
18
|
+
import { mkdir, mkdtemp, rm, stat } from "node:fs/promises";
|
|
19
19
|
import { Readable } from "node:stream";
|
|
20
20
|
import { pipeline } from "node:stream/promises";
|
|
21
21
|
import open from "open";
|
|
@@ -2508,6 +2508,122 @@ function getSkillLinkStatus(options) {
|
|
|
2508
2508
|
return agents.map((agent) => getStatusForAgent(agent, options.skillName));
|
|
2509
2509
|
}
|
|
2510
2510
|
//#endregion
|
|
2511
|
+
//#region src/lib/telemetry.ts
|
|
2512
|
+
const POSTHOG_PROJECT_KEY = process.env.TANK_POSTHOG_KEY ?? "phc_j9KjoTTYWsM4k40f2h61x8TRe8cx4ZhIMIKIVri0G7Z";
|
|
2513
|
+
const POSTHOG_DEFAULT_HOST = "https://eu.i.posthog.com";
|
|
2514
|
+
function getHost() {
|
|
2515
|
+
return process.env.TANK_TELEMETRY_HOST ?? POSTHOG_DEFAULT_HOST;
|
|
2516
|
+
}
|
|
2517
|
+
function isSelfhosted() {
|
|
2518
|
+
return process.env.TANK_MODE === "selfhosted";
|
|
2519
|
+
}
|
|
2520
|
+
function getTelemetryStatus(configDir) {
|
|
2521
|
+
if (isSelfhosted()) return {
|
|
2522
|
+
enabled: false,
|
|
2523
|
+
reason: "onprem"
|
|
2524
|
+
};
|
|
2525
|
+
if (!POSTHOG_PROJECT_KEY) return {
|
|
2526
|
+
enabled: false,
|
|
2527
|
+
reason: "no-key"
|
|
2528
|
+
};
|
|
2529
|
+
const env = process.env.TANK_TELEMETRY?.trim();
|
|
2530
|
+
if (env === "0" || env === "false" || env === "off") return {
|
|
2531
|
+
enabled: false,
|
|
2532
|
+
reason: "env-off"
|
|
2533
|
+
};
|
|
2534
|
+
if (env === "1" || env === "true" || env === "on") return {
|
|
2535
|
+
enabled: true,
|
|
2536
|
+
reason: "env-on"
|
|
2537
|
+
};
|
|
2538
|
+
if (getConfig(configDir).telemetry === true) return {
|
|
2539
|
+
enabled: true,
|
|
2540
|
+
reason: "config"
|
|
2541
|
+
};
|
|
2542
|
+
return {
|
|
2543
|
+
enabled: false,
|
|
2544
|
+
reason: "default-off"
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
function getOrCreateDistinctId(configDir) {
|
|
2548
|
+
const cfg = getConfig(configDir);
|
|
2549
|
+
if (cfg.telemetryDistinctId) return cfg.telemetryDistinctId;
|
|
2550
|
+
const id = randomUUID();
|
|
2551
|
+
setConfig({ telemetryDistinctId: id }, configDir);
|
|
2552
|
+
return id;
|
|
2553
|
+
}
|
|
2554
|
+
function setTelemetry(enabled, configDir) {
|
|
2555
|
+
setConfig({ telemetry: enabled }, configDir);
|
|
2556
|
+
}
|
|
2557
|
+
function captureEvent(evt, configDir) {
|
|
2558
|
+
if (!getTelemetryStatus(configDir).enabled) return;
|
|
2559
|
+
const distinctId = getOrCreateDistinctId(configDir);
|
|
2560
|
+
const payload = {
|
|
2561
|
+
api_key: POSTHOG_PROJECT_KEY,
|
|
2562
|
+
event: evt.event,
|
|
2563
|
+
distinct_id: distinctId,
|
|
2564
|
+
properties: {
|
|
2565
|
+
...evt.properties,
|
|
2566
|
+
cli_version: VERSION,
|
|
2567
|
+
platform: process.platform,
|
|
2568
|
+
node_version: process.versions.node,
|
|
2569
|
+
$lib: "tank-cli"
|
|
2570
|
+
},
|
|
2571
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2572
|
+
};
|
|
2573
|
+
const url = `${getHost()}/i/v0/e/`;
|
|
2574
|
+
const controller = new AbortController();
|
|
2575
|
+
const timer = setTimeout(() => controller.abort(), 2e3);
|
|
2576
|
+
fetch(url, {
|
|
2577
|
+
method: "POST",
|
|
2578
|
+
headers: { "Content-Type": "application/json" },
|
|
2579
|
+
body: JSON.stringify(payload),
|
|
2580
|
+
signal: controller.signal
|
|
2581
|
+
}).catch(() => {}).finally(() => clearTimeout(timer));
|
|
2582
|
+
}
|
|
2583
|
+
function describeTelemetryState(configDir) {
|
|
2584
|
+
const status = getTelemetryStatus(configDir);
|
|
2585
|
+
if (status.reason === "onprem") return "Telemetry: disabled (on-prem mode)";
|
|
2586
|
+
if (status.reason === "no-key") return "Telemetry: disabled (no key compiled in this build)";
|
|
2587
|
+
if (status.reason === "env-off") return "Telemetry: disabled (overridden by TANK_TELEMETRY env var)";
|
|
2588
|
+
if (status.reason === "env-on") return "Telemetry: enabled (overridden by TANK_TELEMETRY env var)";
|
|
2589
|
+
if (status.reason === "config") return "Telemetry: enabled";
|
|
2590
|
+
return "Telemetry: disabled. Run `tank telemetry on` to opt in.";
|
|
2591
|
+
}
|
|
2592
|
+
/**
|
|
2593
|
+
* Prompt the user once for telemetry consent on first interactive use.
|
|
2594
|
+
* Skipped if: a decision is already recorded, no TTY (CI), on-prem, no key compiled.
|
|
2595
|
+
* The decision (true or false) is persisted to config so we never prompt again.
|
|
2596
|
+
*/
|
|
2597
|
+
async function maybePromptForTelemetryConsent(configDir) {
|
|
2598
|
+
if (isSelfhosted()) return;
|
|
2599
|
+
if (!POSTHOG_PROJECT_KEY) return;
|
|
2600
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return;
|
|
2601
|
+
if (process.env.CI || process.env.TANK_TELEMETRY) return;
|
|
2602
|
+
if (typeof getConfig(configDir).telemetry === "boolean") return;
|
|
2603
|
+
const answer = await askYesNo("Help improve Tank by sending anonymous usage analytics? (No package names, paths, or keys are ever sent.)");
|
|
2604
|
+
setConfig({ telemetry: answer }, configDir);
|
|
2605
|
+
if (answer) {
|
|
2606
|
+
captureEvent({
|
|
2607
|
+
event: "cli_opted_in",
|
|
2608
|
+
properties: { source: "first-run-prompt" }
|
|
2609
|
+
}, configDir);
|
|
2610
|
+
process.stderr.write("Telemetry: enabled. Disable any time with `tank telemetry off`.\n");
|
|
2611
|
+
} else process.stderr.write("Telemetry: disabled. Re-enable any time with `tank telemetry on`.\n");
|
|
2612
|
+
}
|
|
2613
|
+
function askYesNo(question) {
|
|
2614
|
+
return new Promise((resolve) => {
|
|
2615
|
+
const rl = createInterface({
|
|
2616
|
+
input: process.stdin,
|
|
2617
|
+
output: process.stderr
|
|
2618
|
+
});
|
|
2619
|
+
rl.question(`${question} [y/N] `, (raw) => {
|
|
2620
|
+
rl.close();
|
|
2621
|
+
const a = raw.trim().toLowerCase();
|
|
2622
|
+
resolve(a === "y" || a === "yes");
|
|
2623
|
+
});
|
|
2624
|
+
});
|
|
2625
|
+
}
|
|
2626
|
+
//#endregion
|
|
2511
2627
|
//#region src/commands/doctor.ts
|
|
2512
2628
|
const parseLockKey$3 = (key) => {
|
|
2513
2629
|
const lastAt = key.lastIndexOf("@");
|
|
@@ -2625,6 +2741,8 @@ async function doctorCommand(options) {
|
|
|
2625
2741
|
console.log(` ${skillName} ${summary.statusText}`);
|
|
2626
2742
|
}
|
|
2627
2743
|
if (localSkills.length === 0 && uniqueGlobal.length === 0 && devLinks.length === 0) suggestions.add("Run `tank install @tank/typescript` to add your first skill");
|
|
2744
|
+
printSectionHeader("Telemetry");
|
|
2745
|
+
console.log(` ${describeTelemetryState()}`);
|
|
2628
2746
|
printSectionHeader("Suggestions");
|
|
2629
2747
|
if (suggestions.size === 0) console.log(" none");
|
|
2630
2748
|
else for (const suggestion of suggestions) console.log(` • ${suggestion}`);
|
|
@@ -2771,6 +2889,7 @@ async function initCommand(options = {}) {
|
|
|
2771
2889
|
}
|
|
2772
2890
|
fs.writeFileSync(filePath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
2773
2891
|
logger.success(`Created ${MANIFEST_FILENAME$1}`);
|
|
2892
|
+
await maybePromptForTelemetryConsent();
|
|
2774
2893
|
return;
|
|
2775
2894
|
}
|
|
2776
2895
|
if (resolved.exists) {
|
|
@@ -2829,6 +2948,7 @@ async function initCommand(options = {}) {
|
|
|
2829
2948
|
}
|
|
2830
2949
|
fs.writeFileSync(filePath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
2831
2950
|
logger.success(`Created ${MANIFEST_FILENAME$1}`);
|
|
2951
|
+
await maybePromptForTelemetryConsent();
|
|
2832
2952
|
}
|
|
2833
2953
|
//#endregion
|
|
2834
2954
|
//#region ../internals-helpers/dist/index.js
|
|
@@ -10476,6 +10596,7 @@ async function loginCommand(options = {}) {
|
|
|
10476
10596
|
}, configDir);
|
|
10477
10597
|
const displayName = user.name ?? user.email ?? "unknown";
|
|
10478
10598
|
logger.success(`Logged in as ${displayName}`);
|
|
10599
|
+
await maybePromptForTelemetryConsent(configDir);
|
|
10479
10600
|
return;
|
|
10480
10601
|
}
|
|
10481
10602
|
if (exchangeRes.status !== 400) {
|
|
@@ -11465,6 +11586,39 @@ async function searchCommand(options) {
|
|
|
11465
11586
|
console.log(`${data.results.length} skill${data.results.length === 1 ? "" : "s"} found`);
|
|
11466
11587
|
}
|
|
11467
11588
|
//#endregion
|
|
11589
|
+
//#region src/commands/telemetry.ts
|
|
11590
|
+
async function telemetryCommand(opts) {
|
|
11591
|
+
const { action, configDir } = opts;
|
|
11592
|
+
if (action === "status") {
|
|
11593
|
+
logger.info(describeTelemetryState(configDir));
|
|
11594
|
+
return;
|
|
11595
|
+
}
|
|
11596
|
+
if (action === "on") {
|
|
11597
|
+
setTelemetry(true, configDir);
|
|
11598
|
+
const status = getTelemetryStatus(configDir);
|
|
11599
|
+
if (status.reason === "onprem") {
|
|
11600
|
+
logger.warn("Telemetry config written, but disabled because TANK_MODE=selfhosted.");
|
|
11601
|
+
return;
|
|
11602
|
+
}
|
|
11603
|
+
if (status.reason === "no-key") {
|
|
11604
|
+
logger.warn("Telemetry config written, but this build has no telemetry key compiled in.");
|
|
11605
|
+
return;
|
|
11606
|
+
}
|
|
11607
|
+
captureEvent({ event: "cli_opted_in" }, configDir);
|
|
11608
|
+
logger.info("Telemetry: enabled. Thanks for helping improve Tank.");
|
|
11609
|
+
logger.info("Disable any time: tank telemetry off");
|
|
11610
|
+
return;
|
|
11611
|
+
}
|
|
11612
|
+
if (action === "off") {
|
|
11613
|
+
captureEvent({ event: "cli_opted_out" }, configDir);
|
|
11614
|
+
setTelemetry(false, configDir);
|
|
11615
|
+
logger.info("Telemetry: disabled.");
|
|
11616
|
+
return;
|
|
11617
|
+
}
|
|
11618
|
+
logger.error(`Unknown telemetry action: ${action}. Use on | off | status.`);
|
|
11619
|
+
process.exitCode = 1;
|
|
11620
|
+
}
|
|
11621
|
+
//#endregion
|
|
11468
11622
|
//#region src/commands/unlink.ts
|
|
11469
11623
|
async function unlinkCommand(options = {}) {
|
|
11470
11624
|
const resolvedManifest = resolveManifestPath(options.directory ?? process.cwd());
|
|
@@ -11923,6 +12077,92 @@ function printUserInfo(user) {
|
|
|
11923
12077
|
logger.info(`Email: ${user.email ?? "unknown"}`);
|
|
11924
12078
|
}
|
|
11925
12079
|
//#endregion
|
|
12080
|
+
//#region src/lib/install-suggestions.ts
|
|
12081
|
+
/**
|
|
12082
|
+
* Best-effort fuzzy lookup of similar skill names. Hits the public /api/v1/search
|
|
12083
|
+
* endpoint (no auth needed for public skills). Returns up to `limit` matches.
|
|
12084
|
+
* Silent failure: never throws — suggestions are advisory, not critical-path.
|
|
12085
|
+
*/
|
|
12086
|
+
async function fetchSimilarSkillNames(query, opts = {}) {
|
|
12087
|
+
const { configDir, limit = 3, timeoutMs = 2e3 } = opts;
|
|
12088
|
+
const config = getConfig(configDir);
|
|
12089
|
+
const searchTerm = query.replace(/^@[^/]+\//, "").replace(/^[^a-z0-9]+/i, "") || query;
|
|
12090
|
+
const controller = new AbortController();
|
|
12091
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
12092
|
+
try {
|
|
12093
|
+
const res = await fetch(`${config.registry}/api/v1/search?q=${encodeURIComponent(searchTerm)}&limit=${limit}`, {
|
|
12094
|
+
headers: { "User-Agent": USER_AGENT },
|
|
12095
|
+
signal: controller.signal
|
|
12096
|
+
});
|
|
12097
|
+
clearTimeout(timer);
|
|
12098
|
+
if (!res.ok) return [];
|
|
12099
|
+
return ((await res.json()).results ?? []).filter((r) => r.name && r.name !== query).slice(0, limit);
|
|
12100
|
+
} catch {
|
|
12101
|
+
clearTimeout(timer);
|
|
12102
|
+
return [];
|
|
12103
|
+
}
|
|
12104
|
+
}
|
|
12105
|
+
function formatInstallSuggestions(name, suggestions) {
|
|
12106
|
+
if (suggestions.length === 0) return `Try \`tank search ${name}\` to find similar packages.`;
|
|
12107
|
+
const lines = ["Did you mean one of these?"];
|
|
12108
|
+
for (const s of suggestions) lines.push(` • ${s.name}${s.description ? ` — ${s.description.slice(0, 60)}` : ""}`);
|
|
12109
|
+
lines.push(`\nOr search: \`tank search ${name}\``);
|
|
12110
|
+
return lines.join("\n");
|
|
12111
|
+
}
|
|
12112
|
+
//#endregion
|
|
12113
|
+
//#region src/lib/install-target.ts
|
|
12114
|
+
/**
|
|
12115
|
+
* Parse a single install target string into a structured target.
|
|
12116
|
+
*
|
|
12117
|
+
* Accepted forms (npm-compatible):
|
|
12118
|
+
* - `@org/pkg` → name with no range
|
|
12119
|
+
* - `@org/pkg@^1.0.0` → name + range (split on the LAST `@` for scoped names)
|
|
12120
|
+
* - `pkg` → unscoped name with no range
|
|
12121
|
+
* - `pkg@1.0.0` → unscoped name + range
|
|
12122
|
+
* - `https://github.com/...` → URL install
|
|
12123
|
+
*
|
|
12124
|
+
* The `@` that separates name from range is the FIRST `@` that is NOT at position 0
|
|
12125
|
+
* (position 0 is the scope marker for `@org/...`).
|
|
12126
|
+
*/
|
|
12127
|
+
function parseInstallTarget(target) {
|
|
12128
|
+
if (isUrl(target)) return {
|
|
12129
|
+
kind: "url",
|
|
12130
|
+
url: target
|
|
12131
|
+
};
|
|
12132
|
+
const searchStart = target.startsWith("@") ? 1 : 0;
|
|
12133
|
+
const versionAt = target.indexOf("@", searchStart);
|
|
12134
|
+
if (versionAt === -1) return {
|
|
12135
|
+
kind: "name",
|
|
12136
|
+
name: target
|
|
12137
|
+
};
|
|
12138
|
+
const name = target.slice(0, versionAt);
|
|
12139
|
+
const versionRange = target.slice(versionAt + 1);
|
|
12140
|
+
if (!name || !versionRange) return {
|
|
12141
|
+
kind: "name",
|
|
12142
|
+
name: target
|
|
12143
|
+
};
|
|
12144
|
+
return {
|
|
12145
|
+
kind: "name",
|
|
12146
|
+
name,
|
|
12147
|
+
versionRange
|
|
12148
|
+
};
|
|
12149
|
+
}
|
|
12150
|
+
/**
|
|
12151
|
+
* Heuristic: does this string look like a bare semver range rather than a skill name?
|
|
12152
|
+
* Used to detect the legacy `tank install @org/skill ^1.0.0` positional form so we can
|
|
12153
|
+
* preserve back-compat with the previous CLI signature.
|
|
12154
|
+
*
|
|
12155
|
+
* Returns true for strings like `^1.0.0`, `~1`, `>=2`, `1.x`, `*`, `latest`, `next`, `1.2.3`.
|
|
12156
|
+
* Returns false for skill names (contain `/`, start with `@`, or are URLs).
|
|
12157
|
+
*/
|
|
12158
|
+
function looksLikeVersionRange(s) {
|
|
12159
|
+
if (!s || s.includes("/") || s.startsWith("@") || isUrl(s)) return false;
|
|
12160
|
+
if (s === "*" || s === "latest" || s === "next") return true;
|
|
12161
|
+
if (/^[\^~><=]/.test(s)) return true;
|
|
12162
|
+
if (/^\d/.test(s)) return true;
|
|
12163
|
+
return false;
|
|
12164
|
+
}
|
|
12165
|
+
//#endregion
|
|
11926
12166
|
//#region src/lib/upgrade-check.ts
|
|
11927
12167
|
function isNewerVersion(candidateVersion, currentVersion) {
|
|
11928
12168
|
if (candidateVersion === currentVersion) return false;
|
|
@@ -11967,6 +12207,14 @@ async function checkForUpgrade(configDir) {
|
|
|
11967
12207
|
//#region src/bin/tank.ts
|
|
11968
12208
|
const program = new Command();
|
|
11969
12209
|
program.name("tank").description("Security-first package manager for AI agent skills").version(VERSION);
|
|
12210
|
+
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
12211
|
+
const name = actionCommand.name();
|
|
12212
|
+
if (name === "telemetry") return;
|
|
12213
|
+
captureEvent({
|
|
12214
|
+
event: "cli_command",
|
|
12215
|
+
properties: { command: name }
|
|
12216
|
+
});
|
|
12217
|
+
});
|
|
11970
12218
|
program.command("init").description("Create a new tank.json in the current directory").option("-y, --yes", "Skip prompts, use defaults").option("--name <name>", "Skill name").option("--skill-version <version>", "Skill version (default: 0.1.0)").option("--description <desc>", "Skill description").option("--private", "Make skill private").option("--force", "Overwrite existing tank.json").action(async (opts) => {
|
|
11971
12219
|
try {
|
|
11972
12220
|
await initCommand({
|
|
@@ -12040,26 +12288,54 @@ program.command("publish").alias("pub").description("Pack and publish a skill to
|
|
|
12040
12288
|
process.exit(1);
|
|
12041
12289
|
}
|
|
12042
12290
|
});
|
|
12043
|
-
program.command("install").alias("i").description("Install
|
|
12044
|
-
|
|
12045
|
-
|
|
12046
|
-
|
|
12047
|
-
|
|
12048
|
-
|
|
12049
|
-
|
|
12050
|
-
|
|
12051
|
-
|
|
12052
|
-
|
|
12053
|
-
|
|
12054
|
-
|
|
12055
|
-
}
|
|
12056
|
-
|
|
12057
|
-
|
|
12058
|
-
|
|
12059
|
-
|
|
12060
|
-
|
|
12061
|
-
const
|
|
12062
|
-
|
|
12291
|
+
program.command("install").alias("i").description("Install one or more skills from the Tank registry, URLs, or all skills from lockfile").argument("[targets...]", "One or more skill specs or URLs (e.g. @org/skill, @org/skill@^1.0.0, https://github.com/owner/repo). Omit to install from lockfile.").option("-g, --global", "Install skill globally (available to all projects)").option("-y, --yes", "Auto-accept flagged scan verdicts").option("--dangerously-no-tank-proxy", "Skip wrapping MCP servers with the tank proxy (no scanning, no enforcement)").action(async (targets, opts) => {
|
|
12292
|
+
const proxyOpt = opts.dangerouslyNoTankProxy ? { dangerouslyNoTankProxy: true } : {};
|
|
12293
|
+
if (targets.length === 0) {
|
|
12294
|
+
try {
|
|
12295
|
+
await installAll({
|
|
12296
|
+
global: opts.global,
|
|
12297
|
+
...proxyOpt
|
|
12298
|
+
});
|
|
12299
|
+
} catch (err) {
|
|
12300
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
12301
|
+
console.error(`Install failed: ${msg}`);
|
|
12302
|
+
process.exit(1);
|
|
12303
|
+
}
|
|
12304
|
+
return;
|
|
12305
|
+
}
|
|
12306
|
+
const effectiveTargets = targets.length === 2 && looksLikeVersionRange(targets[1]) ? [`${targets[0]}@${targets[1]}`] : targets;
|
|
12307
|
+
const failures = [];
|
|
12308
|
+
for (const target of effectiveTargets) {
|
|
12309
|
+
const parsed = parseInstallTarget(target);
|
|
12310
|
+
try {
|
|
12311
|
+
if (parsed.kind === "url") await installFromUrl(parsed.url, {
|
|
12312
|
+
global: opts.global,
|
|
12313
|
+
yes: opts.yes,
|
|
12314
|
+
...proxyOpt
|
|
12315
|
+
});
|
|
12316
|
+
else await installCommand({
|
|
12317
|
+
name: parsed.name,
|
|
12318
|
+
versionRange: parsed.versionRange ?? "*",
|
|
12319
|
+
global: opts.global,
|
|
12320
|
+
...proxyOpt
|
|
12321
|
+
});
|
|
12322
|
+
} catch (err) {
|
|
12323
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
12324
|
+
failures.push({
|
|
12325
|
+
target,
|
|
12326
|
+
error: msg,
|
|
12327
|
+
parsedName: parsed.kind === "name" ? parsed.name : void 0
|
|
12328
|
+
});
|
|
12329
|
+
console.error(`Install failed for ${target}: ${msg}`);
|
|
12330
|
+
}
|
|
12331
|
+
}
|
|
12332
|
+
for (const failure of failures) {
|
|
12333
|
+
if (!failure.parsedName || !/not found/i.test(failure.error)) continue;
|
|
12334
|
+
const suggestions = await fetchSimilarSkillNames(failure.parsedName);
|
|
12335
|
+
console.error(`\n${formatInstallSuggestions(failure.parsedName, suggestions)}`);
|
|
12336
|
+
}
|
|
12337
|
+
if (failures.length > 0) {
|
|
12338
|
+
console.error(`\nInstall finished with ${failures.length}/${effectiveTargets.length} failure(s).`);
|
|
12063
12339
|
process.exit(1);
|
|
12064
12340
|
}
|
|
12065
12341
|
});
|
|
@@ -12246,6 +12522,20 @@ program.command("upgrade").description("Update tank to the latest version").argu
|
|
|
12246
12522
|
}
|
|
12247
12523
|
await flushLogs();
|
|
12248
12524
|
});
|
|
12525
|
+
program.command("telemetry <action>").description("Manage anonymous usage telemetry (on | off | status). Opt-in only, never enabled by default.").action(async (action) => {
|
|
12526
|
+
try {
|
|
12527
|
+
const normalized = action.toLowerCase();
|
|
12528
|
+
if (normalized !== "on" && normalized !== "off" && normalized !== "status") {
|
|
12529
|
+
console.error(`Unknown telemetry action: ${action}. Use: on | off | status.`);
|
|
12530
|
+
process.exit(1);
|
|
12531
|
+
}
|
|
12532
|
+
await telemetryCommand({ action: normalized });
|
|
12533
|
+
} catch (err) {
|
|
12534
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
12535
|
+
console.error(`Telemetry command failed: ${msg}`);
|
|
12536
|
+
process.exit(1);
|
|
12537
|
+
}
|
|
12538
|
+
});
|
|
12249
12539
|
checkForUpgrade().catch(() => {});
|
|
12250
12540
|
program.parse();
|
|
12251
12541
|
//#endregion
|