@skillport/cli 0.1.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/index.js +2692 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2692 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// ../../packages/shared/dist/constants.js
|
|
13
|
+
var SP_VERSION, SP_CONFIG_DIR, SP_KEYS_DIR, SP_AUDIT_DIR, SP_CONFIG_FILE, SP_REGISTRY_FILE, OPENCLAW_SKILLS_DIR, DEFAULT_MARKETPLACE_URL;
|
|
14
|
+
var init_constants = __esm({
|
|
15
|
+
"../../packages/shared/dist/constants.js"() {
|
|
16
|
+
"use strict";
|
|
17
|
+
SP_VERSION = "1.0";
|
|
18
|
+
SP_CONFIG_DIR = ".skillport";
|
|
19
|
+
SP_KEYS_DIR = "keys";
|
|
20
|
+
SP_AUDIT_DIR = "audit";
|
|
21
|
+
SP_CONFIG_FILE = "config.json";
|
|
22
|
+
SP_REGISTRY_FILE = "installed/registry.json";
|
|
23
|
+
OPENCLAW_SKILLS_DIR = ".openclaw/skills";
|
|
24
|
+
DEFAULT_MARKETPLACE_URL = "https://api.skillport.market";
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// ../../packages/shared/dist/index.js
|
|
29
|
+
var init_dist = __esm({
|
|
30
|
+
"../../packages/shared/dist/index.js"() {
|
|
31
|
+
"use strict";
|
|
32
|
+
init_constants();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// src/utils/config.ts
|
|
37
|
+
var config_exports = {};
|
|
38
|
+
__export(config_exports, {
|
|
39
|
+
appendAuditLog: () => appendAuditLog,
|
|
40
|
+
auditLogPath: () => auditLogPath,
|
|
41
|
+
configPath: () => configPath,
|
|
42
|
+
ensureConfigDirs: () => ensureConfigDirs,
|
|
43
|
+
hasKeys: () => hasKeys,
|
|
44
|
+
keysDir: () => keysDir,
|
|
45
|
+
loadConfig: () => loadConfig,
|
|
46
|
+
loadPrivateKey: () => loadPrivateKey2,
|
|
47
|
+
loadPublicKey: () => loadPublicKey2,
|
|
48
|
+
loadRegistry: () => loadRegistry,
|
|
49
|
+
registryPath: () => registryPath,
|
|
50
|
+
saveConfig: () => saveConfig,
|
|
51
|
+
saveRegistry: () => saveRegistry
|
|
52
|
+
});
|
|
53
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, appendFileSync } from "node:fs";
|
|
54
|
+
import { join } from "node:path";
|
|
55
|
+
import { homedir } from "node:os";
|
|
56
|
+
function configDir() {
|
|
57
|
+
return join(homedir(), SP_CONFIG_DIR);
|
|
58
|
+
}
|
|
59
|
+
function ensureConfigDirs() {
|
|
60
|
+
const base = configDir();
|
|
61
|
+
mkdirSync(join(base, SP_KEYS_DIR), { recursive: true });
|
|
62
|
+
mkdirSync(join(base, SP_AUDIT_DIR), { recursive: true });
|
|
63
|
+
mkdirSync(join(base, "installed"), { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
function configPath() {
|
|
66
|
+
return join(configDir(), SP_CONFIG_FILE);
|
|
67
|
+
}
|
|
68
|
+
function keysDir() {
|
|
69
|
+
return join(configDir(), SP_KEYS_DIR);
|
|
70
|
+
}
|
|
71
|
+
function auditLogPath() {
|
|
72
|
+
return join(configDir(), SP_AUDIT_DIR, "audit.log");
|
|
73
|
+
}
|
|
74
|
+
function registryPath() {
|
|
75
|
+
return join(configDir(), SP_REGISTRY_FILE);
|
|
76
|
+
}
|
|
77
|
+
function loadConfig() {
|
|
78
|
+
const path = configPath();
|
|
79
|
+
if (!existsSync(path)) {
|
|
80
|
+
return { marketplace_url: DEFAULT_MARKETPLACE_URL };
|
|
81
|
+
}
|
|
82
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
83
|
+
}
|
|
84
|
+
function saveConfig(config) {
|
|
85
|
+
ensureConfigDirs();
|
|
86
|
+
writeFileSync(configPath(), JSON.stringify(config, null, 2));
|
|
87
|
+
}
|
|
88
|
+
function loadRegistry() {
|
|
89
|
+
const path = registryPath();
|
|
90
|
+
if (!existsSync(path)) {
|
|
91
|
+
return { skills: [] };
|
|
92
|
+
}
|
|
93
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
94
|
+
}
|
|
95
|
+
function saveRegistry(registry) {
|
|
96
|
+
ensureConfigDirs();
|
|
97
|
+
writeFileSync(registryPath(), JSON.stringify(registry, null, 2));
|
|
98
|
+
}
|
|
99
|
+
function appendAuditLog(entry) {
|
|
100
|
+
ensureConfigDirs();
|
|
101
|
+
const logEntry = {
|
|
102
|
+
...entry,
|
|
103
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
104
|
+
};
|
|
105
|
+
const path = auditLogPath();
|
|
106
|
+
const line = JSON.stringify(logEntry) + "\n";
|
|
107
|
+
appendFileSync(path, line);
|
|
108
|
+
}
|
|
109
|
+
function hasKeys() {
|
|
110
|
+
const dir = keysDir();
|
|
111
|
+
return existsSync(join(dir, "default.key")) && existsSync(join(dir, "default.pub"));
|
|
112
|
+
}
|
|
113
|
+
function loadPrivateKey2() {
|
|
114
|
+
return readFileSync(join(keysDir(), "default.key"), "utf-8");
|
|
115
|
+
}
|
|
116
|
+
function loadPublicKey2() {
|
|
117
|
+
return readFileSync(join(keysDir(), "default.pub"), "utf-8");
|
|
118
|
+
}
|
|
119
|
+
var init_config = __esm({
|
|
120
|
+
"src/utils/config.ts"() {
|
|
121
|
+
"use strict";
|
|
122
|
+
init_dist();
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// src/index.ts
|
|
127
|
+
import { Command } from "commander";
|
|
128
|
+
|
|
129
|
+
// src/commands/init.ts
|
|
130
|
+
import { writeFileSync as writeFileSync2 } from "node:fs";
|
|
131
|
+
import { join as join2 } from "node:path";
|
|
132
|
+
import chalk from "chalk";
|
|
133
|
+
|
|
134
|
+
// ../../packages/core/dist/manifest/schema.js
|
|
135
|
+
import { z } from "zod";
|
|
136
|
+
var semverRegex = /^\d+\.\d+\.\d+$/;
|
|
137
|
+
var semverRangeRegex = /^[\^~>=<\s\d.|]+$/;
|
|
138
|
+
var EntrypointSchema = z.object({
|
|
139
|
+
name: z.string().min(1),
|
|
140
|
+
description: z.string().optional(),
|
|
141
|
+
file: z.string().min(1)
|
|
142
|
+
});
|
|
143
|
+
var NetworkPermissionSchema = z.discriminatedUnion("mode", [
|
|
144
|
+
z.object({ mode: z.literal("none") }),
|
|
145
|
+
z.object({ mode: z.literal("allowlist"), domains: z.array(z.string()) })
|
|
146
|
+
]);
|
|
147
|
+
var FilesystemPermissionSchema = z.object({
|
|
148
|
+
read_paths: z.array(z.string()),
|
|
149
|
+
write_paths: z.array(z.string())
|
|
150
|
+
});
|
|
151
|
+
var ExecPermissionSchema = z.object({
|
|
152
|
+
allowed_commands: z.array(z.string()),
|
|
153
|
+
shell: z.boolean()
|
|
154
|
+
});
|
|
155
|
+
var IntegrationLevel = z.enum(["none", "read", "write", "send"]);
|
|
156
|
+
var IntegrationsPermissionSchema = z.object({
|
|
157
|
+
slack: IntegrationLevel.optional(),
|
|
158
|
+
gmail: IntegrationLevel.optional(),
|
|
159
|
+
notion: IntegrationLevel.optional(),
|
|
160
|
+
github: IntegrationLevel.optional()
|
|
161
|
+
});
|
|
162
|
+
var PermissionsSchema = z.object({
|
|
163
|
+
network: NetworkPermissionSchema,
|
|
164
|
+
filesystem: FilesystemPermissionSchema,
|
|
165
|
+
exec: ExecPermissionSchema,
|
|
166
|
+
integrations: IntegrationsPermissionSchema.optional()
|
|
167
|
+
});
|
|
168
|
+
var DangerFlagSchema = z.object({
|
|
169
|
+
code: z.string(),
|
|
170
|
+
severity: z.enum(["info", "low", "medium", "high", "critical"]),
|
|
171
|
+
message: z.string(),
|
|
172
|
+
file: z.string().optional(),
|
|
173
|
+
line: z.number().optional()
|
|
174
|
+
});
|
|
175
|
+
var DependencySchema = z.object({
|
|
176
|
+
name: z.string(),
|
|
177
|
+
type: z.enum(["cli", "npm", "pip", "brew", "apt", "other"]),
|
|
178
|
+
version: z.string().optional(),
|
|
179
|
+
optional: z.boolean().optional()
|
|
180
|
+
});
|
|
181
|
+
var RequiredInputSchema = z.object({
|
|
182
|
+
key: z.string(),
|
|
183
|
+
description: z.string(),
|
|
184
|
+
type: z.enum(["string", "secret", "number", "boolean"]),
|
|
185
|
+
required: z.boolean(),
|
|
186
|
+
default: z.union([z.string(), z.number(), z.boolean()]).optional()
|
|
187
|
+
});
|
|
188
|
+
var InstallSchema = z.object({
|
|
189
|
+
steps: z.array(z.string()),
|
|
190
|
+
required_inputs: z.array(RequiredInputSchema)
|
|
191
|
+
});
|
|
192
|
+
var AuthorSchema = z.object({
|
|
193
|
+
name: z.string().min(1),
|
|
194
|
+
email: z.string().email().optional(),
|
|
195
|
+
signing_key_id: z.string()
|
|
196
|
+
});
|
|
197
|
+
var ManifestSchema = z.object({
|
|
198
|
+
ssp_version: z.literal("1.0"),
|
|
199
|
+
id: z.string().regex(/^[a-z0-9_-]+\/[a-z0-9_-]+$/, "Must be 'author-slug/skill-slug'"),
|
|
200
|
+
name: z.string().min(1).max(100),
|
|
201
|
+
description: z.string().min(1).max(1e3),
|
|
202
|
+
version: z.string().regex(semverRegex, "Must be valid semver (x.y.z)"),
|
|
203
|
+
author: AuthorSchema,
|
|
204
|
+
openclaw_compat: z.string().regex(semverRangeRegex, "Must be a valid semver range"),
|
|
205
|
+
os_compat: z.array(z.enum(["macos", "linux", "windows"])).min(1),
|
|
206
|
+
entrypoints: z.array(EntrypointSchema).min(1),
|
|
207
|
+
permissions: PermissionsSchema,
|
|
208
|
+
dependencies: z.array(DependencySchema),
|
|
209
|
+
danger_flags: z.array(DangerFlagSchema),
|
|
210
|
+
install: InstallSchema,
|
|
211
|
+
hashes: z.record(z.string(), z.string()),
|
|
212
|
+
created_at: z.string().datetime()
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ../../packages/core/dist/crypto/keys.js
|
|
216
|
+
import { generateKeyPairSync, createHash, createPublicKey, createPrivateKey } from "node:crypto";
|
|
217
|
+
function generateKeyPair() {
|
|
218
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
|
|
219
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
220
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
|
221
|
+
});
|
|
222
|
+
const keyId = computeKeyId(publicKey);
|
|
223
|
+
return { publicKey, privateKey, keyId };
|
|
224
|
+
}
|
|
225
|
+
function computeKeyId(publicKeyPem) {
|
|
226
|
+
const hash = createHash("sha256").update(publicKeyPem).digest("hex");
|
|
227
|
+
return hash.substring(0, 16);
|
|
228
|
+
}
|
|
229
|
+
function loadPublicKey(pem) {
|
|
230
|
+
return createPublicKey(pem);
|
|
231
|
+
}
|
|
232
|
+
function loadPrivateKey(pem) {
|
|
233
|
+
return createPrivateKey(pem);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ../../packages/core/dist/crypto/sign.js
|
|
237
|
+
import { sign as cryptoSign } from "node:crypto";
|
|
238
|
+
function signManifest(manifestJson, privateKeyPem) {
|
|
239
|
+
const privateKey = loadPrivateKey(privateKeyPem);
|
|
240
|
+
const signature = cryptoSign(null, Buffer.from(manifestJson), privateKey);
|
|
241
|
+
return signature.toString("base64");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ../../packages/core/dist/crypto/verify.js
|
|
245
|
+
import { verify as cryptoVerify } from "node:crypto";
|
|
246
|
+
function verifySignature(manifestJson, signatureBase64, publicKeyPem) {
|
|
247
|
+
const publicKey = loadPublicKey(publicKeyPem);
|
|
248
|
+
const signature = Buffer.from(signatureBase64, "base64");
|
|
249
|
+
return cryptoVerify(null, Buffer.from(manifestJson), publicKey, signature);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ../../packages/core/dist/crypto/checksum.js
|
|
253
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
254
|
+
function sha256(data) {
|
|
255
|
+
return createHash2("sha256").update(data).digest("hex");
|
|
256
|
+
}
|
|
257
|
+
function computeChecksums(files) {
|
|
258
|
+
const checksums = {};
|
|
259
|
+
for (const [path, content] of files) {
|
|
260
|
+
checksums[path] = sha256(content);
|
|
261
|
+
}
|
|
262
|
+
return checksums;
|
|
263
|
+
}
|
|
264
|
+
function verifyChecksums(files, expected) {
|
|
265
|
+
const mismatches = [];
|
|
266
|
+
for (const [path, expectedHash] of Object.entries(expected)) {
|
|
267
|
+
const content = files.get(path);
|
|
268
|
+
if (!content) {
|
|
269
|
+
mismatches.push(path);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const actual = sha256(content);
|
|
273
|
+
if (actual !== expectedHash) {
|
|
274
|
+
mismatches.push(path);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return { valid: mismatches.length === 0, mismatches };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ../../packages/core/dist/archive/create.js
|
|
281
|
+
import JSZip from "jszip";
|
|
282
|
+
async function createSSP(options) {
|
|
283
|
+
const { manifest, files, privateKeyPem } = options;
|
|
284
|
+
ManifestSchema.parse(manifest);
|
|
285
|
+
const checksumFiles = /* @__PURE__ */ new Map();
|
|
286
|
+
for (const [path, content] of files) {
|
|
287
|
+
if (path === "SKILL.md") {
|
|
288
|
+
checksumFiles.set(path, content);
|
|
289
|
+
} else {
|
|
290
|
+
checksumFiles.set(`payload/${path}`, content);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const checksums = computeChecksums(checksumFiles);
|
|
294
|
+
const finalManifest = {
|
|
295
|
+
...manifest,
|
|
296
|
+
hashes: checksums
|
|
297
|
+
};
|
|
298
|
+
const manifestJson = JSON.stringify(finalManifest, null, 2);
|
|
299
|
+
const manifestBuffer = Buffer.from(manifestJson);
|
|
300
|
+
const authorSignature = signManifest(manifestJson, privateKeyPem);
|
|
301
|
+
const zip = new JSZip();
|
|
302
|
+
zip.file("manifest.json", manifestBuffer);
|
|
303
|
+
zip.file("signatures/author.sig", authorSignature);
|
|
304
|
+
zip.file("checksums.json", JSON.stringify(checksums, null, 2));
|
|
305
|
+
const skillMd = files.get("SKILL.md");
|
|
306
|
+
if (skillMd) {
|
|
307
|
+
zip.file("SKILL.md", skillMd);
|
|
308
|
+
}
|
|
309
|
+
for (const [path, content] of files) {
|
|
310
|
+
if (path !== "SKILL.md") {
|
|
311
|
+
zip.file(`payload/${path}`, content);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const buffer = await zip.generateAsync({
|
|
315
|
+
type: "nodebuffer",
|
|
316
|
+
compression: "DEFLATE",
|
|
317
|
+
compressionOptions: { level: 9 }
|
|
318
|
+
});
|
|
319
|
+
return buffer;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ../../packages/core/dist/archive/extract.js
|
|
323
|
+
import JSZip2 from "jszip";
|
|
324
|
+
async function extractSSP(data) {
|
|
325
|
+
const zip = await JSZip2.loadAsync(data);
|
|
326
|
+
const manifestFile = zip.file("manifest.json");
|
|
327
|
+
if (!manifestFile) {
|
|
328
|
+
throw new Error("Invalid SSP: missing manifest.json");
|
|
329
|
+
}
|
|
330
|
+
const manifestRaw = await manifestFile.async("string");
|
|
331
|
+
const manifestData = JSON.parse(manifestRaw);
|
|
332
|
+
const manifest = ManifestSchema.parse(manifestData);
|
|
333
|
+
const authorSigFile = zip.file("signatures/author.sig");
|
|
334
|
+
const authorSignature = authorSigFile ? await authorSigFile.async("string") : null;
|
|
335
|
+
const platformSigFile = zip.file("signatures/platform.sig");
|
|
336
|
+
const platformSignature = platformSigFile ? await platformSigFile.async("string") : null;
|
|
337
|
+
const checksumsFile = zip.file("checksums.json");
|
|
338
|
+
const checksums = checksumsFile ? JSON.parse(await checksumsFile.async("string")) : {};
|
|
339
|
+
const skillMdFile = zip.file("SKILL.md");
|
|
340
|
+
const skillMd = skillMdFile ? await skillMdFile.async("string") : null;
|
|
341
|
+
const files = /* @__PURE__ */ new Map();
|
|
342
|
+
const entries = Object.entries(zip.files);
|
|
343
|
+
for (const [path, entry] of entries) {
|
|
344
|
+
if (entry.dir)
|
|
345
|
+
continue;
|
|
346
|
+
if (path === "manifest.json" || path === "checksums.json" || path.startsWith("signatures/")) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const content = await entry.async("nodebuffer");
|
|
350
|
+
files.set(path, content);
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
manifest,
|
|
354
|
+
manifestRaw,
|
|
355
|
+
files,
|
|
356
|
+
authorSignature,
|
|
357
|
+
platformSignature,
|
|
358
|
+
checksums,
|
|
359
|
+
skillMd
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ../../packages/core/dist/permissions/validate.js
|
|
364
|
+
function assessNetworkRisk(permissions) {
|
|
365
|
+
if (permissions.network.mode === "none")
|
|
366
|
+
return "safe";
|
|
367
|
+
if (permissions.network.mode === "allowlist") {
|
|
368
|
+
return permissions.network.domains.length <= 2 ? "low" : "medium";
|
|
369
|
+
}
|
|
370
|
+
return "safe";
|
|
371
|
+
}
|
|
372
|
+
function assessFilesystemRisk(permissions) {
|
|
373
|
+
const { read_paths, write_paths } = permissions.filesystem;
|
|
374
|
+
if (read_paths.length === 0 && write_paths.length === 0)
|
|
375
|
+
return "safe";
|
|
376
|
+
if (write_paths.length === 0)
|
|
377
|
+
return "low";
|
|
378
|
+
const hasSensitivePaths = write_paths.some((p) => p === "/" || p === "~" || p.startsWith("/etc") || p.startsWith("/usr"));
|
|
379
|
+
if (hasSensitivePaths)
|
|
380
|
+
return "critical";
|
|
381
|
+
return "medium";
|
|
382
|
+
}
|
|
383
|
+
function assessExecRisk(permissions) {
|
|
384
|
+
const { allowed_commands, shell } = permissions.exec;
|
|
385
|
+
if (allowed_commands.length === 0 && !shell)
|
|
386
|
+
return "safe";
|
|
387
|
+
if (shell)
|
|
388
|
+
return "high";
|
|
389
|
+
return allowed_commands.length <= 3 ? "medium" : "high";
|
|
390
|
+
}
|
|
391
|
+
function assessIntegrationsRisk(permissions) {
|
|
392
|
+
const integrations = permissions.integrations;
|
|
393
|
+
if (!integrations)
|
|
394
|
+
return "safe";
|
|
395
|
+
const levels = [
|
|
396
|
+
integrations.slack,
|
|
397
|
+
integrations.gmail,
|
|
398
|
+
integrations.notion,
|
|
399
|
+
integrations.github
|
|
400
|
+
].filter((l) => l && l !== "none");
|
|
401
|
+
if (levels.length === 0)
|
|
402
|
+
return "safe";
|
|
403
|
+
if (levels.some((l) => l === "send" || l === "write"))
|
|
404
|
+
return "high";
|
|
405
|
+
if (levels.some((l) => l === "read"))
|
|
406
|
+
return "medium";
|
|
407
|
+
return "low";
|
|
408
|
+
}
|
|
409
|
+
var riskOrder = ["safe", "low", "medium", "high", "critical"];
|
|
410
|
+
function maxRisk(...levels) {
|
|
411
|
+
let max = 0;
|
|
412
|
+
for (const level of levels) {
|
|
413
|
+
const idx = riskOrder.indexOf(level);
|
|
414
|
+
if (idx > max)
|
|
415
|
+
max = idx;
|
|
416
|
+
}
|
|
417
|
+
return riskOrder[max];
|
|
418
|
+
}
|
|
419
|
+
function assessPermissions(permissions) {
|
|
420
|
+
const network = assessNetworkRisk(permissions);
|
|
421
|
+
const filesystem = assessFilesystemRisk(permissions);
|
|
422
|
+
const exec = assessExecRisk(permissions);
|
|
423
|
+
const integrations = assessIntegrationsRisk(permissions);
|
|
424
|
+
const overall = maxRisk(network, filesystem, exec, integrations);
|
|
425
|
+
return { network, filesystem, exec, integrations, overall };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ../../packages/core/dist/permissions/display.js
|
|
429
|
+
function formatPermissions(permissions, summary) {
|
|
430
|
+
const lines = [];
|
|
431
|
+
if (permissions.network.mode === "none") {
|
|
432
|
+
lines.push({
|
|
433
|
+
category: "network",
|
|
434
|
+
icon: "\u{1F512}",
|
|
435
|
+
label: "Network",
|
|
436
|
+
detail: "No network access",
|
|
437
|
+
risk: summary.network
|
|
438
|
+
});
|
|
439
|
+
} else {
|
|
440
|
+
lines.push({
|
|
441
|
+
category: "network",
|
|
442
|
+
icon: "\u{1F310}",
|
|
443
|
+
label: "Network",
|
|
444
|
+
detail: `Allowed domains: ${permissions.network.domains.join(", ")}`,
|
|
445
|
+
risk: summary.network
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
const { read_paths, write_paths } = permissions.filesystem;
|
|
449
|
+
if (read_paths.length === 0 && write_paths.length === 0) {
|
|
450
|
+
lines.push({
|
|
451
|
+
category: "filesystem",
|
|
452
|
+
icon: "\u{1F512}",
|
|
453
|
+
label: "Filesystem",
|
|
454
|
+
detail: "No filesystem access",
|
|
455
|
+
risk: summary.filesystem
|
|
456
|
+
});
|
|
457
|
+
} else {
|
|
458
|
+
const parts = [];
|
|
459
|
+
if (read_paths.length > 0)
|
|
460
|
+
parts.push(`Read: ${read_paths.join(", ")}`);
|
|
461
|
+
if (write_paths.length > 0)
|
|
462
|
+
parts.push(`Write: ${write_paths.join(", ")}`);
|
|
463
|
+
lines.push({
|
|
464
|
+
category: "filesystem",
|
|
465
|
+
icon: "\u{1F4C1}",
|
|
466
|
+
label: "Filesystem",
|
|
467
|
+
detail: parts.join(" | "),
|
|
468
|
+
risk: summary.filesystem
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
const { allowed_commands, shell } = permissions.exec;
|
|
472
|
+
if (allowed_commands.length === 0 && !shell) {
|
|
473
|
+
lines.push({
|
|
474
|
+
category: "exec",
|
|
475
|
+
icon: "\u{1F512}",
|
|
476
|
+
label: "Execution",
|
|
477
|
+
detail: "No command execution",
|
|
478
|
+
risk: summary.exec
|
|
479
|
+
});
|
|
480
|
+
} else {
|
|
481
|
+
const parts = [];
|
|
482
|
+
if (allowed_commands.length > 0)
|
|
483
|
+
parts.push(`Commands: ${allowed_commands.join(", ")}`);
|
|
484
|
+
if (shell)
|
|
485
|
+
parts.push("Shell access: YES");
|
|
486
|
+
lines.push({
|
|
487
|
+
category: "exec",
|
|
488
|
+
icon: "\u2699\uFE0F",
|
|
489
|
+
label: "Execution",
|
|
490
|
+
detail: parts.join(" | "),
|
|
491
|
+
risk: summary.exec
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
const integrations = permissions.integrations;
|
|
495
|
+
if (integrations) {
|
|
496
|
+
const active = Object.entries(integrations).filter(([, v]) => v && v !== "none");
|
|
497
|
+
if (active.length > 0) {
|
|
498
|
+
lines.push({
|
|
499
|
+
category: "integrations",
|
|
500
|
+
icon: "\u{1F517}",
|
|
501
|
+
label: "Integrations",
|
|
502
|
+
detail: active.map(([k, v]) => `${k}: ${v}`).join(", "),
|
|
503
|
+
risk: summary.integrations
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return lines;
|
|
508
|
+
}
|
|
509
|
+
function riskColor(risk) {
|
|
510
|
+
switch (risk) {
|
|
511
|
+
case "safe":
|
|
512
|
+
return "green";
|
|
513
|
+
case "low":
|
|
514
|
+
return "blue";
|
|
515
|
+
case "medium":
|
|
516
|
+
return "yellow";
|
|
517
|
+
case "high":
|
|
518
|
+
return "red";
|
|
519
|
+
case "critical":
|
|
520
|
+
return "magenta";
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function severityColor(severity) {
|
|
524
|
+
switch (severity) {
|
|
525
|
+
case "info":
|
|
526
|
+
return "gray";
|
|
527
|
+
case "low":
|
|
528
|
+
return "blue";
|
|
529
|
+
case "medium":
|
|
530
|
+
return "yellow";
|
|
531
|
+
case "high":
|
|
532
|
+
return "red";
|
|
533
|
+
case "critical":
|
|
534
|
+
return "magenta";
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/commands/init.ts
|
|
539
|
+
init_config();
|
|
540
|
+
async function initCommand() {
|
|
541
|
+
if (hasKeys()) {
|
|
542
|
+
console.log(chalk.yellow("Keys already exist."));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
ensureConfigDirs();
|
|
546
|
+
console.log("Generating Ed25519 key pair...");
|
|
547
|
+
const keyPair = generateKeyPair();
|
|
548
|
+
const dir = keysDir();
|
|
549
|
+
writeFileSync2(join2(dir, "default.pub"), keyPair.publicKey);
|
|
550
|
+
writeFileSync2(join2(dir, "default.key"), keyPair.privateKey, { mode: 384 });
|
|
551
|
+
const config = loadConfig();
|
|
552
|
+
config.default_key_id = keyPair.keyId;
|
|
553
|
+
saveConfig(config);
|
|
554
|
+
console.log(chalk.green("Key pair generated successfully!"));
|
|
555
|
+
console.log(` Key ID: ${chalk.bold(keyPair.keyId)}`);
|
|
556
|
+
console.log(` Public key: ${join2(dir, "default.pub")}`);
|
|
557
|
+
console.log(` Private key: ${join2(dir, "default.key")}`);
|
|
558
|
+
console.log();
|
|
559
|
+
console.log(
|
|
560
|
+
chalk.dim("Register your public key with the marketplace using: skillport login")
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// src/commands/scan.ts
|
|
565
|
+
import { readFileSync as readFileSync2, readdirSync, statSync } from "node:fs";
|
|
566
|
+
import { join as join3, relative } from "node:path";
|
|
567
|
+
|
|
568
|
+
// ../../packages/scanner/dist/detectors/secrets.js
|
|
569
|
+
function shannonEntropy(str) {
|
|
570
|
+
const freq = /* @__PURE__ */ new Map();
|
|
571
|
+
for (const ch of str) {
|
|
572
|
+
freq.set(ch, (freq.get(ch) || 0) + 1);
|
|
573
|
+
}
|
|
574
|
+
let entropy = 0;
|
|
575
|
+
const len = str.length;
|
|
576
|
+
for (const count of freq.values()) {
|
|
577
|
+
const p = count / len;
|
|
578
|
+
entropy -= p * Math.log2(p);
|
|
579
|
+
}
|
|
580
|
+
return entropy;
|
|
581
|
+
}
|
|
582
|
+
var patterns = [
|
|
583
|
+
{
|
|
584
|
+
id: "SEC001",
|
|
585
|
+
regex: /AKIA[0-9A-Z]{16}/,
|
|
586
|
+
category: "secret",
|
|
587
|
+
severity: "critical",
|
|
588
|
+
message: "AWS Access Key ID detected",
|
|
589
|
+
remediation: "Remove the key and rotate it in AWS IAM console"
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
id: "SEC002",
|
|
593
|
+
regex: /\b(ghp_|gho_|ghs_|ghr_)[A-Za-z0-9_]{36,}/,
|
|
594
|
+
category: "secret",
|
|
595
|
+
severity: "critical",
|
|
596
|
+
message: "GitHub token detected",
|
|
597
|
+
remediation: "Revoke the token and generate a new one"
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
id: "SEC003",
|
|
601
|
+
regex: /\b(sk_live_|pk_live_|rk_live_)[A-Za-z0-9]+/,
|
|
602
|
+
category: "secret",
|
|
603
|
+
severity: "critical",
|
|
604
|
+
message: "Stripe live key detected",
|
|
605
|
+
remediation: "Rotate the key in Stripe Dashboard"
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
id: "SEC004",
|
|
609
|
+
regex: /\b(sk-[A-Za-z0-9]{20,})/,
|
|
610
|
+
category: "secret",
|
|
611
|
+
severity: "critical",
|
|
612
|
+
message: "OpenAI API key detected",
|
|
613
|
+
remediation: "Rotate the key in OpenAI dashboard"
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
id: "SEC005",
|
|
617
|
+
regex: /xoxb-[0-9]{10,}-[A-Za-z0-9]+/,
|
|
618
|
+
category: "secret",
|
|
619
|
+
severity: "critical",
|
|
620
|
+
message: "Slack bot token detected",
|
|
621
|
+
remediation: "Revoke the token in Slack App settings"
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
id: "SEC006",
|
|
625
|
+
regex: /-----BEGIN\s+(RSA|DSA|EC|OPENSSH|PGP)\s+PRIVATE\s+KEY-----/,
|
|
626
|
+
category: "secret",
|
|
627
|
+
severity: "critical",
|
|
628
|
+
message: "Private key detected",
|
|
629
|
+
remediation: "Remove the private key from the package"
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
id: "SEC007",
|
|
633
|
+
regex: /\b(api[_-]?key|api[_-]?secret|access[_-]?token|auth[_-]?token|secret[_-]?key)\s*[:=]\s*['"`]([A-Za-z0-9+/=_-]{20,})['"`]/i,
|
|
634
|
+
category: "secret",
|
|
635
|
+
severity: "high",
|
|
636
|
+
message: "Potential API key or secret hardcoded",
|
|
637
|
+
remediation: "Use environment variables or required_inputs instead"
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
id: "SEC008",
|
|
641
|
+
regex: /password\s*[:=]\s*['"`]([^'"`]{4,})['"`]/i,
|
|
642
|
+
category: "secret",
|
|
643
|
+
severity: "high",
|
|
644
|
+
message: "Hardcoded password detected",
|
|
645
|
+
remediation: "Use environment variables or required_inputs instead"
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
id: "SEC009",
|
|
649
|
+
regex: /['"`]([A-Za-z0-9+/=_-]{40,})['"`]/,
|
|
650
|
+
category: "secret",
|
|
651
|
+
severity: "medium",
|
|
652
|
+
message: "High-entropy string detected (possible secret)",
|
|
653
|
+
filter: (_match, _line) => {
|
|
654
|
+
const str = _match[1];
|
|
655
|
+
if (!str)
|
|
656
|
+
return false;
|
|
657
|
+
return shannonEntropy(str) >= 4.5;
|
|
658
|
+
},
|
|
659
|
+
remediation: "Verify this string is not a secret; if it is, use required_inputs"
|
|
660
|
+
}
|
|
661
|
+
];
|
|
662
|
+
var secretsDetector = {
|
|
663
|
+
name: "secrets",
|
|
664
|
+
patterns
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
// ../../packages/scanner/dist/detectors/dangerous.js
|
|
668
|
+
var patterns2 = [
|
|
669
|
+
{
|
|
670
|
+
id: "DNG001",
|
|
671
|
+
regex: /\beval\s*\(/,
|
|
672
|
+
category: "dangerous",
|
|
673
|
+
severity: "high",
|
|
674
|
+
message: "eval() call detected \u2014 arbitrary code execution risk",
|
|
675
|
+
remediation: "Avoid eval(); use safer alternatives"
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
id: "DNG002",
|
|
679
|
+
regex: /\bnew\s+Function\s*\(/,
|
|
680
|
+
category: "dangerous",
|
|
681
|
+
severity: "high",
|
|
682
|
+
message: "new Function() detected \u2014 dynamic code execution risk",
|
|
683
|
+
remediation: "Avoid dynamic code generation"
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
id: "DNG003",
|
|
687
|
+
regex: /\bchild_process\b/,
|
|
688
|
+
category: "dangerous",
|
|
689
|
+
severity: "high",
|
|
690
|
+
message: "child_process module usage detected",
|
|
691
|
+
remediation: "Declare required commands in permissions.exec"
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
id: "DNG004",
|
|
695
|
+
regex: /\b(exec|execSync|spawn|spawnSync|execFile|execFileSync|fork)\s*\(/,
|
|
696
|
+
category: "dangerous",
|
|
697
|
+
severity: "high",
|
|
698
|
+
message: "Process execution function detected",
|
|
699
|
+
remediation: "Declare required commands in permissions.exec"
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
id: "DNG005",
|
|
703
|
+
regex: /curl\s+.*\|\s*sh/,
|
|
704
|
+
category: "dangerous",
|
|
705
|
+
severity: "critical",
|
|
706
|
+
message: "curl | sh pipe execution detected \u2014 remote code execution risk",
|
|
707
|
+
remediation: "Download and verify scripts before executing"
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
id: "DNG006",
|
|
711
|
+
regex: /rm\s+-rf\s+[/~]/,
|
|
712
|
+
category: "dangerous",
|
|
713
|
+
severity: "critical",
|
|
714
|
+
message: "Destructive rm -rf command detected",
|
|
715
|
+
remediation: "Use targeted file removal instead"
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
id: "DNG007",
|
|
719
|
+
regex: /\bfs\.(writeFile|writeFileSync|appendFile|appendFileSync|rmSync|rmdirSync|unlinkSync)\s*\(/,
|
|
720
|
+
category: "dangerous",
|
|
721
|
+
severity: "high",
|
|
722
|
+
message: "Filesystem write/delete operation detected",
|
|
723
|
+
remediation: "Declare paths in permissions.filesystem"
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
id: "DNG008",
|
|
727
|
+
regex: /process\.env[\s\S]{0,50}(fetch|http|request|send|post|XMLHttpRequest)/,
|
|
728
|
+
category: "dangerous",
|
|
729
|
+
severity: "critical",
|
|
730
|
+
message: "Potential environment variable exfiltration detected",
|
|
731
|
+
remediation: "Do not send environment variables to external services"
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
id: "DNG009",
|
|
735
|
+
regex: /\bwget\s+/,
|
|
736
|
+
category: "dangerous",
|
|
737
|
+
severity: "medium",
|
|
738
|
+
message: "wget usage detected",
|
|
739
|
+
remediation: "Declare network access in permissions.network"
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
id: "DNG010",
|
|
743
|
+
regex: /\b(nc|ncat|netcat)\s+(-[a-z]*\s+)*\S+\s+\d+/,
|
|
744
|
+
category: "dangerous",
|
|
745
|
+
severity: "critical",
|
|
746
|
+
message: "Reverse shell pattern detected (netcat)",
|
|
747
|
+
remediation: "Remove reverse shell code"
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
id: "DNG011",
|
|
751
|
+
regex: /\/dev\/(tcp|udp)\/\S+\/\d+/,
|
|
752
|
+
category: "dangerous",
|
|
753
|
+
severity: "critical",
|
|
754
|
+
message: "Reverse shell pattern detected (/dev/tcp)",
|
|
755
|
+
remediation: "Remove reverse shell code"
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
id: "DNG012",
|
|
759
|
+
regex: /process\.env\.(HOME|USER|PATH|SHELL|LOGNAME)/,
|
|
760
|
+
category: "dangerous",
|
|
761
|
+
severity: "medium",
|
|
762
|
+
message: "Environment variable access detected",
|
|
763
|
+
remediation: "Ensure environment variables are used locally only"
|
|
764
|
+
}
|
|
765
|
+
];
|
|
766
|
+
var dangerousDetector = {
|
|
767
|
+
name: "dangerous",
|
|
768
|
+
patterns: patterns2
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
// ../../packages/scanner/dist/detectors/pii.js
|
|
772
|
+
var EXAMPLE_EMAIL_DOMAINS = [
|
|
773
|
+
"example.com",
|
|
774
|
+
"example.org",
|
|
775
|
+
"example.net",
|
|
776
|
+
"test.com",
|
|
777
|
+
"localhost",
|
|
778
|
+
"placeholder.com"
|
|
779
|
+
];
|
|
780
|
+
function luhnCheck(num) {
|
|
781
|
+
const digits = num.replace(/\D/g, "");
|
|
782
|
+
if (digits.length < 13 || digits.length > 19)
|
|
783
|
+
return false;
|
|
784
|
+
let sum = 0;
|
|
785
|
+
let alternate = false;
|
|
786
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
787
|
+
let n = parseInt(digits[i], 10);
|
|
788
|
+
if (alternate) {
|
|
789
|
+
n *= 2;
|
|
790
|
+
if (n > 9)
|
|
791
|
+
n -= 9;
|
|
792
|
+
}
|
|
793
|
+
sum += n;
|
|
794
|
+
alternate = !alternate;
|
|
795
|
+
}
|
|
796
|
+
return sum % 10 === 0;
|
|
797
|
+
}
|
|
798
|
+
var patterns3 = [
|
|
799
|
+
{
|
|
800
|
+
id: "PII001",
|
|
801
|
+
regex: /(\/Users\/[a-zA-Z0-9._-]+|\/home\/[a-zA-Z0-9._-]+|C:\\Users\\[a-zA-Z0-9._-]+)/,
|
|
802
|
+
category: "pii",
|
|
803
|
+
severity: "medium",
|
|
804
|
+
message: "User home directory path detected",
|
|
805
|
+
remediation: "Replace with relative paths or use ~ placeholder"
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
id: "PII002",
|
|
809
|
+
regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/,
|
|
810
|
+
category: "pii",
|
|
811
|
+
severity: "low",
|
|
812
|
+
message: "Email address detected",
|
|
813
|
+
filter: (match) => {
|
|
814
|
+
const email = match[0];
|
|
815
|
+
return !EXAMPLE_EMAIL_DOMAINS.some((d) => email.endsWith(`@${d}`));
|
|
816
|
+
},
|
|
817
|
+
remediation: "Remove or anonymize the email address"
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
id: "PII003",
|
|
821
|
+
regex: /\b0[789]0-?\d{4}-?\d{4}\b/,
|
|
822
|
+
category: "pii",
|
|
823
|
+
severity: "medium",
|
|
824
|
+
message: "Japanese phone number detected",
|
|
825
|
+
remediation: "Remove the phone number"
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
id: "PII004",
|
|
829
|
+
regex: /\b(?:\+1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/,
|
|
830
|
+
category: "pii",
|
|
831
|
+
severity: "medium",
|
|
832
|
+
message: "Phone number detected",
|
|
833
|
+
remediation: "Remove the phone number"
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
id: "PII005",
|
|
837
|
+
regex: /\b(?:\d[ -]*?){13,19}\b/,
|
|
838
|
+
category: "pii",
|
|
839
|
+
severity: "critical",
|
|
840
|
+
message: "Credit card number detected (Luhn-verified)",
|
|
841
|
+
filter: (match) => luhnCheck(match[0]),
|
|
842
|
+
remediation: "Remove the credit card number immediately"
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
id: "PII006",
|
|
846
|
+
regex: /\b(?!10\.)(?!172\.(?:1[6-9]|2\d|3[01])\.)(?!192\.168\.)(?!127\.)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/,
|
|
847
|
+
category: "pii",
|
|
848
|
+
severity: "low",
|
|
849
|
+
message: "Public IP address detected",
|
|
850
|
+
remediation: "Remove or anonymize the IP address"
|
|
851
|
+
}
|
|
852
|
+
];
|
|
853
|
+
var piiDetector = {
|
|
854
|
+
name: "pii",
|
|
855
|
+
patterns: patterns3
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
// ../../packages/scanner/dist/detectors/obfuscation.js
|
|
859
|
+
var patterns4 = [
|
|
860
|
+
{
|
|
861
|
+
id: "OBF001",
|
|
862
|
+
regex: /\batob\s*\(/,
|
|
863
|
+
category: "obfuscation",
|
|
864
|
+
severity: "medium",
|
|
865
|
+
message: "Base64 decoding (atob) detected",
|
|
866
|
+
remediation: "Use plain text instead of Base64-encoded strings"
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
id: "OBF002",
|
|
870
|
+
regex: /\bBuffer\.from\s*\([^,]+,\s*['"`]base64['"`]\s*\)/,
|
|
871
|
+
category: "obfuscation",
|
|
872
|
+
severity: "medium",
|
|
873
|
+
message: "Buffer.from base64 decoding detected",
|
|
874
|
+
remediation: "Use plain text instead of Base64-encoded data"
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
id: "OBF003",
|
|
878
|
+
regex: /(?:\\x[0-9a-fA-F]{2}){8,}/,
|
|
879
|
+
category: "obfuscation",
|
|
880
|
+
severity: "high",
|
|
881
|
+
message: "Hex-escaped string sequence detected",
|
|
882
|
+
remediation: "Use readable string literals instead of hex escapes"
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
id: "OBF004",
|
|
886
|
+
regex: /[A-Za-z0-9+/]{100,}={0,2}/,
|
|
887
|
+
category: "obfuscation",
|
|
888
|
+
severity: "medium",
|
|
889
|
+
message: "Long Base64-like string detected (100+ chars)",
|
|
890
|
+
remediation: "Ensure this is not obfuscated code or data"
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
id: "OBF005",
|
|
894
|
+
regex: /String\.fromCharCode\s*\(\s*(\d+\s*,\s*){5,}/,
|
|
895
|
+
category: "obfuscation",
|
|
896
|
+
severity: "high",
|
|
897
|
+
message: "String.fromCharCode with many arguments \u2014 possible obfuscation",
|
|
898
|
+
remediation: "Use readable string literals"
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
id: "OBF006",
|
|
902
|
+
regex: /\bunescape\s*\(\s*['"`]%/,
|
|
903
|
+
category: "obfuscation",
|
|
904
|
+
severity: "medium",
|
|
905
|
+
message: "URL-encoded string decoding detected",
|
|
906
|
+
remediation: "Use readable string literals"
|
|
907
|
+
}
|
|
908
|
+
];
|
|
909
|
+
var obfuscationDetector = {
|
|
910
|
+
name: "obfuscation",
|
|
911
|
+
patterns: patterns4
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
// ../../packages/scanner/dist/detectors/network.js
|
|
915
|
+
var patterns5 = [
|
|
916
|
+
{
|
|
917
|
+
id: "NET001",
|
|
918
|
+
regex: /\bfetch\s*\(\s*['"`](https?:\/\/[^'"`\s]+)['"`]/,
|
|
919
|
+
category: "network",
|
|
920
|
+
severity: "medium",
|
|
921
|
+
message: "External HTTP request via fetch()",
|
|
922
|
+
filter: (match) => {
|
|
923
|
+
const url = match[1];
|
|
924
|
+
if (!url)
|
|
925
|
+
return true;
|
|
926
|
+
return !/^https?:\/\/(localhost|127\.0\.0\.1)/.test(url);
|
|
927
|
+
},
|
|
928
|
+
remediation: "Declare the domain in permissions.network.domains"
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
id: "NET002",
|
|
932
|
+
regex: /\brequire\s*\(\s*['"`](https?|node:https?)['"`]\s*\)/,
|
|
933
|
+
category: "network",
|
|
934
|
+
severity: "low",
|
|
935
|
+
message: "HTTP/HTTPS module import detected",
|
|
936
|
+
remediation: "Declare network usage in permissions.network"
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
id: "NET003",
|
|
940
|
+
regex: /\bhttps?\.request\s*\(/,
|
|
941
|
+
category: "network",
|
|
942
|
+
severity: "medium",
|
|
943
|
+
message: "Direct HTTP request detected",
|
|
944
|
+
remediation: "Declare the domain in permissions.network.domains"
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
id: "NET004",
|
|
948
|
+
regex: /new\s+WebSocket\s*\(\s*['"`](wss?:\/\/[^'"`\s]+)['"`]/,
|
|
949
|
+
category: "network",
|
|
950
|
+
severity: "medium",
|
|
951
|
+
message: "WebSocket connection detected",
|
|
952
|
+
remediation: "Declare the domain in permissions.network.domains"
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
id: "NET005",
|
|
956
|
+
regex: /\baxios\b|\bsuperagent\b|\bgot\b|\bnode-fetch\b|\bundici\b/,
|
|
957
|
+
category: "network",
|
|
958
|
+
severity: "low",
|
|
959
|
+
message: "HTTP client library usage detected",
|
|
960
|
+
remediation: "Declare network usage in permissions.network"
|
|
961
|
+
}
|
|
962
|
+
];
|
|
963
|
+
var networkDetector = {
|
|
964
|
+
name: "network",
|
|
965
|
+
patterns: patterns5
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
// ../../packages/scanner/dist/engine/scanner.js
|
|
969
|
+
var SCANNABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
970
|
+
".md",
|
|
971
|
+
".ts",
|
|
972
|
+
".tsx",
|
|
973
|
+
".js",
|
|
974
|
+
".jsx",
|
|
975
|
+
".py",
|
|
976
|
+
".sh",
|
|
977
|
+
".bash",
|
|
978
|
+
".zsh",
|
|
979
|
+
".json",
|
|
980
|
+
".yaml",
|
|
981
|
+
".yml",
|
|
982
|
+
".txt",
|
|
983
|
+
".toml",
|
|
984
|
+
".cfg",
|
|
985
|
+
".ini",
|
|
986
|
+
".env",
|
|
987
|
+
".conf"
|
|
988
|
+
]);
|
|
989
|
+
var MAX_FILE_SIZE = 1 * 1024 * 1024;
|
|
990
|
+
var MAX_ZIP_SIZE = 10 * 1024 * 1024;
|
|
991
|
+
function isScannable(fileName) {
|
|
992
|
+
const dotIndex = fileName.lastIndexOf(".");
|
|
993
|
+
if (dotIndex === -1)
|
|
994
|
+
return false;
|
|
995
|
+
const ext = fileName.substring(dotIndex).toLowerCase();
|
|
996
|
+
return SCANNABLE_EXTENSIONS.has(ext);
|
|
997
|
+
}
|
|
998
|
+
var allDetectors = [
|
|
999
|
+
secretsDetector,
|
|
1000
|
+
dangerousDetector,
|
|
1001
|
+
piiDetector,
|
|
1002
|
+
obfuscationDetector,
|
|
1003
|
+
networkDetector
|
|
1004
|
+
];
|
|
1005
|
+
function scanFileContent(content, fileName) {
|
|
1006
|
+
const issues = [];
|
|
1007
|
+
const lines = content.split("\n");
|
|
1008
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
1009
|
+
const line = lines[lineIndex];
|
|
1010
|
+
const lineNumber = lineIndex + 1;
|
|
1011
|
+
for (const detector of allDetectors) {
|
|
1012
|
+
for (const pattern of detector.patterns) {
|
|
1013
|
+
const match = line.match(pattern.regex);
|
|
1014
|
+
if (!match)
|
|
1015
|
+
continue;
|
|
1016
|
+
if (pattern.filter && !pattern.filter(match, line))
|
|
1017
|
+
continue;
|
|
1018
|
+
const snippet = line.length > 200 ? line.substring(0, 200) + "..." : line;
|
|
1019
|
+
issues.push({
|
|
1020
|
+
id: pattern.id,
|
|
1021
|
+
severity: pattern.severity,
|
|
1022
|
+
category: pattern.category,
|
|
1023
|
+
message: pattern.message,
|
|
1024
|
+
file: fileName,
|
|
1025
|
+
line: lineNumber,
|
|
1026
|
+
snippet: snippet.trim(),
|
|
1027
|
+
remediation: pattern.remediation
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return issues;
|
|
1033
|
+
}
|
|
1034
|
+
function scanFiles(files) {
|
|
1035
|
+
const issues = [];
|
|
1036
|
+
const scannedFiles = [];
|
|
1037
|
+
const skippedFiles = [];
|
|
1038
|
+
for (const [path, content] of files) {
|
|
1039
|
+
if (!isScannable(path)) {
|
|
1040
|
+
skippedFiles.push(path);
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
const fileIssues = scanFileContent(content, path);
|
|
1044
|
+
issues.push(...fileIssues);
|
|
1045
|
+
scannedFiles.push(path);
|
|
1046
|
+
}
|
|
1047
|
+
return { issues, scannedFiles, skippedFiles };
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// ../../packages/scanner/dist/report/report.js
|
|
1051
|
+
var SEVERITY_WEIGHTS = {
|
|
1052
|
+
info: 0,
|
|
1053
|
+
low: 2,
|
|
1054
|
+
medium: 5,
|
|
1055
|
+
high: 15,
|
|
1056
|
+
critical: 30
|
|
1057
|
+
};
|
|
1058
|
+
var SCANNER_VERSION = "0.1.0";
|
|
1059
|
+
function generateReport(issues, scannedFiles, skippedFiles) {
|
|
1060
|
+
const by_severity = {
|
|
1061
|
+
info: 0,
|
|
1062
|
+
low: 0,
|
|
1063
|
+
medium: 0,
|
|
1064
|
+
high: 0,
|
|
1065
|
+
critical: 0
|
|
1066
|
+
};
|
|
1067
|
+
const by_category = {
|
|
1068
|
+
secret: 0,
|
|
1069
|
+
dangerous: 0,
|
|
1070
|
+
pii: 0,
|
|
1071
|
+
obfuscation: 0,
|
|
1072
|
+
network: 0
|
|
1073
|
+
};
|
|
1074
|
+
let rawScore = 0;
|
|
1075
|
+
for (const issue of issues) {
|
|
1076
|
+
by_severity[issue.severity]++;
|
|
1077
|
+
by_category[issue.category]++;
|
|
1078
|
+
rawScore += SEVERITY_WEIGHTS[issue.severity];
|
|
1079
|
+
}
|
|
1080
|
+
const risk_score = Math.min(rawScore, 100);
|
|
1081
|
+
const passed = by_severity.high === 0 && by_severity.critical === 0;
|
|
1082
|
+
return {
|
|
1083
|
+
passed,
|
|
1084
|
+
risk_score,
|
|
1085
|
+
summary: {
|
|
1086
|
+
total: issues.length,
|
|
1087
|
+
by_severity,
|
|
1088
|
+
by_category
|
|
1089
|
+
},
|
|
1090
|
+
issues,
|
|
1091
|
+
scanned_files: scannedFiles,
|
|
1092
|
+
skipped_files: skippedFiles,
|
|
1093
|
+
scanned_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1094
|
+
scanner_version: SCANNER_VERSION
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// src/utils/display.ts
|
|
1099
|
+
import chalk2 from "chalk";
|
|
1100
|
+
function displayScanReport(report) {
|
|
1101
|
+
console.log();
|
|
1102
|
+
if (report.passed) {
|
|
1103
|
+
console.log(chalk2.green.bold("SCAN PASSED"));
|
|
1104
|
+
} else {
|
|
1105
|
+
console.log(chalk2.red.bold("SCAN FAILED"));
|
|
1106
|
+
}
|
|
1107
|
+
console.log(
|
|
1108
|
+
chalk2.dim(`Risk Score: ${report.risk_score}/100 | `) + chalk2.dim(`Scanned: ${report.scanned_files.length} files | `) + chalk2.dim(`Issues: ${report.summary.total}`)
|
|
1109
|
+
);
|
|
1110
|
+
console.log();
|
|
1111
|
+
if (report.summary.total > 0) {
|
|
1112
|
+
console.log(chalk2.bold("Issues by severity:"));
|
|
1113
|
+
for (const [severity, count] of Object.entries(report.summary.by_severity)) {
|
|
1114
|
+
if (count > 0) {
|
|
1115
|
+
const color = severityColor(severity);
|
|
1116
|
+
console.log(` ${chalk2[color](`${severity}: ${count}`)}`);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
console.log();
|
|
1120
|
+
console.log(chalk2.bold("Details:"));
|
|
1121
|
+
for (const issue of report.issues) {
|
|
1122
|
+
const color = severityColor(issue.severity);
|
|
1123
|
+
const prefix = chalk2[color](`[${issue.severity.toUpperCase()}]`);
|
|
1124
|
+
console.log(` ${prefix} ${issue.message}`);
|
|
1125
|
+
console.log(
|
|
1126
|
+
chalk2.dim(` ${issue.file}:${issue.line} (${issue.id})`)
|
|
1127
|
+
);
|
|
1128
|
+
if (issue.remediation) {
|
|
1129
|
+
console.log(chalk2.dim(` Fix: ${issue.remediation}`));
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
console.log();
|
|
1134
|
+
}
|
|
1135
|
+
function displayPermissions(permissions) {
|
|
1136
|
+
const summary = assessPermissions(permissions);
|
|
1137
|
+
const lines = formatPermissions(permissions, summary);
|
|
1138
|
+
console.log(chalk2.bold("Permissions:"));
|
|
1139
|
+
for (const line of lines) {
|
|
1140
|
+
const color = riskColor(line.risk);
|
|
1141
|
+
console.log(
|
|
1142
|
+
` ${line.icon} ${chalk2[color](line.label)}: ${line.detail}`
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
console.log();
|
|
1146
|
+
}
|
|
1147
|
+
function displayDangerFlags(flags) {
|
|
1148
|
+
if (flags.length === 0) return;
|
|
1149
|
+
console.log(chalk2.bold("Danger Flags:"));
|
|
1150
|
+
for (const flag of flags) {
|
|
1151
|
+
const color = severityColor(flag.severity);
|
|
1152
|
+
const prefix = chalk2[color](`[${flag.severity.toUpperCase()}]`);
|
|
1153
|
+
console.log(` ${prefix} ${flag.message} (${flag.code})`);
|
|
1154
|
+
if (flag.file) {
|
|
1155
|
+
console.log(chalk2.dim(` ${flag.file}${flag.line ? `:${flag.line}` : ""}`));
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
console.log();
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// src/commands/scan.ts
|
|
1162
|
+
function collectFiles(dir, basePath = dir) {
|
|
1163
|
+
const files = /* @__PURE__ */ new Map();
|
|
1164
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
1165
|
+
for (const entry of entries) {
|
|
1166
|
+
const fullPath = join3(dir, entry.name);
|
|
1167
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1168
|
+
if (entry.isDirectory()) {
|
|
1169
|
+
const sub = collectFiles(fullPath, basePath);
|
|
1170
|
+
for (const [k, v] of sub) files.set(k, v);
|
|
1171
|
+
} else if (entry.isFile()) {
|
|
1172
|
+
const relPath = relative(basePath, fullPath);
|
|
1173
|
+
const stat = statSync(fullPath);
|
|
1174
|
+
if (stat.size > MAX_FILE_SIZE) continue;
|
|
1175
|
+
if (!isScannable(relPath)) continue;
|
|
1176
|
+
files.set(relPath, readFileSync2(fullPath, "utf-8"));
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return files;
|
|
1180
|
+
}
|
|
1181
|
+
async function scanCommand(target) {
|
|
1182
|
+
let files;
|
|
1183
|
+
if (target.endsWith(".ssp")) {
|
|
1184
|
+
console.log(`Scanning SkillPort package: ${target}`);
|
|
1185
|
+
const data = readFileSync2(target);
|
|
1186
|
+
const ssp = await extractSSP(data);
|
|
1187
|
+
files = /* @__PURE__ */ new Map();
|
|
1188
|
+
for (const [path, content] of ssp.files) {
|
|
1189
|
+
if (isScannable(path)) {
|
|
1190
|
+
files.set(path, content.toString("utf-8"));
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
if (ssp.skillMd) {
|
|
1194
|
+
files.set("SKILL.md", ssp.skillMd);
|
|
1195
|
+
}
|
|
1196
|
+
} else {
|
|
1197
|
+
console.log(`Scanning directory: ${target}`);
|
|
1198
|
+
files = collectFiles(target);
|
|
1199
|
+
}
|
|
1200
|
+
const result = scanFiles(files);
|
|
1201
|
+
const report = generateReport(
|
|
1202
|
+
result.issues,
|
|
1203
|
+
result.scannedFiles,
|
|
1204
|
+
result.skippedFiles
|
|
1205
|
+
);
|
|
1206
|
+
displayScanReport(report);
|
|
1207
|
+
if (!report.passed) {
|
|
1208
|
+
process.exitCode = 1;
|
|
1209
|
+
} else if (report.summary.total > 0) {
|
|
1210
|
+
process.exitCode = 2;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// src/commands/export.ts
|
|
1215
|
+
import { readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1216
|
+
import { join as join4, relative as relative2, basename } from "node:path";
|
|
1217
|
+
import chalk3 from "chalk";
|
|
1218
|
+
import inquirer from "inquirer";
|
|
1219
|
+
init_dist();
|
|
1220
|
+
init_config();
|
|
1221
|
+
|
|
1222
|
+
// src/utils/skill-parser.ts
|
|
1223
|
+
function parseSkillMd(content) {
|
|
1224
|
+
let frontmatter = "";
|
|
1225
|
+
let body = content;
|
|
1226
|
+
if (body.startsWith("---")) {
|
|
1227
|
+
const endIdx = body.indexOf("---", 3);
|
|
1228
|
+
if (endIdx !== -1) {
|
|
1229
|
+
frontmatter = body.slice(0, endIdx + 3);
|
|
1230
|
+
body = body.slice(endIdx + 3);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
const lines = body.split("\n");
|
|
1234
|
+
let headerLines = [];
|
|
1235
|
+
const sections = [];
|
|
1236
|
+
let currentHeading = "";
|
|
1237
|
+
let currentLines = [];
|
|
1238
|
+
let inSection = false;
|
|
1239
|
+
for (const line of lines) {
|
|
1240
|
+
if (/^## /.test(line)) {
|
|
1241
|
+
if (inSection) {
|
|
1242
|
+
sections.push(buildSection(currentHeading, currentLines));
|
|
1243
|
+
}
|
|
1244
|
+
currentHeading = line.replace(/^## /, "").trim();
|
|
1245
|
+
currentLines = [line];
|
|
1246
|
+
inSection = true;
|
|
1247
|
+
} else if (inSection) {
|
|
1248
|
+
currentLines.push(line);
|
|
1249
|
+
} else {
|
|
1250
|
+
headerLines.push(line);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
if (inSection) {
|
|
1254
|
+
sections.push(buildSection(currentHeading, currentLines));
|
|
1255
|
+
}
|
|
1256
|
+
return {
|
|
1257
|
+
frontmatter,
|
|
1258
|
+
header: headerLines.join("\n"),
|
|
1259
|
+
sections
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
function buildSection(heading, lines) {
|
|
1263
|
+
const raw = lines.join("\n");
|
|
1264
|
+
const referencedFiles = extractFileReferences(raw);
|
|
1265
|
+
return { heading, raw, referencedFiles };
|
|
1266
|
+
}
|
|
1267
|
+
function extractFileReferences(text) {
|
|
1268
|
+
const refs = /* @__PURE__ */ new Set();
|
|
1269
|
+
const backtickPattern = /`([a-zA-Z0-9_./-]+\.[a-zA-Z0-9]+)`/g;
|
|
1270
|
+
let match;
|
|
1271
|
+
while ((match = backtickPattern.exec(text)) !== null) {
|
|
1272
|
+
const path = match[1];
|
|
1273
|
+
if (!path.startsWith("http") && !path.includes("@") && path.includes("/")) {
|
|
1274
|
+
refs.add(path);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
const pathPattern = /(?:scripts|payload|bins)\/[a-zA-Z0-9_.-]+/g;
|
|
1278
|
+
while ((match = pathPattern.exec(text)) !== null) {
|
|
1279
|
+
refs.add(match[0]);
|
|
1280
|
+
}
|
|
1281
|
+
return [...refs];
|
|
1282
|
+
}
|
|
1283
|
+
function reconstructSkillMd(parsed, selectedIndices) {
|
|
1284
|
+
const parts = [];
|
|
1285
|
+
if (parsed.frontmatter) {
|
|
1286
|
+
parts.push(parsed.frontmatter);
|
|
1287
|
+
}
|
|
1288
|
+
parts.push(parsed.header);
|
|
1289
|
+
for (const idx of selectedIndices) {
|
|
1290
|
+
if (idx >= 0 && idx < parsed.sections.length) {
|
|
1291
|
+
parts.push(parsed.sections[idx].raw);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
return parts.join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n";
|
|
1295
|
+
}
|
|
1296
|
+
function sectionSummary(section, maxLen = 60) {
|
|
1297
|
+
const lines = section.raw.split("\n").slice(1);
|
|
1298
|
+
for (const line of lines) {
|
|
1299
|
+
const trimmed = line.trim();
|
|
1300
|
+
if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("```")) {
|
|
1301
|
+
return trimmed.length > maxLen ? trimmed.slice(0, maxLen - 3) + "..." : trimmed;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
return "";
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// src/utils/env-detect.ts
|
|
1308
|
+
import { execSync } from "node:child_process";
|
|
1309
|
+
import { platform as osPlatform } from "node:os";
|
|
1310
|
+
function detectOS() {
|
|
1311
|
+
const p = osPlatform();
|
|
1312
|
+
if (p === "darwin") return "macos";
|
|
1313
|
+
if (p === "win32") return "windows";
|
|
1314
|
+
return p;
|
|
1315
|
+
}
|
|
1316
|
+
function binaryExists(name) {
|
|
1317
|
+
try {
|
|
1318
|
+
execSync(`which ${name}`, { stdio: "pipe" });
|
|
1319
|
+
return true;
|
|
1320
|
+
} catch {
|
|
1321
|
+
return false;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
function envVarExists(name) {
|
|
1325
|
+
return !!process.env[name];
|
|
1326
|
+
}
|
|
1327
|
+
function checkEnvironment(manifest) {
|
|
1328
|
+
const currentOS = detectOS();
|
|
1329
|
+
const osCompatible = manifest.os_compat.includes(currentOS);
|
|
1330
|
+
const binaries = manifest.dependencies.filter((d) => d.type === "cli" || d.type === "brew" || d.type === "apt").map((dep) => {
|
|
1331
|
+
const found = binaryExists(dep.name);
|
|
1332
|
+
return {
|
|
1333
|
+
check: dep.name,
|
|
1334
|
+
status: found ? "ok" : dep.optional ? "warn" : "missing",
|
|
1335
|
+
detail: found ? `Found${dep.version ? ` (requires ${dep.version})` : ""}` : dep.optional ? "Not found (optional)" : "Not found (required)",
|
|
1336
|
+
skippable: dep.optional ?? false
|
|
1337
|
+
};
|
|
1338
|
+
});
|
|
1339
|
+
const envVars = manifest.install.required_inputs.filter((input) => input.key === input.key.toUpperCase()).map((input) => {
|
|
1340
|
+
const found = envVarExists(input.key);
|
|
1341
|
+
return {
|
|
1342
|
+
check: input.key,
|
|
1343
|
+
status: found ? "ok" : input.required ? "missing" : "warn",
|
|
1344
|
+
detail: found ? "Set" : input.required ? `Not set \u2014 ${input.description}` : `Not set (optional) \u2014 ${input.description}`
|
|
1345
|
+
};
|
|
1346
|
+
});
|
|
1347
|
+
const hasMissing = binaries.some((b) => b.status === "missing") || envVars.some((e) => e.status === "missing");
|
|
1348
|
+
return {
|
|
1349
|
+
os: { name: currentOS, compatible: osCompatible },
|
|
1350
|
+
binaries,
|
|
1351
|
+
envVars,
|
|
1352
|
+
incompatibleSections: [],
|
|
1353
|
+
// filled by caller with SKILL.md analysis
|
|
1354
|
+
ready: osCompatible && !hasMissing
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
function findIncompatibleSections(sectionTexts, missingBins) {
|
|
1358
|
+
if (missingBins.length === 0) return [];
|
|
1359
|
+
const incompatible = [];
|
|
1360
|
+
for (let i = 0; i < sectionTexts.length; i++) {
|
|
1361
|
+
const lower = sectionTexts[i].toLowerCase();
|
|
1362
|
+
for (const bin of missingBins) {
|
|
1363
|
+
if (lower.includes(bin.toLowerCase())) {
|
|
1364
|
+
incompatible.push(i);
|
|
1365
|
+
break;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
return incompatible;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// src/utils/quality-check.ts
|
|
1373
|
+
var KNOWN_CLI_TOOLS = /* @__PURE__ */ new Set([
|
|
1374
|
+
"git",
|
|
1375
|
+
"docker",
|
|
1376
|
+
"docker-compose",
|
|
1377
|
+
"npm",
|
|
1378
|
+
"npx",
|
|
1379
|
+
"yarn",
|
|
1380
|
+
"pnpm",
|
|
1381
|
+
"node",
|
|
1382
|
+
"python",
|
|
1383
|
+
"python3",
|
|
1384
|
+
"pip",
|
|
1385
|
+
"pip3",
|
|
1386
|
+
"ruby",
|
|
1387
|
+
"gem",
|
|
1388
|
+
"cargo",
|
|
1389
|
+
"rustc",
|
|
1390
|
+
"go",
|
|
1391
|
+
"curl",
|
|
1392
|
+
"wget",
|
|
1393
|
+
"jq",
|
|
1394
|
+
"yq",
|
|
1395
|
+
"sed",
|
|
1396
|
+
"awk",
|
|
1397
|
+
"grep",
|
|
1398
|
+
"kubectl",
|
|
1399
|
+
"helm",
|
|
1400
|
+
"terraform",
|
|
1401
|
+
"ansible",
|
|
1402
|
+
"aws",
|
|
1403
|
+
"gcloud",
|
|
1404
|
+
"az",
|
|
1405
|
+
"redis-cli",
|
|
1406
|
+
"psql",
|
|
1407
|
+
"mysql",
|
|
1408
|
+
"sqlite3",
|
|
1409
|
+
"mongosh",
|
|
1410
|
+
"ffmpeg",
|
|
1411
|
+
"imagemagick",
|
|
1412
|
+
"convert",
|
|
1413
|
+
"gh",
|
|
1414
|
+
"hub",
|
|
1415
|
+
"slack",
|
|
1416
|
+
"slack_cli",
|
|
1417
|
+
"make",
|
|
1418
|
+
"cmake",
|
|
1419
|
+
"gcc",
|
|
1420
|
+
"g++",
|
|
1421
|
+
"clang",
|
|
1422
|
+
"java",
|
|
1423
|
+
"javac",
|
|
1424
|
+
"mvn",
|
|
1425
|
+
"gradle",
|
|
1426
|
+
"swift",
|
|
1427
|
+
"xcodebuild",
|
|
1428
|
+
"deno",
|
|
1429
|
+
"bun"
|
|
1430
|
+
]);
|
|
1431
|
+
function detectCliDeps(content, source) {
|
|
1432
|
+
const found = /* @__PURE__ */ new Map();
|
|
1433
|
+
const backtickPattern = /`([a-zA-Z0-9_-]+)(?:\s[^`]*)?`/g;
|
|
1434
|
+
let match;
|
|
1435
|
+
while ((match = backtickPattern.exec(content)) !== null) {
|
|
1436
|
+
const tool = match[1].toLowerCase();
|
|
1437
|
+
if (KNOWN_CLI_TOOLS.has(tool) && !found.has(tool)) {
|
|
1438
|
+
found.set(tool, {
|
|
1439
|
+
name: tool,
|
|
1440
|
+
source,
|
|
1441
|
+
available: binaryExists(tool)
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
const shebangPattern = /^#!\/usr\/bin\/env\s+([a-zA-Z0-9_-]+)|^#!\/(?:usr\/(?:local\/)?)?bin\/([a-zA-Z0-9_-]+)/gm;
|
|
1446
|
+
while ((match = shebangPattern.exec(content)) !== null) {
|
|
1447
|
+
const tool = (match[1] || match[2]).toLowerCase();
|
|
1448
|
+
if (!found.has(tool)) {
|
|
1449
|
+
found.set(tool, {
|
|
1450
|
+
name: tool,
|
|
1451
|
+
source,
|
|
1452
|
+
available: binaryExists(tool)
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
const cmdPattern = /^(?:\$\s+|>\s+)?([a-zA-Z0-9_-]+)\s/gm;
|
|
1457
|
+
while ((match = cmdPattern.exec(content)) !== null) {
|
|
1458
|
+
const tool = match[1].toLowerCase();
|
|
1459
|
+
if (KNOWN_CLI_TOOLS.has(tool) && !found.has(tool)) {
|
|
1460
|
+
found.set(tool, {
|
|
1461
|
+
name: tool,
|
|
1462
|
+
source,
|
|
1463
|
+
available: binaryExists(tool)
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
return [...found.values()];
|
|
1468
|
+
}
|
|
1469
|
+
function checkFileReferences(skillMdContent, payloadFiles, sectionHeadings) {
|
|
1470
|
+
const broken = [];
|
|
1471
|
+
const fileSet = new Set(payloadFiles);
|
|
1472
|
+
const backtickPattern = /`([a-zA-Z0-9_./-]+\.[a-zA-Z0-9]+)`/g;
|
|
1473
|
+
let match;
|
|
1474
|
+
while ((match = backtickPattern.exec(skillMdContent)) !== null) {
|
|
1475
|
+
const ref = match[1];
|
|
1476
|
+
if (ref.startsWith("http") || ref.includes("@")) continue;
|
|
1477
|
+
if (!ref.includes("/")) continue;
|
|
1478
|
+
const exists = fileSet.has(ref) || fileSet.has(`payload/${ref}`);
|
|
1479
|
+
if (!exists) {
|
|
1480
|
+
const before = skillMdContent.slice(0, match.index);
|
|
1481
|
+
const lastHeading = before.match(/## ([^\n]+)/g);
|
|
1482
|
+
const source = lastHeading ? `SKILL.md ## ${lastHeading[lastHeading.length - 1].replace("## ", "")}` : "SKILL.md";
|
|
1483
|
+
broken.push({ ref, source });
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
const pathPattern = /(?:scripts|payload|bins)\/[a-zA-Z0-9_.-]+/g;
|
|
1487
|
+
while ((match = pathPattern.exec(skillMdContent)) !== null) {
|
|
1488
|
+
const ref = match[0];
|
|
1489
|
+
const exists = fileSet.has(ref) || fileSet.has(`payload/${ref}`);
|
|
1490
|
+
if (!exists && !broken.some((b) => b.ref === ref)) {
|
|
1491
|
+
const before = skillMdContent.slice(0, match.index);
|
|
1492
|
+
const lastHeading = before.match(/## ([^\n]+)/g);
|
|
1493
|
+
const source = lastHeading ? `SKILL.md ## ${lastHeading[lastHeading.length - 1].replace("## ", "")}` : "SKILL.md";
|
|
1494
|
+
broken.push({ ref, source });
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
return broken;
|
|
1498
|
+
}
|
|
1499
|
+
function checkStructure(skillMdContent, fileCount) {
|
|
1500
|
+
const issues = [];
|
|
1501
|
+
const lines = skillMdContent.split("\n");
|
|
1502
|
+
const nonEmptyLines = lines.filter((l) => l.trim().length > 0);
|
|
1503
|
+
if (!lines.some((l) => /^# /.test(l))) {
|
|
1504
|
+
issues.push({ severity: "error", message: "SKILL.md is missing a # title heading" });
|
|
1505
|
+
}
|
|
1506
|
+
if (nonEmptyLines.length < 5) {
|
|
1507
|
+
issues.push({ severity: "warn", message: "SKILL.md is very short \u2014 add more detailed instructions" });
|
|
1508
|
+
}
|
|
1509
|
+
const sectionCount = lines.filter((l) => /^## /.test(l)).length;
|
|
1510
|
+
if (sectionCount === 0 && nonEmptyLines.length > 20) {
|
|
1511
|
+
issues.push({ severity: "warn", message: "Consider splitting SKILL.md into ## sections for better organization" });
|
|
1512
|
+
}
|
|
1513
|
+
const titleIdx = lines.findIndex((l) => /^# /.test(l));
|
|
1514
|
+
if (titleIdx >= 0) {
|
|
1515
|
+
const afterTitle = lines.slice(titleIdx + 1, titleIdx + 5);
|
|
1516
|
+
const hasIntro = afterTitle.some((l) => l.trim().length > 0 && !/^#/.test(l));
|
|
1517
|
+
if (!hasIntro) {
|
|
1518
|
+
issues.push({ severity: "warn", message: "Add a brief description after the # title" });
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
if (!skillMdContent.startsWith("---")) {
|
|
1522
|
+
issues.push({ severity: "info", message: "No YAML frontmatter \u2014 consider adding name and description metadata" });
|
|
1523
|
+
}
|
|
1524
|
+
if (fileCount <= 1) {
|
|
1525
|
+
issues.push({ severity: "info", message: "Package contains only SKILL.md \u2014 consider adding supporting scripts or configs" });
|
|
1526
|
+
}
|
|
1527
|
+
return issues;
|
|
1528
|
+
}
|
|
1529
|
+
function depsToManifest(deps) {
|
|
1530
|
+
return deps.map((d) => ({
|
|
1531
|
+
name: d.name,
|
|
1532
|
+
type: "cli",
|
|
1533
|
+
optional: false
|
|
1534
|
+
}));
|
|
1535
|
+
}
|
|
1536
|
+
function runQualityCheck(skillMdContent, allFiles) {
|
|
1537
|
+
const payloadFiles = [...allFiles.keys()].filter((f) => f !== "SKILL.md");
|
|
1538
|
+
const depsFromSkillMd = detectCliDeps(skillMdContent, "SKILL.md");
|
|
1539
|
+
const depsFromScripts = [];
|
|
1540
|
+
for (const [path, content] of allFiles) {
|
|
1541
|
+
if (path === "SKILL.md") continue;
|
|
1542
|
+
const ext = path.split(".").pop()?.toLowerCase();
|
|
1543
|
+
if (["sh", "bash", "zsh", "py", "rb", "js", "ts"].includes(ext ?? "")) {
|
|
1544
|
+
const scriptDeps = detectCliDeps(content.toString("utf-8"), path);
|
|
1545
|
+
for (const dep of scriptDeps) {
|
|
1546
|
+
if (!depsFromSkillMd.some((d) => d.name === dep.name) && !depsFromScripts.some((d) => d.name === dep.name)) {
|
|
1547
|
+
depsFromScripts.push(dep);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
const allDeps = [...depsFromSkillMd, ...depsFromScripts];
|
|
1553
|
+
const brokenRefs = checkFileReferences(skillMdContent, payloadFiles);
|
|
1554
|
+
const structureIssues = checkStructure(skillMdContent, allFiles.size);
|
|
1555
|
+
const issues = [...structureIssues];
|
|
1556
|
+
const unavailableDeps = allDeps.filter((d) => !d.available);
|
|
1557
|
+
if (unavailableDeps.length > 0) {
|
|
1558
|
+
issues.push({
|
|
1559
|
+
severity: "warn",
|
|
1560
|
+
message: `${unavailableDeps.length} detected tool(s) not found on your system: ${unavailableDeps.map((d) => d.name).join(", ")}`
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
if (brokenRefs.length > 0) {
|
|
1564
|
+
issues.push({
|
|
1565
|
+
severity: "error",
|
|
1566
|
+
message: `${brokenRefs.length} file reference(s) in SKILL.md point to missing files`
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
let score = 100;
|
|
1570
|
+
for (const issue of issues) {
|
|
1571
|
+
if (issue.severity === "error") score -= 20;
|
|
1572
|
+
else if (issue.severity === "warn") score -= 10;
|
|
1573
|
+
else score -= 2;
|
|
1574
|
+
}
|
|
1575
|
+
const lines = skillMdContent.split("\n");
|
|
1576
|
+
const sectionCount = lines.filter((l) => /^## /.test(l)).length;
|
|
1577
|
+
if (sectionCount >= 2) score = Math.min(100, score + 5);
|
|
1578
|
+
if (skillMdContent.startsWith("---")) score = Math.min(100, score + 5);
|
|
1579
|
+
score = Math.max(0, score);
|
|
1580
|
+
return {
|
|
1581
|
+
detectedDeps: allDeps,
|
|
1582
|
+
brokenRefs,
|
|
1583
|
+
issues,
|
|
1584
|
+
score,
|
|
1585
|
+
passed: !issues.some((i) => i.severity === "error")
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// src/commands/export.ts
|
|
1590
|
+
function collectAllFiles(dir, basePath = dir) {
|
|
1591
|
+
const files = /* @__PURE__ */ new Map();
|
|
1592
|
+
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
1593
|
+
for (const entry of entries) {
|
|
1594
|
+
const fullPath = join4(dir, entry.name);
|
|
1595
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1596
|
+
if (entry.isDirectory()) {
|
|
1597
|
+
const sub = collectAllFiles(fullPath, basePath);
|
|
1598
|
+
for (const [k, v] of sub) files.set(k, v);
|
|
1599
|
+
} else if (entry.isFile()) {
|
|
1600
|
+
const relPath = relative2(basePath, fullPath);
|
|
1601
|
+
const stat = statSync2(fullPath);
|
|
1602
|
+
if (stat.size > MAX_FILE_SIZE) continue;
|
|
1603
|
+
files.set(relPath, readFileSync3(fullPath));
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
return files;
|
|
1607
|
+
}
|
|
1608
|
+
async function selectContent(allFiles) {
|
|
1609
|
+
const skillMdContent = allFiles.get("SKILL.md").toString("utf-8");
|
|
1610
|
+
const parsed = parseSkillMd(skillMdContent);
|
|
1611
|
+
if (parsed.sections.length === 0) {
|
|
1612
|
+
return allFiles;
|
|
1613
|
+
}
|
|
1614
|
+
const payloadFiles = [...allFiles.keys()].filter((f) => f !== "SKILL.md");
|
|
1615
|
+
console.log("");
|
|
1616
|
+
console.log(chalk3.bold("Skill overview:"));
|
|
1617
|
+
console.log(chalk3.dim(` SKILL.md sections: ${parsed.sections.length}`));
|
|
1618
|
+
console.log(chalk3.dim(` Payload files: ${payloadFiles.length}`));
|
|
1619
|
+
console.log("");
|
|
1620
|
+
const { customize } = await inquirer.prompt([
|
|
1621
|
+
{
|
|
1622
|
+
type: "confirm",
|
|
1623
|
+
name: "customize",
|
|
1624
|
+
message: "Select which sections and files to include?",
|
|
1625
|
+
default: false
|
|
1626
|
+
}
|
|
1627
|
+
]);
|
|
1628
|
+
if (!customize) {
|
|
1629
|
+
return allFiles;
|
|
1630
|
+
}
|
|
1631
|
+
console.log("");
|
|
1632
|
+
console.log(chalk3.bold("SKILL.md sections:"));
|
|
1633
|
+
const sectionChoices = parsed.sections.map((section, idx) => {
|
|
1634
|
+
const summary = sectionSummary(section);
|
|
1635
|
+
const label = summary ? `${section.heading} ${chalk3.dim(`\u2014 ${summary}`)}` : section.heading;
|
|
1636
|
+
return { name: label, value: idx, checked: true };
|
|
1637
|
+
});
|
|
1638
|
+
const { selectedSections } = await inquirer.prompt([
|
|
1639
|
+
{
|
|
1640
|
+
type: "checkbox",
|
|
1641
|
+
name: "selectedSections",
|
|
1642
|
+
message: "Include these sections:",
|
|
1643
|
+
choices: sectionChoices,
|
|
1644
|
+
validate: (v) => v.length > 0 || "At least one section must be selected"
|
|
1645
|
+
}
|
|
1646
|
+
]);
|
|
1647
|
+
let selectedFiles = payloadFiles;
|
|
1648
|
+
if (payloadFiles.length > 0) {
|
|
1649
|
+
console.log("");
|
|
1650
|
+
const excludedIndices = new Set(
|
|
1651
|
+
parsed.sections.map((_, i) => i).filter((i) => !selectedSections.includes(i))
|
|
1652
|
+
);
|
|
1653
|
+
const excludedRefs = /* @__PURE__ */ new Set();
|
|
1654
|
+
for (const idx of excludedIndices) {
|
|
1655
|
+
for (const ref of parsed.sections[idx].referencedFiles) {
|
|
1656
|
+
excludedRefs.add(ref);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
const fileChoices = payloadFiles.map((filePath) => {
|
|
1660
|
+
const size = allFiles.get(filePath).length;
|
|
1661
|
+
const sizeStr = size < 1024 ? `${size} B` : `${(size / 1024).toFixed(1)} KB`;
|
|
1662
|
+
const hint = excludedRefs.has(filePath) ? chalk3.yellow(" (referenced by excluded section)") : "";
|
|
1663
|
+
return {
|
|
1664
|
+
name: `${filePath} ${chalk3.dim(`(${sizeStr})`)}${hint}`,
|
|
1665
|
+
value: filePath,
|
|
1666
|
+
checked: !excludedRefs.has(filePath)
|
|
1667
|
+
};
|
|
1668
|
+
});
|
|
1669
|
+
const { files } = await inquirer.prompt([
|
|
1670
|
+
{
|
|
1671
|
+
type: "checkbox",
|
|
1672
|
+
name: "files",
|
|
1673
|
+
message: "Include these files:",
|
|
1674
|
+
choices: fileChoices
|
|
1675
|
+
}
|
|
1676
|
+
]);
|
|
1677
|
+
selectedFiles = files;
|
|
1678
|
+
}
|
|
1679
|
+
const newSkillMd = reconstructSkillMd(parsed, selectedSections);
|
|
1680
|
+
const filtered = /* @__PURE__ */ new Map();
|
|
1681
|
+
filtered.set("SKILL.md", Buffer.from(newSkillMd, "utf-8"));
|
|
1682
|
+
for (const filePath of selectedFiles) {
|
|
1683
|
+
filtered.set(filePath, allFiles.get(filePath));
|
|
1684
|
+
}
|
|
1685
|
+
const removedSections = parsed.sections.length - selectedSections.length;
|
|
1686
|
+
const removedFiles = payloadFiles.length - selectedFiles.length;
|
|
1687
|
+
if (removedSections > 0 || removedFiles > 0) {
|
|
1688
|
+
console.log("");
|
|
1689
|
+
console.log(
|
|
1690
|
+
chalk3.cyan(
|
|
1691
|
+
`Customized: ${removedSections} section(s) and ${removedFiles} file(s) excluded`
|
|
1692
|
+
)
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
return filtered;
|
|
1696
|
+
}
|
|
1697
|
+
function displayQualityReport(report) {
|
|
1698
|
+
console.log("");
|
|
1699
|
+
console.log(chalk3.bold("Quality Report:"));
|
|
1700
|
+
console.log(chalk3.dim("\u2500".repeat(50)));
|
|
1701
|
+
if (report.detectedDeps.length > 0) {
|
|
1702
|
+
console.log(chalk3.bold(" Detected dependencies:"));
|
|
1703
|
+
for (const dep of report.detectedDeps) {
|
|
1704
|
+
const icon = dep.available ? chalk3.green("\u2713") : chalk3.yellow("!");
|
|
1705
|
+
const status = dep.available ? "installed" : "not found";
|
|
1706
|
+
console.log(` ${icon} ${dep.name} ${chalk3.dim(`(${status} \u2014 from ${dep.source})`)}`);
|
|
1707
|
+
}
|
|
1708
|
+
} else {
|
|
1709
|
+
console.log(chalk3.dim(" No CLI dependencies detected."));
|
|
1710
|
+
}
|
|
1711
|
+
if (report.brokenRefs.length > 0) {
|
|
1712
|
+
console.log("");
|
|
1713
|
+
console.log(chalk3.bold(" Broken file references:"));
|
|
1714
|
+
for (const ref of report.brokenRefs) {
|
|
1715
|
+
console.log(` ${chalk3.red("\u2717")} ${ref.ref} ${chalk3.dim(`(in ${ref.source})`)}`);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
if (report.issues.length > 0) {
|
|
1719
|
+
console.log("");
|
|
1720
|
+
for (const issue of report.issues) {
|
|
1721
|
+
const icon = issue.severity === "error" ? chalk3.red("\u2717") : issue.severity === "warn" ? chalk3.yellow("!") : chalk3.blue("i");
|
|
1722
|
+
console.log(` ${icon} ${issue.message}`);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
console.log(chalk3.dim("\u2500".repeat(50)));
|
|
1726
|
+
const scoreColor = report.score >= 80 ? chalk3.green : report.score >= 50 ? chalk3.yellow : chalk3.red;
|
|
1727
|
+
console.log(` Quality score: ${scoreColor(`${report.score}/100`)}${report.passed ? "" : chalk3.red(" \u2014 FAILED")}`);
|
|
1728
|
+
console.log("");
|
|
1729
|
+
}
|
|
1730
|
+
async function exportCommand(path, options) {
|
|
1731
|
+
if (!hasKeys()) {
|
|
1732
|
+
console.log(
|
|
1733
|
+
chalk3.red("No keys found. Run 'skillport init' first to generate keys.")
|
|
1734
|
+
);
|
|
1735
|
+
process.exitCode = 1;
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
console.log(`Reading skill from: ${path}`);
|
|
1739
|
+
const allFiles = collectAllFiles(path);
|
|
1740
|
+
if (!allFiles.has("SKILL.md")) {
|
|
1741
|
+
console.log(chalk3.red("SKILL.md not found in the skill directory."));
|
|
1742
|
+
process.exitCode = 1;
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
const selectedFiles = options.yes ? allFiles : await selectContent(allFiles);
|
|
1746
|
+
console.log("\nRunning quality check...");
|
|
1747
|
+
const skillMdForCheck = selectedFiles.get("SKILL.md").toString("utf-8");
|
|
1748
|
+
const qualityReport = runQualityCheck(skillMdForCheck, selectedFiles);
|
|
1749
|
+
displayQualityReport(qualityReport);
|
|
1750
|
+
if (!qualityReport.passed) {
|
|
1751
|
+
if (options.yes) {
|
|
1752
|
+
console.log(chalk3.yellow("Quality issues found. Continuing (--yes)."));
|
|
1753
|
+
} else {
|
|
1754
|
+
const { continueExport } = await inquirer.prompt([
|
|
1755
|
+
{
|
|
1756
|
+
type: "confirm",
|
|
1757
|
+
name: "continueExport",
|
|
1758
|
+
message: chalk3.yellow("Quality issues found. Continue with export?"),
|
|
1759
|
+
default: false
|
|
1760
|
+
}
|
|
1761
|
+
]);
|
|
1762
|
+
if (!continueExport) {
|
|
1763
|
+
console.log("Export cancelled. Fix the issues above and try again.");
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
console.log("\nRunning security scan...");
|
|
1769
|
+
const textFiles = /* @__PURE__ */ new Map();
|
|
1770
|
+
for (const [p, content] of selectedFiles) {
|
|
1771
|
+
if (isScannable(p)) {
|
|
1772
|
+
textFiles.set(p, content.toString("utf-8"));
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
const scanResult = scanFiles(textFiles);
|
|
1776
|
+
const report = generateReport(
|
|
1777
|
+
scanResult.issues,
|
|
1778
|
+
scanResult.scannedFiles,
|
|
1779
|
+
scanResult.skippedFiles
|
|
1780
|
+
);
|
|
1781
|
+
displayScanReport(report);
|
|
1782
|
+
if (!report.passed) {
|
|
1783
|
+
console.log(
|
|
1784
|
+
chalk3.red(
|
|
1785
|
+
"Export blocked: critical/high severity issues found. Fix them before exporting."
|
|
1786
|
+
)
|
|
1787
|
+
);
|
|
1788
|
+
process.exitCode = 1;
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
const config = loadConfig();
|
|
1792
|
+
const publicKey = loadPublicKey2();
|
|
1793
|
+
const keyId = config.default_key_id || computeKeyId(publicKey);
|
|
1794
|
+
let answers;
|
|
1795
|
+
if (options.yes) {
|
|
1796
|
+
const missing = [];
|
|
1797
|
+
if (!options.id) missing.push("--id");
|
|
1798
|
+
if (!options.name) missing.push("--name");
|
|
1799
|
+
if (!options.description) missing.push("--description");
|
|
1800
|
+
if (!options.author) missing.push("--author");
|
|
1801
|
+
if (missing.length > 0) {
|
|
1802
|
+
console.log(chalk3.red(`Missing required flags for --yes mode: ${missing.join(", ")}`));
|
|
1803
|
+
process.exitCode = 1;
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
answers = {
|
|
1807
|
+
id: options.id,
|
|
1808
|
+
name: options.name,
|
|
1809
|
+
description: options.description,
|
|
1810
|
+
version: options.skillVersion || "1.0.0",
|
|
1811
|
+
authorName: options.author,
|
|
1812
|
+
openclawCompat: options.openclawCompat || ">=1.0.0",
|
|
1813
|
+
osCompat: options.os || ["macos", "linux"]
|
|
1814
|
+
};
|
|
1815
|
+
} else {
|
|
1816
|
+
answers = await inquirer.prompt([
|
|
1817
|
+
{
|
|
1818
|
+
type: "input",
|
|
1819
|
+
name: "id",
|
|
1820
|
+
message: "Skill ID (author-slug/skill-slug):",
|
|
1821
|
+
validate: (v) => /^[a-z0-9_-]+\/[a-z0-9_-]+$/.test(v) || "Format: author-slug/skill-slug"
|
|
1822
|
+
},
|
|
1823
|
+
{ type: "input", name: "name", message: "Skill name:" },
|
|
1824
|
+
{ type: "input", name: "description", message: "Description:" },
|
|
1825
|
+
{
|
|
1826
|
+
type: "input",
|
|
1827
|
+
name: "version",
|
|
1828
|
+
message: "Version:",
|
|
1829
|
+
default: "1.0.0",
|
|
1830
|
+
validate: (v) => /^\d+\.\d+\.\d+$/.test(v) || "Must be semver (x.y.z)"
|
|
1831
|
+
},
|
|
1832
|
+
{ type: "input", name: "authorName", message: "Author name:" },
|
|
1833
|
+
{
|
|
1834
|
+
type: "input",
|
|
1835
|
+
name: "openclawCompat",
|
|
1836
|
+
message: "OpenClaw compatibility range:",
|
|
1837
|
+
default: ">=1.0.0"
|
|
1838
|
+
},
|
|
1839
|
+
{
|
|
1840
|
+
type: "checkbox",
|
|
1841
|
+
name: "osCompat",
|
|
1842
|
+
message: "Compatible OS:",
|
|
1843
|
+
choices: ["macos", "linux", "windows"],
|
|
1844
|
+
default: ["macos", "linux"]
|
|
1845
|
+
}
|
|
1846
|
+
]);
|
|
1847
|
+
}
|
|
1848
|
+
const entrypoints = [{ name: "main", file: "SKILL.md" }];
|
|
1849
|
+
const dangerFlags = report.issues.map((issue) => ({
|
|
1850
|
+
code: issue.id,
|
|
1851
|
+
severity: issue.severity,
|
|
1852
|
+
message: issue.message,
|
|
1853
|
+
file: issue.file,
|
|
1854
|
+
line: issue.line
|
|
1855
|
+
}));
|
|
1856
|
+
const detectedDeps = depsToManifest(qualityReport.detectedDeps);
|
|
1857
|
+
const manifest = {
|
|
1858
|
+
ssp_version: SP_VERSION,
|
|
1859
|
+
id: answers.id,
|
|
1860
|
+
name: answers.name,
|
|
1861
|
+
description: answers.description,
|
|
1862
|
+
version: answers.version,
|
|
1863
|
+
author: {
|
|
1864
|
+
name: answers.authorName,
|
|
1865
|
+
signing_key_id: keyId
|
|
1866
|
+
},
|
|
1867
|
+
openclaw_compat: answers.openclawCompat,
|
|
1868
|
+
os_compat: answers.osCompat,
|
|
1869
|
+
entrypoints,
|
|
1870
|
+
permissions: {
|
|
1871
|
+
network: { mode: "none" },
|
|
1872
|
+
filesystem: { read_paths: [], write_paths: [] },
|
|
1873
|
+
exec: { allowed_commands: [], shell: false }
|
|
1874
|
+
},
|
|
1875
|
+
dependencies: detectedDeps,
|
|
1876
|
+
danger_flags: dangerFlags,
|
|
1877
|
+
install: { steps: [], required_inputs: [] },
|
|
1878
|
+
hashes: {},
|
|
1879
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1880
|
+
};
|
|
1881
|
+
console.log("Creating SkillPort package...");
|
|
1882
|
+
const privateKey = loadPrivateKey2();
|
|
1883
|
+
const sspBuffer = await createSSP({
|
|
1884
|
+
manifest,
|
|
1885
|
+
files: selectedFiles,
|
|
1886
|
+
privateKeyPem: privateKey
|
|
1887
|
+
});
|
|
1888
|
+
const outputPath = options.output || `${basename(path)}.ssp`;
|
|
1889
|
+
writeFileSync3(outputPath, sspBuffer);
|
|
1890
|
+
console.log(chalk3.green(`
|
|
1891
|
+
SkillPort package created: ${outputPath}`));
|
|
1892
|
+
console.log(
|
|
1893
|
+
chalk3.dim(` Size: ${(sspBuffer.length / 1024).toFixed(1)} KB`)
|
|
1894
|
+
);
|
|
1895
|
+
console.log(
|
|
1896
|
+
chalk3.dim(` Files: ${selectedFiles.size} (including SKILL.md)`)
|
|
1897
|
+
);
|
|
1898
|
+
if (detectedDeps.length > 0) {
|
|
1899
|
+
console.log(
|
|
1900
|
+
chalk3.dim(` Dependencies: ${detectedDeps.map((d) => d.name).join(", ")}`)
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
console.log(
|
|
1904
|
+
chalk3.dim(` Quality: ${qualityReport.score}/100`)
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
// src/commands/sign.ts
|
|
1909
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1910
|
+
import chalk4 from "chalk";
|
|
1911
|
+
init_config();
|
|
1912
|
+
async function signCommand(sspPath) {
|
|
1913
|
+
if (!hasKeys()) {
|
|
1914
|
+
console.log(chalk4.red("No keys found. Run 'skillport init' first."));
|
|
1915
|
+
process.exitCode = 1;
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
console.log(`Signing: ${sspPath}`);
|
|
1919
|
+
const data = readFileSync4(sspPath);
|
|
1920
|
+
const extracted = await extractSSP(data);
|
|
1921
|
+
const privateKey = loadPrivateKey2();
|
|
1922
|
+
const files = /* @__PURE__ */ new Map();
|
|
1923
|
+
for (const [path, content] of extracted.files) {
|
|
1924
|
+
const cleanPath = path.startsWith("payload/") ? path.substring(8) : path;
|
|
1925
|
+
files.set(cleanPath, content);
|
|
1926
|
+
}
|
|
1927
|
+
if (extracted.skillMd) {
|
|
1928
|
+
files.set("SKILL.md", Buffer.from(extracted.skillMd));
|
|
1929
|
+
}
|
|
1930
|
+
const sspBuffer = await createSSP({
|
|
1931
|
+
manifest: extracted.manifest,
|
|
1932
|
+
files,
|
|
1933
|
+
privateKeyPem: privateKey
|
|
1934
|
+
});
|
|
1935
|
+
writeFileSync4(sspPath, sspBuffer);
|
|
1936
|
+
console.log(chalk4.green(`Package re-signed successfully: ${sspPath}`));
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
// src/commands/verify.ts
|
|
1940
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
1941
|
+
import chalk5 from "chalk";
|
|
1942
|
+
async function verifyCommand(sspPath, options) {
|
|
1943
|
+
console.log(`Verifying: ${sspPath}`);
|
|
1944
|
+
const data = readFileSync5(sspPath);
|
|
1945
|
+
const extracted = await extractSSP(data);
|
|
1946
|
+
let allPassed = true;
|
|
1947
|
+
if (extracted.authorSignature) {
|
|
1948
|
+
let publicKeyPem = null;
|
|
1949
|
+
if (options.publicKey) {
|
|
1950
|
+
publicKeyPem = readFileSync5(options.publicKey, "utf-8");
|
|
1951
|
+
}
|
|
1952
|
+
if (publicKeyPem) {
|
|
1953
|
+
const manifestJson = JSON.stringify(extracted.manifest, null, 2);
|
|
1954
|
+
const sigValid = verifySignature(
|
|
1955
|
+
manifestJson,
|
|
1956
|
+
extracted.authorSignature,
|
|
1957
|
+
publicKeyPem
|
|
1958
|
+
);
|
|
1959
|
+
if (sigValid) {
|
|
1960
|
+
console.log(chalk5.green(" Author signature: VALID"));
|
|
1961
|
+
} else {
|
|
1962
|
+
console.log(chalk5.red(" Author signature: INVALID"));
|
|
1963
|
+
allPassed = false;
|
|
1964
|
+
}
|
|
1965
|
+
} else {
|
|
1966
|
+
console.log(chalk5.yellow(" Author signature: PRESENT (no public key to verify)"));
|
|
1967
|
+
}
|
|
1968
|
+
} else {
|
|
1969
|
+
console.log(chalk5.red(" Author signature: MISSING"));
|
|
1970
|
+
allPassed = false;
|
|
1971
|
+
}
|
|
1972
|
+
if (extracted.platformSignature) {
|
|
1973
|
+
console.log(chalk5.green(" Platform signature: PRESENT"));
|
|
1974
|
+
} else {
|
|
1975
|
+
console.log(chalk5.dim(" Platform signature: ABSENT"));
|
|
1976
|
+
}
|
|
1977
|
+
const { valid, mismatches } = verifyChecksums(
|
|
1978
|
+
extracted.files,
|
|
1979
|
+
extracted.checksums
|
|
1980
|
+
);
|
|
1981
|
+
if (valid) {
|
|
1982
|
+
console.log(chalk5.green(" Checksums: ALL VALID"));
|
|
1983
|
+
} else {
|
|
1984
|
+
console.log(chalk5.red(` Checksums: ${mismatches.length} MISMATCHES`));
|
|
1985
|
+
for (const path of mismatches) {
|
|
1986
|
+
console.log(chalk5.red(` - ${path}`));
|
|
1987
|
+
}
|
|
1988
|
+
allPassed = false;
|
|
1989
|
+
}
|
|
1990
|
+
console.log();
|
|
1991
|
+
if (allPassed) {
|
|
1992
|
+
console.log(chalk5.green.bold("Verification PASSED"));
|
|
1993
|
+
} else {
|
|
1994
|
+
console.log(chalk5.red.bold("Verification FAILED"));
|
|
1995
|
+
process.exitCode = 1;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
// src/commands/install.ts
|
|
2000
|
+
import { readFileSync as readFileSync6, mkdirSync as mkdirSync2, writeFileSync as writeFileSync5, existsSync as existsSync2 } from "node:fs";
|
|
2001
|
+
import { join as join5 } from "node:path";
|
|
2002
|
+
import { homedir as homedir2 } from "node:os";
|
|
2003
|
+
import chalk6 from "chalk";
|
|
2004
|
+
import inquirer2 from "inquirer";
|
|
2005
|
+
init_dist();
|
|
2006
|
+
init_config();
|
|
2007
|
+
function getSkillsBaseDir() {
|
|
2008
|
+
return process.env.OPENCLAW_SKILLS_DIR || join5(homedir2(), OPENCLAW_SKILLS_DIR);
|
|
2009
|
+
}
|
|
2010
|
+
async function installCommand(target, options) {
|
|
2011
|
+
let data;
|
|
2012
|
+
if (existsSync2(target)) {
|
|
2013
|
+
data = readFileSync6(target);
|
|
2014
|
+
} else {
|
|
2015
|
+
console.log(chalk6.dim(`Resolving from marketplace: ${target}`));
|
|
2016
|
+
const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
2017
|
+
const config = loadConfig2();
|
|
2018
|
+
if (!config.auth_token) {
|
|
2019
|
+
console.log(chalk6.red("Not logged in. Run 'skillport login' first."));
|
|
2020
|
+
process.exitCode = 1;
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
let skillId = target;
|
|
2024
|
+
let version;
|
|
2025
|
+
const atIdx = target.lastIndexOf("@");
|
|
2026
|
+
if (atIdx > 0) {
|
|
2027
|
+
skillId = target.substring(0, atIdx);
|
|
2028
|
+
version = target.substring(atIdx + 1);
|
|
2029
|
+
}
|
|
2030
|
+
try {
|
|
2031
|
+
let resolvedId = skillId;
|
|
2032
|
+
if (skillId.includes("/")) {
|
|
2033
|
+
const searchRes = await fetch(
|
|
2034
|
+
`${config.marketplace_url}/v1/skills?q=${encodeURIComponent(skillId)}&per_page=1`,
|
|
2035
|
+
{ headers: { Authorization: `Bearer ${config.auth_token}` } }
|
|
2036
|
+
);
|
|
2037
|
+
if (!searchRes.ok) {
|
|
2038
|
+
console.log(chalk6.red(`Marketplace search failed: ${searchRes.statusText}`));
|
|
2039
|
+
process.exitCode = 1;
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
const searchData = await searchRes.json();
|
|
2043
|
+
const match = searchData.data.find((s) => s.ssp_id === skillId);
|
|
2044
|
+
if (!match) {
|
|
2045
|
+
console.log(chalk6.red(`Skill not found: ${skillId}`));
|
|
2046
|
+
process.exitCode = 1;
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
resolvedId = match.id;
|
|
2050
|
+
}
|
|
2051
|
+
const dlUrl = version ? `${config.marketplace_url}/v1/skills/${resolvedId}/download?version=${version}` : `${config.marketplace_url}/v1/skills/${resolvedId}/download`;
|
|
2052
|
+
const dlRes = await fetch(dlUrl, {
|
|
2053
|
+
headers: { Authorization: `Bearer ${config.auth_token}` }
|
|
2054
|
+
});
|
|
2055
|
+
if (!dlRes.ok) {
|
|
2056
|
+
const err = await dlRes.json();
|
|
2057
|
+
console.log(chalk6.red(`Download failed: ${err.error || dlRes.statusText}`));
|
|
2058
|
+
process.exitCode = 1;
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2061
|
+
const { url } = await dlRes.json();
|
|
2062
|
+
console.log(chalk6.dim("Downloading package..."));
|
|
2063
|
+
const fileRes = await fetch(url);
|
|
2064
|
+
if (!fileRes.ok) {
|
|
2065
|
+
console.log(chalk6.red("Failed to download package file."));
|
|
2066
|
+
process.exitCode = 1;
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
data = Buffer.from(await fileRes.arrayBuffer());
|
|
2070
|
+
console.log(chalk6.green(` Downloaded ${(data.length / 1024).toFixed(1)} KB`));
|
|
2071
|
+
} catch (err) {
|
|
2072
|
+
console.log(chalk6.red(`Marketplace error: ${err.message}`));
|
|
2073
|
+
process.exitCode = 1;
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
console.log("Extracting SkillPort package...");
|
|
2078
|
+
const extracted = await extractSSP(data);
|
|
2079
|
+
const { manifest } = extracted;
|
|
2080
|
+
console.log("Verifying checksums...");
|
|
2081
|
+
const { valid, mismatches } = verifyChecksums(
|
|
2082
|
+
extracted.files,
|
|
2083
|
+
extracted.checksums
|
|
2084
|
+
);
|
|
2085
|
+
if (!valid) {
|
|
2086
|
+
console.log(chalk6.red("Checksum verification FAILED. Aborting install."));
|
|
2087
|
+
for (const path of mismatches) {
|
|
2088
|
+
console.log(chalk6.red(` Mismatch: ${path}`));
|
|
2089
|
+
}
|
|
2090
|
+
process.exitCode = 1;
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
console.log(chalk6.green(" Checksums verified."));
|
|
2094
|
+
if (extracted.authorSignature) {
|
|
2095
|
+
console.log(chalk6.green(" Author signature present."));
|
|
2096
|
+
} else {
|
|
2097
|
+
console.log(chalk6.red(" No author signature. Aborting install."));
|
|
2098
|
+
process.exitCode = 1;
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
if (extracted.platformSignature) {
|
|
2102
|
+
console.log(chalk6.green(" Platform signature present."));
|
|
2103
|
+
}
|
|
2104
|
+
console.log("");
|
|
2105
|
+
console.log(chalk6.bold("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
2106
|
+
console.log(chalk6.bold(`\u2551 ${manifest.name}`));
|
|
2107
|
+
console.log(chalk6.bold(`\u2551 v${manifest.version} by ${manifest.author.name}`));
|
|
2108
|
+
console.log(chalk6.bold("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
|
|
2109
|
+
console.log(chalk6.dim(` ${manifest.description}`));
|
|
2110
|
+
console.log(chalk6.dim(` OS: ${manifest.os_compat.join(", ")} | ID: ${manifest.id}`));
|
|
2111
|
+
console.log("");
|
|
2112
|
+
console.log(chalk6.bold("Environment Check:"));
|
|
2113
|
+
console.log(chalk6.dim("\u2500".repeat(50)));
|
|
2114
|
+
const envReport = checkEnvironment(manifest);
|
|
2115
|
+
const osIcon = envReport.os.compatible ? chalk6.green("\u2713") : chalk6.red("\u2717");
|
|
2116
|
+
console.log(` ${osIcon} OS: ${envReport.os.name} ${envReport.os.compatible ? "" : chalk6.red("(not compatible)")}`);
|
|
2117
|
+
for (const bin of envReport.binaries) {
|
|
2118
|
+
const icon = bin.status === "ok" ? chalk6.green("\u2713") : bin.status === "warn" ? chalk6.yellow("!") : chalk6.red("\u2717");
|
|
2119
|
+
console.log(` ${icon} ${bin.check}: ${chalk6.dim(bin.detail)}`);
|
|
2120
|
+
}
|
|
2121
|
+
for (const env of envReport.envVars) {
|
|
2122
|
+
const icon = env.status === "ok" ? chalk6.green("\u2713") : env.status === "warn" ? chalk6.yellow("!") : chalk6.red("\u2717");
|
|
2123
|
+
console.log(` ${icon} ${env.check}: ${chalk6.dim(env.detail)}`);
|
|
2124
|
+
}
|
|
2125
|
+
if (envReport.binaries.length === 0 && envReport.envVars.length === 0) {
|
|
2126
|
+
console.log(chalk6.dim(" No specific dependencies required."));
|
|
2127
|
+
}
|
|
2128
|
+
console.log(chalk6.dim("\u2500".repeat(50)));
|
|
2129
|
+
if (!envReport.os.compatible) {
|
|
2130
|
+
console.log(chalk6.red(`
|
|
2131
|
+
This skill is not compatible with your OS (${envReport.os.name}).`));
|
|
2132
|
+
console.log(chalk6.red(`Supported: ${manifest.os_compat.join(", ")}`));
|
|
2133
|
+
process.exitCode = 1;
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
console.log("");
|
|
2137
|
+
console.log("Running local security scan...");
|
|
2138
|
+
const textFiles = /* @__PURE__ */ new Map();
|
|
2139
|
+
for (const [path, content] of extracted.files) {
|
|
2140
|
+
if (isScannable(path)) {
|
|
2141
|
+
textFiles.set(path, content.toString("utf-8"));
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
const scanResult = scanFiles(textFiles);
|
|
2145
|
+
const report = generateReport(
|
|
2146
|
+
scanResult.issues,
|
|
2147
|
+
scanResult.scannedFiles,
|
|
2148
|
+
scanResult.skippedFiles
|
|
2149
|
+
);
|
|
2150
|
+
displayScanReport(report);
|
|
2151
|
+
displayPermissions(manifest.permissions);
|
|
2152
|
+
displayDangerFlags(manifest.danger_flags);
|
|
2153
|
+
const permSummary = assessPermissions(manifest.permissions);
|
|
2154
|
+
const requiresAcceptRisk = manifest.permissions.exec.shell || manifest.danger_flags.some((f) => f.severity === "critical");
|
|
2155
|
+
if (requiresAcceptRisk && !options.acceptRisk) {
|
|
2156
|
+
console.log(
|
|
2157
|
+
chalk6.red(
|
|
2158
|
+
"This skill requires shell access or has critical danger flags."
|
|
2159
|
+
)
|
|
2160
|
+
);
|
|
2161
|
+
console.log(
|
|
2162
|
+
chalk6.red("Use --accept-risk to acknowledge and proceed.")
|
|
2163
|
+
);
|
|
2164
|
+
process.exitCode = 1;
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
let finalSkillMd;
|
|
2168
|
+
let finalFiles = extracted.files;
|
|
2169
|
+
if (extracted.skillMd && !options.yes) {
|
|
2170
|
+
const parsed = parseSkillMd(extracted.skillMd);
|
|
2171
|
+
if (parsed.sections.length > 1) {
|
|
2172
|
+
const missingBins = envReport.binaries.filter((b) => b.status === "missing").map((b) => b.check);
|
|
2173
|
+
const incompatibleIndices = findIncompatibleSections(
|
|
2174
|
+
parsed.sections.map((s) => s.raw),
|
|
2175
|
+
missingBins
|
|
2176
|
+
);
|
|
2177
|
+
console.log(chalk6.bold("Skill Sections:"));
|
|
2178
|
+
for (let i = 0; i < parsed.sections.length; i++) {
|
|
2179
|
+
const section = parsed.sections[i];
|
|
2180
|
+
const summary = sectionSummary(section);
|
|
2181
|
+
const isIncompat = incompatibleIndices.includes(i);
|
|
2182
|
+
const icon = isIncompat ? chalk6.yellow("!") : chalk6.green("\u2713");
|
|
2183
|
+
const hint = isIncompat ? chalk6.yellow(" (requires missing dependency)") : "";
|
|
2184
|
+
console.log(` ${icon} ${section.heading}${hint}`);
|
|
2185
|
+
if (summary) {
|
|
2186
|
+
console.log(chalk6.dim(` ${summary}`));
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
console.log("");
|
|
2190
|
+
const hasIncompat = incompatibleIndices.length > 0;
|
|
2191
|
+
const customizeMessage = hasIncompat ? "Some sections require missing dependencies. Customize installation?" : "Customize which sections to install?";
|
|
2192
|
+
const { customize } = await inquirer2.prompt([
|
|
2193
|
+
{
|
|
2194
|
+
type: "confirm",
|
|
2195
|
+
name: "customize",
|
|
2196
|
+
message: customizeMessage,
|
|
2197
|
+
default: hasIncompat
|
|
2198
|
+
}
|
|
2199
|
+
]);
|
|
2200
|
+
if (customize) {
|
|
2201
|
+
const sectionChoices = parsed.sections.map((section, idx) => {
|
|
2202
|
+
const isIncompat = incompatibleIndices.includes(idx);
|
|
2203
|
+
const summary = sectionSummary(section);
|
|
2204
|
+
const label = isIncompat ? `${section.heading} ${chalk6.yellow("(missing deps)")}` : summary ? `${section.heading} ${chalk6.dim(`\u2014 ${summary}`)}` : section.heading;
|
|
2205
|
+
return { name: label, value: idx, checked: !isIncompat };
|
|
2206
|
+
});
|
|
2207
|
+
const { selectedSections } = await inquirer2.prompt([
|
|
2208
|
+
{
|
|
2209
|
+
type: "checkbox",
|
|
2210
|
+
name: "selectedSections",
|
|
2211
|
+
message: "Select sections to install:",
|
|
2212
|
+
choices: sectionChoices,
|
|
2213
|
+
validate: (v) => v.length > 0 || "At least one section must be selected"
|
|
2214
|
+
}
|
|
2215
|
+
]);
|
|
2216
|
+
finalSkillMd = reconstructSkillMd(parsed, selectedSections);
|
|
2217
|
+
const excludedIndices = new Set(
|
|
2218
|
+
parsed.sections.map((_, i) => i).filter((i) => !selectedSections.includes(i))
|
|
2219
|
+
);
|
|
2220
|
+
if (excludedIndices.size > 0) {
|
|
2221
|
+
const excludedRefs = /* @__PURE__ */ new Set();
|
|
2222
|
+
for (const idx of excludedIndices) {
|
|
2223
|
+
for (const ref of parsed.sections[idx].referencedFiles) {
|
|
2224
|
+
excludedRefs.add(ref);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
if (excludedRefs.size > 0) {
|
|
2228
|
+
const includedRefs = /* @__PURE__ */ new Set();
|
|
2229
|
+
for (const idx of selectedSections) {
|
|
2230
|
+
for (const ref of parsed.sections[idx].referencedFiles) {
|
|
2231
|
+
includedRefs.add(ref);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
const toRemove = /* @__PURE__ */ new Set();
|
|
2235
|
+
for (const ref of excludedRefs) {
|
|
2236
|
+
if (!includedRefs.has(ref)) {
|
|
2237
|
+
toRemove.add(ref);
|
|
2238
|
+
toRemove.add(`payload/${ref}`);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
if (toRemove.size > 0) {
|
|
2242
|
+
finalFiles = new Map(
|
|
2243
|
+
[...extracted.files].filter(([path]) => !toRemove.has(path))
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
const removedCount = parsed.sections.length - selectedSections.length;
|
|
2248
|
+
console.log(chalk6.cyan(`
|
|
2249
|
+
Optimized: ${removedCount} section(s) excluded for your environment.`));
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
if (!options.yes) {
|
|
2255
|
+
const { confirm } = await inquirer2.prompt([
|
|
2256
|
+
{
|
|
2257
|
+
type: "confirm",
|
|
2258
|
+
name: "confirm",
|
|
2259
|
+
message: `Install ${manifest.name} v${manifest.version}?`,
|
|
2260
|
+
default: true
|
|
2261
|
+
}
|
|
2262
|
+
]);
|
|
2263
|
+
if (!confirm) {
|
|
2264
|
+
console.log("Installation cancelled.");
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
const [authorSlug, skillSlug] = manifest.id.split("/");
|
|
2269
|
+
const installDir = join5(
|
|
2270
|
+
getSkillsBaseDir(),
|
|
2271
|
+
authorSlug,
|
|
2272
|
+
skillSlug
|
|
2273
|
+
);
|
|
2274
|
+
mkdirSync2(installDir, { recursive: true });
|
|
2275
|
+
writeFileSync5(
|
|
2276
|
+
join5(installDir, "manifest.json"),
|
|
2277
|
+
JSON.stringify(manifest, null, 2)
|
|
2278
|
+
);
|
|
2279
|
+
const skillMdToWrite = finalSkillMd ?? extracted.skillMd;
|
|
2280
|
+
if (skillMdToWrite) {
|
|
2281
|
+
writeFileSync5(join5(installDir, "SKILL.md"), skillMdToWrite);
|
|
2282
|
+
}
|
|
2283
|
+
for (const [path, content] of finalFiles) {
|
|
2284
|
+
if (path === "SKILL.md") continue;
|
|
2285
|
+
const cleanPath = path.startsWith("payload/") ? path.substring(8) : path;
|
|
2286
|
+
const filePath = join5(installDir, cleanPath);
|
|
2287
|
+
const dir = filePath.substring(0, filePath.lastIndexOf("/"));
|
|
2288
|
+
mkdirSync2(dir, { recursive: true });
|
|
2289
|
+
writeFileSync5(filePath, content);
|
|
2290
|
+
}
|
|
2291
|
+
if (manifest.install.required_inputs.length > 0) {
|
|
2292
|
+
const relevantInputs = manifest.install.required_inputs.filter((input) => {
|
|
2293
|
+
if (finalSkillMd) {
|
|
2294
|
+
return finalSkillMd.toLowerCase().includes(input.key.toLowerCase());
|
|
2295
|
+
}
|
|
2296
|
+
return true;
|
|
2297
|
+
});
|
|
2298
|
+
if (relevantInputs.length > 0) {
|
|
2299
|
+
let inputAnswers;
|
|
2300
|
+
if (options.yes) {
|
|
2301
|
+
inputAnswers = {};
|
|
2302
|
+
for (const input of relevantInputs) {
|
|
2303
|
+
inputAnswers[input.key] = input.default?.toString() || "";
|
|
2304
|
+
}
|
|
2305
|
+
} else {
|
|
2306
|
+
console.log(chalk6.bold("\nRequired configuration:"));
|
|
2307
|
+
inputAnswers = await inquirer2.prompt(
|
|
2308
|
+
relevantInputs.map((input) => ({
|
|
2309
|
+
type: input.type === "secret" ? "password" : "input",
|
|
2310
|
+
name: input.key,
|
|
2311
|
+
message: input.description,
|
|
2312
|
+
default: input.default?.toString()
|
|
2313
|
+
}))
|
|
2314
|
+
);
|
|
2315
|
+
}
|
|
2316
|
+
const envContent = Object.entries(inputAnswers).map(([k, v]) => `${k}=${v}`).join("\n");
|
|
2317
|
+
writeFileSync5(join5(installDir, ".env"), envContent, { mode: 384 });
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
const registry = loadRegistry();
|
|
2321
|
+
registry.skills = registry.skills.filter((s) => s.id !== manifest.id);
|
|
2322
|
+
registry.skills.push({
|
|
2323
|
+
id: manifest.id,
|
|
2324
|
+
version: manifest.version,
|
|
2325
|
+
installed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2326
|
+
install_path: installDir,
|
|
2327
|
+
author_key_id: manifest.author.signing_key_id
|
|
2328
|
+
});
|
|
2329
|
+
saveRegistry(registry);
|
|
2330
|
+
appendAuditLog({
|
|
2331
|
+
action: "install",
|
|
2332
|
+
skill_id: manifest.id,
|
|
2333
|
+
version: manifest.version,
|
|
2334
|
+
risk_score: report.risk_score,
|
|
2335
|
+
install_path: installDir,
|
|
2336
|
+
customized: finalSkillMd !== void 0
|
|
2337
|
+
});
|
|
2338
|
+
console.log(chalk6.green(`
|
|
2339
|
+
Installed: ${manifest.name} v${manifest.version}`));
|
|
2340
|
+
console.log(chalk6.dim(` Location: ${installDir}`));
|
|
2341
|
+
if (finalSkillMd) {
|
|
2342
|
+
console.log(chalk6.dim(" Optimized for your environment."));
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
// src/commands/dry-run.ts
|
|
2347
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
2348
|
+
import { platform } from "node:os";
|
|
2349
|
+
import chalk7 from "chalk";
|
|
2350
|
+
async function dryRunCommand(sspPath) {
|
|
2351
|
+
console.log(`Dry-run diagnostics for: ${sspPath}`);
|
|
2352
|
+
console.log();
|
|
2353
|
+
const data = readFileSync7(sspPath);
|
|
2354
|
+
const extracted = await extractSSP(data);
|
|
2355
|
+
const { manifest } = extracted;
|
|
2356
|
+
const diagnostics = [];
|
|
2357
|
+
diagnostics.push({
|
|
2358
|
+
check: "Author Signature",
|
|
2359
|
+
status: extracted.authorSignature ? "pass" : "fail",
|
|
2360
|
+
detail: extracted.authorSignature ? "Present" : "Missing"
|
|
2361
|
+
});
|
|
2362
|
+
diagnostics.push({
|
|
2363
|
+
check: "Platform Signature",
|
|
2364
|
+
status: extracted.platformSignature ? "pass" : "warn",
|
|
2365
|
+
detail: extracted.platformSignature ? "Present" : "Absent (not required)"
|
|
2366
|
+
});
|
|
2367
|
+
const { valid, mismatches } = verifyChecksums(
|
|
2368
|
+
extracted.files,
|
|
2369
|
+
extracted.checksums
|
|
2370
|
+
);
|
|
2371
|
+
diagnostics.push({
|
|
2372
|
+
check: "Checksums",
|
|
2373
|
+
status: valid ? "pass" : "fail",
|
|
2374
|
+
detail: valid ? `All ${Object.keys(extracted.checksums).length} files verified` : `${mismatches.length} mismatches`
|
|
2375
|
+
});
|
|
2376
|
+
const currentOS = platform() === "darwin" ? "macos" : platform();
|
|
2377
|
+
const osCompat = manifest.os_compat.includes(currentOS);
|
|
2378
|
+
diagnostics.push({
|
|
2379
|
+
check: "OS Compatibility",
|
|
2380
|
+
status: osCompat ? "pass" : "fail",
|
|
2381
|
+
detail: osCompat ? `Current OS (${currentOS}) is supported` : `Current OS (${currentOS}) not in ${manifest.os_compat.join(", ")}`
|
|
2382
|
+
});
|
|
2383
|
+
for (const dep of manifest.dependencies) {
|
|
2384
|
+
if (dep.type === "cli") {
|
|
2385
|
+
const { execSync: execSync2 } = await import("node:child_process");
|
|
2386
|
+
let found = false;
|
|
2387
|
+
try {
|
|
2388
|
+
execSync2(`which ${dep.name}`, { stdio: "pipe" });
|
|
2389
|
+
found = true;
|
|
2390
|
+
} catch {
|
|
2391
|
+
found = false;
|
|
2392
|
+
}
|
|
2393
|
+
diagnostics.push({
|
|
2394
|
+
check: `Dependency: ${dep.name}`,
|
|
2395
|
+
status: found ? "pass" : dep.optional ? "warn" : "fail",
|
|
2396
|
+
detail: found ? "Found" : dep.optional ? "Not found (optional)" : "Not found (required)"
|
|
2397
|
+
});
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
const textFiles = /* @__PURE__ */ new Map();
|
|
2401
|
+
for (const [path, content] of extracted.files) {
|
|
2402
|
+
if (isScannable(path)) {
|
|
2403
|
+
textFiles.set(path, content.toString("utf-8"));
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
const scanResult = scanFiles(textFiles);
|
|
2407
|
+
const report = generateReport(
|
|
2408
|
+
scanResult.issues,
|
|
2409
|
+
scanResult.scannedFiles,
|
|
2410
|
+
scanResult.skippedFiles
|
|
2411
|
+
);
|
|
2412
|
+
diagnostics.push({
|
|
2413
|
+
check: "Security Scan",
|
|
2414
|
+
status: report.passed ? report.summary.total > 0 ? "warn" : "pass" : "fail",
|
|
2415
|
+
detail: `Risk: ${report.risk_score}/100, Issues: ${report.summary.total}`
|
|
2416
|
+
});
|
|
2417
|
+
console.log(chalk7.bold("Diagnostic Results:"));
|
|
2418
|
+
console.log(chalk7.dim("\u2500".repeat(60)));
|
|
2419
|
+
for (const d of diagnostics) {
|
|
2420
|
+
let icon;
|
|
2421
|
+
let color;
|
|
2422
|
+
switch (d.status) {
|
|
2423
|
+
case "pass":
|
|
2424
|
+
icon = "\u2713";
|
|
2425
|
+
color = chalk7.green;
|
|
2426
|
+
break;
|
|
2427
|
+
case "warn":
|
|
2428
|
+
icon = "!";
|
|
2429
|
+
color = chalk7.yellow;
|
|
2430
|
+
break;
|
|
2431
|
+
case "fail":
|
|
2432
|
+
icon = "\u2717";
|
|
2433
|
+
color = chalk7.red;
|
|
2434
|
+
break;
|
|
2435
|
+
}
|
|
2436
|
+
console.log(
|
|
2437
|
+
` ${color(icon)} ${d.check.padEnd(25)} ${chalk7.dim(d.detail)}`
|
|
2438
|
+
);
|
|
2439
|
+
}
|
|
2440
|
+
console.log(chalk7.dim("\u2500".repeat(60)));
|
|
2441
|
+
const hasFail = diagnostics.some((d) => d.status === "fail");
|
|
2442
|
+
if (hasFail) {
|
|
2443
|
+
console.log(chalk7.red.bold("\nDry-run: ISSUES FOUND"));
|
|
2444
|
+
process.exitCode = 1;
|
|
2445
|
+
} else {
|
|
2446
|
+
console.log(chalk7.green.bold("\nDry-run: ALL CHECKS PASSED"));
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
// src/commands/uninstall.ts
|
|
2451
|
+
init_config();
|
|
2452
|
+
import { rmSync, existsSync as existsSync4 } from "node:fs";
|
|
2453
|
+
import chalk8 from "chalk";
|
|
2454
|
+
import inquirer3 from "inquirer";
|
|
2455
|
+
async function uninstallCommand(skillId) {
|
|
2456
|
+
const registry = loadRegistry();
|
|
2457
|
+
const skill = registry.skills.find((s) => s.id === skillId);
|
|
2458
|
+
if (!skill) {
|
|
2459
|
+
console.log(chalk8.red(`Skill not found in registry: ${skillId}`));
|
|
2460
|
+
console.log(chalk8.dim("Installed skills:"));
|
|
2461
|
+
for (const s of registry.skills) {
|
|
2462
|
+
console.log(chalk8.dim(` ${s.id} v${s.version}`));
|
|
2463
|
+
}
|
|
2464
|
+
process.exitCode = 1;
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
const { confirm } = await inquirer3.prompt([
|
|
2468
|
+
{
|
|
2469
|
+
type: "confirm",
|
|
2470
|
+
name: "confirm",
|
|
2471
|
+
message: `Uninstall ${skill.id} v${skill.version}?`,
|
|
2472
|
+
default: false
|
|
2473
|
+
}
|
|
2474
|
+
]);
|
|
2475
|
+
if (!confirm) {
|
|
2476
|
+
console.log("Uninstall cancelled.");
|
|
2477
|
+
return;
|
|
2478
|
+
}
|
|
2479
|
+
if (existsSync4(skill.install_path)) {
|
|
2480
|
+
rmSync(skill.install_path, { recursive: true });
|
|
2481
|
+
console.log(chalk8.dim(` Removed: ${skill.install_path}`));
|
|
2482
|
+
}
|
|
2483
|
+
registry.skills = registry.skills.filter((s) => s.id !== skillId);
|
|
2484
|
+
saveRegistry(registry);
|
|
2485
|
+
appendAuditLog({
|
|
2486
|
+
action: "uninstall",
|
|
2487
|
+
skill_id: skillId,
|
|
2488
|
+
version: skill.version
|
|
2489
|
+
});
|
|
2490
|
+
console.log(chalk8.green(`Uninstalled: ${skillId}`));
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
// src/commands/login.ts
|
|
2494
|
+
init_config();
|
|
2495
|
+
import { createServer } from "node:http";
|
|
2496
|
+
import { randomBytes } from "node:crypto";
|
|
2497
|
+
import chalk9 from "chalk";
|
|
2498
|
+
import inquirer4 from "inquirer";
|
|
2499
|
+
async function loginCommand() {
|
|
2500
|
+
const config = loadConfig();
|
|
2501
|
+
console.log(chalk9.bold("SkillPort Market Login"));
|
|
2502
|
+
console.log(chalk9.dim(`Marketplace: ${config.marketplace_url}`));
|
|
2503
|
+
console.log();
|
|
2504
|
+
const { method } = await inquirer4.prompt([
|
|
2505
|
+
{
|
|
2506
|
+
type: "list",
|
|
2507
|
+
name: "method",
|
|
2508
|
+
message: "Login method:",
|
|
2509
|
+
choices: [
|
|
2510
|
+
{ name: "Browser (GitHub OAuth)", value: "browser" },
|
|
2511
|
+
{ name: "Paste API token", value: "token" }
|
|
2512
|
+
]
|
|
2513
|
+
}
|
|
2514
|
+
]);
|
|
2515
|
+
if (method === "token") {
|
|
2516
|
+
const { token: token2 } = await inquirer4.prompt([
|
|
2517
|
+
{ type: "password", name: "token", message: "Enter your API token:" }
|
|
2518
|
+
]);
|
|
2519
|
+
config.auth_token = token2;
|
|
2520
|
+
saveConfig(config);
|
|
2521
|
+
console.log(chalk9.green("Login successful! Token saved."));
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
const state = randomBytes(16).toString("hex");
|
|
2525
|
+
const port = 9876;
|
|
2526
|
+
const authUrl = `${config.marketplace_url}/auth/cli?state=${state}&port=${port}`;
|
|
2527
|
+
console.log(chalk9.dim(`Opening browser to: ${authUrl}`));
|
|
2528
|
+
console.log(chalk9.dim("Waiting for authentication..."));
|
|
2529
|
+
const { exec } = await import("node:child_process");
|
|
2530
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
2531
|
+
exec(`${openCmd} "${authUrl}"`);
|
|
2532
|
+
const token = await new Promise((resolve, reject) => {
|
|
2533
|
+
const timeout = setTimeout(() => {
|
|
2534
|
+
server.close();
|
|
2535
|
+
reject(new Error("Authentication timed out (60s)"));
|
|
2536
|
+
}, 6e4);
|
|
2537
|
+
const server = createServer(async (req, res) => {
|
|
2538
|
+
const url = new URL(req.url || "", `http://localhost:${port}`);
|
|
2539
|
+
if (url.pathname === "/callback") {
|
|
2540
|
+
const callbackState = url.searchParams.get("state");
|
|
2541
|
+
const accessToken = url.searchParams.get("token");
|
|
2542
|
+
if (callbackState !== state) {
|
|
2543
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
2544
|
+
res.end("<h1>State mismatch. Please try again.</h1>");
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
if (!accessToken) {
|
|
2548
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
2549
|
+
res.end("<h1>No token received. Please try again.</h1>");
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
2553
|
+
res.end("<h1>Logged in to SkillPort! You can close this window.</h1>");
|
|
2554
|
+
clearTimeout(timeout);
|
|
2555
|
+
server.close();
|
|
2556
|
+
resolve(accessToken);
|
|
2557
|
+
}
|
|
2558
|
+
});
|
|
2559
|
+
server.listen(port);
|
|
2560
|
+
});
|
|
2561
|
+
try {
|
|
2562
|
+
const response = await fetch(`${config.marketplace_url}/v1/auth/cli-token`, {
|
|
2563
|
+
method: "POST",
|
|
2564
|
+
headers: {
|
|
2565
|
+
"Content-Type": "application/json",
|
|
2566
|
+
Authorization: `Bearer ${token}`
|
|
2567
|
+
},
|
|
2568
|
+
body: JSON.stringify({ label: "cli", scopes: ["read", "write", "publish"] })
|
|
2569
|
+
});
|
|
2570
|
+
if (response.ok) {
|
|
2571
|
+
const data = await response.json();
|
|
2572
|
+
config.auth_token = data.token;
|
|
2573
|
+
} else {
|
|
2574
|
+
config.auth_token = token;
|
|
2575
|
+
}
|
|
2576
|
+
} catch {
|
|
2577
|
+
config.auth_token = token;
|
|
2578
|
+
}
|
|
2579
|
+
saveConfig(config);
|
|
2580
|
+
if (hasKeys()) {
|
|
2581
|
+
try {
|
|
2582
|
+
const publicKey = loadPublicKey2();
|
|
2583
|
+
await fetch(`${config.marketplace_url}/v1/keys`, {
|
|
2584
|
+
method: "POST",
|
|
2585
|
+
headers: {
|
|
2586
|
+
"Content-Type": "application/json",
|
|
2587
|
+
Authorization: `Bearer ${config.auth_token}`
|
|
2588
|
+
},
|
|
2589
|
+
body: JSON.stringify({ public_key_pem: publicKey, label: "default" })
|
|
2590
|
+
});
|
|
2591
|
+
console.log(chalk9.dim(" Public key registered with marketplace."));
|
|
2592
|
+
} catch {
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
console.log(chalk9.green("Login successful! Token saved."));
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
// src/commands/publish.ts
|
|
2599
|
+
import { readFileSync as readFileSync8, existsSync as existsSync5 } from "node:fs";
|
|
2600
|
+
import { join as join6 } from "node:path";
|
|
2601
|
+
import { homedir as homedir3 } from "node:os";
|
|
2602
|
+
import chalk10 from "chalk";
|
|
2603
|
+
init_config();
|
|
2604
|
+
async function publishCommand(sspPath) {
|
|
2605
|
+
const config = loadConfig();
|
|
2606
|
+
if (!config.auth_token) {
|
|
2607
|
+
console.log(chalk10.red("Not logged in. Run 'skillport login' first."));
|
|
2608
|
+
process.exitCode = 1;
|
|
2609
|
+
return;
|
|
2610
|
+
}
|
|
2611
|
+
console.log(`Validating: ${sspPath}`);
|
|
2612
|
+
const data = readFileSync8(sspPath);
|
|
2613
|
+
const extracted = await extractSSP(data);
|
|
2614
|
+
const { valid } = verifyChecksums(extracted.files, extracted.checksums);
|
|
2615
|
+
if (!valid) {
|
|
2616
|
+
console.log(chalk10.red("Checksum verification failed. Cannot publish."));
|
|
2617
|
+
process.exitCode = 1;
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
if (!extracted.authorSignature) {
|
|
2621
|
+
console.log(chalk10.red("No author signature. Sign the package first."));
|
|
2622
|
+
process.exitCode = 1;
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
const keyId = extracted.manifest.author.signing_key_id;
|
|
2626
|
+
const pubKeyPath = join6(homedir3(), ".skillport", "keys", "default.pub");
|
|
2627
|
+
if (existsSync5(pubKeyPath)) {
|
|
2628
|
+
const pubKeyPem = readFileSync8(pubKeyPath, "utf-8");
|
|
2629
|
+
const sigValid = verifySignature(
|
|
2630
|
+
extracted.manifestRaw,
|
|
2631
|
+
extracted.authorSignature,
|
|
2632
|
+
pubKeyPem
|
|
2633
|
+
);
|
|
2634
|
+
if (!sigValid) {
|
|
2635
|
+
console.log(chalk10.red("Signature verification failed. Package may have been tampered with after signing."));
|
|
2636
|
+
console.log(chalk10.dim(` Key ID: ${keyId}`));
|
|
2637
|
+
process.exitCode = 1;
|
|
2638
|
+
return;
|
|
2639
|
+
}
|
|
2640
|
+
console.log(chalk10.green("\u2713 Signature verified"));
|
|
2641
|
+
} else {
|
|
2642
|
+
console.log(chalk10.yellow("\u26A0 Local public key not found \u2014 skipping local signature check"));
|
|
2643
|
+
console.log(chalk10.dim(" Server will verify signature against registered key."));
|
|
2644
|
+
}
|
|
2645
|
+
console.log("Uploading to marketplace...");
|
|
2646
|
+
try {
|
|
2647
|
+
const formData = new FormData();
|
|
2648
|
+
formData.append("file", new Blob([data]), sspPath.split("/").pop());
|
|
2649
|
+
const response = await fetch(`${config.marketplace_url}/v1/skills`, {
|
|
2650
|
+
method: "POST",
|
|
2651
|
+
headers: {
|
|
2652
|
+
Authorization: `Bearer ${config.auth_token}`
|
|
2653
|
+
},
|
|
2654
|
+
body: formData
|
|
2655
|
+
});
|
|
2656
|
+
if (!response.ok) {
|
|
2657
|
+
const errorBody = await response.json();
|
|
2658
|
+
console.log(chalk10.red(`Upload failed: ${errorBody.error || response.statusText}`));
|
|
2659
|
+
process.exitCode = 1;
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2662
|
+
const result = await response.json();
|
|
2663
|
+
console.log(chalk10.green("Published successfully!"));
|
|
2664
|
+
console.log();
|
|
2665
|
+
console.log(` ${chalk10.bold("Skill ID:")} ${result.id}`);
|
|
2666
|
+
console.log(` ${chalk10.bold("SSP ID:")} ${result.ssp_id}`);
|
|
2667
|
+
console.log(` ${chalk10.bold("Version:")} ${result.version}`);
|
|
2668
|
+
console.log(` ${chalk10.bold("Scan:")} ${result.scan_passed ? chalk10.green("PASSED") : chalk10.red("FAILED")}`);
|
|
2669
|
+
console.log(` ${chalk10.bold("Risk Score:")} ${result.risk_score}/100`);
|
|
2670
|
+
console.log();
|
|
2671
|
+
console.log(chalk10.dim(` URL: ${config.marketplace_url}/skills/${result.id}`));
|
|
2672
|
+
console.log(chalk10.dim(` Install: skillport install ${result.ssp_id}@${result.version}`));
|
|
2673
|
+
} catch (error) {
|
|
2674
|
+
console.log(chalk10.red(`Upload failed: ${error.message}`));
|
|
2675
|
+
process.exitCode = 1;
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
// src/index.ts
|
|
2680
|
+
var program = new Command();
|
|
2681
|
+
program.name("skillport").description("SkillPort \u2014 secure skill distribution for OpenClaw").version("0.1.0");
|
|
2682
|
+
program.command("init").description("Generate Ed25519 key pair for signing").action(initCommand);
|
|
2683
|
+
program.command("scan <path>").description("Run security scan on a skill directory or .ssp file").action(scanCommand);
|
|
2684
|
+
program.command("export <path>").description("Export a skill directory as a SkillPort package (.ssp)").option("-o, --output <file>", "Output file path").option("-y, --yes", "Non-interactive mode (include all, skip prompts)").option("--id <id>", "Skill ID (author-slug/skill-slug)").option("--name <name>", "Skill name").option("--description <desc>", "Skill description").option("--skill-version <ver>", "Skill version (semver)").option("--author <name>", "Author name").option("--openclaw-compat <range>", "OpenClaw compatibility range").option("--os <os...>", "Compatible OS (macos, linux, windows)").action(exportCommand);
|
|
2685
|
+
program.command("sign <ssp>").description("Sign or re-sign a SkillPort package").action(signCommand);
|
|
2686
|
+
program.command("verify <ssp>").description("Verify SkillPort package signatures and checksums").option("--public-key <path>", "Path to author public key for verification").action(verifyCommand);
|
|
2687
|
+
program.command("install <target>").description("Install a SkillPort package").option("--accept-risk", "Accept high-risk permissions (shell, critical flags)").option("-y, --yes", "Non-interactive mode (auto-approve, use defaults)").action(installCommand);
|
|
2688
|
+
program.command("dry-run <ssp>").description("Run installation diagnostics without installing").action(dryRunCommand);
|
|
2689
|
+
program.command("uninstall <id>").description("Uninstall an installed skill").action(uninstallCommand);
|
|
2690
|
+
program.command("login").description("Authenticate with SkillPort Market").action(loginCommand);
|
|
2691
|
+
program.command("publish <ssp>").description("Publish a SkillPort package to the marketplace").action(publishCommand);
|
|
2692
|
+
program.parse();
|