@tankpkg/cli 0.9.0 → 0.10.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 +92 -180
- package/dist/bin/tank.js.map +1 -1
- package/dist/{debug-logger-BJzuguP3.js → debug-logger-DnEQXtQC.js} +3 -3
- package/dist/debug-logger-DnEQXtQC.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/package.json +13 -12
- package/package.json +13 -12
- package/dist/debug-logger-BJzuguP3.js.map +0 -1
package/dist/bin/tank.js
CHANGED
|
@@ -1,44 +1,23 @@
|
|
|
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-DnEQXtQC.js";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import semver from "semver";
|
|
9
8
|
import { z } from "zod";
|
|
10
9
|
import { confirm, input } from "@inquirer/prompts";
|
|
11
10
|
import crypto$1 from "node:crypto";
|
|
11
|
+
import semver from "semver";
|
|
12
12
|
import ora from "ora";
|
|
13
13
|
import { create, extract } from "tar";
|
|
14
14
|
import open from "open";
|
|
15
15
|
import ignore from "ignore";
|
|
16
|
+
process.env.TANK_REGISTRY_URL;
|
|
16
17
|
const MANIFEST_FILENAME = "tank.json";
|
|
17
18
|
const LEGACY_MANIFEST_FILENAME = "skills.json";
|
|
18
19
|
const LOCKFILE_FILENAME = "tank.lock";
|
|
19
20
|
const LEGACY_LOCKFILE_FILENAME = "skills.lock";
|
|
20
|
-
/**
|
|
21
|
-
* Resolves a semver range against a list of available versions.
|
|
22
|
-
* Returns the highest version that satisfies the range, or null if none match.
|
|
23
|
-
*
|
|
24
|
-
* Pre-release versions are excluded from range matching unless the range
|
|
25
|
-
* explicitly includes a pre-release tag (e.g., ">=1.0.0-beta.1").
|
|
26
|
-
* Exact version matches always work, including for pre-release versions.
|
|
27
|
-
*
|
|
28
|
-
* @param range - A semver range string (e.g., "^2.1.0", "~1.0.0", ">=2.0.0 <3.0.0", "*")
|
|
29
|
-
* @param versions - An array of semver version strings to match against
|
|
30
|
-
* @returns The highest matching version string, or null if no match
|
|
31
|
-
*/
|
|
32
|
-
function resolve(range, versions) {
|
|
33
|
-
try {
|
|
34
|
-
if (!range || !semver.validRange(range)) return null;
|
|
35
|
-
const validVersions = versions.filter((v) => semver.valid(v) !== null);
|
|
36
|
-
if (validVersions.length === 0) return null;
|
|
37
|
-
return semver.maxSatisfying(validVersions, range) ?? null;
|
|
38
|
-
} catch {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
21
|
const networkPermissionsSchema = z.object({ outbound: z.array(z.string()).optional() }).strict();
|
|
43
22
|
const filesystemPermissionsSchema = z.object({
|
|
44
23
|
read: z.array(z.string()).optional(),
|
|
@@ -78,9 +57,9 @@ z.enum([
|
|
|
78
57
|
"org.delete"
|
|
79
58
|
]);
|
|
80
59
|
const skillsJsonSchema = z.object({
|
|
81
|
-
name: z.string().min(1, "Name must not be empty").max(214,
|
|
60
|
+
name: z.string().min(1, "Name must not be empty").max(214, `Name must be 214 characters or fewer`).regex(/^@[a-z0-9-]+\/[a-z0-9][a-z0-9-]*$/, "Name must be scoped (@org/name), lowercase alphanumeric and hyphens"),
|
|
82
61
|
version: z.string().regex(/^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/, "Version must be valid semver"),
|
|
83
|
-
description: z.string().max(500,
|
|
62
|
+
description: z.string().max(500, `Description must be 500 characters or fewer`).optional(),
|
|
84
63
|
skills: z.record(z.string(), z.string()).optional(),
|
|
85
64
|
permissions: permissionsSchema.optional(),
|
|
86
65
|
repository: z.string().url("Repository must be a valid URL").optional(),
|
|
@@ -876,7 +855,7 @@ async function initCommand(options = {}) {
|
|
|
876
855
|
for (const issue of result.error.issues) logger.error(` ${issue.path.join(".")}: ${issue.message}`);
|
|
877
856
|
return;
|
|
878
857
|
}
|
|
879
|
-
fs.writeFileSync(filePath, JSON.stringify(manifest, null, 2)
|
|
858
|
+
fs.writeFileSync(filePath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
880
859
|
logger.success(`Created ${MANIFEST_FILENAME}`);
|
|
881
860
|
return;
|
|
882
861
|
}
|
|
@@ -934,10 +913,22 @@ async function initCommand(options = {}) {
|
|
|
934
913
|
for (const issue of result.error.issues) logger.error(` ${issue.path.join(".")}: ${issue.message}`);
|
|
935
914
|
return;
|
|
936
915
|
}
|
|
937
|
-
fs.writeFileSync(filePath, JSON.stringify(manifest, null, 2)
|
|
916
|
+
fs.writeFileSync(filePath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
938
917
|
logger.success(`Created ${MANIFEST_FILENAME}`);
|
|
939
918
|
}
|
|
940
919
|
//#endregion
|
|
920
|
+
//#region ../internals-helpers/dist/index.js
|
|
921
|
+
function resolve(range, versions) {
|
|
922
|
+
try {
|
|
923
|
+
if (!range || !semver.validRange(range)) return null;
|
|
924
|
+
const validVersions = versions.filter((v) => semver.valid(v) !== null);
|
|
925
|
+
if (validVersions.length === 0) return null;
|
|
926
|
+
return semver.maxSatisfying(validVersions, range) ?? null;
|
|
927
|
+
} catch {
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
//#endregion
|
|
941
932
|
//#region src/lib/dependency-resolver.ts
|
|
942
933
|
function buildSkillKey(name, version) {
|
|
943
934
|
return `${name}@${version}`;
|
|
@@ -1291,6 +1282,26 @@ function getResolvedNodesInOrder(nodes, installOrder) {
|
|
|
1291
1282
|
//#endregion
|
|
1292
1283
|
//#region src/lib/permission-checker.ts
|
|
1293
1284
|
/**
|
|
1285
|
+
* Check if a skill's permissions fit within the project's permission budget.
|
|
1286
|
+
* Throws if any permission exceeds the budget.
|
|
1287
|
+
*/
|
|
1288
|
+
function checkPermissionBudget(budget, skillPerms, skillName) {
|
|
1289
|
+
if (!skillPerms) return;
|
|
1290
|
+
if (skillPerms.subprocess === true && budget.subprocess !== true) throw new Error(`Permission denied: ${skillName} requires subprocess access, but project budget does not allow it`);
|
|
1291
|
+
if (skillPerms.network?.outbound && skillPerms.network.outbound.length > 0) {
|
|
1292
|
+
const budgetDomains = budget.network?.outbound ?? [];
|
|
1293
|
+
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`);
|
|
1294
|
+
}
|
|
1295
|
+
if (skillPerms.filesystem?.read && skillPerms.filesystem.read.length > 0) {
|
|
1296
|
+
const budgetPaths = budget.filesystem?.read ?? [];
|
|
1297
|
+
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`);
|
|
1298
|
+
}
|
|
1299
|
+
if (skillPerms.filesystem?.write && skillPerms.filesystem.write.length > 0) {
|
|
1300
|
+
const budgetPaths = budget.filesystem?.write ?? [];
|
|
1301
|
+
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`);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1294
1305
|
* Check if a domain is allowed by the budget's domain list.
|
|
1295
1306
|
* Supports wildcard matching: *.example.com matches sub.example.com
|
|
1296
1307
|
*/
|
|
@@ -1319,91 +1330,6 @@ function isPathAllowed$1(requestedPath, allowedPaths) {
|
|
|
1319
1330
|
}
|
|
1320
1331
|
return false;
|
|
1321
1332
|
}
|
|
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
|
-
}
|
|
1407
1333
|
//#endregion
|
|
1408
1334
|
//#region src/commands/install.ts
|
|
1409
1335
|
function createRegistryFetcher(registry, headers) {
|
|
@@ -1497,30 +1423,15 @@ function buildLockedVersionByName(lock) {
|
|
|
1497
1423
|
function createExtractDirResolver(directory, global, resolvedHome) {
|
|
1498
1424
|
return (skillName) => global ? getGlobalExtractDir(resolvedHome, skillName) : getExtractDir$1(directory, skillName);
|
|
1499
1425
|
}
|
|
1500
|
-
|
|
1426
|
+
function validateResolvedNodes(resolvedNodes, projectPermissions, auditMinScore) {
|
|
1501
1427
|
if (!projectPermissions) logger.warn(`No permission budget defined in ${MANIFEST_FILENAME}. Install proceeding without permission checks.`);
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
if (
|
|
1505
|
-
|
|
1506
|
-
|
|
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
|
-
}
|
|
1428
|
+
for (const node of resolvedNodes) {
|
|
1429
|
+
if (projectPermissions) checkPermissionBudget(projectPermissions, node.meta.permissions, node.name);
|
|
1430
|
+
if (auditMinScore !== void 0) {
|
|
1431
|
+
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.`);
|
|
1432
|
+
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}`);
|
|
1518
1433
|
}
|
|
1519
1434
|
}
|
|
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
|
-
}
|
|
1524
1435
|
}
|
|
1525
1436
|
async function runLegacyFallback(options) {
|
|
1526
1437
|
const { rootSkillNames, resolvedNodeByName, extractDirForSkill, directory, configDir, global, homedir } = options;
|
|
@@ -1570,12 +1481,8 @@ function linkInstalledRoots(options) {
|
|
|
1570
1481
|
if (detectInstalledAgents(homedir).length === 0) logger.warn("No agents detected for linking");
|
|
1571
1482
|
}
|
|
1572
1483
|
async function executeInstallPipeline(options) {
|
|
1573
|
-
const { directory, configDir, global, homedir, resolvedHome, lock, lockPath, resolvedNodes, nodesToInstall, rootSkillNames, projectPermissions, auditMinScore, spinner
|
|
1574
|
-
if (!global)
|
|
1575
|
-
yes,
|
|
1576
|
-
skillsJsonPath,
|
|
1577
|
-
skillsJson
|
|
1578
|
-
});
|
|
1484
|
+
const { directory, configDir, global, homedir, resolvedHome, lock, lockPath, resolvedNodes, nodesToInstall, rootSkillNames, projectPermissions, auditMinScore, spinner } = options;
|
|
1485
|
+
if (!global) validateResolvedNodes(resolvedNodes, projectPermissions, auditMinScore);
|
|
1579
1486
|
const extractDirForSkill = createExtractDirResolver(directory, global, resolvedHome);
|
|
1580
1487
|
const resolvedNodeByName = new Map(resolvedNodes.map((node) => [node.name, node]));
|
|
1581
1488
|
const downloaded = await downloadAllParallel(nodesToInstall, spinner);
|
|
@@ -1613,7 +1520,7 @@ async function executeInstallPipeline(options) {
|
|
|
1613
1520
|
return updatedLock;
|
|
1614
1521
|
}
|
|
1615
1522
|
async function installCommand(options) {
|
|
1616
|
-
const { name, versionRange = "*", directory = process.cwd(), configDir, global = false, homedir, isTransitive = false
|
|
1523
|
+
const { name, versionRange = "*", directory = process.cwd(), configDir, global = false, homedir, isTransitive = false } = options;
|
|
1617
1524
|
const config = getConfig(configDir);
|
|
1618
1525
|
const resolvedHome = homedir ?? os.homedir();
|
|
1619
1526
|
const requestHeaders = { "User-Agent": USER_AGENT };
|
|
@@ -1669,10 +1576,7 @@ async function installCommand(options) {
|
|
|
1669
1576
|
rootSkillNames: [name],
|
|
1670
1577
|
projectPermissions,
|
|
1671
1578
|
auditMinScore,
|
|
1672
|
-
spinner
|
|
1673
|
-
yes,
|
|
1674
|
-
skillsJsonPath,
|
|
1675
|
-
skillsJson
|
|
1579
|
+
spinner
|
|
1676
1580
|
});
|
|
1677
1581
|
if (!global && !isTransitive) {
|
|
1678
1582
|
const skills = skillsJson.skills ?? {};
|
|
@@ -1770,7 +1674,7 @@ async function installFromLockfile(options) {
|
|
|
1770
1674
|
}
|
|
1771
1675
|
}
|
|
1772
1676
|
async function installAll(options) {
|
|
1773
|
-
const { directory = process.cwd(), configDir, global = false, homedir
|
|
1677
|
+
const { directory = process.cwd(), configDir, global = false, homedir } = options;
|
|
1774
1678
|
const resolvedHome = homedir ?? os.homedir();
|
|
1775
1679
|
const config = getConfig(configDir);
|
|
1776
1680
|
const requestHeaders = { "User-Agent": USER_AGENT };
|
|
@@ -1825,10 +1729,7 @@ async function installAll(options) {
|
|
|
1825
1729
|
rootSkillNames: skillEntries.map(([skillName]) => skillName),
|
|
1826
1730
|
projectPermissions,
|
|
1827
1731
|
auditMinScore,
|
|
1828
|
-
spinner
|
|
1829
|
-
yes,
|
|
1830
|
-
skillsJsonPath,
|
|
1831
|
-
skillsJson
|
|
1732
|
+
spinner
|
|
1832
1733
|
});
|
|
1833
1734
|
spinner.succeed(`Installed ${skillEntries.length} root skill${skillEntries.length === 1 ? "" : "s"}`);
|
|
1834
1735
|
} catch (err) {
|
|
@@ -1914,28 +1815,38 @@ async function loginCommand(options = {}) {
|
|
|
1914
1815
|
const state = crypto.randomUUID();
|
|
1915
1816
|
authFlowLog.info({ state: `${state.slice(0, 8)}...` }, "Login flow started");
|
|
1916
1817
|
logger.info("Starting login...");
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1818
|
+
let authUrl;
|
|
1819
|
+
let sessionCode;
|
|
1820
|
+
try {
|
|
1821
|
+
const startRes = await fetch(`${baseUrl}/api/v1/cli-auth/start`, {
|
|
1822
|
+
method: "POST",
|
|
1823
|
+
headers: { "Content-Type": "application/json" },
|
|
1824
|
+
body: JSON.stringify({ state })
|
|
1825
|
+
});
|
|
1826
|
+
if (!startRes.ok) {
|
|
1827
|
+
const body = await startRes.json().catch(() => null);
|
|
1828
|
+
authFlowLog.error({
|
|
1829
|
+
status: startRes.status,
|
|
1830
|
+
error: body?.error
|
|
1831
|
+
}, "Start request failed");
|
|
1832
|
+
throw new Error(`Failed to start auth session: ${body?.error ?? startRes.statusText}`);
|
|
1833
|
+
}
|
|
1834
|
+
authFlowLog.info({
|
|
1835
|
+
ok: startRes.ok,
|
|
1836
|
+
status: startRes.status
|
|
1837
|
+
}, "Start response received");
|
|
1838
|
+
const startData = await startRes.json();
|
|
1839
|
+
authUrl = startData.authUrl;
|
|
1840
|
+
sessionCode = startData.sessionCode;
|
|
1841
|
+
authFlowLog.info({
|
|
1842
|
+
authUrl,
|
|
1843
|
+
sessionCode: `${sessionCode.slice(0, 8)}...`
|
|
1844
|
+
}, "Session created, opening browser");
|
|
1845
|
+
} catch (err) {
|
|
1846
|
+
if (err instanceof Error && err.message.startsWith("Failed to start auth session:")) throw err;
|
|
1847
|
+
authFlowLog.error({ error: err instanceof Error ? err.message : String(err) }, "Start request failed");
|
|
1848
|
+
throw new Error(`Could not reach registry at ${baseUrl}. Check your internet connection or registry URL.\n Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1849
|
+
}
|
|
1939
1850
|
try {
|
|
1940
1851
|
await open(authUrl);
|
|
1941
1852
|
logger.info("Opened browser for authentication.");
|
|
@@ -2293,6 +2204,7 @@ async function packForScan(directory) {
|
|
|
2293
2204
|
readmeContent = "";
|
|
2294
2205
|
}
|
|
2295
2206
|
const files = collectFiles(absDir, absDir, buildIgnoreFilter(absDir));
|
|
2207
|
+
if (files.length === 0) throw new Error(`No manifest found and no files to scan in ${absDir}`);
|
|
2296
2208
|
if (files.length > MAX_FILE_COUNT) throw new Error(`Too many files: ${files.length} exceeds maximum of ${MAX_FILE_COUNT}`);
|
|
2297
2209
|
let totalSize = 0;
|
|
2298
2210
|
for (const file of files) {
|
|
@@ -2568,7 +2480,7 @@ async function removeCommand(options) {
|
|
|
2568
2480
|
if (!(name in skills)) throw new Error(`Skill "${name}" is not installed (not found in ${path.basename(resolvedManifest.path)})`);
|
|
2569
2481
|
delete skills[name];
|
|
2570
2482
|
skillsJson.skills = skills;
|
|
2571
|
-
fs.writeFileSync(resolvedManifest.path, JSON.stringify(skillsJson, null, 2)
|
|
2483
|
+
fs.writeFileSync(resolvedManifest.path, `${JSON.stringify(skillsJson, null, 2)}\n`);
|
|
2572
2484
|
const resolvedLocalLock = resolveLockfilePath(directory);
|
|
2573
2485
|
const lockPath = resolvedLocalLock.path;
|
|
2574
2486
|
if (resolvedLocalLock.exists) {
|
|
@@ -3130,6 +3042,10 @@ async function upgradeCommand(opts) {
|
|
|
3130
3042
|
console.log(chalk.yellow("Tank was installed via Homebrew. Run `brew upgrade tank` instead."));
|
|
3131
3043
|
return;
|
|
3132
3044
|
}
|
|
3045
|
+
if (currentBinaryPath.includes("node_modules") || currentBinaryPath.endsWith(".js") || currentBinaryPath.endsWith(".mjs")) {
|
|
3046
|
+
console.log(chalk.yellow("Tank was installed via npm/npx. Run `npm update -g @tankpkg/cli` to upgrade instead."));
|
|
3047
|
+
return;
|
|
3048
|
+
}
|
|
3133
3049
|
let targetVersion;
|
|
3134
3050
|
if (opts?.version) targetVersion = opts.version;
|
|
3135
3051
|
else {
|
|
@@ -3384,18 +3300,14 @@ program.command("publish").alias("pub").description("Pack and publish a skill to
|
|
|
3384
3300
|
process.exit(1);
|
|
3385
3301
|
}
|
|
3386
3302
|
});
|
|
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)").
|
|
3303
|
+
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) => {
|
|
3388
3304
|
try {
|
|
3389
3305
|
if (name) await installCommand({
|
|
3390
3306
|
name,
|
|
3391
3307
|
versionRange,
|
|
3392
|
-
global: opts.global
|
|
3393
|
-
yes: opts.yes
|
|
3394
|
-
});
|
|
3395
|
-
else await installAll({
|
|
3396
|
-
global: opts.global,
|
|
3397
|
-
yes: opts.yes
|
|
3308
|
+
global: opts.global
|
|
3398
3309
|
});
|
|
3310
|
+
else await installAll({ global: opts.global });
|
|
3399
3311
|
} catch (err) {
|
|
3400
3312
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3401
3313
|
console.error(`Install failed: ${msg}`);
|