@tankpkg/cli 0.8.1 → 0.9.0
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 +130 -36
- package/dist/bin/tank.js.map +1 -1
- package/dist/{debug-logger-BjGZcIhg.js → debug-logger-BJzuguP3.js} +2 -2
- package/dist/{debug-logger-BjGZcIhg.js.map → debug-logger-BJzuguP3.js.map} +1 -1
- package/dist/index.js +1 -1
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/bin/tank.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as VERSION, c as getConfigDir, i as USER_AGENT, n as flushLogs, o as logger, s as getConfig, t as authFlowLog, u as setConfig } from "../debug-logger-
|
|
2
|
+
import { a as VERSION, c as getConfigDir, i as USER_AGENT, n as flushLogs, o as logger, s as getConfig, t as authFlowLog, u as setConfig } from "../debug-logger-BJzuguP3.js";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import fs from "node:fs";
|
|
@@ -1291,26 +1291,6 @@ function getResolvedNodesInOrder(nodes, installOrder) {
|
|
|
1291
1291
|
//#endregion
|
|
1292
1292
|
//#region src/lib/permission-checker.ts
|
|
1293
1293
|
/**
|
|
1294
|
-
* Check if a skill's permissions fit within the project's permission budget.
|
|
1295
|
-
* Throws if any permission exceeds the budget.
|
|
1296
|
-
*/
|
|
1297
|
-
function checkPermissionBudget(budget, skillPerms, skillName) {
|
|
1298
|
-
if (!skillPerms) return;
|
|
1299
|
-
if (skillPerms.subprocess === true && budget.subprocess !== true) throw new Error(`Permission denied: ${skillName} requires subprocess access, but project budget does not allow it`);
|
|
1300
|
-
if (skillPerms.network?.outbound && skillPerms.network.outbound.length > 0) {
|
|
1301
|
-
const budgetDomains = budget.network?.outbound ?? [];
|
|
1302
|
-
for (const domain of skillPerms.network.outbound) if (!isDomainAllowed$1(domain, budgetDomains)) throw new Error(`Permission denied: ${skillName} requests network access to "${domain}", which is not in the project's permission budget`);
|
|
1303
|
-
}
|
|
1304
|
-
if (skillPerms.filesystem?.read && skillPerms.filesystem.read.length > 0) {
|
|
1305
|
-
const budgetPaths = budget.filesystem?.read ?? [];
|
|
1306
|
-
for (const p of skillPerms.filesystem.read) if (!isPathAllowed$1(p, budgetPaths)) throw new Error(`Permission denied: ${skillName} requests filesystem read access to "${p}", which is not in the project's permission budget`);
|
|
1307
|
-
}
|
|
1308
|
-
if (skillPerms.filesystem?.write && skillPerms.filesystem.write.length > 0) {
|
|
1309
|
-
const budgetPaths = budget.filesystem?.write ?? [];
|
|
1310
|
-
for (const p of skillPerms.filesystem.write) if (!isPathAllowed$1(p, budgetPaths)) throw new Error(`Permission denied: ${skillName} requests filesystem write access to "${p}", which is not in the project's permission budget`);
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
/**
|
|
1314
1294
|
* Check if a domain is allowed by the budget's domain list.
|
|
1315
1295
|
* Supports wildcard matching: *.example.com matches sub.example.com
|
|
1316
1296
|
*/
|
|
@@ -1339,6 +1319,91 @@ function isPathAllowed$1(requestedPath, allowedPaths) {
|
|
|
1339
1319
|
}
|
|
1340
1320
|
return false;
|
|
1341
1321
|
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Collect all permission violations without throwing.
|
|
1324
|
+
* Mirrors checkPermissionBudget() logic but returns violations as an array.
|
|
1325
|
+
*/
|
|
1326
|
+
function collectPermissionViolations(budget, skillPerms, skillName) {
|
|
1327
|
+
if (!skillPerms) return [];
|
|
1328
|
+
const violations = [];
|
|
1329
|
+
if (skillPerms.subprocess === true && budget.subprocess !== true) violations.push({
|
|
1330
|
+
skillName,
|
|
1331
|
+
type: "subprocess",
|
|
1332
|
+
requested: "true"
|
|
1333
|
+
});
|
|
1334
|
+
if (skillPerms.network?.outbound && skillPerms.network.outbound.length > 0) {
|
|
1335
|
+
const budgetDomains = budget.network?.outbound ?? [];
|
|
1336
|
+
for (const domain of skillPerms.network.outbound) if (!isDomainAllowed$1(domain, budgetDomains)) violations.push({
|
|
1337
|
+
skillName,
|
|
1338
|
+
type: "network.outbound",
|
|
1339
|
+
requested: domain
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
if (skillPerms.filesystem?.read && skillPerms.filesystem.read.length > 0) {
|
|
1343
|
+
const budgetPaths = budget.filesystem?.read ?? [];
|
|
1344
|
+
for (const p of skillPerms.filesystem.read) if (!isPathAllowed$1(p, budgetPaths)) violations.push({
|
|
1345
|
+
skillName,
|
|
1346
|
+
type: "filesystem.read",
|
|
1347
|
+
requested: p
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
if (skillPerms.filesystem?.write && skillPerms.filesystem.write.length > 0) {
|
|
1351
|
+
const budgetPaths = budget.filesystem?.write ?? [];
|
|
1352
|
+
for (const p of skillPerms.filesystem.write) if (!isPathAllowed$1(p, budgetPaths)) violations.push({
|
|
1353
|
+
skillName,
|
|
1354
|
+
type: "filesystem.write",
|
|
1355
|
+
requested: p
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
return violations;
|
|
1359
|
+
}
|
|
1360
|
+
//#endregion
|
|
1361
|
+
//#region src/lib/permission-prompt.ts
|
|
1362
|
+
async function promptForPermissionExpansion(violations, options) {
|
|
1363
|
+
if (options.yes === true) return "accept";
|
|
1364
|
+
if (options.isInteractive === false) return "decline";
|
|
1365
|
+
logger.warn("The following permissions exceed your project budget:");
|
|
1366
|
+
for (const v of violations) logger.warn(` ${v.skillName}: ${v.type} → ${v.requested}`);
|
|
1367
|
+
return await confirm({
|
|
1368
|
+
message: "Would you like to add these permissions to tank.json?",
|
|
1369
|
+
default: true
|
|
1370
|
+
}) ? "accept" : "decline";
|
|
1371
|
+
}
|
|
1372
|
+
function mergePermissionsIntoBudget(currentBudget, violations) {
|
|
1373
|
+
const result = {
|
|
1374
|
+
...currentBudget,
|
|
1375
|
+
network: currentBudget.network ? {
|
|
1376
|
+
...currentBudget.network,
|
|
1377
|
+
outbound: [...currentBudget.network.outbound ?? []]
|
|
1378
|
+
} : void 0,
|
|
1379
|
+
filesystem: currentBudget.filesystem ? {
|
|
1380
|
+
...currentBudget.filesystem,
|
|
1381
|
+
read: currentBudget.filesystem.read ? [...currentBudget.filesystem.read] : void 0,
|
|
1382
|
+
write: currentBudget.filesystem.write ? [...currentBudget.filesystem.write] : void 0
|
|
1383
|
+
} : void 0
|
|
1384
|
+
};
|
|
1385
|
+
for (const v of violations) switch (v.type) {
|
|
1386
|
+
case "filesystem.read":
|
|
1387
|
+
if (!result.filesystem) result.filesystem = {};
|
|
1388
|
+
if (!result.filesystem.read) result.filesystem.read = [];
|
|
1389
|
+
if (!result.filesystem.read.includes(v.requested)) result.filesystem.read.push(v.requested);
|
|
1390
|
+
break;
|
|
1391
|
+
case "filesystem.write":
|
|
1392
|
+
if (!result.filesystem) result.filesystem = {};
|
|
1393
|
+
if (!result.filesystem.write) result.filesystem.write = [];
|
|
1394
|
+
if (!result.filesystem.write.includes(v.requested)) result.filesystem.write.push(v.requested);
|
|
1395
|
+
break;
|
|
1396
|
+
case "network.outbound":
|
|
1397
|
+
if (!result.network) result.network = {};
|
|
1398
|
+
if (!result.network.outbound) result.network.outbound = [];
|
|
1399
|
+
if (!result.network.outbound.includes(v.requested)) result.network.outbound.push(v.requested);
|
|
1400
|
+
break;
|
|
1401
|
+
case "subprocess":
|
|
1402
|
+
result.subprocess = true;
|
|
1403
|
+
break;
|
|
1404
|
+
}
|
|
1405
|
+
return result;
|
|
1406
|
+
}
|
|
1342
1407
|
//#endregion
|
|
1343
1408
|
//#region src/commands/install.ts
|
|
1344
1409
|
function createRegistryFetcher(registry, headers) {
|
|
@@ -1432,15 +1497,30 @@ function buildLockedVersionByName(lock) {
|
|
|
1432
1497
|
function createExtractDirResolver(directory, global, resolvedHome) {
|
|
1433
1498
|
return (skillName) => global ? getGlobalExtractDir(resolvedHome, skillName) : getExtractDir$1(directory, skillName);
|
|
1434
1499
|
}
|
|
1435
|
-
function validateResolvedNodes(resolvedNodes, projectPermissions, auditMinScore) {
|
|
1500
|
+
async function validateResolvedNodes(resolvedNodes, projectPermissions, auditMinScore, options) {
|
|
1436
1501
|
if (!projectPermissions) logger.warn(`No permission budget defined in ${MANIFEST_FILENAME}. Install proceeding without permission checks.`);
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
if (
|
|
1440
|
-
|
|
1441
|
-
|
|
1502
|
+
if (projectPermissions) {
|
|
1503
|
+
const allViolations = resolvedNodes.flatMap((node) => collectPermissionViolations(projectPermissions, node.meta.permissions, node.name));
|
|
1504
|
+
if (allViolations.length > 0) {
|
|
1505
|
+
const isInteractive = !process.env.CI && process.stdout.isTTY === true;
|
|
1506
|
+
const decision = await promptForPermissionExpansion(allViolations, {
|
|
1507
|
+
yes: options?.yes,
|
|
1508
|
+
isInteractive
|
|
1509
|
+
});
|
|
1510
|
+
if (decision === "accept" && options?.skillsJsonPath && options?.skillsJson) {
|
|
1511
|
+
const merged = mergePermissionsIntoBudget(projectPermissions, allViolations);
|
|
1512
|
+
options.skillsJson.permissions = merged;
|
|
1513
|
+
fs.writeFileSync(options.skillsJsonPath, `${JSON.stringify(options.skillsJson, null, 2)}\n`);
|
|
1514
|
+
} else if (decision === "decline") {
|
|
1515
|
+
const first = allViolations[0];
|
|
1516
|
+
throw new Error(`Permission denied: ${first.skillName} requests ${first.type} access to "${first.requested}", which is not in the project's permission budget`);
|
|
1517
|
+
}
|
|
1442
1518
|
}
|
|
1443
1519
|
}
|
|
1520
|
+
for (const node of resolvedNodes) if (auditMinScore !== void 0) {
|
|
1521
|
+
if (node.meta.auditScore === null || node.meta.auditScore === void 0) logger.warn(`Audit score not yet available for ${node.name}. Install proceeding without audit score check.`);
|
|
1522
|
+
else if (node.meta.auditScore < auditMinScore) throw new Error(`Audit score ${node.meta.auditScore} for ${node.name} is below minimum threshold ${auditMinScore} defined in ${MANIFEST_FILENAME}`);
|
|
1523
|
+
}
|
|
1444
1524
|
}
|
|
1445
1525
|
async function runLegacyFallback(options) {
|
|
1446
1526
|
const { rootSkillNames, resolvedNodeByName, extractDirForSkill, directory, configDir, global, homedir } = options;
|
|
@@ -1490,8 +1570,12 @@ function linkInstalledRoots(options) {
|
|
|
1490
1570
|
if (detectInstalledAgents(homedir).length === 0) logger.warn("No agents detected for linking");
|
|
1491
1571
|
}
|
|
1492
1572
|
async function executeInstallPipeline(options) {
|
|
1493
|
-
const { directory, configDir, global, homedir, resolvedHome, lock, lockPath, resolvedNodes, nodesToInstall, rootSkillNames, projectPermissions, auditMinScore, spinner } = options;
|
|
1494
|
-
if (!global) validateResolvedNodes(resolvedNodes, projectPermissions, auditMinScore
|
|
1573
|
+
const { directory, configDir, global, homedir, resolvedHome, lock, lockPath, resolvedNodes, nodesToInstall, rootSkillNames, projectPermissions, auditMinScore, spinner, yes, skillsJsonPath, skillsJson } = options;
|
|
1574
|
+
if (!global) await validateResolvedNodes(resolvedNodes, projectPermissions, auditMinScore, {
|
|
1575
|
+
yes,
|
|
1576
|
+
skillsJsonPath,
|
|
1577
|
+
skillsJson
|
|
1578
|
+
});
|
|
1495
1579
|
const extractDirForSkill = createExtractDirResolver(directory, global, resolvedHome);
|
|
1496
1580
|
const resolvedNodeByName = new Map(resolvedNodes.map((node) => [node.name, node]));
|
|
1497
1581
|
const downloaded = await downloadAllParallel(nodesToInstall, spinner);
|
|
@@ -1529,7 +1613,7 @@ async function executeInstallPipeline(options) {
|
|
|
1529
1613
|
return updatedLock;
|
|
1530
1614
|
}
|
|
1531
1615
|
async function installCommand(options) {
|
|
1532
|
-
const { name, versionRange = "*", directory = process.cwd(), configDir, global = false, homedir, isTransitive = false } = options;
|
|
1616
|
+
const { name, versionRange = "*", directory = process.cwd(), configDir, global = false, homedir, isTransitive = false, yes } = options;
|
|
1533
1617
|
const config = getConfig(configDir);
|
|
1534
1618
|
const resolvedHome = homedir ?? os.homedir();
|
|
1535
1619
|
const requestHeaders = { "User-Agent": USER_AGENT };
|
|
@@ -1585,7 +1669,10 @@ async function installCommand(options) {
|
|
|
1585
1669
|
rootSkillNames: [name],
|
|
1586
1670
|
projectPermissions,
|
|
1587
1671
|
auditMinScore,
|
|
1588
|
-
spinner
|
|
1672
|
+
spinner,
|
|
1673
|
+
yes,
|
|
1674
|
+
skillsJsonPath,
|
|
1675
|
+
skillsJson
|
|
1589
1676
|
});
|
|
1590
1677
|
if (!global && !isTransitive) {
|
|
1591
1678
|
const skills = skillsJson.skills ?? {};
|
|
@@ -1683,7 +1770,7 @@ async function installFromLockfile(options) {
|
|
|
1683
1770
|
}
|
|
1684
1771
|
}
|
|
1685
1772
|
async function installAll(options) {
|
|
1686
|
-
const { directory = process.cwd(), configDir, global = false, homedir } = options;
|
|
1773
|
+
const { directory = process.cwd(), configDir, global = false, homedir, yes } = options;
|
|
1687
1774
|
const resolvedHome = homedir ?? os.homedir();
|
|
1688
1775
|
const config = getConfig(configDir);
|
|
1689
1776
|
const requestHeaders = { "User-Agent": USER_AGENT };
|
|
@@ -1738,7 +1825,10 @@ async function installAll(options) {
|
|
|
1738
1825
|
rootSkillNames: skillEntries.map(([skillName]) => skillName),
|
|
1739
1826
|
projectPermissions,
|
|
1740
1827
|
auditMinScore,
|
|
1741
|
-
spinner
|
|
1828
|
+
spinner,
|
|
1829
|
+
yes,
|
|
1830
|
+
skillsJsonPath,
|
|
1831
|
+
skillsJson
|
|
1742
1832
|
});
|
|
1743
1833
|
spinner.succeed(`Installed ${skillEntries.length} root skill${skillEntries.length === 1 ? "" : "s"}`);
|
|
1744
1834
|
} catch (err) {
|
|
@@ -3294,14 +3384,18 @@ program.command("publish").alias("pub").description("Pack and publish a skill to
|
|
|
3294
3384
|
process.exit(1);
|
|
3295
3385
|
}
|
|
3296
3386
|
});
|
|
3297
|
-
program.command("install").alias("i").description("Install a skill from the Tank registry, or all skills from lockfile").argument("[name]", "Skill name (e.g., @org/skill-name). Omit to install from lockfile.").argument("[version-range]", "Semver range (default: *)", "*").option("-g, --global", "Install skill globally (available to all projects)").action(async (name, versionRange, opts) => {
|
|
3387
|
+
program.command("install").alias("i").description("Install a skill from the Tank registry, or all skills from lockfile").argument("[name]", "Skill name (e.g., @org/skill-name). Omit to install from lockfile.").argument("[version-range]", "Semver range (default: *)", "*").option("-g, --global", "Install skill globally (available to all projects)").option("-y, --yes", "Auto-accept permission budget expansion").action(async (name, versionRange, opts) => {
|
|
3298
3388
|
try {
|
|
3299
3389
|
if (name) await installCommand({
|
|
3300
3390
|
name,
|
|
3301
3391
|
versionRange,
|
|
3302
|
-
global: opts.global
|
|
3392
|
+
global: opts.global,
|
|
3393
|
+
yes: opts.yes
|
|
3394
|
+
});
|
|
3395
|
+
else await installAll({
|
|
3396
|
+
global: opts.global,
|
|
3397
|
+
yes: opts.yes
|
|
3303
3398
|
});
|
|
3304
|
-
else await installAll({ global: opts.global });
|
|
3305
3399
|
} catch (err) {
|
|
3306
3400
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3307
3401
|
console.error(`Install failed: ${msg}`);
|