guardskills 1.1.0 → 1.2.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/README.md +24 -5
- package/dist/cli.cjs +524 -109
- package/dist/cli.js +524 -109
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -176,9 +176,13 @@ function enforceSourcePolicy(repoInput, policy) {
|
|
|
176
176
|
|
|
177
177
|
// src/install/skills.ts
|
|
178
178
|
import { execa } from "execa";
|
|
179
|
-
async function
|
|
179
|
+
async function runProviderInstall(provider, repo, skill) {
|
|
180
|
+
if (provider === "playbooks" && !skill) {
|
|
181
|
+
return 30;
|
|
182
|
+
}
|
|
183
|
+
const args = provider === "skills" ? skill ? ["skills", "add", repo, "--skill", skill] : ["skills", "add", repo] : provider === "playbooks" ? ["playbooks", "add", "skill", repo, "--skill", skill] : provider === "skillkit" ? skill ? ["skillkit", "install", repo, "--skill", skill] : ["skillkit", "install", repo] : skill ? ["openskills", "install", repo, skill] : ["openskills", "install", repo];
|
|
180
184
|
try {
|
|
181
|
-
await execa("npx",
|
|
185
|
+
await execa("npx", args, {
|
|
182
186
|
stdio: "inherit"
|
|
183
187
|
});
|
|
184
188
|
return 0;
|
|
@@ -1043,6 +1047,22 @@ var cliAddOptionsSchema = z2.object({
|
|
|
1043
1047
|
maxAuxFiles: z2.coerce.number().int().min(1).max(200).optional(),
|
|
1044
1048
|
maxTotalFiles: z2.coerce.number().int().min(1).max(400).optional()
|
|
1045
1049
|
});
|
|
1050
|
+
var cliBulkAddOptionsSchema = z2.object({
|
|
1051
|
+
config: z2.string().optional(),
|
|
1052
|
+
strict: z2.boolean().optional(),
|
|
1053
|
+
ci: z2.boolean().optional(),
|
|
1054
|
+
json: z2.boolean().optional(),
|
|
1055
|
+
yes: z2.boolean().optional(),
|
|
1056
|
+
dryRun: z2.boolean().optional(),
|
|
1057
|
+
force: z2.boolean().optional(),
|
|
1058
|
+
allowUnverifiable: z2.boolean().optional(),
|
|
1059
|
+
githubTimeoutMs: z2.coerce.number().int().min(1e3).max(12e4).optional(),
|
|
1060
|
+
githubRetries: z2.coerce.number().int().min(0).max(6).optional(),
|
|
1061
|
+
githubRetryBaseMs: z2.coerce.number().int().min(50).max(5e3).optional(),
|
|
1062
|
+
maxFileBytes: z2.coerce.number().int().min(4096).max(5e6).optional(),
|
|
1063
|
+
maxAuxFiles: z2.coerce.number().int().min(1).max(200).optional(),
|
|
1064
|
+
maxTotalFiles: z2.coerce.number().int().min(1).max(400).optional()
|
|
1065
|
+
});
|
|
1046
1066
|
var effectiveAddOptionsSchema = z2.object({
|
|
1047
1067
|
skill: z2.string().min(1),
|
|
1048
1068
|
strict: z2.boolean(),
|
|
@@ -1162,9 +1182,11 @@ function evaluateGate(level, options) {
|
|
|
1162
1182
|
gateNote: level === "WARNING" ? "WARNING accepted via --yes." : "SAFE to proceed."
|
|
1163
1183
|
};
|
|
1164
1184
|
}
|
|
1165
|
-
async function runAddCommand(repo, rawOptions) {
|
|
1185
|
+
async function runAddCommand(repo, rawOptions, context = {}) {
|
|
1166
1186
|
const cliOptions = cliAddOptionsSchema.parse(rawOptions);
|
|
1167
1187
|
const loadedConfig = loadGuardSkillsConfig(cliOptions.config);
|
|
1188
|
+
const provider = context.provider ?? "skills";
|
|
1189
|
+
const commandName = context.commandName ?? "guardskills add";
|
|
1168
1190
|
const options = resolveEffectiveAddOptions(cliOptions, loadedConfig.config);
|
|
1169
1191
|
enforceSourcePolicy(repo, loadedConfig.config.policy);
|
|
1170
1192
|
enforceOptionPolicy(options, loadedConfig.config);
|
|
@@ -1185,7 +1207,7 @@ async function runAddCommand(repo, rawOptions) {
|
|
|
1185
1207
|
const gate = evaluateGate(decision.level, options);
|
|
1186
1208
|
const configNote = loadedConfig.path ? ` Config: ${loadedConfig.path}` : "";
|
|
1187
1209
|
const report = {
|
|
1188
|
-
command:
|
|
1210
|
+
command: commandName,
|
|
1189
1211
|
repo,
|
|
1190
1212
|
skill: options.skill,
|
|
1191
1213
|
strict: options.strict,
|
|
@@ -1210,14 +1232,353 @@ async function runAddCommand(repo, rawOptions) {
|
|
|
1210
1232
|
if (options.dryRun || options.ci) {
|
|
1211
1233
|
return 0;
|
|
1212
1234
|
}
|
|
1213
|
-
return
|
|
1235
|
+
return runProviderInstall(provider, repo, options.skill);
|
|
1214
1236
|
}
|
|
1215
1237
|
|
|
1216
|
-
// src/commands/
|
|
1238
|
+
// src/commands/openskills-install.ts
|
|
1239
|
+
import fs2 from "fs";
|
|
1240
|
+
import os from "os";
|
|
1241
|
+
import path4 from "path";
|
|
1242
|
+
import { execa as execa2 } from "execa";
|
|
1217
1243
|
import { z as z3 } from "zod";
|
|
1244
|
+
var ALLOWED_TEXT_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
1245
|
+
".md",
|
|
1246
|
+
".txt",
|
|
1247
|
+
".sh",
|
|
1248
|
+
".bash",
|
|
1249
|
+
".zsh",
|
|
1250
|
+
".ps1",
|
|
1251
|
+
".py",
|
|
1252
|
+
".js",
|
|
1253
|
+
".ts",
|
|
1254
|
+
".mjs",
|
|
1255
|
+
".cjs",
|
|
1256
|
+
".json",
|
|
1257
|
+
".yaml",
|
|
1258
|
+
".yml",
|
|
1259
|
+
".toml",
|
|
1260
|
+
".ini",
|
|
1261
|
+
".cfg"
|
|
1262
|
+
]);
|
|
1263
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".turbo"]);
|
|
1264
|
+
var cliOpenSkillsOptionsSchema = z3.object({
|
|
1265
|
+
config: z3.string().optional(),
|
|
1266
|
+
strict: z3.boolean().optional(),
|
|
1267
|
+
ci: z3.boolean().optional(),
|
|
1268
|
+
json: z3.boolean().optional(),
|
|
1269
|
+
yes: z3.boolean().optional(),
|
|
1270
|
+
dryRun: z3.boolean().optional(),
|
|
1271
|
+
force: z3.boolean().optional(),
|
|
1272
|
+
allowUnverifiable: z3.boolean().optional(),
|
|
1273
|
+
githubTimeoutMs: z3.coerce.number().int().min(1e3).max(12e4).optional(),
|
|
1274
|
+
githubRetries: z3.coerce.number().int().min(0).max(6).optional(),
|
|
1275
|
+
githubRetryBaseMs: z3.coerce.number().int().min(50).max(5e3).optional(),
|
|
1276
|
+
maxFileBytes: z3.coerce.number().int().min(4096).max(5e6).optional(),
|
|
1277
|
+
maxAuxFiles: z3.coerce.number().int().min(1).max(200).optional(),
|
|
1278
|
+
maxTotalFiles: z3.coerce.number().int().min(1).max(400).optional()
|
|
1279
|
+
});
|
|
1280
|
+
var DEFAULT_OPTIONS2 = {
|
|
1281
|
+
strict: false,
|
|
1282
|
+
ci: false,
|
|
1283
|
+
json: false,
|
|
1284
|
+
yes: false,
|
|
1285
|
+
dryRun: false,
|
|
1286
|
+
force: false,
|
|
1287
|
+
allowUnverifiable: false,
|
|
1288
|
+
githubTimeoutMs: 15e3,
|
|
1289
|
+
githubRetries: 2,
|
|
1290
|
+
githubRetryBaseMs: 300,
|
|
1291
|
+
maxFileBytes: 25e4,
|
|
1292
|
+
maxAuxFiles: 40,
|
|
1293
|
+
maxTotalFiles: 120
|
|
1294
|
+
};
|
|
1295
|
+
function resolveEffectiveOptions(cliOptions, config) {
|
|
1296
|
+
const defaults = config.defaults ?? {};
|
|
1297
|
+
const resolver = config.resolver ?? {};
|
|
1298
|
+
return {
|
|
1299
|
+
strict: cliOptions.strict ?? defaults.strict ?? DEFAULT_OPTIONS2.strict,
|
|
1300
|
+
ci: cliOptions.ci ?? defaults.ci ?? DEFAULT_OPTIONS2.ci,
|
|
1301
|
+
json: cliOptions.json ?? defaults.json ?? DEFAULT_OPTIONS2.json,
|
|
1302
|
+
yes: cliOptions.yes ?? defaults.yes ?? DEFAULT_OPTIONS2.yes,
|
|
1303
|
+
dryRun: cliOptions.dryRun ?? defaults.dryRun ?? DEFAULT_OPTIONS2.dryRun,
|
|
1304
|
+
force: cliOptions.force ?? defaults.force ?? DEFAULT_OPTIONS2.force,
|
|
1305
|
+
allowUnverifiable: cliOptions.allowUnverifiable ?? defaults.allowUnverifiable ?? DEFAULT_OPTIONS2.allowUnverifiable,
|
|
1306
|
+
githubTimeoutMs: cliOptions.githubTimeoutMs ?? resolver.githubTimeoutMs ?? DEFAULT_OPTIONS2.githubTimeoutMs,
|
|
1307
|
+
githubRetries: cliOptions.githubRetries ?? resolver.githubRetries ?? DEFAULT_OPTIONS2.githubRetries,
|
|
1308
|
+
githubRetryBaseMs: cliOptions.githubRetryBaseMs ?? resolver.githubRetryBaseMs ?? DEFAULT_OPTIONS2.githubRetryBaseMs,
|
|
1309
|
+
maxFileBytes: cliOptions.maxFileBytes ?? resolver.maxFileBytes ?? DEFAULT_OPTIONS2.maxFileBytes,
|
|
1310
|
+
maxAuxFiles: cliOptions.maxAuxFiles ?? resolver.maxAuxFiles ?? DEFAULT_OPTIONS2.maxAuxFiles,
|
|
1311
|
+
maxTotalFiles: cliOptions.maxTotalFiles ?? resolver.maxTotalFiles ?? DEFAULT_OPTIONS2.maxTotalFiles
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
function enforceOptionPolicy2(options, config) {
|
|
1315
|
+
const policy = config.policy;
|
|
1316
|
+
if (!policy) {
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
if (options.force && policy.allowForce === false) {
|
|
1320
|
+
throw new GuardSkillsError(
|
|
1321
|
+
"POLICY_VIOLATION",
|
|
1322
|
+
"Policy blocks --force overrides (allowForce=false)."
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
if (options.allowUnverifiable && policy.allowUnverifiableOverride === false) {
|
|
1326
|
+
throw new GuardSkillsError(
|
|
1327
|
+
"POLICY_VIOLATION",
|
|
1328
|
+
"Policy blocks --allow-unverifiable overrides (allowUnverifiableOverride=false)."
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
function findSkillDirs(rootDir) {
|
|
1333
|
+
const found = /* @__PURE__ */ new Set();
|
|
1334
|
+
const stack = [{ dir: rootDir, depth: 0 }];
|
|
1335
|
+
while (stack.length > 0) {
|
|
1336
|
+
const current = stack.pop();
|
|
1337
|
+
if (!current) {
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
const skillFile = path4.join(current.dir, "SKILL.md");
|
|
1341
|
+
if (fs2.existsSync(skillFile) && fs2.statSync(skillFile).isFile()) {
|
|
1342
|
+
found.add(current.dir);
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
if (current.depth >= 8) {
|
|
1346
|
+
continue;
|
|
1347
|
+
}
|
|
1348
|
+
let entries;
|
|
1349
|
+
try {
|
|
1350
|
+
entries = fs2.readdirSync(current.dir, { withFileTypes: true });
|
|
1351
|
+
} catch {
|
|
1352
|
+
continue;
|
|
1353
|
+
}
|
|
1354
|
+
for (const entry of entries) {
|
|
1355
|
+
if (!entry.isDirectory()) {
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
if (SKIP_DIRS.has(entry.name)) {
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
stack.push({ dir: path4.join(current.dir, entry.name), depth: current.depth + 1 });
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
return [...found].sort();
|
|
1365
|
+
}
|
|
1366
|
+
function collectLocalFiles(skillDir, options) {
|
|
1367
|
+
const files = [];
|
|
1368
|
+
const unverifiableReasons = [];
|
|
1369
|
+
const stack = [skillDir];
|
|
1370
|
+
while (stack.length > 0) {
|
|
1371
|
+
const currentDir = stack.pop();
|
|
1372
|
+
if (!currentDir) {
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
let entries;
|
|
1376
|
+
try {
|
|
1377
|
+
entries = fs2.readdirSync(currentDir, { withFileTypes: true });
|
|
1378
|
+
} catch {
|
|
1379
|
+
unverifiableReasons.push(`Cannot read directory: ${currentDir}`);
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
for (const entry of entries) {
|
|
1383
|
+
const fullPath = path4.join(currentDir, entry.name);
|
|
1384
|
+
if (entry.isDirectory()) {
|
|
1385
|
+
if (!SKIP_DIRS.has(entry.name)) {
|
|
1386
|
+
stack.push(fullPath);
|
|
1387
|
+
}
|
|
1388
|
+
continue;
|
|
1389
|
+
}
|
|
1390
|
+
if (!entry.isFile()) {
|
|
1391
|
+
continue;
|
|
1392
|
+
}
|
|
1393
|
+
const relativePath = path4.relative(skillDir, fullPath).replace(/\\/g, "/");
|
|
1394
|
+
const ext = path4.extname(relativePath).toLowerCase();
|
|
1395
|
+
const isSkillFile2 = path4.basename(relativePath).toLowerCase() === "skill.md";
|
|
1396
|
+
if (!isSkillFile2 && !ALLOWED_TEXT_EXTENSIONS2.has(ext)) {
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
if (files.length >= options.maxTotalFiles) {
|
|
1400
|
+
unverifiableReasons.push(
|
|
1401
|
+
`Reached maxTotalFiles=${options.maxTotalFiles}. Remaining files were not scanned.`
|
|
1402
|
+
);
|
|
1403
|
+
return { files, unverifiableReasons };
|
|
1404
|
+
}
|
|
1405
|
+
let sizeBytes = 0;
|
|
1406
|
+
try {
|
|
1407
|
+
sizeBytes = fs2.statSync(fullPath).size;
|
|
1408
|
+
} catch {
|
|
1409
|
+
unverifiableReasons.push(`Cannot stat file: ${relativePath}`);
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1412
|
+
if (sizeBytes > options.maxFileBytes) {
|
|
1413
|
+
unverifiableReasons.push(
|
|
1414
|
+
`Skipped oversized file (${sizeBytes} bytes > ${options.maxFileBytes}): ${relativePath}`
|
|
1415
|
+
);
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
try {
|
|
1419
|
+
files.push({
|
|
1420
|
+
path: relativePath,
|
|
1421
|
+
content: fs2.readFileSync(fullPath, "utf8")
|
|
1422
|
+
});
|
|
1423
|
+
} catch {
|
|
1424
|
+
unverifiableReasons.push(`Cannot read text content: ${relativePath}`);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
return { files, unverifiableReasons };
|
|
1429
|
+
}
|
|
1430
|
+
function resolveSource(source) {
|
|
1431
|
+
const maybePath = path4.resolve(source);
|
|
1432
|
+
if (fs2.existsSync(maybePath)) {
|
|
1433
|
+
return { kind: "local", path: maybePath };
|
|
1434
|
+
}
|
|
1435
|
+
const shorthand = source.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/);
|
|
1436
|
+
if (shorthand) {
|
|
1437
|
+
return { kind: "git", cloneUrl: `https://github.com/${shorthand[1]}/${shorthand[2]}.git` };
|
|
1438
|
+
}
|
|
1439
|
+
try {
|
|
1440
|
+
const parsed = new URL(source);
|
|
1441
|
+
if (parsed.hostname === "github.com" || parsed.hostname === "www.github.com") {
|
|
1442
|
+
return { kind: "git", cloneUrl: source.endsWith(".git") ? source : `${source}.git` };
|
|
1443
|
+
}
|
|
1444
|
+
} catch {
|
|
1445
|
+
}
|
|
1446
|
+
return { kind: "git", cloneUrl: source };
|
|
1447
|
+
}
|
|
1448
|
+
function aggregateLevel(levels) {
|
|
1449
|
+
if (levels.includes("UNVERIFIABLE")) {
|
|
1450
|
+
return "UNVERIFIABLE";
|
|
1451
|
+
}
|
|
1452
|
+
if (levels.includes("CRITICAL")) {
|
|
1453
|
+
return "CRITICAL";
|
|
1454
|
+
}
|
|
1455
|
+
if (levels.includes("UNSAFE")) {
|
|
1456
|
+
return "UNSAFE";
|
|
1457
|
+
}
|
|
1458
|
+
if (levels.includes("WARNING")) {
|
|
1459
|
+
return "WARNING";
|
|
1460
|
+
}
|
|
1461
|
+
return "SAFE";
|
|
1462
|
+
}
|
|
1463
|
+
async function runInteractiveInstallCommand(provider, source, skillName, rawOptions) {
|
|
1464
|
+
if (provider !== "openskills" && provider !== "skills" && provider !== "skillkit") {
|
|
1465
|
+
throw new GuardSkillsError(
|
|
1466
|
+
"INVALID_OPTIONS",
|
|
1467
|
+
`Interactive install flow is not supported for provider '${provider}'.`
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
const cliOptions = cliOpenSkillsOptionsSchema.parse(rawOptions);
|
|
1471
|
+
const loadedConfig = loadGuardSkillsConfig(cliOptions.config);
|
|
1472
|
+
const options = resolveEffectiveOptions(cliOptions, loadedConfig.config);
|
|
1473
|
+
enforceSourcePolicy(source, loadedConfig.config.policy);
|
|
1474
|
+
enforceOptionPolicy2(options, loadedConfig.config);
|
|
1475
|
+
const resolvedSource = resolveSource(source);
|
|
1476
|
+
const tempDir = resolvedSource.kind === "git" ? fs2.mkdtempSync(path4.join(os.tmpdir(), `guardskills-${provider}-`)) : null;
|
|
1477
|
+
const scanRoot = resolvedSource.kind === "git" ? tempDir ?? "" : resolvedSource.path;
|
|
1478
|
+
try {
|
|
1479
|
+
if (resolvedSource.kind === "git") {
|
|
1480
|
+
try {
|
|
1481
|
+
await execa2("git", ["clone", "--depth", "1", resolvedSource.cloneUrl, scanRoot], {
|
|
1482
|
+
timeout: options.githubTimeoutMs
|
|
1483
|
+
});
|
|
1484
|
+
} catch (error) {
|
|
1485
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1486
|
+
throw new GuardSkillsError(
|
|
1487
|
+
"GITHUB_UNKNOWN",
|
|
1488
|
+
`Failed to clone source '${source}' for ${provider} scan: ${message}`,
|
|
1489
|
+
{ cause: error }
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
const discovered = findSkillDirs(scanRoot);
|
|
1494
|
+
if (discovered.length === 0) {
|
|
1495
|
+
throw new GuardSkillsError("SKILL_NOT_FOUND", "No SKILL.md files were found in the source.");
|
|
1496
|
+
}
|
|
1497
|
+
const selectedDirs = skillName ? discovered.filter((dir) => path4.basename(dir).toLowerCase() === skillName.toLowerCase()) : discovered;
|
|
1498
|
+
if (selectedDirs.length === 0) {
|
|
1499
|
+
throw new GuardSkillsError(
|
|
1500
|
+
"SKILL_NOT_FOUND",
|
|
1501
|
+
`Skill '${skillName}' was not found. Available: ${discovered.map((dir) => path4.basename(dir)).join(", ")}`
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
const levels = [];
|
|
1505
|
+
const summaries = [];
|
|
1506
|
+
for (const skillDir of selectedDirs) {
|
|
1507
|
+
const skill = path4.basename(skillDir);
|
|
1508
|
+
const { files, unverifiableReasons } = collectLocalFiles(skillDir, options);
|
|
1509
|
+
const resolvedSkill = {
|
|
1510
|
+
source: `local:${skillDir}`,
|
|
1511
|
+
owner: "local",
|
|
1512
|
+
repo: "local",
|
|
1513
|
+
defaultBranch: "local",
|
|
1514
|
+
commitSha: "local",
|
|
1515
|
+
skillName: skill,
|
|
1516
|
+
skillDir: skillDir.replace(/\\/g, "/"),
|
|
1517
|
+
skillFilePath: "SKILL.md",
|
|
1518
|
+
files,
|
|
1519
|
+
unverifiableReasons
|
|
1520
|
+
};
|
|
1521
|
+
const scan = scanResolvedSkill(resolvedSkill);
|
|
1522
|
+
const decision = calculateRiskScore(scan.findings, {
|
|
1523
|
+
strict: options.strict,
|
|
1524
|
+
trustCredits: 0,
|
|
1525
|
+
hasUnverifiableContent: scan.hasUnverifiableContent
|
|
1526
|
+
});
|
|
1527
|
+
levels.push(decision.level);
|
|
1528
|
+
summaries.push({ skill, level: decision.level, riskScore: decision.riskScore });
|
|
1529
|
+
}
|
|
1530
|
+
const overall = aggregateLevel(levels);
|
|
1531
|
+
const gate = evaluateGate(overall, { ...options, skill: skillName ?? "ALL_SKILLS" });
|
|
1532
|
+
const label = provider === "openskills" ? "OpenSkills" : provider === "skillkit" ? "skillkit" : "skills.sh";
|
|
1533
|
+
const note = `${gate.gateNote} ${label} flow: ${skillName ? "single skill" : "interactive skill selection"}.`;
|
|
1534
|
+
if (options.json) {
|
|
1535
|
+
console.log(
|
|
1536
|
+
JSON.stringify(
|
|
1537
|
+
{
|
|
1538
|
+
command: `guardskills ${provider} ${provider === "skills" ? "add" : "install"}`,
|
|
1539
|
+
source,
|
|
1540
|
+
skill: skillName ?? null,
|
|
1541
|
+
scannedSkills: summaries.length,
|
|
1542
|
+
overallLevel: overall,
|
|
1543
|
+
skills: summaries,
|
|
1544
|
+
note
|
|
1545
|
+
},
|
|
1546
|
+
null,
|
|
1547
|
+
2
|
|
1548
|
+
)
|
|
1549
|
+
);
|
|
1550
|
+
} else {
|
|
1551
|
+
console.log(`Command: guardskills ${provider} ${provider === "skills" ? "add" : "install"}`);
|
|
1552
|
+
console.log(`Source: ${source}`);
|
|
1553
|
+
console.log(`Selection: ${skillName ?? "interactive (all scanned)"}`);
|
|
1554
|
+
console.log(`Scanned Skills: ${summaries.length}`);
|
|
1555
|
+
console.log(`Decision: ${overall}`);
|
|
1556
|
+
console.log("Per-skill results:");
|
|
1557
|
+
for (const summary of summaries) {
|
|
1558
|
+
const score = summary.riskScore === null ? "n/a" : summary.riskScore.toFixed(1);
|
|
1559
|
+
console.log(`- ${summary.skill}: ${summary.level} (risk ${score})`);
|
|
1560
|
+
}
|
|
1561
|
+
console.log(`Note: ${note}`);
|
|
1562
|
+
}
|
|
1563
|
+
if (!gate.canInstall) {
|
|
1564
|
+
return gate.exitCode;
|
|
1565
|
+
}
|
|
1566
|
+
if (options.dryRun || options.ci) {
|
|
1567
|
+
return 0;
|
|
1568
|
+
}
|
|
1569
|
+
return runProviderInstall(provider, source, skillName);
|
|
1570
|
+
} finally {
|
|
1571
|
+
if (tempDir) {
|
|
1572
|
+
fs2.rmSync(tempDir, { recursive: true, force: true });
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// src/commands/scan-clawhub.ts
|
|
1578
|
+
import { z as z4 } from "zod";
|
|
1218
1579
|
|
|
1219
1580
|
// src/resolver/clawhub.ts
|
|
1220
|
-
import
|
|
1581
|
+
import path5 from "path";
|
|
1221
1582
|
import JSZip from "jszip";
|
|
1222
1583
|
var DEFAULT_REGISTRY_BASE_URL = "https://clawhub.ai";
|
|
1223
1584
|
var DEFAULT_ARCHIVE_BASE_URL = "https://auth.clawdhub.com";
|
|
@@ -1225,7 +1586,7 @@ var DEFAULT_REQUEST_TIMEOUT_MS2 = 15e3;
|
|
|
1225
1586
|
var DEFAULT_MAX_FILE_SIZE_BYTES = 25e4;
|
|
1226
1587
|
var DEFAULT_MAX_TOTAL_FILES = 120;
|
|
1227
1588
|
var RETRYABLE_STATUS2 = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
1228
|
-
var
|
|
1589
|
+
var ALLOWED_TEXT_EXTENSIONS3 = /* @__PURE__ */ new Set([
|
|
1229
1590
|
".md",
|
|
1230
1591
|
".txt",
|
|
1231
1592
|
".sh",
|
|
@@ -1467,8 +1828,8 @@ function isLikelyTextFile2(filePath) {
|
|
|
1467
1828
|
if (lower.endsWith("/skill.md") || lower === "skill.md") {
|
|
1468
1829
|
return true;
|
|
1469
1830
|
}
|
|
1470
|
-
const ext =
|
|
1471
|
-
return
|
|
1831
|
+
const ext = path5.posix.extname(lower);
|
|
1832
|
+
return ALLOWED_TEXT_EXTENSIONS3.has(ext);
|
|
1472
1833
|
}
|
|
1473
1834
|
function isBinaryContent2(content) {
|
|
1474
1835
|
return content.includes("\0");
|
|
@@ -1611,7 +1972,7 @@ async function resolveSkillFromArchive(metadata, identifier, options) {
|
|
|
1611
1972
|
defaultBranch: "archive",
|
|
1612
1973
|
commitSha: archiveMeta.version ?? "archive",
|
|
1613
1974
|
skillName: options.skillNameOverride ?? archiveMeta.slug,
|
|
1614
|
-
skillDir:
|
|
1975
|
+
skillDir: path5.posix.dirname(skillFilePath),
|
|
1615
1976
|
skillFilePath,
|
|
1616
1977
|
files,
|
|
1617
1978
|
unverifiableReasons: [...unverifiableReasons],
|
|
@@ -1759,34 +2120,34 @@ async function resolveSkillFromClawHub(identifier, options = {}) {
|
|
|
1759
2120
|
}
|
|
1760
2121
|
|
|
1761
2122
|
// src/commands/scan-clawhub.ts
|
|
1762
|
-
var cliScanClawHubOptionsSchema =
|
|
1763
|
-
config:
|
|
1764
|
-
strict:
|
|
1765
|
-
json:
|
|
1766
|
-
skill:
|
|
1767
|
-
version:
|
|
1768
|
-
clawhubRegistry:
|
|
1769
|
-
githubTimeoutMs:
|
|
1770
|
-
githubRetries:
|
|
1771
|
-
githubRetryBaseMs:
|
|
1772
|
-
maxFileBytes:
|
|
1773
|
-
maxAuxFiles:
|
|
1774
|
-
maxTotalFiles:
|
|
2123
|
+
var cliScanClawHubOptionsSchema = z4.object({
|
|
2124
|
+
config: z4.string().optional(),
|
|
2125
|
+
strict: z4.boolean().optional(),
|
|
2126
|
+
json: z4.boolean().optional(),
|
|
2127
|
+
skill: z4.string().min(1).optional(),
|
|
2128
|
+
version: z4.string().min(1).optional(),
|
|
2129
|
+
clawhubRegistry: z4.string().min(1).optional(),
|
|
2130
|
+
githubTimeoutMs: z4.coerce.number().int().min(1e3).max(12e4).optional(),
|
|
2131
|
+
githubRetries: z4.coerce.number().int().min(0).max(6).optional(),
|
|
2132
|
+
githubRetryBaseMs: z4.coerce.number().int().min(50).max(5e3).optional(),
|
|
2133
|
+
maxFileBytes: z4.coerce.number().int().min(4096).max(5e6).optional(),
|
|
2134
|
+
maxAuxFiles: z4.coerce.number().int().min(1).max(200).optional(),
|
|
2135
|
+
maxTotalFiles: z4.coerce.number().int().min(1).max(400).optional()
|
|
1775
2136
|
});
|
|
1776
|
-
var effectiveScanClawHubOptionsSchema =
|
|
1777
|
-
strict:
|
|
1778
|
-
json:
|
|
1779
|
-
skill:
|
|
1780
|
-
version:
|
|
1781
|
-
clawhubRegistry:
|
|
1782
|
-
githubTimeoutMs:
|
|
1783
|
-
githubRetries:
|
|
1784
|
-
githubRetryBaseMs:
|
|
1785
|
-
maxFileBytes:
|
|
1786
|
-
maxAuxFiles:
|
|
1787
|
-
maxTotalFiles:
|
|
2137
|
+
var effectiveScanClawHubOptionsSchema = z4.object({
|
|
2138
|
+
strict: z4.boolean(),
|
|
2139
|
+
json: z4.boolean(),
|
|
2140
|
+
skill: z4.string().min(1).optional(),
|
|
2141
|
+
version: z4.string().min(1).optional(),
|
|
2142
|
+
clawhubRegistry: z4.string().min(1),
|
|
2143
|
+
githubTimeoutMs: z4.number().int().min(1e3).max(12e4),
|
|
2144
|
+
githubRetries: z4.number().int().min(0).max(6),
|
|
2145
|
+
githubRetryBaseMs: z4.number().int().min(50).max(5e3),
|
|
2146
|
+
maxFileBytes: z4.number().int().min(4096).max(5e6),
|
|
2147
|
+
maxAuxFiles: z4.number().int().min(1).max(200),
|
|
2148
|
+
maxTotalFiles: z4.number().int().min(1).max(400)
|
|
1788
2149
|
});
|
|
1789
|
-
var
|
|
2150
|
+
var DEFAULT_OPTIONS3 = {
|
|
1790
2151
|
strict: false,
|
|
1791
2152
|
json: false,
|
|
1792
2153
|
skill: void 0,
|
|
@@ -1834,17 +2195,17 @@ function resolveEffectiveScanClawHubOptions(cliOptions, config) {
|
|
|
1834
2195
|
const defaults = config.defaults ?? {};
|
|
1835
2196
|
const resolver = config.resolver ?? {};
|
|
1836
2197
|
return effectiveScanClawHubOptionsSchema.parse({
|
|
1837
|
-
strict: cliOptions.strict ?? defaults.strict ??
|
|
1838
|
-
json: cliOptions.json ?? defaults.json ??
|
|
1839
|
-
skill: cliOptions.skill ??
|
|
1840
|
-
version: cliOptions.version ??
|
|
1841
|
-
clawhubRegistry: cliOptions.clawhubRegistry ??
|
|
1842
|
-
githubTimeoutMs: cliOptions.githubTimeoutMs ?? resolver.githubTimeoutMs ??
|
|
1843
|
-
githubRetries: cliOptions.githubRetries ?? resolver.githubRetries ??
|
|
1844
|
-
githubRetryBaseMs: cliOptions.githubRetryBaseMs ?? resolver.githubRetryBaseMs ??
|
|
1845
|
-
maxFileBytes: cliOptions.maxFileBytes ?? resolver.maxFileBytes ??
|
|
1846
|
-
maxAuxFiles: cliOptions.maxAuxFiles ?? resolver.maxAuxFiles ??
|
|
1847
|
-
maxTotalFiles: cliOptions.maxTotalFiles ?? resolver.maxTotalFiles ??
|
|
2198
|
+
strict: cliOptions.strict ?? defaults.strict ?? DEFAULT_OPTIONS3.strict,
|
|
2199
|
+
json: cliOptions.json ?? defaults.json ?? DEFAULT_OPTIONS3.json,
|
|
2200
|
+
skill: cliOptions.skill ?? DEFAULT_OPTIONS3.skill,
|
|
2201
|
+
version: cliOptions.version ?? DEFAULT_OPTIONS3.version,
|
|
2202
|
+
clawhubRegistry: cliOptions.clawhubRegistry ?? DEFAULT_OPTIONS3.clawhubRegistry,
|
|
2203
|
+
githubTimeoutMs: cliOptions.githubTimeoutMs ?? resolver.githubTimeoutMs ?? DEFAULT_OPTIONS3.githubTimeoutMs,
|
|
2204
|
+
githubRetries: cliOptions.githubRetries ?? resolver.githubRetries ?? DEFAULT_OPTIONS3.githubRetries,
|
|
2205
|
+
githubRetryBaseMs: cliOptions.githubRetryBaseMs ?? resolver.githubRetryBaseMs ?? DEFAULT_OPTIONS3.githubRetryBaseMs,
|
|
2206
|
+
maxFileBytes: cliOptions.maxFileBytes ?? resolver.maxFileBytes ?? DEFAULT_OPTIONS3.maxFileBytes,
|
|
2207
|
+
maxAuxFiles: cliOptions.maxAuxFiles ?? resolver.maxAuxFiles ?? DEFAULT_OPTIONS3.maxAuxFiles,
|
|
2208
|
+
maxTotalFiles: cliOptions.maxTotalFiles ?? resolver.maxTotalFiles ?? DEFAULT_OPTIONS3.maxTotalFiles
|
|
1848
2209
|
});
|
|
1849
2210
|
}
|
|
1850
2211
|
async function runScanClawHubCommand(identifier, rawOptions) {
|
|
@@ -1906,10 +2267,10 @@ async function runScanClawHubCommand(identifier, rawOptions) {
|
|
|
1906
2267
|
}
|
|
1907
2268
|
|
|
1908
2269
|
// src/commands/scan-local.ts
|
|
1909
|
-
import
|
|
1910
|
-
import
|
|
1911
|
-
import { z as
|
|
1912
|
-
var
|
|
2270
|
+
import fs3 from "fs";
|
|
2271
|
+
import path6 from "path";
|
|
2272
|
+
import { z as z5 } from "zod";
|
|
2273
|
+
var ALLOWED_TEXT_EXTENSIONS4 = /* @__PURE__ */ new Set([
|
|
1913
2274
|
".md",
|
|
1914
2275
|
".txt",
|
|
1915
2276
|
".sh",
|
|
@@ -1928,23 +2289,23 @@ var ALLOWED_TEXT_EXTENSIONS3 = /* @__PURE__ */ new Set([
|
|
|
1928
2289
|
".ini",
|
|
1929
2290
|
".cfg"
|
|
1930
2291
|
]);
|
|
1931
|
-
var
|
|
1932
|
-
var cliScanLocalOptionsSchema =
|
|
1933
|
-
config:
|
|
1934
|
-
strict:
|
|
1935
|
-
json:
|
|
1936
|
-
skill:
|
|
1937
|
-
maxFileBytes:
|
|
1938
|
-
maxTotalFiles:
|
|
2292
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".turbo"]);
|
|
2293
|
+
var cliScanLocalOptionsSchema = z5.object({
|
|
2294
|
+
config: z5.string().optional(),
|
|
2295
|
+
strict: z5.boolean().optional(),
|
|
2296
|
+
json: z5.boolean().optional(),
|
|
2297
|
+
skill: z5.string().min(1).optional(),
|
|
2298
|
+
maxFileBytes: z5.coerce.number().int().min(4096).max(5e6).optional(),
|
|
2299
|
+
maxTotalFiles: z5.coerce.number().int().min(1).max(400).optional()
|
|
1939
2300
|
});
|
|
1940
|
-
var effectiveScanLocalOptionsSchema =
|
|
1941
|
-
strict:
|
|
1942
|
-
json:
|
|
1943
|
-
skill:
|
|
1944
|
-
maxFileBytes:
|
|
1945
|
-
maxTotalFiles:
|
|
2301
|
+
var effectiveScanLocalOptionsSchema = z5.object({
|
|
2302
|
+
strict: z5.boolean(),
|
|
2303
|
+
json: z5.boolean(),
|
|
2304
|
+
skill: z5.string().min(1).optional(),
|
|
2305
|
+
maxFileBytes: z5.number().int().min(4096).max(5e6),
|
|
2306
|
+
maxTotalFiles: z5.number().int().min(1).max(400)
|
|
1946
2307
|
});
|
|
1947
|
-
var
|
|
2308
|
+
var DEFAULT_OPTIONS4 = {
|
|
1948
2309
|
strict: false,
|
|
1949
2310
|
json: false,
|
|
1950
2311
|
skill: void 0,
|
|
@@ -1955,30 +2316,30 @@ function toPosixPath(filePath) {
|
|
|
1955
2316
|
return filePath.replace(/\\/g, "/");
|
|
1956
2317
|
}
|
|
1957
2318
|
function getNearbyPathSuggestions(targetPath) {
|
|
1958
|
-
const parent =
|
|
1959
|
-
if (!
|
|
2319
|
+
const parent = path6.dirname(targetPath);
|
|
2320
|
+
if (!fs3.existsSync(parent)) {
|
|
1960
2321
|
return [];
|
|
1961
2322
|
}
|
|
1962
|
-
const needle =
|
|
2323
|
+
const needle = path6.basename(targetPath).toLowerCase();
|
|
1963
2324
|
const suggestions = [];
|
|
1964
|
-
for (const entry of
|
|
2325
|
+
for (const entry of fs3.readdirSync(parent, { withFileTypes: true })) {
|
|
1965
2326
|
if (entry.name.toLowerCase().includes(needle)) {
|
|
1966
|
-
suggestions.push(
|
|
2327
|
+
suggestions.push(path6.join(parent, entry.name));
|
|
1967
2328
|
}
|
|
1968
2329
|
}
|
|
1969
2330
|
return suggestions.slice(0, 5);
|
|
1970
2331
|
}
|
|
1971
2332
|
function isSkillFile(filePath) {
|
|
1972
|
-
return
|
|
2333
|
+
return path6.basename(filePath).toLowerCase() === "skill.md";
|
|
1973
2334
|
}
|
|
1974
2335
|
function isScannableTextFile(filePath) {
|
|
1975
2336
|
if (isSkillFile(filePath)) {
|
|
1976
2337
|
return true;
|
|
1977
2338
|
}
|
|
1978
|
-
const ext =
|
|
1979
|
-
return
|
|
2339
|
+
const ext = path6.extname(filePath).toLowerCase();
|
|
2340
|
+
return ALLOWED_TEXT_EXTENSIONS4.has(ext);
|
|
1980
2341
|
}
|
|
1981
|
-
function
|
|
2342
|
+
function findSkillDirs2(rootDir) {
|
|
1982
2343
|
const found = /* @__PURE__ */ new Set();
|
|
1983
2344
|
const stack = [{ dir: rootDir, depth: 0 }];
|
|
1984
2345
|
let seen = 0;
|
|
@@ -1991,8 +2352,8 @@ function findSkillDirs(rootDir) {
|
|
|
1991
2352
|
if (seen > 5e3) {
|
|
1992
2353
|
break;
|
|
1993
2354
|
}
|
|
1994
|
-
const skillFile =
|
|
1995
|
-
if (
|
|
2355
|
+
const skillFile = path6.join(current.dir, "SKILL.md");
|
|
2356
|
+
if (fs3.existsSync(skillFile) && fs3.statSync(skillFile).isFile()) {
|
|
1996
2357
|
found.add(current.dir);
|
|
1997
2358
|
continue;
|
|
1998
2359
|
}
|
|
@@ -2001,7 +2362,7 @@ function findSkillDirs(rootDir) {
|
|
|
2001
2362
|
}
|
|
2002
2363
|
let entries;
|
|
2003
2364
|
try {
|
|
2004
|
-
entries =
|
|
2365
|
+
entries = fs3.readdirSync(current.dir, { withFileTypes: true });
|
|
2005
2366
|
} catch {
|
|
2006
2367
|
continue;
|
|
2007
2368
|
}
|
|
@@ -2009,10 +2370,10 @@ function findSkillDirs(rootDir) {
|
|
|
2009
2370
|
if (!entry.isDirectory()) {
|
|
2010
2371
|
continue;
|
|
2011
2372
|
}
|
|
2012
|
-
if (
|
|
2373
|
+
if (SKIP_DIRS2.has(entry.name)) {
|
|
2013
2374
|
continue;
|
|
2014
2375
|
}
|
|
2015
|
-
stack.push({ dir:
|
|
2376
|
+
stack.push({ dir: path6.join(current.dir, entry.name), depth: current.depth + 1 });
|
|
2016
2377
|
}
|
|
2017
2378
|
}
|
|
2018
2379
|
return [...found].sort();
|
|
@@ -2021,8 +2382,8 @@ function formatCandidates(candidates) {
|
|
|
2021
2382
|
return candidates.map((candidate) => `- ${toPosixPath(candidate)}`).join("\n");
|
|
2022
2383
|
}
|
|
2023
2384
|
function resolveSkillDirectory(inputPath, preferredSkillName) {
|
|
2024
|
-
const absoluteInput =
|
|
2025
|
-
if (!
|
|
2385
|
+
const absoluteInput = path6.resolve(inputPath);
|
|
2386
|
+
if (!fs3.existsSync(absoluteInput)) {
|
|
2026
2387
|
const suggestions = getNearbyPathSuggestions(absoluteInput);
|
|
2027
2388
|
const suggestionText = suggestions.length > 0 ? `
|
|
2028
2389
|
Nearby paths:
|
|
@@ -2032,7 +2393,7 @@ ${formatCandidates(suggestions)}` : "";
|
|
|
2032
2393
|
`Local path not found: ${toPosixPath(absoluteInput)}${suggestionText}`
|
|
2033
2394
|
);
|
|
2034
2395
|
}
|
|
2035
|
-
const stat =
|
|
2396
|
+
const stat = fs3.statSync(absoluteInput);
|
|
2036
2397
|
if (stat.isFile()) {
|
|
2037
2398
|
if (!isSkillFile(absoluteInput)) {
|
|
2038
2399
|
throw new GuardSkillsError(
|
|
@@ -2041,15 +2402,15 @@ ${formatCandidates(suggestions)}` : "";
|
|
|
2041
2402
|
);
|
|
2042
2403
|
}
|
|
2043
2404
|
return {
|
|
2044
|
-
skillDir:
|
|
2405
|
+
skillDir: path6.dirname(absoluteInput),
|
|
2045
2406
|
note: "Using parent directory of provided SKILL.md file."
|
|
2046
2407
|
};
|
|
2047
2408
|
}
|
|
2048
|
-
const directSkillFile =
|
|
2049
|
-
if (
|
|
2409
|
+
const directSkillFile = path6.join(absoluteInput, "SKILL.md");
|
|
2410
|
+
if (fs3.existsSync(directSkillFile) && fs3.statSync(directSkillFile).isFile()) {
|
|
2050
2411
|
return { skillDir: absoluteInput };
|
|
2051
2412
|
}
|
|
2052
|
-
const discovered =
|
|
2413
|
+
const discovered = findSkillDirs2(absoluteInput);
|
|
2053
2414
|
if (discovered.length === 0) {
|
|
2054
2415
|
throw new GuardSkillsError(
|
|
2055
2416
|
"INVALID_LOCAL_PATH",
|
|
@@ -2058,7 +2419,7 @@ ${formatCandidates(suggestions)}` : "";
|
|
|
2058
2419
|
}
|
|
2059
2420
|
if (preferredSkillName) {
|
|
2060
2421
|
const matches = discovered.filter(
|
|
2061
|
-
(directory) =>
|
|
2422
|
+
(directory) => path6.basename(directory).toLowerCase() === preferredSkillName.toLowerCase()
|
|
2062
2423
|
);
|
|
2063
2424
|
if (matches.length === 1) {
|
|
2064
2425
|
const selected = matches[0];
|
|
@@ -2070,7 +2431,7 @@ ${formatCandidates(suggestions)}` : "";
|
|
|
2070
2431
|
note: `Auto-selected skill '${preferredSkillName}' under the provided path.`
|
|
2071
2432
|
};
|
|
2072
2433
|
}
|
|
2073
|
-
const available = discovered.map((directory) =>
|
|
2434
|
+
const available = discovered.map((directory) => path6.basename(directory));
|
|
2074
2435
|
throw new GuardSkillsError(
|
|
2075
2436
|
"INVALID_LOCAL_PATH",
|
|
2076
2437
|
`Requested --skill '${preferredSkillName}' was not found.
|
|
@@ -2093,7 +2454,7 @@ Available skills: ${available.join(", ")}`
|
|
|
2093
2454
|
${formatCandidates(discovered)}`
|
|
2094
2455
|
);
|
|
2095
2456
|
}
|
|
2096
|
-
function
|
|
2457
|
+
function collectLocalFiles2(skillDir, options) {
|
|
2097
2458
|
const files = [];
|
|
2098
2459
|
const unverifiableReasons = [];
|
|
2099
2460
|
const stack = [skillDir];
|
|
@@ -2104,15 +2465,15 @@ function collectLocalFiles(skillDir, options) {
|
|
|
2104
2465
|
}
|
|
2105
2466
|
let entries;
|
|
2106
2467
|
try {
|
|
2107
|
-
entries =
|
|
2468
|
+
entries = fs3.readdirSync(currentDir, { withFileTypes: true });
|
|
2108
2469
|
} catch {
|
|
2109
2470
|
unverifiableReasons.push(`Cannot read directory: ${toPosixPath(currentDir)}`);
|
|
2110
2471
|
continue;
|
|
2111
2472
|
}
|
|
2112
2473
|
for (const entry of entries) {
|
|
2113
|
-
const fullPath =
|
|
2474
|
+
const fullPath = path6.join(currentDir, entry.name);
|
|
2114
2475
|
if (entry.isDirectory()) {
|
|
2115
|
-
if (!
|
|
2476
|
+
if (!SKIP_DIRS2.has(entry.name)) {
|
|
2116
2477
|
stack.push(fullPath);
|
|
2117
2478
|
}
|
|
2118
2479
|
continue;
|
|
@@ -2120,7 +2481,7 @@ function collectLocalFiles(skillDir, options) {
|
|
|
2120
2481
|
if (!entry.isFile()) {
|
|
2121
2482
|
continue;
|
|
2122
2483
|
}
|
|
2123
|
-
const relativePath = toPosixPath(
|
|
2484
|
+
const relativePath = toPosixPath(path6.relative(skillDir, fullPath));
|
|
2124
2485
|
if (!isScannableTextFile(relativePath)) {
|
|
2125
2486
|
continue;
|
|
2126
2487
|
}
|
|
@@ -2132,7 +2493,7 @@ function collectLocalFiles(skillDir, options) {
|
|
|
2132
2493
|
}
|
|
2133
2494
|
let sizeBytes = 0;
|
|
2134
2495
|
try {
|
|
2135
|
-
sizeBytes =
|
|
2496
|
+
sizeBytes = fs3.statSync(fullPath).size;
|
|
2136
2497
|
} catch {
|
|
2137
2498
|
unverifiableReasons.push(`Cannot stat file: ${relativePath}`);
|
|
2138
2499
|
continue;
|
|
@@ -2146,7 +2507,7 @@ function collectLocalFiles(skillDir, options) {
|
|
|
2146
2507
|
try {
|
|
2147
2508
|
files.push({
|
|
2148
2509
|
path: relativePath,
|
|
2149
|
-
content:
|
|
2510
|
+
content: fs3.readFileSync(fullPath, "utf8")
|
|
2150
2511
|
});
|
|
2151
2512
|
} catch {
|
|
2152
2513
|
unverifiableReasons.push(`Cannot read text content: ${relativePath}`);
|
|
@@ -2159,11 +2520,11 @@ function resolveEffectiveScanLocalOptions(cliOptions, config) {
|
|
|
2159
2520
|
const defaults = config.defaults ?? {};
|
|
2160
2521
|
const resolver = config.resolver ?? {};
|
|
2161
2522
|
return effectiveScanLocalOptionsSchema.parse({
|
|
2162
|
-
strict: cliOptions.strict ?? defaults.strict ??
|
|
2163
|
-
json: cliOptions.json ?? defaults.json ??
|
|
2164
|
-
skill: cliOptions.skill ??
|
|
2165
|
-
maxFileBytes: cliOptions.maxFileBytes ?? resolver.maxFileBytes ??
|
|
2166
|
-
maxTotalFiles: cliOptions.maxTotalFiles ?? resolver.maxTotalFiles ??
|
|
2523
|
+
strict: cliOptions.strict ?? defaults.strict ?? DEFAULT_OPTIONS4.strict,
|
|
2524
|
+
json: cliOptions.json ?? defaults.json ?? DEFAULT_OPTIONS4.json,
|
|
2525
|
+
skill: cliOptions.skill ?? DEFAULT_OPTIONS4.skill,
|
|
2526
|
+
maxFileBytes: cliOptions.maxFileBytes ?? resolver.maxFileBytes ?? DEFAULT_OPTIONS4.maxFileBytes,
|
|
2527
|
+
maxTotalFiles: cliOptions.maxTotalFiles ?? resolver.maxTotalFiles ?? DEFAULT_OPTIONS4.maxTotalFiles
|
|
2167
2528
|
});
|
|
2168
2529
|
}
|
|
2169
2530
|
async function runScanLocalCommand(inputPath, rawOptions) {
|
|
@@ -2171,7 +2532,7 @@ async function runScanLocalCommand(inputPath, rawOptions) {
|
|
|
2171
2532
|
const loadedConfig = loadGuardSkillsConfig(cliOptions.config);
|
|
2172
2533
|
const options = resolveEffectiveScanLocalOptions(cliOptions, loadedConfig.config);
|
|
2173
2534
|
const target = resolveSkillDirectory(inputPath, options.skill);
|
|
2174
|
-
const { files, unverifiableReasons } =
|
|
2535
|
+
const { files, unverifiableReasons } = collectLocalFiles2(target.skillDir, options);
|
|
2175
2536
|
if (files.length === 0) {
|
|
2176
2537
|
throw new GuardSkillsError(
|
|
2177
2538
|
"INVALID_LOCAL_PATH",
|
|
@@ -2184,7 +2545,7 @@ async function runScanLocalCommand(inputPath, rawOptions) {
|
|
|
2184
2545
|
repo: "local",
|
|
2185
2546
|
defaultBranch: "local",
|
|
2186
2547
|
commitSha: "local",
|
|
2187
|
-
skillName: options.skill ??
|
|
2548
|
+
skillName: options.skill ?? path6.basename(target.skillDir),
|
|
2188
2549
|
skillDir: toPosixPath(target.skillDir),
|
|
2189
2550
|
skillFilePath: "SKILL.md",
|
|
2190
2551
|
files,
|
|
@@ -2226,15 +2587,69 @@ async function runScanLocalCommand(inputPath, rawOptions) {
|
|
|
2226
2587
|
}
|
|
2227
2588
|
|
|
2228
2589
|
// src/cli.ts
|
|
2590
|
+
function withCommonAddOptions(command, requireSkill) {
|
|
2591
|
+
const configured = command.option("--config <path>", "Path to guardskills.config.json").option("--strict", "Use stricter risk thresholds").option("--ci", "Deterministic CI mode: scan + gate only, no install handoff").option("--json", "Output machine-readable JSON").option("--yes", "Auto-confirm warnings").option("--dry-run", "Scan only, do not install").option("--force", "Override UNSAFE outcome").option("--allow-unverifiable", "Override UNVERIFIABLE outcome").option("--github-timeout-ms <ms>", "GitHub API request timeout in milliseconds").option("--github-retries <count>", "Retry count for retryable GitHub errors").option("--github-retry-base-ms <ms>", "Base backoff delay for GitHub retries").option("--max-file-bytes <bytes>", "Max file size to scan").option("--max-aux-files <count>", "Max auxiliary files from scripts/src folders").option("--max-total-files <count>", "Max total resolved files to scan");
|
|
2592
|
+
return requireSkill ? configured.requiredOption("--skill <name>", "Skill name to install") : configured.option("--skill <name>", "Skill name to install");
|
|
2593
|
+
}
|
|
2229
2594
|
async function main() {
|
|
2230
2595
|
const program = new Command();
|
|
2231
|
-
program.name("guardskills").description("Security wrapper around
|
|
2232
|
-
program.command("add").description("Scan a skill source and conditionally install it via skills CLI").argument("<repo>", "GitHub repository URL or owner/repo")
|
|
2233
|
-
|
|
2596
|
+
program.name("guardskills").description("Security wrapper around skill installation CLIs").version("1.1.0");
|
|
2597
|
+
const legacyAdd = program.command("add").description("Scan a skill source and conditionally install it via skills CLI").argument("<repo>", "GitHub repository URL or owner/repo");
|
|
2598
|
+
withCommonAddOptions(legacyAdd, true).action(
|
|
2599
|
+
async (repo, options) => {
|
|
2600
|
+
const code = await runAddCommand(repo, options);
|
|
2601
|
+
process.exitCode = code;
|
|
2602
|
+
}
|
|
2603
|
+
);
|
|
2604
|
+
const skills = program.command("skills").description("Guarded wrapper for skills.sh install commands");
|
|
2605
|
+
withCommonAddOptions(
|
|
2606
|
+
skills.command("add").description("Scan a skill source and conditionally install it via skills CLI").argument("<repo>", "GitHub repository URL or owner/repo"),
|
|
2607
|
+
false
|
|
2608
|
+
).action(async (repo, options) => {
|
|
2609
|
+
const resolvedSkill = typeof options.skill === "string" ? options.skill : void 0;
|
|
2610
|
+
const code = resolvedSkill ? await runAddCommand(repo, options, {
|
|
2611
|
+
provider: "skills",
|
|
2612
|
+
commandName: "guardskills skills add"
|
|
2613
|
+
}) : await runInteractiveInstallCommand("skills", repo, void 0, options);
|
|
2614
|
+
process.exitCode = code;
|
|
2615
|
+
});
|
|
2616
|
+
const playbooks = program.command("playbooks").description("Guarded wrapper for Playbooks install commands");
|
|
2617
|
+
withCommonAddOptions(
|
|
2618
|
+
playbooks.command("add").description("Scan a skill source and conditionally install it via Playbooks CLI").argument("<resource>", "Playbooks resource type (must be 'skill')").argument("<repo>", "GitHub repository URL or owner/repo"),
|
|
2619
|
+
true
|
|
2620
|
+
).action(async (resource, repo, options) => {
|
|
2621
|
+
if (resource !== "skill") {
|
|
2622
|
+
throw new GuardSkillsError(
|
|
2623
|
+
"INVALID_OPTIONS",
|
|
2624
|
+
`Unsupported playbooks resource '${resource}'. Use: guardskills playbooks add skill <repo> --skill <name>`
|
|
2625
|
+
);
|
|
2626
|
+
}
|
|
2627
|
+
const code = await runAddCommand(repo, options, {
|
|
2628
|
+
provider: "playbooks",
|
|
2629
|
+
commandName: "guardskills playbooks add skill"
|
|
2630
|
+
});
|
|
2631
|
+
process.exitCode = code;
|
|
2632
|
+
});
|
|
2633
|
+
const openskills = program.command("openskills").description("Guarded wrapper for OpenSkills install commands");
|
|
2634
|
+
withCommonAddOptions(
|
|
2635
|
+
openskills.command("install").description("Scan a skill source and conditionally install it via OpenSkills CLI").argument("<repo>", "GitHub repository URL or owner/repo").argument("[skill]", "Skill name to install"),
|
|
2636
|
+
false
|
|
2637
|
+
).action(async (repo, skill, options) => {
|
|
2638
|
+
const resolvedSkill = skill ?? (typeof options.skill === "string" ? options.skill : void 0);
|
|
2639
|
+
const code = await runInteractiveInstallCommand("openskills", repo, resolvedSkill, options);
|
|
2640
|
+
process.exitCode = code;
|
|
2641
|
+
});
|
|
2642
|
+
const skillkit = program.command("skillkit").description("Guarded wrapper for skillkit install commands");
|
|
2643
|
+
withCommonAddOptions(
|
|
2644
|
+
skillkit.command("install").description("Scan a skill source and conditionally install it via skillkit CLI").argument("<repo>", "GitHub repository URL or owner/repo").argument("[skill]", "Skill name to install"),
|
|
2645
|
+
false
|
|
2646
|
+
).action(async (repo, skill, options) => {
|
|
2647
|
+
const resolvedSkill = skill ?? (typeof options.skill === "string" ? options.skill : void 0);
|
|
2648
|
+
const code = await runInteractiveInstallCommand("skillkit", repo, resolvedSkill, options);
|
|
2234
2649
|
process.exitCode = code;
|
|
2235
2650
|
});
|
|
2236
|
-
program.command("scan-local").description("Scan a local skill folder and print a risk decision").argument("<path>", "Local folder path (or SKILL.md file path)").option("--skill <name>", "Skill directory name when path contains multiple skills").option("--config <path>", "Path to guardskills.config.json").option("--strict", "Use stricter risk thresholds").option("--json", "Output machine-readable JSON").option("--max-file-bytes <bytes>", "Max file size to scan").option("--max-total-files <count>", "Max total files to scan").action(async (
|
|
2237
|
-
const code = await runScanLocalCommand(
|
|
2651
|
+
program.command("scan-local").description("Scan a local skill folder and print a risk decision").argument("<path>", "Local folder path (or SKILL.md file path)").option("--skill <name>", "Skill directory name when path contains multiple skills").option("--config <path>", "Path to guardskills.config.json").option("--strict", "Use stricter risk thresholds").option("--json", "Output machine-readable JSON").option("--max-file-bytes <bytes>", "Max file size to scan").option("--max-total-files <count>", "Max total files to scan").action(async (repo, options) => {
|
|
2652
|
+
const code = await runScanLocalCommand(repo, options);
|
|
2238
2653
|
process.exitCode = code;
|
|
2239
2654
|
});
|
|
2240
2655
|
program.command("scan-clawhub").description("Scan a ClawHub skill package and print a risk decision").argument("<identifier>", "ClawHub package identifier").option("--skill <name>", "Override skill folder name to resolve in source repository").option("--version <version>", "Preferred package version/tag").option("--clawhub-registry <url>", "ClawHub registry base URL").option("--config <path>", "Path to guardskills.config.json").option("--strict", "Use stricter risk thresholds").option("--json", "Output machine-readable JSON").option("--github-timeout-ms <ms>", "Upstream resolver request timeout in milliseconds").option("--github-retries <count>", "Retry count for retryable upstream errors").option("--github-retry-base-ms <ms>", "Base backoff delay for upstream retries").option("--max-file-bytes <bytes>", "Max file size to scan").option("--max-aux-files <count>", "Max auxiliary files from scripts/src folders").option("--max-total-files <count>", "Max total resolved files to scan").action(async (identifier, options) => {
|