自动同步: 2026-07-01 02:10
This commit is contained in:
+2
@@ -0,0 +1,2 @@
|
||||
export declare function getOptionalAuthToken(): Promise<string | undefined>;
|
||||
export declare function requireAuthToken(): Promise<string>;
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { readGlobalConfig } from "../config.js";
|
||||
import { fail } from "./ui.js";
|
||||
export async function getOptionalAuthToken() {
|
||||
const cfg = await readGlobalConfig();
|
||||
return cfg?.token ?? undefined;
|
||||
}
|
||||
export async function requireAuthToken() {
|
||||
const token = await getOptionalAuthToken();
|
||||
if (!token)
|
||||
fail("Not logged in. Run: clawhub login");
|
||||
return token;
|
||||
}
|
||||
//# sourceMappingURL=authToken.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"authToken.js","sourceRoot":"","sources":["../../src/cli/authToken.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE/B,MAAM,CAAC,KAAK,UAAU,oBAAoB;IACxC,MAAM,GAAG,GAAG,MAAM,gBAAgB,EAAE,CAAC;IACrC,OAAO,GAAG,EAAE,KAAK,IAAI,SAAS,CAAC;AACjC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,MAAM,KAAK,GAAG,MAAM,oBAAoB,EAAE,CAAC;IAC3C,IAAI,CAAC,KAAK;QAAE,IAAI,CAAC,mCAAmC,CAAC,CAAC;IACtD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export declare function getCliVersion(): string;
|
||||
export declare function getCliBuildLabel(): string;
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, readFileSync, statSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
function readPackageVersion() {
|
||||
let current = dirname(fileURLToPath(import.meta.url));
|
||||
for (;;) {
|
||||
try {
|
||||
const raw = readFileSync(join(current, "package.json"), "utf8");
|
||||
const pkg = JSON.parse(raw);
|
||||
if (typeof pkg.version === "string")
|
||||
return pkg.version;
|
||||
}
|
||||
catch {
|
||||
// Keep walking until the package root is found.
|
||||
}
|
||||
const parent = dirname(current);
|
||||
if (parent === current)
|
||||
return "0.0.0";
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
function shortCommit(value) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed)
|
||||
return null;
|
||||
if (trimmed.length <= 8)
|
||||
return trimmed;
|
||||
return trimmed.slice(0, 8);
|
||||
}
|
||||
function getCliCommit() {
|
||||
const candidates = [
|
||||
process.env.CLAWHUB_COMMIT,
|
||||
process.env.CLAWDHUB_COMMIT,
|
||||
process.env.VERCEL_GIT_COMMIT_SHA,
|
||||
process.env.GITHUB_SHA,
|
||||
process.env.COMMIT_SHA,
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (!candidate)
|
||||
continue;
|
||||
const short = shortCommit(candidate);
|
||||
if (short)
|
||||
return short;
|
||||
}
|
||||
return readGitCommitFromCwd();
|
||||
}
|
||||
export function getCliVersion() {
|
||||
return readPackageVersion();
|
||||
}
|
||||
export function getCliBuildLabel() {
|
||||
const version = getCliVersion();
|
||||
const commit = getCliCommit();
|
||||
return commit ? `v${version} (${commit})` : `v${version}`;
|
||||
}
|
||||
function readGitCommitFromCwd() {
|
||||
const revParseCommit = readGitCommitFromRevParse();
|
||||
if (revParseCommit)
|
||||
return revParseCommit;
|
||||
try {
|
||||
const gitDir = findGitDir(process.cwd());
|
||||
if (!gitDir)
|
||||
return null;
|
||||
const headPath = join(gitDir, "HEAD");
|
||||
if (!existsSync(headPath))
|
||||
return null;
|
||||
const head = readFileSync(headPath, "utf8").trim();
|
||||
if (!head)
|
||||
return null;
|
||||
if (!head.startsWith("ref:"))
|
||||
return shortCommit(head);
|
||||
const ref = head.replace(/^ref:\s*/, "").trim();
|
||||
if (!ref)
|
||||
return null;
|
||||
const refPath = join(gitDir, ref);
|
||||
if (!existsSync(refPath))
|
||||
return null;
|
||||
const sha = readFileSync(refPath, "utf8").trim();
|
||||
return shortCommit(sha);
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function readGitCommitFromRevParse() {
|
||||
try {
|
||||
const sha = execFileSync("git", ["rev-parse", "HEAD"], {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
});
|
||||
return shortCommit(sha);
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function findGitDir(start) {
|
||||
let current = resolve(start);
|
||||
for (;;) {
|
||||
const dotGit = join(current, ".git");
|
||||
if (existsSync(dotGit)) {
|
||||
try {
|
||||
const stat = statSync(dotGit);
|
||||
if (stat.isDirectory())
|
||||
return dotGit;
|
||||
}
|
||||
catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
const content = readFileSync(dotGit, "utf8").trim();
|
||||
const match = content.match(/^gitdir:\s*(.+)$/);
|
||||
if (match?.[1])
|
||||
return resolve(current, match[1]);
|
||||
}
|
||||
catch {
|
||||
return dotGit;
|
||||
}
|
||||
return dotGit;
|
||||
}
|
||||
const parent = resolve(current, "..");
|
||||
if (parent === current)
|
||||
return null;
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=buildInfo.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"buildInfo.js","sourceRoot":"","sources":["../../src/cli/buildInfo.ts"],"names":[],"mappings":"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"}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export declare function resolveClawdbotDefaultWorkspace(): Promise<string | null>;
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
import JSON5 from "json5";
|
||||
import { resolveHome } from "../homedir.js";
|
||||
export async function resolveClawdbotDefaultWorkspace() {
|
||||
const config = await readClawdbotConfig();
|
||||
const openclawConfig = await readOpenclawConfig();
|
||||
if (!config && !openclawConfig)
|
||||
return null;
|
||||
const defaultsWorkspace = resolveUserPath(config?.agents?.defaults?.workspace ?? config?.agent?.workspace ?? "");
|
||||
if (defaultsWorkspace)
|
||||
return defaultsWorkspace;
|
||||
const listedAgents = getAgentList(config);
|
||||
const defaultAgent = listedAgents.find((entry) => entry.default) ??
|
||||
listedAgents.find((entry) => entry.id === "main");
|
||||
const listWorkspace = resolveUserPath(defaultAgent?.workspace ?? "");
|
||||
if (listWorkspace)
|
||||
return listWorkspace;
|
||||
if (!openclawConfig)
|
||||
return null;
|
||||
const openclawDefaults = resolveUserPath(openclawConfig.agents?.defaults?.workspace ?? openclawConfig.agent?.workspace ?? "");
|
||||
if (openclawDefaults)
|
||||
return openclawDefaults;
|
||||
const openclawAgents = getAgentList(openclawConfig);
|
||||
const openclawDefaultAgent = openclawAgents.find((entry) => entry.default) ??
|
||||
openclawAgents.find((entry) => entry.id === "main");
|
||||
const openclawWorkspace = resolveUserPath(openclawDefaultAgent?.workspace ?? "");
|
||||
return openclawWorkspace || null;
|
||||
}
|
||||
function resolveClawdbotStateDir() {
|
||||
const override = process.env.CLAWDBOT_STATE_DIR?.trim();
|
||||
if (override)
|
||||
return resolveUserPath(override);
|
||||
return join(resolveHome(), ".clawdbot");
|
||||
}
|
||||
function resolveClawdbotConfigPath() {
|
||||
const override = process.env.CLAWDBOT_CONFIG_PATH?.trim();
|
||||
if (override)
|
||||
return resolveUserPath(override);
|
||||
return join(resolveClawdbotStateDir(), "clawdbot.json");
|
||||
}
|
||||
function resolveOpenclawStateDir() {
|
||||
const override = process.env.OPENCLAW_STATE_DIR?.trim();
|
||||
if (override)
|
||||
return resolveUserPath(override);
|
||||
return join(resolveHome(), ".openclaw");
|
||||
}
|
||||
function resolveOpenclawConfigPath() {
|
||||
const override = process.env.OPENCLAW_CONFIG_PATH?.trim();
|
||||
if (override)
|
||||
return resolveUserPath(override);
|
||||
return join(resolveOpenclawStateDir(), "openclaw.json");
|
||||
}
|
||||
function resolveUserPath(input) {
|
||||
if (typeof input !== "string")
|
||||
return "";
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed)
|
||||
return "";
|
||||
if (trimmed.startsWith("~")) {
|
||||
return resolve(trimmed.replace(/^~(?=$|[\\/])/, resolveHome()));
|
||||
}
|
||||
return resolve(trimmed);
|
||||
}
|
||||
function getAgentList(config) {
|
||||
const list = config?.agents?.list;
|
||||
return Array.isArray(list) ? list.filter(isRecord) : [];
|
||||
}
|
||||
function isRecord(value) {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
async function readClawdbotConfig() {
|
||||
return readConfigFile(resolveClawdbotConfigPath());
|
||||
}
|
||||
async function readOpenclawConfig() {
|
||||
return readConfigFile(resolveOpenclawConfigPath());
|
||||
}
|
||||
async function readConfigFile(path) {
|
||||
try {
|
||||
const raw = await readFile(path, "utf8");
|
||||
const parsed = JSON5.parse(raw);
|
||||
if (!parsed || typeof parsed !== "object")
|
||||
return null;
|
||||
return parsed;
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=clawdbotConfig.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"clawdbotConfig.js","sourceRoot":"","sources":["../../src/cli/clawdbotConfig.ts"],"names":[],"mappings":"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"}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import type { GlobalOpts } from "../types.js";
|
||||
export declare function cmdLoginFlow(opts: GlobalOpts, options: {
|
||||
token?: string;
|
||||
label?: string;
|
||||
browser?: boolean;
|
||||
device?: boolean;
|
||||
}, inputAllowed: boolean): Promise<void>;
|
||||
export declare function cmdLogout(opts: GlobalOpts): Promise<void>;
|
||||
export declare function cmdWhoami(opts: GlobalOpts): Promise<void>;
|
||||
export declare function cmdToken(): Promise<void>;
|
||||
/**
|
||||
* Device Flow login for headless environments.
|
||||
* Requests a device code, displays it to the user, then polls until authorized.
|
||||
*/
|
||||
export declare function cmdDeviceLogin(opts: GlobalOpts, options?: {
|
||||
label?: string;
|
||||
}): Promise<void>;
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
import { readGlobalConfig, writeGlobalConfig } from "../../config.js";
|
||||
import { pollForDeviceToken, requestDeviceCode } from "../../deviceAuth.js";
|
||||
import { discoverRegistryFromSite } from "../../discovery.js";
|
||||
import { apiRequest } from "../../http.js";
|
||||
import { ApiRoutes, ApiV1WhoamiResponseSchema } from "../../schema/index.js";
|
||||
import { requireAuthToken } from "../authToken.js";
|
||||
import { getRegistry } from "../registry.js";
|
||||
import { createCrabLoader, fail, formatError, promptHidden } from "../ui.js";
|
||||
export async function cmdLoginFlow(opts, options, inputAllowed) {
|
||||
if (options.token) {
|
||||
await cmdLogin(opts, options.token, inputAllowed);
|
||||
return;
|
||||
}
|
||||
await cmdDeviceLogin(opts, { label: options.label });
|
||||
}
|
||||
async function cmdLogin(opts, tokenFlag, inputAllowed) {
|
||||
if (!tokenFlag && !inputAllowed)
|
||||
fail("Token required (use --token or remove --no-input)");
|
||||
const token = tokenFlag || (await promptHidden("ClawHub token: "));
|
||||
if (!token)
|
||||
fail("Token required");
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
const spinner = createCrabLoader("Verifying token");
|
||||
try {
|
||||
const whoami = await apiRequest(registry, { method: "GET", path: ApiRoutes.whoami, token }, ApiV1WhoamiResponseSchema);
|
||||
if (!whoami.user)
|
||||
fail("Login failed");
|
||||
await writeGlobalConfig({ registry, token });
|
||||
const handle = whoami.user.handle ? `@${whoami.user.handle}` : "unknown user";
|
||||
spinner.succeed(`OK. Logged in as ${handle}.`);
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(formatError(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
export async function cmdLogout(opts) {
|
||||
const cfg = await readGlobalConfig();
|
||||
const registry = cfg?.registry || (await getRegistry(opts, { cache: true }));
|
||||
await writeGlobalConfig({ registry, token: undefined });
|
||||
console.log("OK. Logged out locally. Token still valid until revoked (Settings -> API tokens).");
|
||||
}
|
||||
export async function cmdWhoami(opts) {
|
||||
const token = await requireAuthToken();
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
const spinner = createCrabLoader("Checking token");
|
||||
try {
|
||||
const whoami = await apiRequest(registry, { method: "GET", path: ApiRoutes.whoami, token }, ApiV1WhoamiResponseSchema);
|
||||
spinner.succeed(whoami.user.handle ?? "unknown");
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(formatError(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
export async function cmdToken() {
|
||||
const token = await requireAuthToken();
|
||||
console.log(token);
|
||||
}
|
||||
/**
|
||||
* Device Flow login for headless environments.
|
||||
* Requests a device code, displays it to the user, then polls until authorized.
|
||||
*/
|
||||
export async function cmdDeviceLogin(opts, options) {
|
||||
const discovery = await discoverRegistryFromSite(opts.site).catch(() => null);
|
||||
const authBase = discovery?.authBase?.trim() || opts.site;
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
const label = (options?.label ?? "CLI device login").trim() || "CLI device login";
|
||||
const spinner = createCrabLoader("Requesting device code");
|
||||
let deviceCode;
|
||||
try {
|
||||
deviceCode = await requestDeviceCode({ apiUrl: registry, siteUrl: authBase, label });
|
||||
spinner.succeed("Device code received");
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(formatError(error));
|
||||
throw error;
|
||||
}
|
||||
// Display the code and URL for the user
|
||||
console.log();
|
||||
console.log(" To authenticate, visit:");
|
||||
console.log(` ${deviceCode.verification_uri}`);
|
||||
console.log();
|
||||
console.log(` And enter code: ${deviceCode.user_code}`);
|
||||
console.log();
|
||||
console.log(` Code expires in ${Math.floor(deviceCode.expires_in / 60)} minutes.`);
|
||||
console.log();
|
||||
const pollSpinner = createCrabLoader("Waiting for authorization");
|
||||
try {
|
||||
const tokenResponse = await pollForDeviceToken({ apiUrl: registry, siteUrl: authBase }, deviceCode.device_code, { interval: deviceCode.interval, expiresIn: deviceCode.expires_in });
|
||||
pollSpinner.succeed("Authorized");
|
||||
// Store the token
|
||||
await cmdLogin({ ...opts, registry, registrySource: "cli" }, tokenResponse.access_token, true);
|
||||
}
|
||||
catch (error) {
|
||||
pollSpinner.fail(formatError(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=auth.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../src/cli/commands/auth.ts"],"names":[],"mappings":"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"}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import type { GlobalOpts } from "../types.js";
|
||||
type SkillActionLabels = {
|
||||
verb: string;
|
||||
progress: string;
|
||||
past: string;
|
||||
promptSuffix?: string;
|
||||
};
|
||||
type SkillActionOptions = {
|
||||
yes?: boolean;
|
||||
reason?: string;
|
||||
note?: string;
|
||||
};
|
||||
type SkillDeleteOptions = SkillActionOptions & {
|
||||
version?: string;
|
||||
};
|
||||
export declare function cmdDeleteSkill(opts: GlobalOpts, slugArg: string, options: SkillDeleteOptions, inputAllowed: boolean, labels?: SkillActionLabels): Promise<{
|
||||
ok: true;
|
||||
slugReservedUntil?: number | undefined;
|
||||
} | undefined>;
|
||||
export declare function cmdUndeleteSkill(opts: GlobalOpts, slugArg: string, options: SkillActionOptions, inputAllowed: boolean, labels?: SkillActionLabels): Promise<{
|
||||
ok: true;
|
||||
slugReservedUntil?: number | undefined;
|
||||
} | undefined>;
|
||||
export declare function cmdHideSkill(opts: GlobalOpts, slugArg: string, options: SkillActionOptions, inputAllowed: boolean): Promise<{
|
||||
ok: true;
|
||||
slugReservedUntil?: number | undefined;
|
||||
} | undefined>;
|
||||
export declare function cmdUnhideSkill(opts: GlobalOpts, slugArg: string, options: SkillActionOptions, inputAllowed: boolean): Promise<{
|
||||
ok: true;
|
||||
slugReservedUntil?: number | undefined;
|
||||
} | undefined>;
|
||||
export {};
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
import { apiRequest } from "../../http.js";
|
||||
import { ApiRoutes, ApiV1DeleteResponseSchema, parseArk } from "../../schema/index.js";
|
||||
import { requireAuthToken } from "../authToken.js";
|
||||
import { getRegistry } from "../registry.js";
|
||||
import { createCrabLoader, fail, formatError, isInteractive, promptConfirm } from "../ui.js";
|
||||
const deleteLabels = {
|
||||
verb: "Delete",
|
||||
progress: "Deleting",
|
||||
past: "Deleted",
|
||||
promptSuffix: "soft delete; owner slug reservation expires after 30 days",
|
||||
};
|
||||
const undeleteLabels = {
|
||||
verb: "Undelete",
|
||||
progress: "Undeleting",
|
||||
past: "Undeleted",
|
||||
promptSuffix: "owner/moderator/admin",
|
||||
};
|
||||
const hideLabels = {
|
||||
verb: "Hide",
|
||||
progress: "Hiding",
|
||||
past: "Hidden",
|
||||
promptSuffix: "owner/moderator/admin",
|
||||
};
|
||||
const unhideLabels = {
|
||||
verb: "Unhide",
|
||||
progress: "Unhiding",
|
||||
past: "Unhidden",
|
||||
promptSuffix: "owner/moderator/admin",
|
||||
};
|
||||
export async function cmdDeleteSkill(opts, slugArg, options, inputAllowed, labels = deleteLabels) {
|
||||
const ref = parseSkillRef(slugArg);
|
||||
const slug = ref.slug;
|
||||
const reason = normalizeReason(options);
|
||||
const version = normalizeVersion(options.version);
|
||||
if (version && reason)
|
||||
fail("--reason/--note apply only to whole-skill deletion");
|
||||
const allowPrompt = isInteractive() && inputAllowed !== false;
|
||||
if (!options.yes) {
|
||||
if (!allowPrompt)
|
||||
fail("Pass --yes (no input)");
|
||||
const ok = await promptConfirm(formatPrompt(labels, formatSkillRef(ref), version));
|
||||
if (!ok)
|
||||
return undefined;
|
||||
}
|
||||
const token = await requireAuthToken();
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
const target = version ? `${formatSkillRef(ref)} version ${version}` : formatSkillRef(ref);
|
||||
const spinner = createCrabLoader(`${labels.progress} ${target}`);
|
||||
try {
|
||||
const body = buildBody(reason, ref.ownerHandle, version);
|
||||
const result = await apiRequest(registry, {
|
||||
method: "DELETE",
|
||||
path: `${ApiRoutes.skills}/${encodeURIComponent(slug)}${version ? `/versions/${encodeURIComponent(version)}` : ""}`,
|
||||
token,
|
||||
body,
|
||||
...(version ? { retryCount: 0 } : {}),
|
||||
}, ApiV1DeleteResponseSchema);
|
||||
const parsed = parseArk(ApiV1DeleteResponseSchema, result, "Delete response");
|
||||
spinner.succeed(`OK. ${labels.past} ${target}${version ? "" : formatSlugReservation(parsed)}`);
|
||||
return parsed;
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(formatError(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
export async function cmdUndeleteSkill(opts, slugArg, options, inputAllowed, labels = undeleteLabels) {
|
||||
const ref = parseSkillRef(slugArg);
|
||||
const slug = ref.slug;
|
||||
const reason = normalizeReason(options);
|
||||
const allowPrompt = isInteractive() && inputAllowed !== false;
|
||||
if (!options.yes) {
|
||||
if (!allowPrompt)
|
||||
fail("Pass --yes (no input)");
|
||||
const ok = await promptConfirm(formatPrompt(labels, formatSkillRef(ref)));
|
||||
if (!ok)
|
||||
return undefined;
|
||||
}
|
||||
const token = await requireAuthToken();
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
const spinner = createCrabLoader(`${labels.progress} ${formatSkillRef(ref)}`);
|
||||
try {
|
||||
const body = buildBody(reason, ref.ownerHandle);
|
||||
const result = await apiRequest(registry, {
|
||||
method: "POST",
|
||||
path: `${ApiRoutes.skills}/${encodeURIComponent(slug)}/undelete`,
|
||||
token,
|
||||
body,
|
||||
}, ApiV1DeleteResponseSchema);
|
||||
spinner.succeed(`OK. ${labels.past} ${formatSkillRef(ref)}`);
|
||||
return parseArk(ApiV1DeleteResponseSchema, result, "Undelete response");
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(formatError(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
export async function cmdHideSkill(opts, slugArg, options, inputAllowed) {
|
||||
return cmdDeleteSkill(opts, slugArg, options, inputAllowed, hideLabels);
|
||||
}
|
||||
export async function cmdUnhideSkill(opts, slugArg, options, inputAllowed) {
|
||||
return cmdUndeleteSkill(opts, slugArg, options, inputAllowed, unhideLabels);
|
||||
}
|
||||
function normalizeOwnerHandle(raw) {
|
||||
const handle = raw?.trim().replace(/^@+/, "").toLowerCase();
|
||||
if (!handle)
|
||||
return undefined;
|
||||
if (handle.includes("/") || handle.includes("\\") || handle.includes("..")) {
|
||||
fail(`Invalid owner handle: ${raw}`);
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
function normalizeSlug(slugArg) {
|
||||
const slug = slugArg.trim().toLowerCase();
|
||||
if (!slug)
|
||||
fail("Slug required");
|
||||
if (slug.includes("/") || slug.includes("\\") || slug.includes("..")) {
|
||||
fail(`Invalid slug: ${slugArg}`);
|
||||
}
|
||||
return slug;
|
||||
}
|
||||
function parseSkillRef(raw) {
|
||||
const ref = raw.trim();
|
||||
if (!ref)
|
||||
fail("Slug required");
|
||||
const slashIndex = ref.indexOf("/");
|
||||
if (slashIndex < 0)
|
||||
return { slug: normalizeSlug(ref) };
|
||||
if (ref.indexOf("/", slashIndex + 1) >= 0)
|
||||
fail(`Invalid skill ref: ${ref}`);
|
||||
const ownerHandle = normalizeOwnerHandle(ref.slice(0, slashIndex));
|
||||
const slug = normalizeSlug(ref.slice(slashIndex + 1));
|
||||
if (!ownerHandle)
|
||||
fail(`Invalid skill ref: ${ref}`);
|
||||
return { slug, ownerHandle };
|
||||
}
|
||||
function formatSkillRef(ref) {
|
||||
return ref.ownerHandle ? `@${ref.ownerHandle}/${ref.slug}` : ref.slug;
|
||||
}
|
||||
function buildBody(reason, ownerHandle, version) {
|
||||
const body = {
|
||||
...(version ? { version } : {}),
|
||||
...(reason ? { reason } : {}),
|
||||
...(ownerHandle ? { ownerHandle } : {}),
|
||||
};
|
||||
return Object.keys(body).length > 0 ? body : undefined;
|
||||
}
|
||||
function normalizeReason(options) {
|
||||
const reason = options.reason?.trim();
|
||||
const note = options.note?.trim();
|
||||
if (reason && note && reason !== note)
|
||||
fail("Pass only one of --reason or --note");
|
||||
const value = reason || note;
|
||||
if ((options.reason !== undefined || options.note !== undefined) && !value) {
|
||||
fail("--reason cannot be empty");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function normalizeVersion(value) {
|
||||
if (value === undefined)
|
||||
return undefined;
|
||||
const version = value.trim();
|
||||
if (!version)
|
||||
fail("--version cannot be empty");
|
||||
return version;
|
||||
}
|
||||
function formatPrompt(labels, slug, version) {
|
||||
if (version) {
|
||||
return `${labels.verb} ${slug} version ${version}? (permanent; cannot be restored or republished; publish a replacement first if deleting the current latest version)`;
|
||||
}
|
||||
const suffix = labels.promptSuffix ? ` (${labels.promptSuffix})` : "";
|
||||
return `${labels.verb} ${slug}?${suffix}`;
|
||||
}
|
||||
function formatSlugReservation(result) {
|
||||
if (typeof result.slugReservedUntil !== "number")
|
||||
return "";
|
||||
return `. Slug reserved until ${new Date(result.slugReservedUntil).toISOString()}`;
|
||||
}
|
||||
//# sourceMappingURL=delete.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+40
@@ -0,0 +1,40 @@
|
||||
type ResolvedPublishSource = {
|
||||
kind: "local";
|
||||
path: string;
|
||||
} | {
|
||||
kind: "github";
|
||||
owner: string;
|
||||
repo: string;
|
||||
ref?: string;
|
||||
path: string;
|
||||
url: string;
|
||||
};
|
||||
type LocalGitInfo = {
|
||||
root: string;
|
||||
path: string;
|
||||
repo?: string;
|
||||
commit?: string;
|
||||
ref?: string;
|
||||
};
|
||||
export declare function resolveSourceInput(input: string, options: {
|
||||
workdir: string;
|
||||
localWorkdirs?: string[];
|
||||
}): Promise<ResolvedPublishSource>;
|
||||
export declare function fetchGitHubSource(source: Extract<ResolvedPublishSource, {
|
||||
kind: "github";
|
||||
}>): Promise<{
|
||||
dir: string;
|
||||
source: {
|
||||
kind: "github";
|
||||
url: string;
|
||||
repo: string;
|
||||
ref: string;
|
||||
commit: string;
|
||||
path: string;
|
||||
importedAt: number;
|
||||
};
|
||||
cleanup: () => Promise<void>;
|
||||
}>;
|
||||
export declare function resolveLocalGitInfo(folder: string): LocalGitInfo | null;
|
||||
export declare function normalizeGitHubRepo(value: string): string | undefined;
|
||||
export {};
|
||||
+348
@@ -0,0 +1,348 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdir, mkdtemp, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { homedir, tmpdir } from "node:os";
|
||||
import { dirname, join, resolve, sep } from "node:path";
|
||||
import { unzipSync } from "fflate";
|
||||
const GITHUB_API = "https://api.github.com";
|
||||
const GITHUB_HOSTS = new Set(["github.com", "www.github.com"]);
|
||||
const ZIP_USER_AGENT = "clawhub/package-publish";
|
||||
export async function resolveSourceInput(input, options) {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed)
|
||||
throw new Error("Path required");
|
||||
const localWorkdirs = normalizeLocalWorkdirs(options.workdir, options.localWorkdirs);
|
||||
if (trimmed.startsWith("https://")) {
|
||||
return await parseGitHubUrl(trimmed);
|
||||
}
|
||||
const shorthand = parseGitHubShorthand(trimmed);
|
||||
if (shorthand) {
|
||||
for (const workdir of localWorkdirs) {
|
||||
const localPath = resolveLocalPath(workdir, trimmed);
|
||||
const localStat = await stat(localPath).catch(() => null);
|
||||
if (localStat?.isDirectory()) {
|
||||
return { kind: "local", path: localPath };
|
||||
}
|
||||
}
|
||||
return shorthand;
|
||||
}
|
||||
for (const workdir of localWorkdirs) {
|
||||
const localPath = resolveLocalPath(workdir, trimmed);
|
||||
if (await stat(localPath).catch(() => null)) {
|
||||
return { kind: "local", path: localPath };
|
||||
}
|
||||
}
|
||||
return { kind: "local", path: resolveLocalPath(localWorkdirs[0] ?? options.workdir, trimmed) };
|
||||
}
|
||||
export async function fetchGitHubSource(source) {
|
||||
const token = process.env.GITHUB_TOKEN?.trim() || undefined;
|
||||
const repo = `${source.owner}/${source.repo}`;
|
||||
const repoUrl = `https://github.com/${repo}`;
|
||||
const resolvedRef = source.ref?.trim() || (await resolveDefaultBranch(source.owner, source.repo, token));
|
||||
const commit = await resolveCommitSha(source.owner, source.repo, resolvedRef, token);
|
||||
const archiveBytes = await downloadGitHubZip(source.owner, source.repo, commit, token);
|
||||
const entries = stripSingleTopLevelFolder(unzipSync(archiveBytes));
|
||||
const publishPath = normalizeRepoSubpath(source.path);
|
||||
const tempDir = await mkdtemp(join(tmpdir(), "clawhub-github-publish-"));
|
||||
try {
|
||||
const subdirEntries = filterEntriesForSubpath(entries, publishPath);
|
||||
if (Object.keys(subdirEntries).length === 0) {
|
||||
throw new Error(`GitHub path "${publishPath}" does not contain any files`);
|
||||
}
|
||||
await writeEntries(tempDir, subdirEntries);
|
||||
}
|
||||
catch (error) {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
dir: tempDir,
|
||||
source: {
|
||||
kind: "github",
|
||||
url: repoUrl,
|
||||
repo,
|
||||
ref: resolvedRef,
|
||||
commit,
|
||||
path: publishPath,
|
||||
importedAt: Date.now(),
|
||||
},
|
||||
cleanup: async () => {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
export function resolveLocalGitInfo(folder) {
|
||||
const root = runGit(folder, ["rev-parse", "--show-toplevel"]);
|
||||
if (!root)
|
||||
return null;
|
||||
const prefix = runGit(folder, ["rev-parse", "--show-prefix"]);
|
||||
const commit = runGit(folder, ["rev-parse", "HEAD"]) || undefined;
|
||||
const ref = runGit(folder, ["describe", "--tags", "--exact-match"]) ||
|
||||
runGit(folder, ["branch", "--show-current"]) ||
|
||||
commit;
|
||||
const repo = normalizeGitHubRepo(runGit(folder, ["remote", "get-url", "origin"]) || "");
|
||||
return {
|
||||
root: root,
|
||||
path: normalizePath(prefix || "") || ".",
|
||||
repo: repo || undefined,
|
||||
commit,
|
||||
ref: ref || undefined,
|
||||
};
|
||||
}
|
||||
export function normalizeGitHubRepo(value) {
|
||||
const trimmed = value
|
||||
.trim()
|
||||
.replace(/^git\+/, "")
|
||||
.replace(/\.git$/i, "")
|
||||
.replace(/^git@github\.com:/i, "https://github.com/");
|
||||
if (!trimmed)
|
||||
return undefined;
|
||||
const shorthand = trimmed.match(/^([a-z0-9_.-]+)\/([a-z0-9_.-]+)$/i);
|
||||
if (shorthand)
|
||||
return `${shorthand[1]}/${shorthand[2]}`;
|
||||
try {
|
||||
const url = new URL(trimmed);
|
||||
if (!GITHUB_HOSTS.has(url.hostname))
|
||||
return undefined;
|
||||
const segments = decodePathSegments(url.pathname);
|
||||
const owner = segments[0] ?? "";
|
||||
const repo = (segments[1] ?? "").replace(/\.git$/i, "");
|
||||
if (!owner || !repo)
|
||||
return undefined;
|
||||
return `${owner}/${repo}`;
|
||||
}
|
||||
catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
function parseGitHubShorthand(input) {
|
||||
const atIndex = input.lastIndexOf("@");
|
||||
const rawRepo = atIndex > 0 ? input.slice(0, atIndex) : input;
|
||||
const rawRef = atIndex > 0 ? input.slice(atIndex + 1).trim() : "";
|
||||
if (!rawRepo ||
|
||||
rawRepo.startsWith(".") ||
|
||||
rawRepo.startsWith("~") ||
|
||||
rawRepo.startsWith("/") ||
|
||||
rawRepo.includes("\\")) {
|
||||
return null;
|
||||
}
|
||||
const match = rawRepo.match(/^([a-z0-9_.-]+)\/([a-z0-9_.-]+)$/i);
|
||||
if (!match)
|
||||
return null;
|
||||
return {
|
||||
kind: "github",
|
||||
owner: match[1],
|
||||
repo: match[2],
|
||||
...(rawRef ? { ref: rawRef } : {}),
|
||||
path: ".",
|
||||
url: `https://github.com/${match[1]}/${match[2]}`,
|
||||
};
|
||||
}
|
||||
async function parseGitHubUrl(input) {
|
||||
let url;
|
||||
try {
|
||||
url = new URL(input);
|
||||
}
|
||||
catch {
|
||||
throw new Error("Invalid GitHub URL");
|
||||
}
|
||||
if (url.protocol !== "https:")
|
||||
throw new Error("Only https:// GitHub URLs are supported");
|
||||
if (!GITHUB_HOSTS.has(url.hostname))
|
||||
throw new Error("Only github.com URLs are supported");
|
||||
const segments = decodePathSegments(url.pathname);
|
||||
const owner = segments[0] ?? "";
|
||||
const repo = (segments[1] ?? "").replace(/\.git$/i, "");
|
||||
if (!owner || !repo)
|
||||
throw new Error("GitHub URL must be /<owner>/<repo>");
|
||||
const kind = segments[2] ?? "";
|
||||
if (!kind || (kind !== "tree" && kind !== "blob")) {
|
||||
return {
|
||||
kind: "github",
|
||||
owner,
|
||||
repo,
|
||||
path: ".",
|
||||
url: `https://github.com/${owner}/${repo}`,
|
||||
};
|
||||
}
|
||||
const { ref, path } = await resolveGitHubUrlRefAndPath(owner, repo, kind, segments.slice(3));
|
||||
return {
|
||||
kind: "github",
|
||||
owner,
|
||||
repo,
|
||||
ref,
|
||||
path,
|
||||
url: `https://github.com/${owner}/${repo}`,
|
||||
};
|
||||
}
|
||||
function normalizeRepoSubpath(value) {
|
||||
const normalized = normalizePath(value.trim());
|
||||
if (!normalized || normalized === ".")
|
||||
return ".";
|
||||
const segments = normalized.split("/");
|
||||
if (segments.some((segment) => !segment || segment === "." || segment === "..")) {
|
||||
throw new Error("Invalid GitHub path");
|
||||
}
|
||||
return segments.join("/");
|
||||
}
|
||||
function resolveLocalPath(workdir, input) {
|
||||
if (input === "~")
|
||||
return homedir();
|
||||
if (input.startsWith("~/"))
|
||||
return resolve(homedir(), input.slice(2));
|
||||
return resolve(workdir, input);
|
||||
}
|
||||
function normalizeLocalWorkdirs(workdir, localWorkdirs) {
|
||||
const values = localWorkdirs?.length ? localWorkdirs : [workdir];
|
||||
return Array.from(new Set(values.map((value) => resolve(value))));
|
||||
}
|
||||
function normalizePath(pathValue) {
|
||||
return pathValue
|
||||
.split(/[\\/]+/)
|
||||
.filter(Boolean)
|
||||
.join("/")
|
||||
.replace(/^\.\/+/, "");
|
||||
}
|
||||
function decodePathSegments(pathname) {
|
||||
return pathname
|
||||
.split("/")
|
||||
.map((segment) => segment.trim())
|
||||
.filter(Boolean)
|
||||
.map((segment) => {
|
||||
try {
|
||||
return decodeURIComponent(segment);
|
||||
}
|
||||
catch {
|
||||
throw new Error("Invalid GitHub URL");
|
||||
}
|
||||
});
|
||||
}
|
||||
async function resolveDefaultBranch(owner, repo, token) {
|
||||
const response = await fetch(`${GITHUB_API}/repos/${owner}/${repo}`, {
|
||||
headers: buildGitHubHeaders(token),
|
||||
});
|
||||
if (!response.ok)
|
||||
throw new Error(`GitHub repo not found: ${owner}/${repo}`);
|
||||
const parsed = (await response.json());
|
||||
const defaultBranch = typeof parsed.default_branch === "string" ? parsed.default_branch.trim() : "";
|
||||
if (!defaultBranch)
|
||||
throw new Error("GitHub repo default branch missing");
|
||||
return defaultBranch;
|
||||
}
|
||||
async function resolveCommitSha(owner, repo, ref, token) {
|
||||
const response = await fetch(`${GITHUB_API}/repos/${owner}/${repo}/commits/${encodeURIComponent(ref)}`, {
|
||||
headers: buildGitHubHeaders(token),
|
||||
});
|
||||
if (!response.ok)
|
||||
throw new Error(`GitHub ref not found: ${owner}/${repo}@${ref}`);
|
||||
const parsed = (await response.json());
|
||||
const sha = typeof parsed.sha === "string" ? parsed.sha.trim().toLowerCase() : "";
|
||||
if (!/^[a-f0-9]{40}$/.test(sha))
|
||||
throw new Error("GitHub commit sha missing");
|
||||
return sha;
|
||||
}
|
||||
async function tryResolveCommitSha(owner, repo, ref, token) {
|
||||
const response = await fetch(`${GITHUB_API}/repos/${owner}/${repo}/commits/${encodeURIComponent(ref)}`, {
|
||||
headers: buildGitHubHeaders(token),
|
||||
});
|
||||
if (!response.ok)
|
||||
return null;
|
||||
const parsed = (await response.json());
|
||||
const sha = typeof parsed.sha === "string" ? parsed.sha.trim().toLowerCase() : "";
|
||||
return /^[a-f0-9]{40}$/.test(sha) ? sha : null;
|
||||
}
|
||||
async function downloadGitHubZip(owner, repo, ref, token) {
|
||||
const response = await fetch(`${GITHUB_API}/repos/${owner}/${repo}/zipball/${encodeURIComponent(ref)}`, {
|
||||
headers: buildGitHubHeaders(token),
|
||||
});
|
||||
if (!response.ok)
|
||||
throw new Error(`GitHub archive download failed: ${owner}/${repo}@${ref}`);
|
||||
return new Uint8Array(await response.arrayBuffer());
|
||||
}
|
||||
function buildGitHubHeaders(token) {
|
||||
const headers = {
|
||||
Accept: "application/vnd.github+json",
|
||||
"User-Agent": ZIP_USER_AGENT,
|
||||
};
|
||||
if (token)
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
return headers;
|
||||
}
|
||||
function stripSingleTopLevelFolder(entries) {
|
||||
const paths = Object.keys(entries);
|
||||
if (paths.length === 0)
|
||||
return {};
|
||||
const firstRoot = paths[0]?.split("/")[0] ?? "";
|
||||
if (!firstRoot)
|
||||
return entries;
|
||||
const prefix = `${firstRoot}/`;
|
||||
if (!paths.every((path) => path.startsWith(prefix)))
|
||||
return entries;
|
||||
const stripped = {};
|
||||
for (const [path, bytes] of Object.entries(entries)) {
|
||||
const next = path.slice(prefix.length);
|
||||
if (!next)
|
||||
continue;
|
||||
stripped[next] = bytes;
|
||||
}
|
||||
return stripped;
|
||||
}
|
||||
function filterEntriesForSubpath(entries, subpath) {
|
||||
if (subpath === ".")
|
||||
return entries;
|
||||
const prefix = `${subpath}/`;
|
||||
const filtered = {};
|
||||
for (const [path, bytes] of Object.entries(entries)) {
|
||||
if (!path.startsWith(prefix))
|
||||
continue;
|
||||
const relPath = path.slice(prefix.length);
|
||||
if (!relPath)
|
||||
continue;
|
||||
filtered[relPath] = bytes;
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
async function writeEntries(root, entries) {
|
||||
const absRoot = resolve(root);
|
||||
for (const [path, bytes] of Object.entries(entries)) {
|
||||
if (!path || path.endsWith("/"))
|
||||
continue;
|
||||
const absPath = resolve(absRoot, ...path.split("/"));
|
||||
if (absPath !== absRoot && !absPath.startsWith(`${absRoot}${sep}`)) {
|
||||
throw new Error(`Unsafe path in archive: ${path}`);
|
||||
}
|
||||
await mkdir(dirname(absPath), { recursive: true });
|
||||
await writeFile(absPath, Buffer.from(bytes));
|
||||
}
|
||||
}
|
||||
async function resolveGitHubUrlRefAndPath(owner, repo, kind, segments) {
|
||||
if (segments.length === 0)
|
||||
throw new Error("Missing ref in GitHub URL");
|
||||
const token = process.env.GITHUB_TOKEN?.trim() || undefined;
|
||||
const minPathSegments = kind === "blob" ? 1 : 0;
|
||||
const maxRefSegments = segments.length - minPathSegments;
|
||||
for (let refSegmentCount = maxRefSegments; refSegmentCount >= 1; refSegmentCount -= 1) {
|
||||
const ref = segments.slice(0, refSegmentCount).join("/");
|
||||
const pathRemainder = segments.slice(refSegmentCount).join("/");
|
||||
if (kind === "blob" && !pathRemainder)
|
||||
continue;
|
||||
const commit = await tryResolveCommitSha(owner, repo, ref, token);
|
||||
if (!commit)
|
||||
continue;
|
||||
const path = kind === "blob"
|
||||
? normalizeRepoSubpath(pathRemainder.split("/").slice(0, -1).join("/") || ".")
|
||||
: normalizeRepoSubpath(pathRemainder || ".");
|
||||
return { ref, path };
|
||||
}
|
||||
throw new Error("GitHub ref not found in URL");
|
||||
}
|
||||
function runGit(cwd, args) {
|
||||
const result = spawnSync("git", ["-C", cwd, ...args], {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
});
|
||||
if (result.status !== 0)
|
||||
return null;
|
||||
const value = result.stdout.trim();
|
||||
return value || null;
|
||||
}
|
||||
//# sourceMappingURL=github.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+18
@@ -0,0 +1,18 @@
|
||||
import type { GlobalOpts } from "../types.js";
|
||||
type InspectOptions = {
|
||||
version?: string;
|
||||
tag?: string;
|
||||
versions?: boolean;
|
||||
limit?: number;
|
||||
files?: boolean;
|
||||
file?: string;
|
||||
json?: boolean;
|
||||
};
|
||||
type VerifySkillOptions = {
|
||||
version?: string;
|
||||
tag?: string;
|
||||
card?: boolean;
|
||||
};
|
||||
export declare function cmdInspect(opts: GlobalOpts, slug: string, options?: InspectOptions): Promise<void>;
|
||||
export declare function cmdVerifySkill(opts: GlobalOpts, slug: string, options?: VerifySkillOptions): Promise<void>;
|
||||
export {};
|
||||
+515
@@ -0,0 +1,515 @@
|
||||
import { apiRequest, fetchText, registryUrl } from "../../http.js";
|
||||
import { ApiRoutes, PLATFORM_SKILL_LICENSE, PLATFORM_SKILL_LICENSE_SUMMARY, ApiV1SkillModerationResponseSchema, ApiV1SkillResponseSchema, ApiV1SkillVerifyResponseSchema, ApiV1SkillVersionListResponseSchema, ApiV1SkillVersionResponseSchema, } from "../../schema/index.js";
|
||||
import { getOptionalAuthToken } from "../authToken.js";
|
||||
import { getRegistry } from "../registry.js";
|
||||
import { createCrabLoader, fail, formatError, styleText } from "../ui.js";
|
||||
export async function cmdInspect(opts, slug, options = {}) {
|
||||
const requested = parseSkillRef(slug);
|
||||
const trimmed = requested.slug;
|
||||
if (!trimmed)
|
||||
fail("Skill required");
|
||||
if (options.version && options.tag)
|
||||
fail("Use either --version or --tag");
|
||||
const token = await getOptionalAuthToken();
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
const spinner = createCrabLoader("Fetching skill");
|
||||
try {
|
||||
let skillResult = null;
|
||||
let moderationDiagnostics = null;
|
||||
try {
|
||||
skillResult = await fetchSkillDetail(registry, trimmed, requested.ownerHandle, token);
|
||||
}
|
||||
catch (error) {
|
||||
moderationDiagnostics = await fetchModerationDiagnostics(registry, trimmed, requested.ownerHandle, token);
|
||||
if (moderationDiagnostics?.moderation) {
|
||||
spinner.stop();
|
||||
const output = {
|
||||
skill: null,
|
||||
latestVersion: null,
|
||||
owner: null,
|
||||
moderation: moderationDiagnostics.moderation,
|
||||
version: null,
|
||||
versions: null,
|
||||
file: null,
|
||||
};
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
return;
|
||||
}
|
||||
printHiddenSkillModeration(trimmed, moderationDiagnostics.moderation, formatError(error));
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
if (!skillResult.skill) {
|
||||
spinner.fail("Skill not found");
|
||||
return;
|
||||
}
|
||||
moderationDiagnostics = await fetchModerationDiagnostics(registry, trimmed, requested.ownerHandle, token);
|
||||
const skill = skillResult.skill;
|
||||
const tags = normalizeTags(skill.tags);
|
||||
const latestVersion = skillResult.latestVersion?.version ?? tags.latest ?? null;
|
||||
const taggedVersion = options.tag ? (tags[options.tag] ?? null) : null;
|
||||
if (options.tag && !taggedVersion) {
|
||||
spinner.fail(`Unknown tag "${options.tag}"`);
|
||||
return;
|
||||
}
|
||||
const requestedVersion = options.version ?? taggedVersion ?? null;
|
||||
let versionResult = null;
|
||||
if (options.files || options.file || options.version || options.tag) {
|
||||
const targetVersion = requestedVersion ?? latestVersion;
|
||||
if (!targetVersion)
|
||||
fail("Could not resolve latest version");
|
||||
spinner.text = `Fetching ${trimmed}@${targetVersion}`;
|
||||
versionResult = await apiRequest(registry, {
|
||||
method: "GET",
|
||||
url: ownerScopedUrl(registry, `${ApiRoutes.skills}/${encodeURIComponent(trimmed)}/versions/${encodeURIComponent(targetVersion)}`, requested.ownerHandle),
|
||||
token,
|
||||
}, ApiV1SkillVersionResponseSchema);
|
||||
}
|
||||
let versionsList = null;
|
||||
if (options.versions) {
|
||||
const limit = clampLimit(options.limit ?? 25, 25);
|
||||
const url = registryUrl(`${ApiRoutes.skills}/${encodeURIComponent(trimmed)}/versions`, registry);
|
||||
if (requested.ownerHandle)
|
||||
url.searchParams.set("ownerHandle", requested.ownerHandle);
|
||||
url.searchParams.set("limit", String(limit));
|
||||
spinner.text = `Fetching versions (${limit})`;
|
||||
versionsList = await apiRequest(registry, { method: "GET", url: url.toString(), token }, ApiV1SkillVersionListResponseSchema);
|
||||
}
|
||||
let fileContent = null;
|
||||
if (options.file) {
|
||||
const url = registryUrl(`${ApiRoutes.skills}/${encodeURIComponent(trimmed)}/file`, registry);
|
||||
if (requested.ownerHandle)
|
||||
url.searchParams.set("ownerHandle", requested.ownerHandle);
|
||||
url.searchParams.set("path", options.file);
|
||||
if (options.version) {
|
||||
url.searchParams.set("version", options.version);
|
||||
}
|
||||
else if (options.tag) {
|
||||
url.searchParams.set("tag", options.tag);
|
||||
}
|
||||
else if (latestVersion) {
|
||||
url.searchParams.set("version", latestVersion);
|
||||
}
|
||||
spinner.text = `Fetching ${options.file}`;
|
||||
fileContent = await fetchText(registry, { url: url.toString(), token });
|
||||
}
|
||||
spinner.stop();
|
||||
const output = {
|
||||
skill: skillResult.skill,
|
||||
latestVersion: skillResult.latestVersion,
|
||||
owner: skillResult.owner,
|
||||
moderation: moderationDiagnostics?.moderation ?? skillResult.moderation ?? null,
|
||||
version: versionResult?.version ?? null,
|
||||
versions: versionsList?.items ?? null,
|
||||
file: options.file ? { path: options.file, content: fileContent } : null,
|
||||
};
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
return;
|
||||
}
|
||||
const shouldPrintMeta = !options.file || options.files || options.versions || options.version;
|
||||
if (shouldPrintMeta) {
|
||||
printSkillSummary({
|
||||
skill,
|
||||
latestVersion: skillResult.latestVersion,
|
||||
versionLicense: versionResult?.version?.license ?? null,
|
||||
owner: skillResult.owner,
|
||||
});
|
||||
printModerationSummary(moderationDiagnostics?.moderation ?? skillResult.moderation ?? null);
|
||||
}
|
||||
if (shouldPrintMeta && versionResult?.version) {
|
||||
printVersionSummary(versionResult.version);
|
||||
printSecuritySummary(versionResult.version);
|
||||
}
|
||||
if (shouldPrintMeta)
|
||||
printInspectFooter();
|
||||
if (versionsList?.items && Array.isArray(versionsList.items)) {
|
||||
if (versionsList.items.length === 0) {
|
||||
console.log("No versions found.");
|
||||
}
|
||||
else {
|
||||
console.log("Versions:");
|
||||
for (const item of versionsList.items) {
|
||||
console.log(formatVersionLine(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (versionResult?.version) {
|
||||
const files = normalizeFiles(versionResult.version.files);
|
||||
if (options.files) {
|
||||
if (files.length === 0) {
|
||||
console.log("No files found.");
|
||||
}
|
||||
else {
|
||||
console.log("Files:");
|
||||
for (const file of files) {
|
||||
console.log(formatFileLine(file));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (options.file && fileContent !== null) {
|
||||
if (shouldPrintMeta)
|
||||
console.log(`\n${options.file}:\n`);
|
||||
process.stdout.write(fileContent);
|
||||
if (!fileContent.endsWith("\n"))
|
||||
process.stdout.write("\n");
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(formatError(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
export async function cmdVerifySkill(opts, slug, options = {}) {
|
||||
const requested = parseSkillRef(slug);
|
||||
const trimmed = requested.slug;
|
||||
if (!trimmed)
|
||||
fail("Skill required");
|
||||
if (options.version && options.tag)
|
||||
fail("Use either --version or --tag");
|
||||
const token = await getOptionalAuthToken();
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
const spinner = createCrabLoader("Fetching skill verification");
|
||||
try {
|
||||
const url = registryUrl(`${ApiRoutes.skills}/${encodeURIComponent(trimmed)}/verify`, registry);
|
||||
if (requested.ownerHandle)
|
||||
url.searchParams.set("ownerHandle", requested.ownerHandle);
|
||||
if (options.version) {
|
||||
url.searchParams.set("version", options.version);
|
||||
}
|
||||
else if (options.tag) {
|
||||
url.searchParams.set("tag", options.tag);
|
||||
}
|
||||
const result = await apiRequest(registry, { method: "GET", url: url.toString(), token }, ApiV1SkillVerifyResponseSchema);
|
||||
if (options.card) {
|
||||
const cardUrl = readSkillCardUrl(result);
|
||||
if (!cardUrl)
|
||||
fail("Skill Card is not available");
|
||||
spinner.text = "Fetching Skill Card";
|
||||
const card = await fetchText(registry, { url: cardUrl, token });
|
||||
spinner.stop();
|
||||
process.stdout.write(card);
|
||||
if (!card.endsWith("\n"))
|
||||
process.stdout.write("\n");
|
||||
if (!readBoolean(result, "ok"))
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
spinner.stop();
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
if (!readBoolean(result, "ok"))
|
||||
process.exitCode = 1;
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(formatError(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
function parseSkillRef(raw) {
|
||||
const value = raw.trim();
|
||||
if (!value)
|
||||
fail("Skill required");
|
||||
const slashIndex = value.indexOf("/");
|
||||
if (slashIndex < 0)
|
||||
return { slug: value };
|
||||
if (value.indexOf("/", slashIndex + 1) >= 0)
|
||||
fail(`Invalid skill: ${value}`);
|
||||
const ownerHandle = value.slice(0, slashIndex).trim().replace(/^@+/, "");
|
||||
const slug = value.slice(slashIndex + 1).trim();
|
||||
if (!ownerHandle || !slug)
|
||||
fail(`Invalid skill: ${value}`);
|
||||
return { slug, ownerHandle };
|
||||
}
|
||||
function ownerScopedUrl(registry, path, ownerHandle) {
|
||||
const url = registryUrl(path, registry);
|
||||
if (ownerHandle)
|
||||
url.searchParams.set("ownerHandle", ownerHandle);
|
||||
return url.toString();
|
||||
}
|
||||
function fetchSkillDetail(registry, slug, ownerHandle, token) {
|
||||
return apiRequest(registry, {
|
||||
method: "GET",
|
||||
url: ownerScopedUrl(registry, `${ApiRoutes.skills}/${encodeURIComponent(slug)}`, ownerHandle),
|
||||
token,
|
||||
}, ApiV1SkillResponseSchema);
|
||||
}
|
||||
async function fetchModerationDiagnostics(registry, slug, ownerHandle, token) {
|
||||
if (!token)
|
||||
return null;
|
||||
try {
|
||||
return await apiRequest(registry, {
|
||||
method: "GET",
|
||||
url: ownerScopedUrl(registry, `${ApiRoutes.skills}/${encodeURIComponent(slug)}/moderation`, ownerHandle),
|
||||
token,
|
||||
}, ApiV1SkillModerationResponseSchema);
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function printHiddenSkillModeration(slug, moderation, detailError) {
|
||||
console.log(`${slug} is not publicly visible.`);
|
||||
console.log(`Detail: ${detailError}`);
|
||||
printModerationSummary(moderation);
|
||||
}
|
||||
function printSkillSummary(result) {
|
||||
const { skill } = result;
|
||||
console.log("");
|
||||
console.log(`${inspectRail("┌─")} ${styleText("inspect", "brand")} ${styleText("─".repeat(43), "muted")}`);
|
||||
console.log(`${inspectRail("│")} ${styleText(skill.slug, "brand")} ${styleText(skill.displayName, "strong")}`);
|
||||
const owner = formatOwner(result.owner);
|
||||
const tags = normalizeTags(skill.tags);
|
||||
const tagEntries = Object.entries(tags);
|
||||
const compactMeta = [
|
||||
owner,
|
||||
result.latestVersion?.version ? `v${result.latestVersion.version}` : null,
|
||||
tagEntries.map(([tag, version]) => `${tag}=${version}`).join(", "),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
if (compactMeta)
|
||||
console.log(`${inspectRail("│")} ${styleText(compactMeta, "muted")}`);
|
||||
console.log(inspectRail("│"));
|
||||
if (skill.summary)
|
||||
printInspectRow("Summary", skill.summary);
|
||||
if (owner)
|
||||
printInspectRow("Owner", owner);
|
||||
if (result.latestVersion?.version) {
|
||||
printInspectRow("Latest", result.latestVersion.version);
|
||||
}
|
||||
printInspectRow("License", `${result.versionLicense ?? result.latestVersion?.license ?? PLATFORM_SKILL_LICENSE} (${PLATFORM_SKILL_LICENSE_SUMMARY})`);
|
||||
printInspectRow("Updated", formatTimestamp(skill.updatedAt));
|
||||
printInspectRow("Created", formatTimestamp(skill.createdAt));
|
||||
if (tagEntries.length > 0) {
|
||||
printInspectRow("Tags", tagEntries.map(([tag, version]) => `${tag}=${version}`).join(", "));
|
||||
}
|
||||
}
|
||||
function printInspectRow(label, value) {
|
||||
console.log(`${inspectRail("│")} ${styleText(label.padEnd(8), "brand")} ${value}`);
|
||||
}
|
||||
function printInspectFooter() {
|
||||
console.log(`${inspectRail("└")}${styleText("─".repeat(54), "muted")}`);
|
||||
}
|
||||
function inspectRail(value) {
|
||||
return styleText(value, "brand");
|
||||
}
|
||||
function formatOwner(owner) {
|
||||
if (owner?.handle)
|
||||
return `@${owner.handle}`;
|
||||
return owner?.displayName ?? null;
|
||||
}
|
||||
function printVersionSummary(version) {
|
||||
if (!version || typeof version !== "object")
|
||||
return;
|
||||
const entry = version;
|
||||
const value = typeof entry.version === "string" ? entry.version : null;
|
||||
if (!value)
|
||||
return;
|
||||
printInspectRow("Selected", value);
|
||||
if (typeof entry.createdAt === "number") {
|
||||
printInspectRow("Sel Time", formatTimestamp(entry.createdAt));
|
||||
}
|
||||
if (typeof entry.changelog === "string" && entry.changelog.trim()) {
|
||||
printInspectRow("Change", truncate(entry.changelog, 120));
|
||||
}
|
||||
}
|
||||
function printModerationSummary(moderation) {
|
||||
const status = normalizeModeration(moderation);
|
||||
if (!status)
|
||||
return;
|
||||
const label = status.isMalwareBlocked
|
||||
? "MALICIOUS"
|
||||
: status.isSuspicious
|
||||
? "SUSPICIOUS"
|
||||
: (status.verdict ?? "clean").toUpperCase();
|
||||
printInspectRow("Moderate", label);
|
||||
if (status.reasonCodes?.length) {
|
||||
printInspectRow("Reasons", status.reasonCodes.join(", "));
|
||||
}
|
||||
if (status.legacyReason) {
|
||||
printInspectRow("Reason", status.legacyReason);
|
||||
}
|
||||
if (typeof status.updatedAt === "number") {
|
||||
printInspectRow("Mod Time", formatTimestamp(status.updatedAt));
|
||||
}
|
||||
if (status.engineVersion) {
|
||||
printInspectRow("Engine", status.engineVersion);
|
||||
}
|
||||
if (status.summary) {
|
||||
printInspectRow("Mod Note", truncate(status.summary, 160));
|
||||
}
|
||||
if (status.legacyReason === "quality.low") {
|
||||
printInspectRow("Guidance", "Visibility Guidance: publish a substantive update that passes quality assessment, then re-run inspect.");
|
||||
}
|
||||
}
|
||||
function normalizeModeration(moderation) {
|
||||
if (!moderation || typeof moderation !== "object")
|
||||
return null;
|
||||
const value = moderation;
|
||||
if (typeof value.isSuspicious !== "boolean")
|
||||
return null;
|
||||
if (typeof value.isMalwareBlocked !== "boolean")
|
||||
return null;
|
||||
const verdict = value.verdict === "clean" || value.verdict === "suspicious" || value.verdict === "malicious"
|
||||
? value.verdict
|
||||
: undefined;
|
||||
const reasonCodes = Array.isArray(value.reasonCodes)
|
||||
? value.reasonCodes.filter((reason) => typeof reason === "string")
|
||||
: undefined;
|
||||
return {
|
||||
isSuspicious: value.isSuspicious,
|
||||
isMalwareBlocked: value.isMalwareBlocked,
|
||||
verdict,
|
||||
reasonCodes,
|
||||
updatedAt: typeof value.updatedAt === "number" ? value.updatedAt : null,
|
||||
engineVersion: typeof value.engineVersion === "string" ? value.engineVersion : null,
|
||||
summary: typeof value.summary === "string" && value.summary.trim() ? value.summary : null,
|
||||
legacyReason: typeof value.legacyReason === "string" ? value.legacyReason : null,
|
||||
};
|
||||
}
|
||||
function normalizeTags(tags) {
|
||||
if (!tags || typeof tags !== "object")
|
||||
return {};
|
||||
const entries = Object.entries(tags);
|
||||
const resolved = {};
|
||||
for (const [tag, version] of entries) {
|
||||
if (typeof version === "string")
|
||||
resolved[tag] = version;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
function readRecord(value) {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
function readString(record, key) {
|
||||
const value = record?.[key];
|
||||
return typeof value === "string" && value.trim() ? value.trim() : null;
|
||||
}
|
||||
function readBoolean(value, key) {
|
||||
const record = readRecord(value);
|
||||
return record?.[key] === true;
|
||||
}
|
||||
function readSkillCardUrl(result) {
|
||||
const root = readRecord(result);
|
||||
const card = readRecord(root?.["card"]);
|
||||
if (!readBoolean(card, "available"))
|
||||
return null;
|
||||
return readString(card, "url");
|
||||
}
|
||||
function normalizeFiles(files) {
|
||||
if (!Array.isArray(files))
|
||||
return [];
|
||||
return files
|
||||
.map((file) => {
|
||||
if (!file || typeof file !== "object")
|
||||
return null;
|
||||
const entry = file;
|
||||
if (typeof entry.path !== "string")
|
||||
return null;
|
||||
const size = typeof entry.size === "number" ? entry.size : Number(entry.size);
|
||||
const sha256 = typeof entry.sha256 === "string" ? entry.sha256 : null;
|
||||
const contentType = typeof entry.contentType === "string" ? entry.contentType : null;
|
||||
return {
|
||||
path: entry.path,
|
||||
size: Number.isFinite(size) ? size : null,
|
||||
sha256,
|
||||
contentType,
|
||||
};
|
||||
})
|
||||
.filter((entry) => Boolean(entry));
|
||||
}
|
||||
function formatVersionLine(item) {
|
||||
if (!item || typeof item !== "object")
|
||||
return "-";
|
||||
const entry = item;
|
||||
const version = typeof entry.version === "string" ? entry.version : "?";
|
||||
const createdAt = typeof entry.createdAt === "number" ? formatTimestamp(entry.createdAt) : "unknown";
|
||||
const changelog = typeof entry.changelog === "string" ? entry.changelog : "";
|
||||
const snippet = changelog ? ` ${truncate(changelog, 80)}` : "";
|
||||
return `${version} ${createdAt}${snippet}`;
|
||||
}
|
||||
function printSecuritySummary(version) {
|
||||
if (!version || typeof version !== "object")
|
||||
return;
|
||||
const sec = normalizeSecurity(version.security);
|
||||
if (!sec)
|
||||
return;
|
||||
printInspectRow("Security", sec.status.toUpperCase());
|
||||
if (sec.hasWarnings) {
|
||||
printInspectRow("Warnings", "yes");
|
||||
}
|
||||
if (typeof sec.checkedAt === "number") {
|
||||
printInspectRow("Checked", formatTimestamp(sec.checkedAt));
|
||||
}
|
||||
if (sec.model) {
|
||||
printInspectRow("Model", sec.model);
|
||||
}
|
||||
}
|
||||
function normalizeSecurity(security) {
|
||||
if (!security || typeof security !== "object")
|
||||
return null;
|
||||
const value = security;
|
||||
if (value.status !== "clean" &&
|
||||
value.status !== "suspicious" &&
|
||||
value.status !== "malicious" &&
|
||||
value.status !== "pending" &&
|
||||
value.status !== "error") {
|
||||
return null;
|
||||
}
|
||||
if (typeof value.hasWarnings !== "boolean")
|
||||
return null;
|
||||
const checkedAt = typeof value.checkedAt === "number" ? value.checkedAt : null;
|
||||
const model = typeof value.model === "string" ? value.model : null;
|
||||
return {
|
||||
status: value.status,
|
||||
hasWarnings: value.hasWarnings,
|
||||
checkedAt,
|
||||
model,
|
||||
};
|
||||
}
|
||||
function formatFileLine(file) {
|
||||
const size = file.size === null ? "?" : formatBytes(file.size);
|
||||
const sha = file.sha256 ?? "?";
|
||||
const type = file.contentType ? ` ${file.contentType}` : "";
|
||||
return `${file.path} ${size} ${sha}${type}`;
|
||||
}
|
||||
function formatTimestamp(timestamp) {
|
||||
if (!Number.isFinite(timestamp))
|
||||
return "unknown";
|
||||
const date = new Date(timestamp);
|
||||
const year = date.getUTCFullYear();
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getUTCDate()).padStart(2, "0");
|
||||
const hours = String(date.getUTCHours()).padStart(2, "0");
|
||||
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
|
||||
return `${year}-${month}-${day} ${hours}:${minutes} UTC`;
|
||||
}
|
||||
function formatBytes(bytes) {
|
||||
if (!Number.isFinite(bytes))
|
||||
return "?";
|
||||
const units = ["B", "KB", "MB", "GB"];
|
||||
let value = bytes;
|
||||
let index = 0;
|
||||
while (value >= 1024 && index < units.length - 1) {
|
||||
value /= 1024;
|
||||
index += 1;
|
||||
}
|
||||
const rounded = value >= 10 ? Math.round(value) : Math.round(value * 10) / 10;
|
||||
return `${rounded}${units[index]}`;
|
||||
}
|
||||
function clampLimit(limit, fallback) {
|
||||
if (!Number.isFinite(limit))
|
||||
return fallback;
|
||||
return Math.min(Math.max(1, Math.round(limit)), 200);
|
||||
}
|
||||
function truncate(str, maxLen) {
|
||||
if (str.length <= maxLen)
|
||||
return str;
|
||||
return `${str.slice(0, maxLen - 3)}...`;
|
||||
}
|
||||
//# sourceMappingURL=inspect.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+6
@@ -0,0 +1,6 @@
|
||||
export declare function reportInstalledSkillsTelemetryIfEnabled(params: {
|
||||
token: string | undefined;
|
||||
registry: string;
|
||||
slug: string;
|
||||
version?: string | null;
|
||||
}): Promise<void>;
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { apiRequest } from "../../http.js";
|
||||
import { ApiCliTelemetryInstallResponseSchema, LegacyApiRoutes } from "../../schema/index.js";
|
||||
export async function reportInstalledSkillsTelemetryIfEnabled(params) {
|
||||
if (!params.token || isTelemetryDisabled())
|
||||
return;
|
||||
const slug = params.slug.trim();
|
||||
if (!slug)
|
||||
return;
|
||||
try {
|
||||
await apiRequest(params.registry, {
|
||||
method: "POST",
|
||||
path: LegacyApiRoutes.cliTelemetryInstall,
|
||||
token: params.token,
|
||||
body: {
|
||||
event: "install",
|
||||
slug,
|
||||
version: params.version ?? undefined,
|
||||
},
|
||||
}, ApiCliTelemetryInstallResponseSchema);
|
||||
}
|
||||
catch {
|
||||
// Install telemetry is best-effort; local installs must not fail because
|
||||
// metrics reporting is unavailable.
|
||||
}
|
||||
}
|
||||
function isTelemetryDisabled() {
|
||||
const raw = process.env.CLAWHUB_DISABLE_TELEMETRY ?? process.env.CLAWDHUB_DISABLE_TELEMETRY;
|
||||
if (!raw)
|
||||
return false;
|
||||
return ["1", "true", "yes", "on"].includes(raw.trim().toLowerCase());
|
||||
}
|
||||
//# sourceMappingURL=installTelemetry.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"installTelemetry.js","sourceRoot":"","sources":["../../../src/cli/commands/installTelemetry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,oCAAoC,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE9F,MAAM,CAAC,KAAK,UAAU,uCAAuC,CAAC,MAK7D;IACC,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,mBAAmB,EAAE;QAAE,OAAO;IACnD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IAChC,IAAI,CAAC,IAAI;QAAE,OAAO;IAElB,IAAI,CAAC;QACH,MAAM,UAAU,CACd,MAAM,CAAC,QAAQ,EACf;YACE,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,eAAe,CAAC,mBAAmB;YACzC,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,IAAI,EAAE;gBACJ,KAAK,EAAE,SAAS;gBAChB,IAAI;gBACJ,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,SAAS;aACrC;SACF,EACD,oCAAoC,CACrC,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,yEAAyE;QACzE,oCAAoC;IACtC,CAAC;AACH,CAAC;AAED,SAAS,mBAAmB;IAC1B,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,yBAAyB,IAAI,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC;IAC5F,IAAI,CAAC,GAAG;QAAE,OAAO,KAAK,CAAC;IACvB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;AACvE,CAAC"}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
type ModerationPlanOptions = {
|
||||
json?: boolean;
|
||||
yes?: boolean;
|
||||
};
|
||||
type ModerationPlan = {
|
||||
subject: string;
|
||||
outcome: string;
|
||||
impacts: string[];
|
||||
requiresConfirmation: boolean;
|
||||
confirmPrompt: string;
|
||||
};
|
||||
export declare function reportModerationPlan(params: {
|
||||
entityLabel: "skill" | "package";
|
||||
reportId: string;
|
||||
status: "open" | "confirmed" | "dismissed";
|
||||
finalAction?: "none" | "hide" | "quarantine" | "revoke";
|
||||
}): ModerationPlan;
|
||||
export declare function presentModerationPlan(plan: ModerationPlan, options: ModerationPlanOptions): Promise<void>;
|
||||
export {};
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { fail, isInteractive, promptConfirm } from "../ui.js";
|
||||
export function reportModerationPlan(params) {
|
||||
const impacts = [];
|
||||
if (params.status === "open") {
|
||||
impacts.push("Reopen the report for review.");
|
||||
}
|
||||
else if (params.status === "confirmed") {
|
||||
impacts.push("Mark the report as confirmed.");
|
||||
}
|
||||
else {
|
||||
impacts.push("Dismiss the report without changing artifact availability.");
|
||||
}
|
||||
if (params.finalAction === "hide") {
|
||||
impacts.push("Hide the skill from public availability.");
|
||||
}
|
||||
else if (params.finalAction === "quarantine") {
|
||||
impacts.push("Quarantine the package release.");
|
||||
}
|
||||
else if (params.finalAction === "revoke") {
|
||||
impacts.push("Revoke the package release.");
|
||||
}
|
||||
const action = params.finalAction && params.finalAction !== "none" ? params.finalAction : "none";
|
||||
return {
|
||||
subject: `${params.entityLabel} report ${params.reportId}`,
|
||||
outcome: `set status to ${params.status}; final action ${action}`,
|
||||
impacts,
|
||||
requiresConfirmation: action !== "none",
|
||||
confirmPrompt: `Apply this ${params.entityLabel} report action?`,
|
||||
};
|
||||
}
|
||||
export async function presentModerationPlan(plan, options) {
|
||||
if (!options.json) {
|
||||
console.log("Moderation action summary");
|
||||
console.log(` case: ${plan.subject}`);
|
||||
console.log(` outcome: ${plan.outcome}`);
|
||||
console.log(" public impact:");
|
||||
for (const impact of plan.impacts) {
|
||||
console.log(` - ${impact}`);
|
||||
}
|
||||
}
|
||||
if (!plan.requiresConfirmation || options.yes)
|
||||
return;
|
||||
if (!isInteractive())
|
||||
fail("Pass --yes (no input)");
|
||||
const confirmed = await promptConfirm(plan.confirmPrompt);
|
||||
if (!confirmed)
|
||||
fail("Canceled");
|
||||
}
|
||||
//# sourceMappingURL=moderationPlan.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"moderationPlan.js","sourceRoot":"","sources":["../../../src/cli/commands/moderationPlan.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAe9D,MAAM,UAAU,oBAAoB,CAAC,MAKpC;IACC,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QAC7B,OAAO,CAAC,IAAI,CAAC,+BAA+B,CAAC,CAAC;IAChD,CAAC;SAAM,IAAI,MAAM,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;QACzC,OAAO,CAAC,IAAI,CAAC,+BAA+B,CAAC,CAAC;IAChD,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,IAAI,CAAC,4DAA4D,CAAC,CAAC;IAC7E,CAAC;IAED,IAAI,MAAM,CAAC,WAAW,KAAK,MAAM,EAAE,CAAC;QAClC,OAAO,CAAC,IAAI,CAAC,0CAA0C,CAAC,CAAC;IAC3D,CAAC;SAAM,IAAI,MAAM,CAAC,WAAW,KAAK,YAAY,EAAE,CAAC;QAC/C,OAAO,CAAC,IAAI,CAAC,iCAAiC,CAAC,CAAC;IAClD,CAAC;SAAM,IAAI,MAAM,CAAC,WAAW,KAAK,QAAQ,EAAE,CAAC;QAC3C,OAAO,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;IAC9C,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,IAAI,MAAM,CAAC,WAAW,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC;IACjG,OAAO;QACL,OAAO,EAAE,GAAG,MAAM,CAAC,WAAW,WAAW,MAAM,CAAC,QAAQ,EAAE;QAC1D,OAAO,EAAE,iBAAiB,MAAM,CAAC,MAAM,kBAAkB,MAAM,EAAE;QACjE,OAAO;QACP,oBAAoB,EAAE,MAAM,KAAK,MAAM;QACvC,aAAa,EAAE,cAAc,MAAM,CAAC,WAAW,iBAAiB;KACjE,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,IAAoB,EAAE,OAA8B;IAC9F,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QAClB,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC;QACzC,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QACvC,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QAC1C,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;QAChC,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAClC,OAAO,CAAC,GAAG,CAAC,OAAO,MAAM,EAAE,CAAC,CAAC;QAC/B,CAAC;IACH,CAAC;IAED,IAAI,CAAC,IAAI,CAAC,oBAAoB,IAAI,OAAO,CAAC,GAAG;QAAE,OAAO;IACtD,IAAI,CAAC,aAAa,EAAE;QAAE,IAAI,CAAC,uBAAuB,CAAC,CAAC;IACpD,MAAM,SAAS,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC1D,IAAI,CAAC,SAAS;QAAE,IAAI,CAAC,UAAU,CAAC,CAAC;AACnC,CAAC"}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import type { GlobalOpts } from "../types.js";
|
||||
type ConfirmOptions = {
|
||||
yes?: boolean;
|
||||
};
|
||||
export declare function cmdRenameSkill(opts: GlobalOpts, slugArg: string, newSlugArg: string, options: ConfirmOptions, inputAllowed: boolean): Promise<{
|
||||
ok: true;
|
||||
slug: string;
|
||||
previousSlug: string;
|
||||
} | undefined>;
|
||||
export declare function cmdMergeSkill(opts: GlobalOpts, sourceSlugArg: string, targetSlugArg: string, options: ConfirmOptions, inputAllowed: boolean): Promise<{
|
||||
ok: true;
|
||||
sourceSlug: string;
|
||||
targetSlug: string;
|
||||
} | undefined>;
|
||||
export {};
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
import { apiRequest } from "../../http.js";
|
||||
import { ApiRoutes, ApiV1SkillMergeResponseSchema, ApiV1SkillRenameResponseSchema, parseArk, } from "../../schema/index.js";
|
||||
import { requireAuthToken } from "../authToken.js";
|
||||
import { getRegistry } from "../registry.js";
|
||||
import { createCrabLoader, fail, formatError, isInteractive, promptConfirm } from "../ui.js";
|
||||
function normalizeOwnerHandle(raw) {
|
||||
const handle = raw?.trim().replace(/^@+/, "").toLowerCase();
|
||||
if (!handle)
|
||||
return undefined;
|
||||
if (handle.includes("/") || handle.includes("\\") || handle.includes("..")) {
|
||||
fail(`Invalid owner handle: ${raw}`);
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
function normalizeSlug(slugArg, label = "Skill") {
|
||||
const slug = slugArg.trim().toLowerCase();
|
||||
if (!slug)
|
||||
fail(`${label} required`);
|
||||
if (slug.includes("/") || slug.includes("\\") || slug.includes("..")) {
|
||||
fail(`Invalid ${label.toLowerCase()}: ${slugArg}`);
|
||||
}
|
||||
return slug;
|
||||
}
|
||||
function parseSkillRef(raw, label = "Skill") {
|
||||
const ref = raw.trim();
|
||||
if (!ref)
|
||||
fail(`${label} required`);
|
||||
const slashIndex = ref.indexOf("/");
|
||||
if (slashIndex < 0)
|
||||
return { slug: normalizeSlug(ref, label) };
|
||||
if (ref.indexOf("/", slashIndex + 1) >= 0)
|
||||
fail(`Invalid ${label.toLowerCase()} ref: ${ref}`);
|
||||
const ownerHandle = normalizeOwnerHandle(ref.slice(0, slashIndex));
|
||||
const slug = normalizeSlug(ref.slice(slashIndex + 1), label);
|
||||
if (!ownerHandle)
|
||||
fail(`Invalid ${label.toLowerCase()} ref: ${ref}`);
|
||||
return { slug, ownerHandle };
|
||||
}
|
||||
function formatSkillRef(ref) {
|
||||
return ref.ownerHandle ? `@${ref.ownerHandle}/${ref.slug}` : ref.slug;
|
||||
}
|
||||
function canPrompt(inputAllowed) {
|
||||
return isInteractive() && inputAllowed !== false;
|
||||
}
|
||||
async function requireYesOrConfirm(options, inputAllowed, prompt) {
|
||||
if (options.yes)
|
||||
return true;
|
||||
if (!canPrompt(inputAllowed))
|
||||
fail("Pass --yes (no input)");
|
||||
return promptConfirm(prompt);
|
||||
}
|
||||
export async function cmdRenameSkill(opts, slugArg, newSlugArg, options, inputAllowed) {
|
||||
const source = parseSkillRef(slugArg);
|
||||
const slug = source.slug;
|
||||
const newSlug = normalizeSlug(newSlugArg, "New slug");
|
||||
if (slug === newSlug)
|
||||
fail("New slug must be different");
|
||||
const confirmed = await requireYesOrConfirm(options, inputAllowed, `Rename ${formatSkillRef(source)} to ${newSlug}? Old slug will redirect.`);
|
||||
if (!confirmed)
|
||||
return undefined;
|
||||
const token = await requireAuthToken();
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
const spinner = createCrabLoader(`Renaming ${formatSkillRef(source)} to ${newSlug}`);
|
||||
try {
|
||||
const result = await apiRequest(registry, {
|
||||
method: "POST",
|
||||
path: `${ApiRoutes.skills}/${encodeURIComponent(slug)}/rename`,
|
||||
token,
|
||||
body: {
|
||||
newSlug,
|
||||
...(source.ownerHandle ? { ownerHandle: source.ownerHandle } : {}),
|
||||
},
|
||||
}, ApiV1SkillRenameResponseSchema);
|
||||
const parsed = parseArk(ApiV1SkillRenameResponseSchema, result, "Rename skill response");
|
||||
spinner.succeed(`Renamed ${parsed.previousSlug} to ${parsed.slug}`);
|
||||
return parsed;
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(formatError(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
export async function cmdMergeSkill(opts, sourceSlugArg, targetSlugArg, options, inputAllowed) {
|
||||
const source = parseSkillRef(sourceSlugArg, "Source skill");
|
||||
const target = parseSkillRef(targetSlugArg, "Target skill");
|
||||
const sourceSlug = source.slug;
|
||||
const targetSlug = target.slug;
|
||||
const targetOwnerHandle = target.ownerHandle ?? source.ownerHandle;
|
||||
if (sourceSlug === targetSlug && source.ownerHandle === targetOwnerHandle) {
|
||||
fail("Target slug must be different");
|
||||
}
|
||||
const confirmed = await requireYesOrConfirm(options, inputAllowed, `Merge ${formatSkillRef(source)} into ${formatSkillRef(target)}? Source slug will redirect and stop listing publicly.`);
|
||||
if (!confirmed)
|
||||
return undefined;
|
||||
const token = await requireAuthToken();
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
const spinner = createCrabLoader(`Merging ${formatSkillRef(source)} into ${formatSkillRef(target)}`);
|
||||
try {
|
||||
const result = await apiRequest(registry, {
|
||||
method: "POST",
|
||||
path: `${ApiRoutes.skills}/${encodeURIComponent(sourceSlug)}/merge`,
|
||||
token,
|
||||
body: {
|
||||
targetSlug,
|
||||
...(source.ownerHandle
|
||||
? { ownerHandle: source.ownerHandle, sourceOwnerHandle: source.ownerHandle }
|
||||
: {}),
|
||||
...(targetOwnerHandle ? { targetOwnerHandle } : {}),
|
||||
},
|
||||
}, ApiV1SkillMergeResponseSchema);
|
||||
const parsed = parseArk(ApiV1SkillMergeResponseSchema, result, "Merge skill response");
|
||||
spinner.succeed(`Merged ${parsed.sourceSlug} into ${parsed.targetSlug}`);
|
||||
return parsed;
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(formatError(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=ownership.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+132
@@ -0,0 +1,132 @@
|
||||
import { type PackageFamily } from "../../schema/index.js";
|
||||
import type { GlobalOpts } from "../types.js";
|
||||
type PackageInspectOptions = {
|
||||
version?: string;
|
||||
tag?: string;
|
||||
versions?: boolean;
|
||||
limit?: number;
|
||||
files?: boolean;
|
||||
file?: string;
|
||||
json?: boolean;
|
||||
};
|
||||
type PackageExploreOptions = {
|
||||
family?: PackageFamily;
|
||||
official?: boolean;
|
||||
limit?: number;
|
||||
json?: boolean;
|
||||
};
|
||||
type PackagePublishOptions = {
|
||||
family?: "code-plugin" | "bundle-plugin";
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
owner?: string;
|
||||
version?: string;
|
||||
changelog?: string;
|
||||
manualOverrideReason?: string;
|
||||
tags?: string;
|
||||
categories?: string;
|
||||
topics?: string;
|
||||
bundleFormat?: string;
|
||||
hostTargets?: string;
|
||||
sourceRepo?: string;
|
||||
sourceCommit?: string;
|
||||
sourceRef?: string;
|
||||
sourcePath?: string;
|
||||
dryRun?: boolean;
|
||||
json?: boolean;
|
||||
};
|
||||
type PackagePackOptions = {
|
||||
packDestination?: string;
|
||||
json?: boolean;
|
||||
};
|
||||
type PackageValidateOptions = {
|
||||
out?: string;
|
||||
openclaw?: string;
|
||||
runtime?: boolean;
|
||||
allowExecute?: boolean;
|
||||
mockSdk?: boolean;
|
||||
json?: boolean;
|
||||
};
|
||||
type PackageDownloadOptions = {
|
||||
version?: string;
|
||||
tag?: string;
|
||||
output?: string;
|
||||
force?: boolean;
|
||||
json?: boolean;
|
||||
};
|
||||
type PackageVerifyOptions = {
|
||||
packageName?: string;
|
||||
version?: string;
|
||||
tag?: string;
|
||||
sha256?: string;
|
||||
npmIntegrity?: string;
|
||||
npmShasum?: string;
|
||||
json?: boolean;
|
||||
};
|
||||
type PackageReportOptions = {
|
||||
version?: string;
|
||||
reason?: string;
|
||||
json?: boolean;
|
||||
};
|
||||
type PackageModerationStatusOptions = {
|
||||
json?: boolean;
|
||||
};
|
||||
type PackageReadinessOptions = {
|
||||
json?: boolean;
|
||||
};
|
||||
type PackageMigrationStatusOptions = PackageReadinessOptions;
|
||||
type PackageTrustedPublisherGetOptions = {
|
||||
json?: boolean;
|
||||
};
|
||||
type PackageTrustedPublisherSetOptions = {
|
||||
repository?: string;
|
||||
workflowFilename?: string;
|
||||
environment?: string;
|
||||
json?: boolean;
|
||||
};
|
||||
type PackageTrustedPublisherDeleteOptions = {
|
||||
json?: boolean;
|
||||
};
|
||||
type PackageDeleteOptions = {
|
||||
yes?: boolean;
|
||||
json?: boolean;
|
||||
version?: string;
|
||||
};
|
||||
type PackageUndeleteOptions = Omit<PackageDeleteOptions, "version">;
|
||||
type PackageTransferOptions = {
|
||||
to: string;
|
||||
reason?: string;
|
||||
json?: boolean;
|
||||
};
|
||||
export declare function cmdExplorePackages(opts: GlobalOpts, query: string, options?: PackageExploreOptions): Promise<void>;
|
||||
export declare function cmdInspectPackage(opts: GlobalOpts, packageName: string, options?: PackageInspectOptions): Promise<void>;
|
||||
export declare function cmdGetPackageTrustedPublisher(opts: GlobalOpts, packageName: string, options?: PackageTrustedPublisherGetOptions): Promise<void>;
|
||||
export declare function cmdSetPackageTrustedPublisher(opts: GlobalOpts, packageName: string, options: PackageTrustedPublisherSetOptions): Promise<void>;
|
||||
export declare function cmdDeletePackageTrustedPublisher(opts: GlobalOpts, packageName: string, options?: PackageTrustedPublisherDeleteOptions): Promise<void>;
|
||||
export declare function cmdPackPackage(opts: GlobalOpts, sourceArg: string, options?: PackagePackOptions): Promise<void>;
|
||||
export declare function cmdValidatePackage(opts: GlobalOpts, sourceArg: string, options?: PackageValidateOptions): Promise<void>;
|
||||
export declare function cmdPublishPackage(opts: GlobalOpts, sourceArg: string, options?: PackagePublishOptions): Promise<void>;
|
||||
export declare function cmdDownloadPackage(opts: GlobalOpts, packageName: string, options?: PackageDownloadOptions): Promise<void>;
|
||||
export declare function cmdVerifyPackage(opts: GlobalOpts, filePath: string, options?: PackageVerifyOptions): Promise<void>;
|
||||
export declare function cmdDeletePackage(opts: GlobalOpts, nameArg: string, options?: PackageDeleteOptions, inputAllowed?: boolean): Promise<{
|
||||
ok: true;
|
||||
slugReservedUntil?: number | undefined;
|
||||
} | undefined>;
|
||||
export declare function cmdUndeletePackage(opts: GlobalOpts, nameArg: string, options?: PackageUndeleteOptions, inputAllowed?: boolean): Promise<{
|
||||
ok: true;
|
||||
slugReservedUntil?: number | undefined;
|
||||
} | undefined>;
|
||||
export declare function cmdTransferPackage(opts: GlobalOpts, nameArg: string, options: PackageTransferOptions): Promise<{
|
||||
ok: true;
|
||||
packageId: string;
|
||||
name: string;
|
||||
ownerUserId: string;
|
||||
channel: "official" | "community" | "private";
|
||||
isOfficial: boolean;
|
||||
ownerPublisherId?: string | undefined;
|
||||
}>;
|
||||
export declare function cmdReportPackage(opts: GlobalOpts, packageName: string, options?: PackageReportOptions): Promise<void>;
|
||||
export declare function cmdPackageModerationStatus(opts: GlobalOpts, packageName: string, options?: PackageModerationStatusOptions): Promise<void>;
|
||||
export declare function cmdPackageReadiness(opts: GlobalOpts, packageName: string, options?: PackageReadinessOptions): Promise<void>;
|
||||
export declare function cmdPackageMigrationStatus(opts: GlobalOpts, packageName: string, options?: PackageMigrationStatusOptions): Promise<void>;
|
||||
export {};
|
||||
+2035
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+40
@@ -0,0 +1,40 @@
|
||||
import type { GlobalOpts } from "../types.js";
|
||||
type SkillPublishResult = {
|
||||
ok: true;
|
||||
status: "unchanged" | "would-publish" | "published";
|
||||
slug: string;
|
||||
displayName: string;
|
||||
folder: string;
|
||||
version: string;
|
||||
latestVersion: string | null;
|
||||
fileCount: number;
|
||||
fingerprint: string;
|
||||
versionId?: string;
|
||||
};
|
||||
export declare function cmdPublish(opts: GlobalOpts, folderArg: string, options: {
|
||||
slug?: string;
|
||||
name?: string;
|
||||
owner?: string;
|
||||
sourceOwner?: string;
|
||||
version?: string;
|
||||
changelog?: string;
|
||||
tags?: string;
|
||||
categories?: string;
|
||||
topics?: string;
|
||||
forkOf?: string;
|
||||
migrateOwner?: boolean;
|
||||
sourceRepo?: string;
|
||||
sourceCommit?: string;
|
||||
sourceRef?: string;
|
||||
sourcePath?: string;
|
||||
dryRun?: boolean;
|
||||
json?: boolean;
|
||||
quiet?: boolean;
|
||||
}): Promise<SkillPublishResult>;
|
||||
export declare function resolveDefaultOwnerHandle(registry: string, token: string): Promise<string>;
|
||||
export declare function prepareSkillFilesForPublish(folder: string): Promise<{
|
||||
relPath: string;
|
||||
bytes: Uint8Array;
|
||||
contentType?: string;
|
||||
}[]>;
|
||||
export {};
|
||||
+314
@@ -0,0 +1,314 @@
|
||||
import { readFile, readdir, stat } from "node:fs/promises";
|
||||
import { basename, join, resolve } from "node:path";
|
||||
import semver from "semver";
|
||||
import { apiRequest, apiRequestForm, registryUrl } from "../../http.js";
|
||||
import { ApiRoutes, ApiV1PublishResponseSchema, ApiV1SkillResolveResponseSchema, ApiV1WhoamiResponseSchema, } from "../../schema/index.js";
|
||||
import { hashSkillFiles, listTextFiles } from "../../skills.js";
|
||||
import { getOptionalAuthToken, requireAuthToken } from "../authToken.js";
|
||||
import { getRegistry } from "../registry.js";
|
||||
import { sanitizeSlug, titleCase } from "../slug.js";
|
||||
import { createCrabLoader, fail, formatError } from "../ui.js";
|
||||
import { normalizeGitHubRepo } from "./github.js";
|
||||
export async function cmdPublish(opts, folderArg, options) {
|
||||
const folder = folderArg ? resolve(opts.workdir, folderArg) : null;
|
||||
if (!folder)
|
||||
fail("Path required");
|
||||
const folderStat = await stat(folder).catch(() => null);
|
||||
if (!folderStat || !folderStat.isDirectory())
|
||||
fail("Path must be a folder");
|
||||
if (await looksLikePluginFolder(folder)) {
|
||||
fail('This looks like a plugin. Use "clawhub package publish <source>" instead.');
|
||||
}
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
const slug = options.slug ?? sanitizeSlug(basename(folder));
|
||||
const displayName = options.name ?? titleCase(basename(folder));
|
||||
const explicitOwnerHandle = options.owner?.trim().replace(/^@+/, "");
|
||||
const explicitSourceOwnerHandle = options.sourceOwner?.trim().replace(/^@+/, "");
|
||||
const explicitVersion = options.version;
|
||||
const changelog = options.changelog ?? "";
|
||||
const tagsValue = options.tags ?? "latest";
|
||||
const tags = tagsValue
|
||||
.split(",")
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean);
|
||||
const hasExplicitCatalogMetadata = options.categories !== undefined || options.topics !== undefined;
|
||||
const categories = parseCsv(options.categories);
|
||||
const topics = parseCsv(options.topics);
|
||||
const forkOfRaw = options.forkOf?.trim();
|
||||
const forkOf = forkOfRaw ? parseForkOf(forkOfRaw) : undefined;
|
||||
const source = buildPublishSource(options);
|
||||
if (!slug)
|
||||
fail("--slug required");
|
||||
if (!displayName)
|
||||
fail("--name required");
|
||||
if (explicitVersion && !semver.valid(explicitVersion))
|
||||
fail("--version must be valid semver");
|
||||
const spinner = options.json || options.quiet ? null : createCrabLoader(`Preparing ${slug}`);
|
||||
try {
|
||||
const filesOnDisk = await prepareSkillFilesForPublish(folder);
|
||||
if (filesOnDisk.length === 0)
|
||||
fail("No files found");
|
||||
if (!filesOnDisk.some((file) => {
|
||||
const lower = file.relPath.toLowerCase();
|
||||
return lower === "skill.md" || lower === "skills.md";
|
||||
})) {
|
||||
fail("SKILL.md required");
|
||||
}
|
||||
const hashed = hashSkillFiles(filesOnDisk);
|
||||
const optionalToken = await getOptionalAuthToken();
|
||||
let defaultOwnerHandle;
|
||||
const getDefaultOwnerHandle = async (token) => {
|
||||
defaultOwnerHandle ??= await resolveDefaultOwnerHandle(registry, token);
|
||||
return defaultOwnerHandle;
|
||||
};
|
||||
const ownerHandle = explicitOwnerHandle ||
|
||||
(optionalToken ? await getDefaultOwnerHandle(optionalToken) : undefined);
|
||||
const sourceOwnerHandle = options.migrateOwner && ownerHandle
|
||||
? explicitSourceOwnerHandle ||
|
||||
(optionalToken ? await getDefaultOwnerHandle(optionalToken) : undefined)
|
||||
: undefined;
|
||||
const resolved = await resolveSkillVersion(registry, slug, hashed.fingerprint, ownerHandle, optionalToken);
|
||||
const latestVersion = resolved.latestVersion?.version ?? null;
|
||||
if (!explicitVersion && resolved.match && !hasExplicitCatalogMetadata) {
|
||||
const result = buildPublishResult({
|
||||
status: "unchanged",
|
||||
slug,
|
||||
displayName,
|
||||
folder,
|
||||
version: resolved.match.version,
|
||||
latestVersion,
|
||||
fileCount: filesOnDisk.length,
|
||||
fingerprint: hashed.fingerprint,
|
||||
});
|
||||
spinner?.succeed(`OK. ${slug}@${result.version} is already published`);
|
||||
writePublishJsonIfRequested(options.json, result);
|
||||
return result;
|
||||
}
|
||||
const version = explicitVersion ?? resolveAutomaticVersion(latestVersion);
|
||||
if (options.dryRun) {
|
||||
const result = buildPublishResult({
|
||||
status: "would-publish",
|
||||
slug,
|
||||
displayName,
|
||||
folder,
|
||||
version,
|
||||
latestVersion,
|
||||
fileCount: filesOnDisk.length,
|
||||
fingerprint: hashed.fingerprint,
|
||||
});
|
||||
spinner?.succeed(`Would publish ${slug}@${version}`);
|
||||
writePublishJsonIfRequested(options.json, result);
|
||||
return result;
|
||||
}
|
||||
const token = optionalToken ?? (await requireAuthToken());
|
||||
const publishOwnerHandle = ownerHandle || (await getDefaultOwnerHandle(token));
|
||||
const publishSourceOwnerHandle = options.migrateOwner && publishOwnerHandle
|
||||
? sourceOwnerHandle || explicitSourceOwnerHandle || (await getDefaultOwnerHandle(token))
|
||||
: undefined;
|
||||
const form = new FormData();
|
||||
form.set("payload", JSON.stringify({
|
||||
slug,
|
||||
displayName,
|
||||
ownerHandle: publishOwnerHandle,
|
||||
...(publishSourceOwnerHandle ? { sourceOwnerHandle: publishSourceOwnerHandle } : {}),
|
||||
...(options.migrateOwner ? { migrateOwner: true } : {}),
|
||||
version,
|
||||
changelog,
|
||||
acceptLicenseTerms: true,
|
||||
tags,
|
||||
...(options.categories !== undefined ? { categories } : {}),
|
||||
...(options.topics !== undefined ? { topics } : {}),
|
||||
...(source ? { source } : {}),
|
||||
...(forkOf ? { forkOf } : {}),
|
||||
}));
|
||||
let index = 0;
|
||||
for (const file of filesOnDisk) {
|
||||
index += 1;
|
||||
if (spinner)
|
||||
spinner.text = `Uploading ${file.relPath} (${index}/${filesOnDisk.length})`;
|
||||
const blob = new Blob([Buffer.from(file.bytes)], { type: file.contentType ?? "text/plain" });
|
||||
form.append("files", blob, file.relPath);
|
||||
}
|
||||
if (spinner)
|
||||
spinner.text = `Publishing ${slug}@${version}`;
|
||||
const result = await apiRequestForm(registry, { method: "POST", path: ApiRoutes.skills, token, form }, ApiV1PublishResponseSchema);
|
||||
const publishResult = buildPublishResult({
|
||||
status: "published",
|
||||
slug,
|
||||
displayName,
|
||||
folder,
|
||||
version,
|
||||
latestVersion,
|
||||
fileCount: filesOnDisk.length,
|
||||
fingerprint: hashed.fingerprint,
|
||||
versionId: result.versionId,
|
||||
});
|
||||
spinner?.succeed(`OK. Published ${slug}@${version} (${result.versionId})`);
|
||||
writePublishJsonIfRequested(options.json, publishResult);
|
||||
return publishResult;
|
||||
}
|
||||
catch (error) {
|
||||
spinner?.fail(formatError(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
function parseCsv(value) {
|
||||
return (value ?? "")
|
||||
.split(",")
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
export async function resolveDefaultOwnerHandle(registry, token) {
|
||||
const whoami = await apiRequest(registry, { method: "GET", path: ApiRoutes.whoami, token }, ApiV1WhoamiResponseSchema);
|
||||
const handle = whoami.user.handle?.trim().replace(/^@+/, "");
|
||||
if (!handle)
|
||||
fail("Unable to resolve your publisher handle. Pass --owner explicitly.");
|
||||
return handle;
|
||||
}
|
||||
async function resolveSkillVersion(registry, slug, fingerprint, ownerHandle, token) {
|
||||
const url = registryUrl(ApiRoutes.resolve, registry);
|
||||
url.searchParams.set("slug", slug);
|
||||
if (ownerHandle)
|
||||
url.searchParams.set("ownerHandle", ownerHandle);
|
||||
url.searchParams.set("hash", fingerprint);
|
||||
try {
|
||||
return await apiRequest(registry, { method: "GET", url: url.toString(), token }, ApiV1SkillResolveResponseSchema);
|
||||
}
|
||||
catch (error) {
|
||||
if (/skill not found|HTTP 404/i.test(formatError(error))) {
|
||||
return { match: null, latestVersion: null };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
function resolveAutomaticVersion(latestVersion) {
|
||||
if (!latestVersion)
|
||||
return "1.0.0";
|
||||
const nextVersion = semver.inc(latestVersion, "patch");
|
||||
if (!nextVersion)
|
||||
fail(`Latest ClawHub version is not valid semver: ${latestVersion}`);
|
||||
return nextVersion;
|
||||
}
|
||||
function buildPublishResult(result) {
|
||||
return { ok: true, ...result };
|
||||
}
|
||||
function writePublishJsonIfRequested(json, result) {
|
||||
if (json)
|
||||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
||||
}
|
||||
export async function prepareSkillFilesForPublish(folder) {
|
||||
return stripGeneratedSkillCards(await ensureRootManifestFile(folder, await listTextFiles(folder)));
|
||||
}
|
||||
function stripGeneratedSkillCards(files) {
|
||||
return files.filter((file) => file.relPath.trim().toLowerCase() !== "skill-card.md");
|
||||
}
|
||||
async function ensureRootManifestFile(folder, files) {
|
||||
if (files.some((file) => {
|
||||
const lower = file.relPath.toLowerCase();
|
||||
return lower === "skill.md" || lower === "skills.md";
|
||||
})) {
|
||||
return files;
|
||||
}
|
||||
const entries = await readdir(folder, { withFileTypes: true }).catch(() => []);
|
||||
const manifest = entries.find((entry) => {
|
||||
const lower = entry.name.toLowerCase();
|
||||
return entry.isFile() && (lower === "skill.md" || lower === "skills.md");
|
||||
});
|
||||
if (!manifest)
|
||||
return files;
|
||||
return [
|
||||
...files,
|
||||
{
|
||||
relPath: manifest.name,
|
||||
bytes: new Uint8Array(await readFile(join(folder, manifest.name))),
|
||||
contentType: "text/markdown",
|
||||
},
|
||||
];
|
||||
}
|
||||
async function looksLikePluginFolder(folder) {
|
||||
const checks = [
|
||||
join(folder, "openclaw.plugin.json"),
|
||||
join(folder, "package.json"),
|
||||
join(folder, ".codex-plugin", "plugin.json"),
|
||||
join(folder, ".claude-plugin", "plugin.json"),
|
||||
join(folder, ".cursor-plugin", "plugin.json"),
|
||||
];
|
||||
const stats = await Promise.all(checks.map((candidate) => stat(candidate).catch(() => null)));
|
||||
if (stats[0]?.isFile() || stats[2]?.isFile() || stats[3]?.isFile() || stats[4]?.isFile()) {
|
||||
return true;
|
||||
}
|
||||
if (!stats[1]?.isFile()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const raw = JSON.parse(await readFile(checks[1], "utf8"));
|
||||
return Boolean(raw && typeof raw === "object" && raw.openclaw && typeof raw.openclaw === "object");
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function parseForkOf(value) {
|
||||
const trimmed = value.trim();
|
||||
const ref = parseOwnerQualifiedSkillRef(trimmed);
|
||||
const [slugRaw, versionRaw] = splitForkSlugAndVersion(ref.slugAndVersion);
|
||||
const slug = (slugRaw ?? "").trim().toLowerCase();
|
||||
if (!slug)
|
||||
fail("--fork-of must be <slug> or <slug@version>");
|
||||
const version = (versionRaw ?? "").trim();
|
||||
if (version && !semver.valid(version))
|
||||
fail("--fork-of version must be valid semver");
|
||||
return {
|
||||
slug,
|
||||
...(ref.ownerHandle ? { ownerHandle: ref.ownerHandle } : {}),
|
||||
version: version || undefined,
|
||||
};
|
||||
}
|
||||
function parseOwnerQualifiedSkillRef(value) {
|
||||
if (!value.startsWith("@"))
|
||||
return { slugAndVersion: value };
|
||||
const slashIndex = value.indexOf("/");
|
||||
if (slashIndex < 0)
|
||||
fail("--fork-of must be <slug>, <slug@version>, or @<owner>/<slug@version>");
|
||||
const ownerHandle = value.slice(1, slashIndex).trim().replace(/^@+/, "");
|
||||
const slugAndVersion = value.slice(slashIndex + 1).trim();
|
||||
if (!ownerHandle || !slugAndVersion) {
|
||||
fail("--fork-of must be <slug>, <slug@version>, or @<owner>/<slug@version>");
|
||||
}
|
||||
return { ownerHandle, slugAndVersion };
|
||||
}
|
||||
function splitForkSlugAndVersion(value) {
|
||||
const atIndex = value.lastIndexOf("@");
|
||||
if (atIndex <= 0)
|
||||
return [value, ""];
|
||||
return [value.slice(0, atIndex), value.slice(atIndex + 1)];
|
||||
}
|
||||
function buildPublishSource(options) {
|
||||
const rawRepo = options.sourceRepo?.trim();
|
||||
const commit = options.sourceCommit?.trim();
|
||||
const ref = options.sourceRef?.trim();
|
||||
const path = normalizeSourcePath(options.sourcePath);
|
||||
if (!rawRepo && !commit && !ref && !options.sourcePath?.trim())
|
||||
return undefined;
|
||||
if (!rawRepo || !commit)
|
||||
fail("--source-repo and --source-commit must be provided together");
|
||||
const repo = normalizeGitHubRepo(rawRepo);
|
||||
if (!repo)
|
||||
fail("--source-repo must be a GitHub repo or URL");
|
||||
return {
|
||||
kind: "github",
|
||||
url: `https://github.com/${repo}`,
|
||||
repo,
|
||||
ref: ref || commit,
|
||||
commit,
|
||||
path,
|
||||
importedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
function normalizeSourcePath(value) {
|
||||
const normalized = (value?.trim() || ".").replaceAll("\\", "/").replace(/^\.\/+/, "");
|
||||
if (!normalized || normalized === ".")
|
||||
return ".";
|
||||
return normalized.replace(/\/+$/, "") || ".";
|
||||
}
|
||||
//# sourceMappingURL=publish.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+7
@@ -0,0 +1,7 @@
|
||||
import type { GlobalOpts } from "../types.js";
|
||||
type PublisherCreateOptions = {
|
||||
displayName?: string;
|
||||
json?: boolean;
|
||||
};
|
||||
export declare function cmdCreatePublisher(opts: GlobalOpts, handle: string, options?: PublisherCreateOptions): Promise<void>;
|
||||
export {};
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { apiRequest } from "../../http.js";
|
||||
import { ApiRoutes, ApiV1PublisherCreateResponseSchema } from "../../schema/index.js";
|
||||
import { requireAuthToken } from "../authToken.js";
|
||||
import { getRegistry } from "../registry.js";
|
||||
function normalizePublisherHandleOrFail(handle) {
|
||||
const normalized = handle.trim().replace(/^@+/, "").toLowerCase();
|
||||
if (!normalized)
|
||||
throw new Error("Publisher handle is required");
|
||||
return normalized;
|
||||
}
|
||||
export async function cmdCreatePublisher(opts, handle, options = {}) {
|
||||
const normalizedHandle = normalizePublisherHandleOrFail(handle);
|
||||
const displayName = options.displayName?.trim();
|
||||
const token = await requireAuthToken();
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
const result = await apiRequest(registry, {
|
||||
method: "POST",
|
||||
path: ApiRoutes.publishers,
|
||||
token,
|
||||
body: {
|
||||
handle: normalizedHandle,
|
||||
...(displayName ? { displayName } : {}),
|
||||
},
|
||||
}, ApiV1PublisherCreateResponseSchema);
|
||||
if (options.json) {
|
||||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
console.log(`OK. Created publisher @${result.handle}.`);
|
||||
}
|
||||
//# sourceMappingURL=publishers.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"publishers.js","sourceRoot":"","sources":["../../../src/cli/commands/publishers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,kCAAkC,EAAE,MAAM,uBAAuB,CAAC;AACtF,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAQ7C,SAAS,8BAA8B,CAAC,MAAc;IACpD,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IAClE,IAAI,CAAC,UAAU;QAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;IACjE,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,IAAgB,EAChB,MAAc,EACd,UAAkC,EAAE;IAEpC,MAAM,gBAAgB,GAAG,8BAA8B,CAAC,MAAM,CAAC,CAAC;IAChE,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC;IAChD,MAAM,KAAK,GAAG,MAAM,gBAAgB,EAAE,CAAC;IACvC,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1D,MAAM,MAAM,GAAG,MAAM,UAAU,CAC7B,QAAQ,EACR;QACE,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,SAAS,CAAC,UAAU;QAC1B,KAAK;QACL,IAAI,EAAE;YACJ,MAAM,EAAE,gBAAgB;YACxB,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACxC;KACF,EACD,kCAAkC,CACnC,CAAC;IAEF,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;QACjB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;QAC7D,OAAO;IACT,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,0BAA0B,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;AAC1D,CAAC"}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import type { GlobalOpts } from "../types.js";
|
||||
type ScanOptions = {
|
||||
slug?: string;
|
||||
version?: string;
|
||||
update?: boolean;
|
||||
output?: string;
|
||||
json?: boolean;
|
||||
};
|
||||
type ScanDownloadOptions = {
|
||||
kind?: "skill" | "plugin";
|
||||
version?: string;
|
||||
output?: string;
|
||||
};
|
||||
export declare function cmdScan(opts: GlobalOpts, pathArg: string | undefined, options: ScanOptions): Promise<void>;
|
||||
export declare function cmdScanDownload(opts: GlobalOpts, nameArg: string, options: ScanDownloadOptions): Promise<void>;
|
||||
export {};
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { apiRequest, fetchBinary } from "../../http.js";
|
||||
import { ApiRoutes, ApiV1SkillScanStatusResponseSchema, ApiV1SkillScanSubmitResponseSchema, } from "../../schema/index.js";
|
||||
import { requireAuthToken } from "../authToken.js";
|
||||
import { getRegistry } from "../registry.js";
|
||||
import { createCrabLoader, fail, formatError } from "../ui.js";
|
||||
const DEFAULT_POLL_INTERVAL_MS = 2_000;
|
||||
const MAX_POLL_ATTEMPTS = 900;
|
||||
export async function cmdScan(opts, pathArg, options) {
|
||||
validateScanOptions(pathArg, options);
|
||||
if (pathArg?.trim())
|
||||
rejectLocalScan();
|
||||
const token = await requireAuthToken();
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
const spinner = createCrabLoader("Submitting scan");
|
||||
try {
|
||||
const submitted = await submitPublishedScan(registry, token, options);
|
||||
spinner.text = formatScanProgress("queued", submitted.scanId, submitted.queue);
|
||||
const status = await pollScan(registry, token, submitted.scanId, spinner);
|
||||
if (status.status === "failed") {
|
||||
spinner.fail(`Scan failed (${status.scanId})`);
|
||||
if (options.json)
|
||||
printJson(status);
|
||||
else
|
||||
printScanReport(status);
|
||||
throw new Error(status.lastError ?? "Scan failed");
|
||||
}
|
||||
spinner.succeed(`Scan complete (${status.scanId})`);
|
||||
if (options.json)
|
||||
printJson(status);
|
||||
else
|
||||
printScanReport(status);
|
||||
if (options.output) {
|
||||
const bytes = await fetchBinary(registry, {
|
||||
path: `${ApiRoutes.skillScans}/${encodeURIComponent(status.scanId)}/download`,
|
||||
token,
|
||||
});
|
||||
await mkdir(dirname(resolve(opts.workdir, options.output)), { recursive: true });
|
||||
await writeFile(resolve(opts.workdir, options.output), bytes);
|
||||
if (!options.json)
|
||||
console.log(`Report ZIP: ${resolve(opts.workdir, options.output)}`);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(formatError(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
export async function cmdScanDownload(opts, nameArg, options) {
|
||||
const name = nameArg.trim();
|
||||
if (!name)
|
||||
fail("Skill or plugin name required");
|
||||
const version = options.version?.trim();
|
||||
if (!version)
|
||||
fail("--version required");
|
||||
const kind = options.kind ?? "skill";
|
||||
if (kind !== "skill" && kind !== "plugin")
|
||||
fail('--kind must be "skill" or "plugin"');
|
||||
const token = await requireAuthToken();
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
const output = resolve(opts.workdir, options.output ?? `clawhub-scan-${safeOutputName(name)}-${safeOutputName(version)}.zip`);
|
||||
const query = new URLSearchParams({ version, kind });
|
||||
const bytes = await fetchBinary(registry, {
|
||||
path: `${ApiRoutes.skillScans}/download/${encodeURIComponent(name)}?${query.toString()}`,
|
||||
token,
|
||||
});
|
||||
await mkdir(dirname(output), { recursive: true });
|
||||
await writeFile(output, bytes);
|
||||
console.log(`Report ZIP: ${output}`);
|
||||
}
|
||||
function validateScanOptions(pathArg, options) {
|
||||
const hasPath = Boolean(pathArg?.trim());
|
||||
const hasSlug = Boolean(options.slug?.trim());
|
||||
if (hasPath && hasSlug)
|
||||
fail("Choose either a local path or --slug, not both");
|
||||
if (!hasPath && !hasSlug)
|
||||
fail("Provide a local path or --slug");
|
||||
if (hasPath && options.update)
|
||||
fail("--update is only valid with --slug");
|
||||
}
|
||||
function rejectLocalScan() {
|
||||
fail("Local folder scans are no longer supported. Upload a new version, then run `clawhub scan download <slug> --version <version>` to retrieve stored scan results.");
|
||||
}
|
||||
async function submitPublishedScan(registry, token, options) {
|
||||
const slug = options.slug?.trim();
|
||||
if (!slug)
|
||||
fail("--slug required");
|
||||
const version = options.version?.trim();
|
||||
return await apiRequest(registry, {
|
||||
method: "POST",
|
||||
path: ApiRoutes.skillScans,
|
||||
token,
|
||||
body: {
|
||||
source: {
|
||||
kind: "published",
|
||||
slug,
|
||||
...(version ? { version } : {}),
|
||||
},
|
||||
update: options.update === true,
|
||||
},
|
||||
}, ApiV1SkillScanSubmitResponseSchema);
|
||||
}
|
||||
async function pollScan(registry, token, scanId, spinner) {
|
||||
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt += 1) {
|
||||
const status = await apiRequest(registry, {
|
||||
method: "GET",
|
||||
path: `${ApiRoutes.skillScans}/${encodeURIComponent(scanId)}`,
|
||||
token,
|
||||
}, ApiV1SkillScanStatusResponseSchema);
|
||||
spinner.text = formatScanProgress(status.status, scanId, status.queue);
|
||||
if (status.status === "succeeded" || status.status === "failed")
|
||||
return status;
|
||||
await sleep(DEFAULT_POLL_INTERVAL_MS);
|
||||
}
|
||||
throw new Error(`Timed out waiting for scan ${scanId}`);
|
||||
}
|
||||
function sleep(ms) {
|
||||
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
||||
}
|
||||
function formatScanProgress(status, scanId, queue) {
|
||||
const queueProgress = formatQueueProgress(status, queue);
|
||||
return `Scan ${status} (${scanId})${queueProgress ? ` - ${queueProgress}` : ""}`;
|
||||
}
|
||||
function formatQueueProgress(status, queue) {
|
||||
if (!queue)
|
||||
return undefined;
|
||||
if (status === "queued") {
|
||||
const aheadCount = `${queue.queuedAhead}${queue.queuedAheadIsEstimate ? "+" : ""}`;
|
||||
const ahead = queue.queuedAhead === 0
|
||||
? "no scans ahead"
|
||||
: `${aheadCount} scan${queue.queuedAhead === 1 && !queue.queuedAheadIsEstimate ? "" : "s"} ahead`;
|
||||
const position = typeof queue.position === "number" ? `position ${queue.position}` : undefined;
|
||||
const running = `${queue.running}${queue.runningIsEstimate ? "+" : ""} running`;
|
||||
return [position, ahead, running, queue.note].filter(Boolean).join("; ");
|
||||
}
|
||||
if (status === "running")
|
||||
return `${queue.running}${queue.runningIsEstimate ? "+" : ""} running`;
|
||||
return undefined;
|
||||
}
|
||||
function printJson(status) {
|
||||
console.log(JSON.stringify(status, null, 2));
|
||||
}
|
||||
function printScanReport(status) {
|
||||
const artifact = asRecord(status.artifact) ?? {};
|
||||
const report = asRecord(status.report) ?? {};
|
||||
const clawscan = asRecord(report.clawscan) ?? {};
|
||||
const skillspector = asRecord(report.skillspector) ?? {};
|
||||
const staticAnalysis = asRecord(report.staticAnalysis) ?? {};
|
||||
const virustotal = asRecord(report.virustotal);
|
||||
console.log("");
|
||||
console.log("ClawHub Scan Report");
|
||||
console.log(`Scan ID: ${status.scanId}`);
|
||||
console.log(`Status: ${status.status.toUpperCase()}`);
|
||||
console.log(`Source: ${status.sourceKind}`);
|
||||
console.log(`Update requested: ${status.update ? "yes" : "no"}`);
|
||||
console.log(`Written back: ${status.writtenBack ? "yes" : "no"}`);
|
||||
printOptional("Slug", stringValue(artifact.slug));
|
||||
printOptional("Name", stringValue(artifact.displayName));
|
||||
printOptional("Version", stringValue(artifact.version));
|
||||
printOptional("Created", dateValue(status.createdAt));
|
||||
printOptional("Completed", dateValue(status.completedAt));
|
||||
console.log("");
|
||||
console.log("ClawScan");
|
||||
printOptional("Verdict", upperValue(clawscan.verdict ?? clawscan.status));
|
||||
printOptional("Confidence", stringValue(clawscan.confidence));
|
||||
printOptional("Summary", stringValue(clawscan.summary));
|
||||
printOptional("Guidance", stringValue(clawscan.guidance));
|
||||
printFindings(clawscan.findings);
|
||||
printAgenticRisks(clawscan.agenticRiskFindings);
|
||||
console.log("");
|
||||
console.log("SkillSpector");
|
||||
printOptional("Status", upperValue(skillspector.status));
|
||||
printOptional("Score", numberValue(skillspector.score));
|
||||
printOptional("Severity", stringValue(skillspector.severity));
|
||||
printOptional("Issue count", numberValue(skillspector.issueCount));
|
||||
printIssueList(skillspector.issues);
|
||||
console.log("");
|
||||
console.log("Static Analysis");
|
||||
printOptional("Status", upperValue(staticAnalysis.status));
|
||||
printOptional("Reason codes", arrayValue(staticAnalysis.reasonCodes));
|
||||
printOptional("Summary", stringValue(staticAnalysis.summary));
|
||||
printIssueList(staticAnalysis.findings);
|
||||
console.log("");
|
||||
console.log("VirusTotal");
|
||||
if (!virustotal) {
|
||||
console.log("Status: not available");
|
||||
}
|
||||
else {
|
||||
printOptional("Status", stringValue(virustotal.status));
|
||||
printOptional("Malicious", numberValue(virustotal.malicious));
|
||||
printOptional("Suspicious", numberValue(virustotal.suspicious));
|
||||
printOptional("Harmless", numberValue(virustotal.harmless));
|
||||
printOptional("Undetected", numberValue(virustotal.undetected));
|
||||
}
|
||||
}
|
||||
function printOptional(label, value) {
|
||||
if (!value)
|
||||
return;
|
||||
console.log(`${label}: ${value}`);
|
||||
}
|
||||
function printFindings(value) {
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
console.log(`Findings: ${value.trim()}`);
|
||||
}
|
||||
}
|
||||
function printAgenticRisks(value) {
|
||||
if (!Array.isArray(value) || value.length === 0)
|
||||
return;
|
||||
console.log("Agentic risk findings:");
|
||||
for (const item of value.slice(0, 20)) {
|
||||
const finding = asRecord(item);
|
||||
if (!finding)
|
||||
continue;
|
||||
const label = stringValue(finding.categoryLabel) ?? stringValue(finding.categoryId) ?? "risk";
|
||||
const status = stringValue(finding.status) ?? "unknown";
|
||||
const severity = stringValue(finding.severity) ?? "unknown";
|
||||
console.log(`- ${label}: ${status} (${severity})`);
|
||||
printIndented("Impact", stringValue(finding.userImpact));
|
||||
printIndented("Recommendation", stringValue(finding.recommendation));
|
||||
}
|
||||
}
|
||||
function printIssueList(value) {
|
||||
if (!Array.isArray(value) || value.length === 0)
|
||||
return;
|
||||
for (const item of value.slice(0, 25)) {
|
||||
const issue = asRecord(item);
|
||||
if (!issue)
|
||||
continue;
|
||||
const code = stringValue(issue.code ?? issue.issueId ?? issue.pattern) ?? "issue";
|
||||
const severity = stringValue(issue.severity) ?? "unknown";
|
||||
const file = stringValue(issue.file);
|
||||
const message = stringValue(issue.message ?? issue.explanation ?? issue.finding);
|
||||
console.log(`- ${code}: ${severity}${file ? ` in ${file}` : ""}`);
|
||||
printIndented("Detail", message);
|
||||
printIndented("Remediation", stringValue(issue.remediation));
|
||||
}
|
||||
}
|
||||
function printIndented(label, value) {
|
||||
if (!value)
|
||||
return;
|
||||
console.log(` ${label}: ${value}`);
|
||||
}
|
||||
function asRecord(value) {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
function stringValue(value) {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
function upperValue(value) {
|
||||
return stringValue(value)?.toUpperCase();
|
||||
}
|
||||
function numberValue(value) {
|
||||
return typeof value === "number" && Number.isFinite(value) ? String(value) : undefined;
|
||||
}
|
||||
function arrayValue(value) {
|
||||
return Array.isArray(value) && value.length > 0
|
||||
? value.map((item) => String(item)).join(", ")
|
||||
: undefined;
|
||||
}
|
||||
function dateValue(value) {
|
||||
return typeof value === "number" && Number.isFinite(value)
|
||||
? new Date(value).toISOString()
|
||||
: undefined;
|
||||
}
|
||||
function safeOutputName(value) {
|
||||
return value
|
||||
.replace(/^@/, "")
|
||||
.replaceAll("/", "-")
|
||||
.replaceAll(/[^a-zA-Z0-9._-]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
}
|
||||
//# sourceMappingURL=scan.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+55
@@ -0,0 +1,55 @@
|
||||
import { type SkillReportFinalAction, type SkillReportListStatus, type SkillReportStatus } from "../../schema/index.js";
|
||||
import type { GlobalOpts } from "../types.js";
|
||||
type SkillReportOptions = {
|
||||
version?: string;
|
||||
reason?: string;
|
||||
json?: boolean;
|
||||
};
|
||||
type SkillReportListOptions = {
|
||||
status?: SkillReportListStatus;
|
||||
cursor?: string;
|
||||
limit?: number;
|
||||
json?: boolean;
|
||||
};
|
||||
type SkillReportTriageOptions = {
|
||||
status?: SkillReportStatus;
|
||||
action?: SkillReportFinalAction;
|
||||
finalAction?: SkillReportFinalAction;
|
||||
note?: string;
|
||||
json?: boolean;
|
||||
yes?: boolean;
|
||||
};
|
||||
export declare function cmdSearch(opts: GlobalOpts, query: string, limit?: number): Promise<void>;
|
||||
export declare function cmdInstall(opts: GlobalOpts, slug: string, versionFlag?: string, force?: boolean, forceInstall?: boolean): Promise<void>;
|
||||
export declare function cmdUpdate(opts: GlobalOpts, slugArg: string | undefined, options: {
|
||||
all?: boolean;
|
||||
version?: string;
|
||||
force?: boolean;
|
||||
forceInstall?: boolean;
|
||||
}, inputAllowed: boolean): Promise<void>;
|
||||
export declare function cmdList(opts: GlobalOpts): Promise<void>;
|
||||
export declare function cmdPin(opts: GlobalOpts, slug: string, options?: {
|
||||
reason?: string;
|
||||
}): Promise<void>;
|
||||
export declare function cmdUnpin(opts: GlobalOpts, slug: string): Promise<void>;
|
||||
export declare function cmdUninstall(opts: GlobalOpts, slug: string, options: {
|
||||
yes?: boolean;
|
||||
} | undefined, inputAllowed: boolean): Promise<void>;
|
||||
export declare function cmdExplore(opts: GlobalOpts, options?: {
|
||||
limit?: number;
|
||||
sort?: string;
|
||||
json?: boolean;
|
||||
}): Promise<void>;
|
||||
export declare function formatExploreLine(item: {
|
||||
slug: string;
|
||||
summary?: string | null;
|
||||
updatedAt: number;
|
||||
latestVersion?: {
|
||||
version: string;
|
||||
} | null;
|
||||
}): string;
|
||||
export declare function clampLimit(limit: number, fallback?: number): number;
|
||||
export declare function cmdReportSkill(opts: GlobalOpts, slug: string, options?: SkillReportOptions): Promise<void>;
|
||||
export declare function cmdListSkillReports(opts: GlobalOpts, options?: SkillReportListOptions): Promise<void>;
|
||||
export declare function cmdTriageSkillReport(opts: GlobalOpts, reportId: string, options?: SkillReportTriageOptions): Promise<void>;
|
||||
export {};
|
||||
+936
@@ -0,0 +1,936 @@
|
||||
import { mkdir, mkdtemp, rename, rm, stat } from "node:fs/promises";
|
||||
import { basename, dirname, join } from "node:path";
|
||||
import semver from "semver";
|
||||
import { apiRequest, downloadZip, fetchBinary, registryUrl } from "../../http.js";
|
||||
import { ApiRoutes, ApiV1SkillInstallResolveResponseSchema, ApiV1SearchResponseSchema, ApiV1SkillListResponseSchema, ApiV1SkillReportListResponseSchema, ApiV1SkillReportResponseSchema, ApiV1SkillReportTriageResponseSchema, ApiV1SkillResolveResponseSchema, ApiV1SkillResponseSchema, ApiV1SkillVersionResponseSchema, } from "../../schema/index.js";
|
||||
import { extractGitHubZipPathToDir, extractZipToDir, hashSkillFiles, listManualSkills, listTextFiles, readLockfile, readSkillOrigin, writeLockfile, writeSkillOrigin, } from "../../skills.js";
|
||||
import { getOptionalAuthToken, requireAuthToken } from "../authToken.js";
|
||||
import { getRegistry } from "../registry.js";
|
||||
import { createCrabLoader, fail, formatError, isInteractive, promptConfirm, styleText, } from "../ui.js";
|
||||
import { reportInstalledSkillsTelemetryIfEnabled } from "./installTelemetry.js";
|
||||
import { presentModerationPlan, reportModerationPlan } from "./moderationPlan.js";
|
||||
function normalizeOwnerHandle(raw) {
|
||||
const handle = raw?.trim().replace(/^@+/, "").toLowerCase();
|
||||
if (!handle)
|
||||
return undefined;
|
||||
if (handle.includes("/") || handle.includes("\\") || handle.includes("..")) {
|
||||
fail(`Invalid owner handle: ${raw}`);
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
function normalizeSkillSlugOrFail(raw) {
|
||||
const slug = raw.trim();
|
||||
if (!slug)
|
||||
fail("Slug required");
|
||||
// Safety: never allow path traversal or nested paths to become filesystem operations.
|
||||
if (slug.includes("/") || slug.includes("\\") || slug.includes("..")) {
|
||||
fail(`Invalid slug: ${slug}`);
|
||||
}
|
||||
return slug;
|
||||
}
|
||||
function normalizeSkillSlugForRemote(raw) {
|
||||
if (typeof raw !== "string")
|
||||
return undefined;
|
||||
const slug = raw.trim();
|
||||
if (!isSafeSkillSlug(slug))
|
||||
return undefined;
|
||||
return slug;
|
||||
}
|
||||
function parseSkillRefOrFail(raw) {
|
||||
const ref = raw.trim();
|
||||
if (!ref)
|
||||
fail("Slug required");
|
||||
const slashIndex = ref.indexOf("/");
|
||||
if (slashIndex < 0) {
|
||||
return { slug: normalizeSkillSlugOrFail(ref) };
|
||||
}
|
||||
if (ref.indexOf("/", slashIndex + 1) >= 0) {
|
||||
fail(`Invalid skill ref: ${ref}`);
|
||||
}
|
||||
const rawOwnerHandle = ref.slice(0, slashIndex);
|
||||
if (rawOwnerHandle.includes("\\") || rawOwnerHandle.includes("..")) {
|
||||
fail(`Invalid slug: ${ref}`);
|
||||
}
|
||||
const ownerHandle = normalizeOwnerHandle(rawOwnerHandle);
|
||||
const slug = normalizeSkillSlugOrFail(ref.slice(slashIndex + 1));
|
||||
if (!ownerHandle)
|
||||
fail(`Invalid skill ref: ${ref}`);
|
||||
return { slug, ownerHandle };
|
||||
}
|
||||
function isSafeSkillSlug(slug) {
|
||||
return Boolean(slug) && !slug.includes("/") && !slug.includes("\\") && !slug.includes("..");
|
||||
}
|
||||
function skillIdentity(ref) {
|
||||
return ref.ownerHandle ? `@${ref.ownerHandle}/${ref.slug}` : ref.slug;
|
||||
}
|
||||
function skillTarget(dir, ref) {
|
||||
return ref.ownerHandle ? join(dir, `@${ref.ownerHandle}`, ref.slug) : join(dir, ref.slug);
|
||||
}
|
||||
function isSafeSkillIdentity(value) {
|
||||
const slashIndex = value.indexOf("/");
|
||||
if (slashIndex < 0)
|
||||
return isSafeSkillSlug(value);
|
||||
if (value.indexOf("/", slashIndex + 1) >= 0)
|
||||
return false;
|
||||
const ownerHandle = value.slice(0, slashIndex).replace(/^@+/, "");
|
||||
const slug = value.slice(slashIndex + 1);
|
||||
return Boolean(ownerHandle) && isSafeSkillSlug(ownerHandle) && isSafeSkillSlug(slug);
|
||||
}
|
||||
function findExistingLockKey(lock, ref) {
|
||||
const key = skillIdentity(ref);
|
||||
if (lock.skills[key])
|
||||
return key;
|
||||
if (ref.ownerHandle) {
|
||||
const legacyEntry = lock.skills[ref.slug];
|
||||
if (legacyEntry && normalizeOwnerHandle(legacyEntry.ownerHandle) === ref.ownerHandle) {
|
||||
return ref.slug;
|
||||
}
|
||||
}
|
||||
return key;
|
||||
}
|
||||
function ownerScopedUrl(registry, path, ownerHandle) {
|
||||
if (!ownerHandle)
|
||||
return null;
|
||||
const url = registryUrl(path, registry);
|
||||
url.searchParams.set("ownerHandle", ownerHandle);
|
||||
return url.toString();
|
||||
}
|
||||
function skillRequestArgs(registry, slug, ownerHandle, token) {
|
||||
const path = `${ApiRoutes.skills}/${encodeURIComponent(slug)}`;
|
||||
const url = ownerScopedUrl(registry, path, ownerHandle);
|
||||
return url ? { method: "GET", url, token } : { method: "GET", path, token };
|
||||
}
|
||||
function skillVersionRequestArgs(registry, slug, version, ownerHandle, token) {
|
||||
const path = `${ApiRoutes.skills}/${encodeURIComponent(slug)}/versions/${encodeURIComponent(version)}`;
|
||||
const url = ownerScopedUrl(registry, path, ownerHandle);
|
||||
return url ? { method: "GET", url, token } : { method: "GET", path, token };
|
||||
}
|
||||
function withOwnerMetadata(version, installedAt, ownerHandle, existing) {
|
||||
return {
|
||||
...withPinnedMetadata(version, installedAt, existing),
|
||||
...(ownerHandle ? { ownerHandle } : {}),
|
||||
};
|
||||
}
|
||||
function isPinnedSkillEntry(entry) {
|
||||
return entry?.pinned === true;
|
||||
}
|
||||
function withPinnedMetadata(version, installedAt, existing) {
|
||||
return {
|
||||
version,
|
||||
installedAt,
|
||||
...(existing?.pinned ? { pinned: true } : {}),
|
||||
...(existing?.pinned && existing.pinReason ? { pinReason: existing.pinReason } : {}),
|
||||
};
|
||||
}
|
||||
function formatPinnedDetails(entry) {
|
||||
return entry?.pinReason ? ` (${entry.pinReason})` : "";
|
||||
}
|
||||
function formatSearchOwner(entry) {
|
||||
const handle = entry.ownerHandle ?? entry.owner?.handle;
|
||||
if (handle)
|
||||
return `@${handle}`;
|
||||
return entry.owner?.displayName ?? "unknown owner";
|
||||
}
|
||||
export async function cmdSearch(opts, query, limit) {
|
||||
if (!query)
|
||||
fail("Query required");
|
||||
const token = await getOptionalAuthToken();
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
const spinner = createCrabLoader("Searching");
|
||||
try {
|
||||
const url = registryUrl(ApiRoutes.search, registry);
|
||||
url.searchParams.set("q", query);
|
||||
const effectiveLimit = typeof limit === "number" && Number.isFinite(limit) ? limit : 25;
|
||||
url.searchParams.set("limit", String(effectiveLimit));
|
||||
const result = await apiRequest(registry, { method: "GET", url: url.toString(), token }, ApiV1SearchResponseSchema);
|
||||
spinner.stop();
|
||||
const rows = result.results.map((entry) => {
|
||||
const slug = entry.slug ?? "unknown";
|
||||
return {
|
||||
slug: entry.version ? `${slug} v${entry.version}` : slug,
|
||||
owner: formatSearchOwner(entry),
|
||||
name: entry.displayName ?? slug,
|
||||
metric: formatSearchMetric(entry),
|
||||
};
|
||||
});
|
||||
const slugWidth = maxColumnWidth(rows.map((row) => row.slug));
|
||||
const ownerWidth = maxColumnWidth(rows.map((row) => row.owner));
|
||||
const nameWidth = maxColumnWidth(rows.map((row) => row.name));
|
||||
for (const row of rows) {
|
||||
console.log(`${styleText(row.slug.padEnd(slugWidth), "brand")} ${styleText(row.owner.padEnd(ownerWidth), "muted")} ${styleText(row.name.padEnd(nameWidth), "strong")} ${styleText(row.metric, "muted")}`);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(formatError(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
function maxColumnWidth(values) {
|
||||
return values.reduce((max, value) => Math.max(max, value.length), 0);
|
||||
}
|
||||
function formatSearchMetric(entry) {
|
||||
if (typeof entry.downloads === "number") {
|
||||
const value = new Intl.NumberFormat("en-US").format(entry.downloads);
|
||||
return `${value} ${entry.downloads === 1 ? "download" : "downloads"}`;
|
||||
}
|
||||
return `score ${entry.score.toFixed(3)}`;
|
||||
}
|
||||
export async function cmdInstall(opts, slug, versionFlag, force = false, forceInstall = false) {
|
||||
const requested = parseSkillRefOrFail(slug);
|
||||
const trimmed = requested.slug;
|
||||
const token = await getOptionalAuthToken();
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
await mkdir(opts.dir, { recursive: true });
|
||||
const lock = await readLockfile(opts.workdir);
|
||||
const lockKey = findExistingLockKey(lock, requested);
|
||||
const localRef = lockKey === skillIdentity(requested) ? requested : parseSkillRefOrFail(lockKey);
|
||||
const target = skillTarget(opts.dir, localRef);
|
||||
const targetExists = await fileExists(target);
|
||||
if (!force && targetExists) {
|
||||
fail(`Already installed: ${target} (use --force)`);
|
||||
}
|
||||
const existingEntry = lock.skills[lockKey];
|
||||
if (isPinnedSkillEntry(existingEntry)) {
|
||||
fail(`skill "${skillIdentity(localRef)}" is pinned; run \`clawhub unpin ${skillIdentity(localRef)}\` first`);
|
||||
}
|
||||
const spinner = createCrabLoader(`Resolving ${trimmed}`);
|
||||
try {
|
||||
// Fetch skill metadata including moderation status
|
||||
const skillMeta = await apiRequest(registry, skillRequestArgs(registry, trimmed, requested.ownerHandle, token), ApiV1SkillResponseSchema);
|
||||
const resolvedOwnerHandle = normalizeOwnerHandle(requested.ownerHandle ?? skillMeta.owner?.handle);
|
||||
const resolvedSlug = normalizeSkillSlugForRemote(skillMeta.skill?.slug) ?? trimmed;
|
||||
const remoteSlug = requested.ownerHandle ? trimmed : resolvedSlug;
|
||||
// Check moderation status before proceeding
|
||||
if (skillMeta.moderation?.isMalwareBlocked) {
|
||||
spinner.fail(`Blocked: ${trimmed} is flagged as malicious`);
|
||||
fail("This skill has been flagged as malware and cannot be installed.");
|
||||
}
|
||||
if (skillMeta.moderation?.isSuspicious && !force) {
|
||||
spinner.stop();
|
||||
console.log(`\n⚠️ Warning: "${trimmed}" is flagged for ClawHub security review.\n` +
|
||||
" This skill may contain risky patterns (crypto keys, external APIs, eval, etc.)\n" +
|
||||
" Review the skill code before use.\n");
|
||||
if (isInteractive()) {
|
||||
const confirm = await promptConfirm("Install anyway?");
|
||||
if (!confirm)
|
||||
fail("Installation cancelled");
|
||||
spinner.start(`Resolving ${trimmed}`);
|
||||
}
|
||||
else {
|
||||
fail("Use --force to install suspicious skills in non-interactive mode");
|
||||
}
|
||||
}
|
||||
let resolvedVersion = versionFlag ?? skillMeta.latestVersion?.version ?? null;
|
||||
let githubInstall = null;
|
||||
if (!resolvedVersion && !versionFlag) {
|
||||
const resolvedInstall = await resolveLatestSkillInstall(registry, remoteSlug, resolvedOwnerHandle, token, { forceInstall });
|
||||
if (!resolvedInstall.ok)
|
||||
fail(resolvedInstall.message);
|
||||
if (resolvedInstall.installKind === "github") {
|
||||
githubInstall = resolvedInstall;
|
||||
}
|
||||
else {
|
||||
resolvedVersion = resolvedInstall.archive.version;
|
||||
}
|
||||
}
|
||||
if (!resolvedVersion && !githubInstall)
|
||||
fail("Could not resolve latest version");
|
||||
if (versionFlag) {
|
||||
await apiRequest(registry, skillVersionRequestArgs(registry, remoteSlug, versionFlag, resolvedOwnerHandle, token), ApiV1SkillVersionResponseSchema);
|
||||
}
|
||||
if (githubInstall) {
|
||||
spinner.text = `Downloading ${trimmed} ${formatGitHubVersion(githubInstall.github.commit)}`;
|
||||
await installSkillWithOptionalStaging(target, targetExists, (installTarget) => installGitHubSkill(registry, githubInstall, installTarget));
|
||||
resolvedVersion = githubInstall.github.commit;
|
||||
}
|
||||
else {
|
||||
const archiveVersion = resolvedVersion;
|
||||
if (!archiveVersion)
|
||||
fail("Could not resolve latest version");
|
||||
spinner.text = `Downloading ${trimmed} v${archiveVersion}`;
|
||||
await installSkillWithOptionalStaging(target, targetExists, async (installTarget) => {
|
||||
const zip = await downloadZip(registry, {
|
||||
slug: remoteSlug,
|
||||
...(resolvedOwnerHandle ? { ownerHandle: resolvedOwnerHandle } : {}),
|
||||
version: archiveVersion,
|
||||
token,
|
||||
});
|
||||
await extractZipToDir(zip, installTarget);
|
||||
});
|
||||
}
|
||||
const installedFiles = await listTextFiles(target);
|
||||
const installedFingerprint = installedFiles.length > 0 ? hashSkillFiles(installedFiles).fingerprint : undefined;
|
||||
const installedAt = Date.now();
|
||||
await writeSkillOrigin(target, {
|
||||
version: 1,
|
||||
registry,
|
||||
slug: remoteSlug,
|
||||
...(resolvedOwnerHandle ? { ownerHandle: resolvedOwnerHandle } : {}),
|
||||
installedVersion: resolvedVersion,
|
||||
installedAt,
|
||||
fingerprint: installedFingerprint,
|
||||
});
|
||||
lock.skills[lockKey] = withOwnerMetadata(resolvedVersion, installedAt, resolvedOwnerHandle, existingEntry);
|
||||
await writeLockfile(opts.workdir, lock);
|
||||
await reportInstalledSkillsTelemetryIfEnabled({
|
||||
token,
|
||||
registry,
|
||||
slug: skillMeta.skill?.slug ?? trimmed,
|
||||
version: resolvedVersion,
|
||||
});
|
||||
spinner.succeed(`${styleText("Installed", "brand")} ${styleText(trimmed, "strong")} ${styleText(githubInstall ? formatGitHubVersion(resolvedVersion) : `v${resolvedVersion}`, "muted")} -> ${styleText(target, "muted")}`);
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(formatError(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
export async function cmdUpdate(opts, slugArg, options, inputAllowed) {
|
||||
const requestedRef = slugArg ? parseSkillRefOrFail(slugArg) : null;
|
||||
const slug = requestedRef?.slug;
|
||||
const all = Boolean(options.all);
|
||||
if (!slug && !all)
|
||||
fail("Provide <skill> or --all");
|
||||
if (slug && all)
|
||||
fail("Use either <skill> or --all");
|
||||
if (options.version && !slug)
|
||||
fail("--version requires a single <skill>");
|
||||
if (options.version && !semver.valid(options.version))
|
||||
fail("--version must be valid semver");
|
||||
const lock = await readLockfile(opts.workdir);
|
||||
const requestedLockKey = requestedRef ? findExistingLockKey(lock, requestedRef) : undefined;
|
||||
if (requestedLockKey && isPinnedSkillEntry(lock.skills[requestedLockKey])) {
|
||||
fail(`skill "${skillIdentity(requestedRef)}" is pinned; run \`clawhub unpin ${skillIdentity(requestedRef)}\` first`);
|
||||
}
|
||||
const allowPrompt = isInteractive() && inputAllowed;
|
||||
const token = await getOptionalAuthToken();
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
const requestedSlugs = slug
|
||||
? [requestedLockKey ?? skillIdentity(requestedRef)]
|
||||
: Object.keys(lock.skills).filter(isSafeSkillIdentity);
|
||||
const skippedPinned = slug
|
||||
? []
|
||||
: requestedSlugs.filter((entry) => isPinnedSkillEntry(lock.skills[entry]));
|
||||
const slugs = slug
|
||||
? requestedSlugs
|
||||
: requestedSlugs.filter((entry) => !isPinnedSkillEntry(lock.skills[entry]));
|
||||
if (slugs.length === 0) {
|
||||
if (skippedPinned.length > 0) {
|
||||
const suffix = skippedPinned.length === 1 ? "" : "s";
|
||||
console.log(`Skipped ${skippedPinned.length} pinned skill${suffix}: ${skippedPinned.join(", ")}`);
|
||||
return;
|
||||
}
|
||||
console.log("No installed skills.");
|
||||
return;
|
||||
}
|
||||
let lockDirty = false;
|
||||
const markLockDirty = () => {
|
||||
lockDirty = true;
|
||||
};
|
||||
const flushLockfile = async () => {
|
||||
if (!lockDirty)
|
||||
return;
|
||||
await writeLockfile(opts.workdir, lock);
|
||||
lockDirty = false;
|
||||
};
|
||||
for (const entry of slugs) {
|
||||
const entryRef = parseSkillRefOrFail(entry);
|
||||
const entryLock = lock.skills[entry];
|
||||
const spinner = createCrabLoader(`Checking ${entry}`);
|
||||
try {
|
||||
const target = skillTarget(opts.dir, entryRef);
|
||||
const exists = await fileExists(target);
|
||||
const existingOrigin = exists ? await readSkillOrigin(target) : null;
|
||||
const requestedOwnerHandle = normalizeOwnerHandle(requestedRef?.ownerHandle ??
|
||||
entryRef.ownerHandle ??
|
||||
entryLock?.ownerHandle ??
|
||||
existingOrigin?.ownerHandle);
|
||||
// Always fetch skill metadata to check moderation status
|
||||
const skillMeta = await apiRequest(registry, skillRequestArgs(registry, entryRef.slug, requestedOwnerHandle, token), ApiV1SkillResponseSchema);
|
||||
const resolvedOwnerHandle = normalizeOwnerHandle(requestedOwnerHandle ?? skillMeta.owner?.handle);
|
||||
const resolvedSlug = normalizeSkillSlugForRemote(skillMeta.skill?.slug) ?? entryRef.slug;
|
||||
const remoteSlug = existingOrigin?.slug ?? (requestedOwnerHandle ? entryRef.slug : resolvedSlug);
|
||||
// Check moderation status before proceeding
|
||||
if (skillMeta.moderation?.isMalwareBlocked) {
|
||||
spinner.fail(`${entry}: blocked as malicious`);
|
||||
console.log(" This skill has been flagged as malware and cannot be updated.");
|
||||
continue;
|
||||
}
|
||||
if (skillMeta.moderation?.isSuspicious && !options.force) {
|
||||
spinner.stop();
|
||||
console.log(`\n⚠️ Warning: "${entry}" is flagged for ClawHub security review.\n` +
|
||||
" This skill may contain risky patterns (crypto keys, external APIs, eval, etc.)\n");
|
||||
if (allowPrompt) {
|
||||
const confirm = await promptConfirm("Update anyway?");
|
||||
if (!confirm) {
|
||||
console.log(`${entry}: skipped`);
|
||||
continue;
|
||||
}
|
||||
spinner.start(`Checking ${entry}`);
|
||||
}
|
||||
else {
|
||||
console.log(`${entry}: skipped (use --force to update suspicious skills)`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let localFingerprint = null;
|
||||
if (exists) {
|
||||
const filesOnDisk = await listTextFiles(target);
|
||||
if (filesOnDisk.length > 0) {
|
||||
const hashed = hashSkillFiles(filesOnDisk);
|
||||
localFingerprint = hashed.fingerprint;
|
||||
}
|
||||
}
|
||||
let latestInstall = null;
|
||||
if (!skillMeta.latestVersion && !options.version) {
|
||||
latestInstall = await resolveLatestSkillInstall(registry, remoteSlug, resolvedOwnerHandle, token, {
|
||||
forceInstall: Boolean(options.forceInstall),
|
||||
});
|
||||
if (!latestInstall.ok) {
|
||||
spinner.fail(`${entry}: ${latestInstall.message}`);
|
||||
continue;
|
||||
}
|
||||
if (latestInstall.installKind === "github") {
|
||||
const targetVersion = latestInstall.github.commit;
|
||||
const originFingerprint = existingOrigin?.slug === remoteSlug ? existingOrigin.fingerprint : undefined;
|
||||
const hasLocalChanges = Boolean(exists &&
|
||||
localFingerprint &&
|
||||
(!originFingerprint || originFingerprint !== localFingerprint));
|
||||
const matched = existingOrigin?.slug === remoteSlug &&
|
||||
originFingerprint &&
|
||||
localFingerprint &&
|
||||
originFingerprint === localFingerprint
|
||||
? existingOrigin.installedVersion
|
||||
: null;
|
||||
if (hasLocalChanges && !options.force) {
|
||||
spinner.stop();
|
||||
if (!allowPrompt) {
|
||||
console.log(`${entry}: local changes (no match). Use --force to overwrite.`);
|
||||
continue;
|
||||
}
|
||||
const confirm = await promptConfirm(`${entry}: local changes (no match). Overwrite with ${formatGitHubVersion(targetVersion)}?`);
|
||||
if (!confirm) {
|
||||
console.log(`${entry}: skipped`);
|
||||
continue;
|
||||
}
|
||||
spinner.start(`Updating ${entry} -> ${formatGitHubVersion(targetVersion)}`);
|
||||
}
|
||||
if (matched === targetVersion && !options.force && !hasLocalChanges) {
|
||||
if (lock.skills[entry]?.version !== targetVersion) {
|
||||
lock.skills[entry] = withPinnedMetadata(targetVersion, lock.skills[entry]?.installedAt ?? Date.now(), lock.skills[entry]);
|
||||
markLockDirty();
|
||||
await flushLockfile();
|
||||
}
|
||||
spinner.succeed(`${entry}: up to date (${formatGitHubVersion(targetVersion)})`);
|
||||
continue;
|
||||
}
|
||||
if (spinner.isSpinning) {
|
||||
spinner.text = `Updating ${entry} -> ${formatGitHubVersion(targetVersion)}`;
|
||||
}
|
||||
else {
|
||||
spinner.start(`Updating ${entry} -> ${formatGitHubVersion(targetVersion)}`);
|
||||
}
|
||||
const githubResolution = latestInstall;
|
||||
await installSkillWithOptionalStaging(target, exists, (installTarget) => installGitHubSkill(registry, githubResolution, installTarget));
|
||||
const installedFiles = await listTextFiles(target);
|
||||
const installedFingerprint = installedFiles.length > 0 ? hashSkillFiles(installedFiles).fingerprint : undefined;
|
||||
const installedAt = existingOrigin?.installedAt ?? Date.now();
|
||||
await writeSkillOrigin(target, {
|
||||
version: 1,
|
||||
registry: existingOrigin?.registry ?? registry,
|
||||
slug: remoteSlug,
|
||||
...(resolvedOwnerHandle ? { ownerHandle: resolvedOwnerHandle } : {}),
|
||||
installedVersion: targetVersion,
|
||||
installedAt,
|
||||
fingerprint: installedFingerprint,
|
||||
});
|
||||
lock.skills[entry] = withOwnerMetadata(targetVersion, installedAt, resolvedOwnerHandle, lock.skills[entry]);
|
||||
markLockDirty();
|
||||
await flushLockfile();
|
||||
spinner.succeed(`${entry}: updated -> ${formatGitHubVersion(targetVersion)}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const latestVersion = skillMeta.latestVersion ??
|
||||
(latestInstall?.ok && latestInstall.installKind === "archive"
|
||||
? { version: latestInstall.archive.version }
|
||||
: null);
|
||||
let resolveResult;
|
||||
if (localFingerprint) {
|
||||
resolveResult = await resolveSkillVersion(registry, remoteSlug, localFingerprint, resolvedOwnerHandle, token);
|
||||
}
|
||||
else {
|
||||
resolveResult = { match: null, latestVersion };
|
||||
}
|
||||
const originOwnerMatches = !resolvedOwnerHandle ||
|
||||
!existingOrigin?.ownerHandle ||
|
||||
normalizeOwnerHandle(existingOrigin.ownerHandle) === resolvedOwnerHandle;
|
||||
const latest = resolveResult.latestVersion?.version ?? null;
|
||||
const matched = resolveResult.match?.version ??
|
||||
(localFingerprint &&
|
||||
existingOrigin?.fingerprint === localFingerprint &&
|
||||
existingOrigin.slug === remoteSlug &&
|
||||
originOwnerMatches
|
||||
? existingOrigin.installedVersion
|
||||
: null);
|
||||
if (matched &&
|
||||
(lock.skills[entry]?.version !== matched ||
|
||||
(resolvedOwnerHandle && lock.skills[entry]?.ownerHandle !== resolvedOwnerHandle))) {
|
||||
lock.skills[entry] = withOwnerMetadata(matched, lock.skills[entry]?.installedAt ?? Date.now(), resolvedOwnerHandle, lock.skills[entry]);
|
||||
markLockDirty();
|
||||
await flushLockfile();
|
||||
}
|
||||
if (!latest) {
|
||||
spinner.fail(`${entry}: not found`);
|
||||
continue;
|
||||
}
|
||||
if (!matched && localFingerprint && !options.force) {
|
||||
spinner.stop();
|
||||
if (!allowPrompt) {
|
||||
console.log(`${entry}: local changes (no match). Use --force to overwrite.`);
|
||||
continue;
|
||||
}
|
||||
const confirm = await promptConfirm(`${entry}: local changes (no match). Overwrite with ${options.version ?? latest}?`);
|
||||
if (!confirm) {
|
||||
console.log(`${entry}: skipped`);
|
||||
continue;
|
||||
}
|
||||
spinner.start(`Updating ${entry} -> ${options.version ?? latest}`);
|
||||
}
|
||||
const targetVersion = options.version ?? latest;
|
||||
if (options.version) {
|
||||
if (matched && matched === targetVersion) {
|
||||
spinner.succeed(`${styleText(entry, "strong")} ${styleText("already at", "brand")} ${styleText(`v${matched}`, "muted")}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (matched && semver.valid(matched) && semver.gte(matched, targetVersion)) {
|
||||
spinner.succeed(`${styleText(entry, "strong")} ${styleText("up to date", "brand")} ${styleText(`v${matched}`, "muted")}`);
|
||||
continue;
|
||||
}
|
||||
if (spinner.isSpinning) {
|
||||
spinner.text = `Updating ${entry} -> ${targetVersion}`;
|
||||
}
|
||||
else {
|
||||
spinner.start(`Updating ${entry} -> ${targetVersion}`);
|
||||
}
|
||||
await installSkillWithOptionalStaging(target, exists, async (installTarget) => {
|
||||
const zip = await downloadZip(registry, {
|
||||
slug: remoteSlug,
|
||||
...(resolvedOwnerHandle ? { ownerHandle: resolvedOwnerHandle } : {}),
|
||||
version: targetVersion,
|
||||
token,
|
||||
});
|
||||
await extractZipToDir(zip, installTarget);
|
||||
});
|
||||
const installedFiles = await listTextFiles(target);
|
||||
const installedFingerprint = installedFiles.length > 0 ? hashSkillFiles(installedFiles).fingerprint : undefined;
|
||||
const installedAt = existingOrigin?.installedAt ?? Date.now();
|
||||
await writeSkillOrigin(target, {
|
||||
version: 1,
|
||||
registry: existingOrigin?.registry ?? registry,
|
||||
slug: remoteSlug,
|
||||
...(resolvedOwnerHandle ? { ownerHandle: resolvedOwnerHandle } : {}),
|
||||
installedVersion: targetVersion,
|
||||
installedAt,
|
||||
fingerprint: installedFingerprint,
|
||||
});
|
||||
lock.skills[entry] = withOwnerMetadata(targetVersion, installedAt, resolvedOwnerHandle, lock.skills[entry]);
|
||||
markLockDirty();
|
||||
await flushLockfile();
|
||||
spinner.succeed(`${styleText("Updated", "brand")} ${styleText(entry, "strong")} -> ${styleText(`v${targetVersion}`, "muted")}`);
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(formatError(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
await flushLockfile();
|
||||
if (skippedPinned.length > 0) {
|
||||
const suffix = skippedPinned.length === 1 ? "" : "s";
|
||||
console.log(`Skipped ${skippedPinned.length} pinned skill${suffix}: ${skippedPinned.join(", ")}`);
|
||||
}
|
||||
}
|
||||
export async function cmdList(opts) {
|
||||
const lock = await readLockfile(opts.workdir);
|
||||
const entries = Object.entries(lock.skills);
|
||||
const manualSkills = await listManualSkills(opts.dir, new Set(Object.keys(lock.skills)));
|
||||
if (entries.length === 0 && manualSkills.length === 0) {
|
||||
console.log("No installed skills.");
|
||||
return;
|
||||
}
|
||||
for (const [slug, entry] of entries) {
|
||||
const pinned = isPinnedSkillEntry(entry) ? ` pinned${formatPinnedDetails(entry)}` : "";
|
||||
console.log(`${slug} ${entry.version ?? "latest"}${pinned}`);
|
||||
}
|
||||
if (manualSkills.length > 0) {
|
||||
if (entries.length > 0)
|
||||
console.log();
|
||||
console.log("Manually installed (not tracked by clawhub):");
|
||||
for (const slug of manualSkills) {
|
||||
console.log(` ${slug}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
export async function cmdPin(opts, slug, options = {}) {
|
||||
const requested = parseSkillRefOrFail(slug);
|
||||
const lock = await readLockfile(opts.workdir);
|
||||
const lockKey = findExistingLockKey(lock, requested);
|
||||
const existing = lock.skills[lockKey];
|
||||
const label = skillIdentity(requested);
|
||||
if (!existing)
|
||||
fail(`Not installed: ${label}`);
|
||||
const reason = options.reason?.trim() || existing.pinReason;
|
||||
if (isPinnedSkillEntry(existing) && reason === existing.pinReason) {
|
||||
console.log(`Skill "${label}" is already pinned${reason ? `: ${reason}` : ""}`);
|
||||
return;
|
||||
}
|
||||
lock.skills[lockKey] = {
|
||||
...existing,
|
||||
pinned: true,
|
||||
...(reason ? { pinReason: reason } : {}),
|
||||
};
|
||||
await writeLockfile(opts.workdir, lock);
|
||||
console.log(`Pinned ${label}${reason ? `: ${reason}` : ""}`);
|
||||
}
|
||||
export async function cmdUnpin(opts, slug) {
|
||||
const requested = parseSkillRefOrFail(slug);
|
||||
const lock = await readLockfile(opts.workdir);
|
||||
const lockKey = findExistingLockKey(lock, requested);
|
||||
const existing = lock.skills[lockKey];
|
||||
const label = skillIdentity(requested);
|
||||
if (!existing)
|
||||
fail(`Not installed: ${label}`);
|
||||
if (!isPinnedSkillEntry(existing))
|
||||
fail(`Skill "${label}" is not pinned`);
|
||||
lock.skills[lockKey] = {
|
||||
version: existing.version,
|
||||
installedAt: existing.installedAt,
|
||||
...(existing.ownerHandle ? { ownerHandle: existing.ownerHandle } : {}),
|
||||
};
|
||||
await writeLockfile(opts.workdir, lock);
|
||||
console.log(`Unpinned ${label}`);
|
||||
}
|
||||
export async function cmdUninstall(opts, slug, options = {}, inputAllowed) {
|
||||
const requested = parseSkillRefOrFail(slug);
|
||||
const lock = await readLockfile(opts.workdir);
|
||||
const lockKey = findExistingLockKey(lock, requested);
|
||||
if (!lock.skills[lockKey]) {
|
||||
fail(`Not installed: ${skillIdentity(requested)}`);
|
||||
}
|
||||
const allowPrompt = isInteractive() && inputAllowed;
|
||||
if (!options.yes) {
|
||||
if (!allowPrompt)
|
||||
fail("Pass --yes (no input)");
|
||||
const confirm = await promptConfirm(`Uninstall ${skillIdentity(requested)}?`);
|
||||
if (!confirm) {
|
||||
console.log("Cancelled.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
const localRef = lockKey === skillIdentity(requested) ? requested : parseSkillRefOrFail(lockKey);
|
||||
const spinner = createCrabLoader(`Uninstalling ${skillIdentity(localRef)}`);
|
||||
try {
|
||||
const target = skillTarget(opts.dir, localRef);
|
||||
await rm(target, { recursive: true, force: true });
|
||||
delete lock.skills[lockKey];
|
||||
await writeLockfile(opts.workdir, lock);
|
||||
spinner.succeed(`${styleText("Uninstalled", "brand")} ${styleText(skillIdentity(localRef), "strong")}`);
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(formatError(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
export async function cmdExplore(opts, options = {}) {
|
||||
const token = await getOptionalAuthToken();
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
const spinner = createCrabLoader("Fetching latest skills");
|
||||
try {
|
||||
const url = registryUrl(ApiRoutes.skills, registry);
|
||||
const boundedLimit = clampLimit(options.limit ?? 25);
|
||||
const { apiSort } = resolveExploreSort(options.sort);
|
||||
url.searchParams.set("limit", String(boundedLimit));
|
||||
if (apiSort !== "updated")
|
||||
url.searchParams.set("sort", apiSort);
|
||||
const result = await apiRequest(registry, { method: "GET", url: url.toString(), token }, ApiV1SkillListResponseSchema);
|
||||
spinner.stop();
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
if (result.items.length === 0) {
|
||||
console.log("No skills found.");
|
||||
return;
|
||||
}
|
||||
for (const item of result.items) {
|
||||
console.log(formatExploreLine(item));
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(formatError(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
export function formatExploreLine(item) {
|
||||
const version = item.latestVersion?.version ?? "?";
|
||||
const age = formatRelativeTime(item.updatedAt);
|
||||
const summary = item.summary ? ` ${styleText(truncate(item.summary, 50), "muted")}` : "";
|
||||
return `${styleText(item.slug, "brand")} ${styleText(`v${version}`, "muted")} ${styleText(age, "muted")}${summary}`;
|
||||
}
|
||||
export function clampLimit(limit, fallback = 25) {
|
||||
if (!Number.isFinite(limit))
|
||||
return fallback;
|
||||
return Math.min(Math.max(1, limit), 200);
|
||||
}
|
||||
export async function cmdReportSkill(opts, slug, options = {}) {
|
||||
const trimmed = normalizeSkillSlugOrFail(slug);
|
||||
const reason = options.reason?.trim();
|
||||
if (!reason)
|
||||
fail("--reason required");
|
||||
const token = await requireAuthToken();
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
const result = await apiRequest(registry, {
|
||||
method: "POST",
|
||||
path: `${ApiRoutes.skills}/${encodeURIComponent(trimmed)}/report`,
|
||||
token,
|
||||
body: {
|
||||
reason,
|
||||
...(options.version?.trim() ? { version: options.version.trim() } : {}),
|
||||
},
|
||||
}, ApiV1SkillReportResponseSchema);
|
||||
if (options.json) {
|
||||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
if (result.alreadyReported) {
|
||||
console.log(`Already reported ${trimmed}.`);
|
||||
}
|
||||
else {
|
||||
console.log(`OK. Reported ${trimmed} (${result.reportId}).`);
|
||||
}
|
||||
}
|
||||
export async function cmdListSkillReports(opts, options = {}) {
|
||||
const status = options.status?.trim() || "open";
|
||||
if (!["open", "confirmed", "dismissed", "all"].includes(status)) {
|
||||
fail("--status must be open, confirmed, dismissed, or all");
|
||||
}
|
||||
const token = await requireAuthToken();
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
const url = registryUrl(`${ApiRoutes.skills}/-/reports`, registry);
|
||||
url.searchParams.set("status", status);
|
||||
if (options.cursor?.trim())
|
||||
url.searchParams.set("cursor", options.cursor.trim());
|
||||
url.searchParams.set("limit", String(clampLimit(options.limit ?? 25, 25)));
|
||||
const result = await apiRequest(registry, { method: "GET", url: url.toString(), token }, ApiV1SkillReportListResponseSchema);
|
||||
if (options.json) {
|
||||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
if (result.items.length === 0) {
|
||||
console.log("No skill reports found.");
|
||||
}
|
||||
else {
|
||||
for (const item of result.items) {
|
||||
const reporter = item.reporter.handle ?? item.reporter.userId;
|
||||
console.log(`${item.reportId} ${item.status} ${item.slug}`);
|
||||
console.log(` reporter: ${reporter}`);
|
||||
if (item.reason)
|
||||
console.log(` reason: ${item.reason}`);
|
||||
if (item.triageNote)
|
||||
console.log(` note: ${item.triageNote}`);
|
||||
}
|
||||
}
|
||||
if (!result.done && result.nextCursor)
|
||||
console.log(`Next cursor: ${result.nextCursor}`);
|
||||
}
|
||||
export async function cmdTriageSkillReport(opts, reportId, options = {}) {
|
||||
const trimmed = reportId.trim();
|
||||
if (!trimmed)
|
||||
fail("Report id required");
|
||||
const statusValue = options.status?.trim();
|
||||
if (!statusValue || !["open", "confirmed", "dismissed"].includes(statusValue)) {
|
||||
fail("--status must be open, confirmed, or dismissed");
|
||||
}
|
||||
const status = statusValue;
|
||||
const finalAction = (options.finalAction ?? options.action)?.trim();
|
||||
if (finalAction && !["none", "hide"].includes(finalAction)) {
|
||||
fail("--action must be none or hide");
|
||||
}
|
||||
const note = options.note?.trim();
|
||||
if (status !== "open" && !note)
|
||||
fail("--note required unless reopening");
|
||||
const token = await requireAuthToken();
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
await presentModerationPlan(reportModerationPlan({
|
||||
entityLabel: "skill",
|
||||
reportId: trimmed,
|
||||
status,
|
||||
finalAction: finalAction ?? "none",
|
||||
}), options);
|
||||
const result = await apiRequest(registry, {
|
||||
method: "POST",
|
||||
path: `${ApiRoutes.skills}/-/reports/${encodeURIComponent(trimmed)}/triage`,
|
||||
token,
|
||||
body: {
|
||||
status,
|
||||
...(note ? { note } : {}),
|
||||
...(finalAction ? { finalAction } : {}),
|
||||
},
|
||||
}, ApiV1SkillReportTriageResponseSchema);
|
||||
if (options.json) {
|
||||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
const actionSuffix = result.actionTaken && result.actionTaken !== "none" ? `; action ${result.actionTaken}` : "";
|
||||
console.log(`OK. Skill report ${trimmed} set to ${result.status}${actionSuffix}.`);
|
||||
}
|
||||
function formatRelativeTime(timestamp) {
|
||||
const now = Date.now();
|
||||
const diff = now - timestamp;
|
||||
const seconds = Math.floor(diff / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
if (days > 30) {
|
||||
const months = Math.floor(days / 30);
|
||||
return `${months}mo ago`;
|
||||
}
|
||||
if (days > 0)
|
||||
return `${days}d ago`;
|
||||
if (hours > 0)
|
||||
return `${hours}h ago`;
|
||||
if (minutes > 0)
|
||||
return `${minutes}m ago`;
|
||||
return "just now";
|
||||
}
|
||||
function truncate(str, maxLen) {
|
||||
if (str.length <= maxLen)
|
||||
return str;
|
||||
return `${str.slice(0, maxLen - 1)}…`;
|
||||
}
|
||||
function resolveExploreSort(raw) {
|
||||
const normalized = raw?.trim().toLowerCase();
|
||||
if (!normalized ||
|
||||
normalized === "newest" ||
|
||||
normalized === "createdat" ||
|
||||
normalized === "created-at") {
|
||||
return { sort: "newest", apiSort: "createdAt" };
|
||||
}
|
||||
if (normalized === "updated") {
|
||||
return { sort: "newest", apiSort: "updated" };
|
||||
}
|
||||
if (normalized === "rating" || normalized === "stars" || normalized === "star") {
|
||||
return { sort: "rating", apiSort: "stars" };
|
||||
}
|
||||
if (normalized === "downloads" || normalized === "download") {
|
||||
return { sort: "downloads", apiSort: "downloads" };
|
||||
}
|
||||
if (normalized === "installs" || normalized === "install") {
|
||||
return { sort: "installs", apiSort: "installs" };
|
||||
}
|
||||
if (normalized === "installscurrent" ||
|
||||
normalized === "installs-current" ||
|
||||
normalized === "current" ||
|
||||
normalized === "installsalltime" ||
|
||||
normalized === "installs-all-time") {
|
||||
return { sort: "installs", apiSort: "installs" };
|
||||
}
|
||||
if (normalized === "trending") {
|
||||
return { sort: "trending", apiSort: "trending" };
|
||||
}
|
||||
return fail(`Invalid sort "${raw}". Use newest, updated, rating, downloads, installs, or trending.`);
|
||||
}
|
||||
async function resolveSkillVersion(registry, slug, hash, ownerHandle, token) {
|
||||
const url = registryUrl(ApiRoutes.resolve, registry);
|
||||
url.searchParams.set("slug", slug);
|
||||
if (ownerHandle)
|
||||
url.searchParams.set("ownerHandle", ownerHandle);
|
||||
url.searchParams.set("hash", hash);
|
||||
return apiRequest(registry, { method: "GET", url: url.toString(), token }, ApiV1SkillResolveResponseSchema);
|
||||
}
|
||||
async function resolveLatestSkillInstall(registry, slug, ownerHandle, token, options = {}) {
|
||||
const path = `${ApiRoutes.skills}/${encodeURIComponent(slug)}/install`;
|
||||
const url = ownerScopedUrl(registry, path, ownerHandle);
|
||||
const requestUrl = url ? new URL(url) : null;
|
||||
if (options.forceInstall) {
|
||||
if (requestUrl)
|
||||
requestUrl.searchParams.set("forceInstall", "1");
|
||||
}
|
||||
return await apiRequest(registry, requestUrl
|
||||
? {
|
||||
method: "GET",
|
||||
url: requestUrl.toString(),
|
||||
token,
|
||||
acceptedStatuses: [403, 409, 410, 423],
|
||||
}
|
||||
: {
|
||||
method: "GET",
|
||||
path: `${path}${options.forceInstall ? "?forceInstall=1" : ""}`,
|
||||
token,
|
||||
acceptedStatuses: [403, 409, 410, 423],
|
||||
}, ApiV1SkillInstallResolveResponseSchema);
|
||||
}
|
||||
async function installGitHubSkill(registry, resolution, target) {
|
||||
const zip = await fetchBinary(registry, {
|
||||
url: gitHubZipUrl(resolution.github.repo, resolution.github.commit),
|
||||
});
|
||||
await extractGitHubZipPathToDir(zip, target, resolution.github.path);
|
||||
}
|
||||
async function installSkillWithOptionalStaging(target, targetExists, install) {
|
||||
if (!targetExists) {
|
||||
await install(target);
|
||||
return;
|
||||
}
|
||||
const parent = dirname(target);
|
||||
await mkdir(parent, { recursive: true });
|
||||
const stage = await mkdtemp(join(parent, `.${basename(target)}.tmp-`));
|
||||
const backup = join(parent, `.${basename(target)}.backup-${process.pid}-${Date.now()}`);
|
||||
let stageMoved = false;
|
||||
let backupCreated = false;
|
||||
try {
|
||||
await install(stage);
|
||||
await rename(target, backup);
|
||||
backupCreated = true;
|
||||
await rename(stage, target);
|
||||
stageMoved = true;
|
||||
await rm(backup, { recursive: true, force: true });
|
||||
}
|
||||
catch (error) {
|
||||
if (backupCreated) {
|
||||
await rm(target, { recursive: true, force: true }).catch(() => { });
|
||||
await rename(backup, target).catch(() => { });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
finally {
|
||||
if (!stageMoved) {
|
||||
await rm(stage, { recursive: true, force: true }).catch(() => { });
|
||||
}
|
||||
}
|
||||
}
|
||||
function gitHubZipUrl(repo, commit) {
|
||||
const base = (process.env.CLAWHUB_GITHUB_CODELOAD_BASE_URL ||
|
||||
process.env.OPENCLAW_CLAWHUB_GITHUB_CODELOAD_BASE_URL ||
|
||||
"https://codeload.github.com").replace(/\/+$/, "");
|
||||
return `${base}/${encodeGitHubRepo(repo)}/zip/${encodeURIComponent(commit)}`;
|
||||
}
|
||||
function encodeGitHubRepo(repo) {
|
||||
return repo
|
||||
.split("/")
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join("/");
|
||||
}
|
||||
function formatGitHubVersion(commit) {
|
||||
return commit.length > 12 ? commit.slice(0, 12) : commit;
|
||||
}
|
||||
async function fileExists(path) {
|
||||
try {
|
||||
await stat(path);
|
||||
return true;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=skills.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+8
@@ -0,0 +1,8 @@
|
||||
import type { GlobalOpts } from "../types.js";
|
||||
export declare function cmdStarSkill(opts: GlobalOpts, slugArg: string, options: {
|
||||
yes?: boolean;
|
||||
}, inputAllowed: boolean): Promise<{
|
||||
ok: true;
|
||||
starred: boolean;
|
||||
alreadyStarred: boolean;
|
||||
} | undefined>;
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { apiRequest, registryUrl } from "../../http.js";
|
||||
import { ApiRoutes, ApiV1StarResponseSchema } from "../../schema/index.js";
|
||||
import { requireAuthToken } from "../authToken.js";
|
||||
import { getRegistry } from "../registry.js";
|
||||
import { createCrabLoader, fail, formatError, isInteractive, promptConfirm } from "../ui.js";
|
||||
function parseSkillRef(skillArg) {
|
||||
const value = skillArg.trim().toLowerCase();
|
||||
if (!value)
|
||||
fail("Skill required");
|
||||
const slashIndex = value.indexOf("/");
|
||||
if (slashIndex < 0)
|
||||
return { slug: value, ownerHandle: undefined };
|
||||
if (value.indexOf("/", slashIndex + 1) >= 0)
|
||||
fail(`Invalid skill ref: ${skillArg}`);
|
||||
const ownerHandle = value.slice(0, slashIndex).replace(/^@+/, "");
|
||||
const slug = value.slice(slashIndex + 1);
|
||||
if (!ownerHandle || !slug)
|
||||
fail(`Invalid skill ref: ${skillArg}`);
|
||||
return { slug, ownerHandle };
|
||||
}
|
||||
function starRequestArgs(registry, slug, ownerHandle, token) {
|
||||
const path = `${ApiRoutes.stars}/${encodeURIComponent(slug)}`;
|
||||
if (!ownerHandle)
|
||||
return { method: "POST", path, token };
|
||||
const url = registryUrl(path, registry);
|
||||
url.searchParams.set("ownerHandle", ownerHandle);
|
||||
return { method: "POST", url: url.toString(), token };
|
||||
}
|
||||
export async function cmdStarSkill(opts, slugArg, options, inputAllowed) {
|
||||
const requested = parseSkillRef(slugArg);
|
||||
const slug = requested.slug;
|
||||
const allowPrompt = isInteractive() && inputAllowed !== false;
|
||||
if (!options.yes) {
|
||||
if (!allowPrompt)
|
||||
fail("Pass --yes (no input)");
|
||||
const ok = await promptConfirm(`Star ${slug}?`);
|
||||
if (!ok)
|
||||
return undefined;
|
||||
}
|
||||
const token = await requireAuthToken();
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
const spinner = createCrabLoader(`Starring ${slug}`);
|
||||
try {
|
||||
const result = await apiRequest(registry, starRequestArgs(registry, slug, requested.ownerHandle, token), ApiV1StarResponseSchema);
|
||||
spinner.succeed(result.alreadyStarred ? `OK. ${slug} already starred.` : `OK. Starred ${slug}`);
|
||||
return result;
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(formatError(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=star.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"star.js","sourceRoot":"","sources":["../../../src/cli/commands/star.ts"],"names":[],"mappings":"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"}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import type { GlobalOpts } from "../types.js";
|
||||
import type { SyncOptions } from "./syncTypes.js";
|
||||
export declare function cmdSync(opts: GlobalOpts, options: SyncOptions, inputAllowed: boolean): Promise<void>;
|
||||
+421
@@ -0,0 +1,421 @@
|
||||
import { isAbsolute, relative } from "node:path";
|
||||
import { intro, outro } from "@clack/prompts";
|
||||
import { hashSkillFiles, readSkillOrigin } from "../../skills.js";
|
||||
import { getOptionalAuthToken, requireAuthToken } from "../authToken.js";
|
||||
import { getRegistry } from "../registry.js";
|
||||
import { getFallbackSkillRoots } from "../scanSkills.js";
|
||||
import { createCrabLoader, fail, formatError, isInteractive } from "../ui.js";
|
||||
import { normalizeGitHubRepo } from "./github.js";
|
||||
import { cmdPublish, prepareSkillFilesForPublish, resolveDefaultOwnerHandle } from "./publish.js";
|
||||
import { buildScanRoots, checkRegistrySyncState, dedupeSkillsBySlug, formatActionableLine, formatBulletList, formatCommaList, formatList, formatSyncedDisplay, formatSyncedSummary, mapWithConcurrency, normalizeConcurrency, printSection, resolvePublishMeta, scanRootsWithLabels, selectToUpload, } from "./syncHelpers.js";
|
||||
export async function cmdSync(opts, options, inputAllowed) {
|
||||
const jsonMode = options.json === true;
|
||||
const allowPrompt = !jsonMode && isInteractive() && inputAllowed !== false;
|
||||
if (!jsonMode)
|
||||
intro("ClawHub sync");
|
||||
const token = options.dryRun ? await getOptionalAuthToken() : await requireAuthToken();
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
const ownerHandle = await resolveSyncOwnerHandle(registry, token, options.owner);
|
||||
const selectedRoots = buildScanRoots(opts, options.root);
|
||||
const concurrency = normalizeConcurrency(options.concurrency);
|
||||
const spinner = jsonMode ? null : createCrabLoader("Scanning for local skills");
|
||||
const primaryScan = await scanRootsWithLabels(selectedRoots);
|
||||
let scan = primaryScan;
|
||||
let outputRoots = primaryScan.roots;
|
||||
if (primaryScan.skills.length === 0) {
|
||||
const fallbackScan = await scanRootsWithLabels(getFallbackSkillRoots(opts.workdir));
|
||||
spinner?.stop();
|
||||
scan = fallbackScan;
|
||||
outputRoots = fallbackScan.roots;
|
||||
if (fallbackScan.skills.length === 0)
|
||||
fail("No skills found (checked configured roots)");
|
||||
if (!options.dryRun && options.all) {
|
||||
fail("Refusing to publish fallback skill roots with --all. Pass --root for the skills to sync.");
|
||||
}
|
||||
if (!jsonMode) {
|
||||
printSection(`No skills in workdir. Found ${fallbackScan.skills.length} in fallback locations.`, formatList(fallbackScan.rootsWithSkills, 10));
|
||||
}
|
||||
}
|
||||
else {
|
||||
spinner?.stop();
|
||||
if (!jsonMode && primaryScan.rootsWithSkills.length > 0) {
|
||||
printSection("Roots with skills", formatList(primaryScan.rootsWithSkills, 10));
|
||||
}
|
||||
}
|
||||
const deduped = dedupeSkillsBySlug(scan.skills);
|
||||
const skills = deduped.skills;
|
||||
if (!jsonMode && deduped.duplicates.length > 0) {
|
||||
printSection("Skipped duplicate slugs", formatCommaList(deduped.duplicates, 16));
|
||||
}
|
||||
const parsingLoader = jsonMode ? null : createCrabLoader("Parsing local skills");
|
||||
const locals = [];
|
||||
try {
|
||||
let done = 0;
|
||||
const parsed = await mapWithConcurrency(skills, Math.min(concurrency, 12), async (skill) => {
|
||||
const filesOnDisk = await prepareSkillFilesForPublish(skill.folder);
|
||||
const hashed = hashSkillFiles(filesOnDisk);
|
||||
const origin = await readSkillOrigin(skill.folder);
|
||||
done += 1;
|
||||
if (parsingLoader)
|
||||
parsingLoader.text = `Parsing local skills ${done}/${skills.length}`;
|
||||
return {
|
||||
...skill,
|
||||
fingerprint: hashed.fingerprint,
|
||||
fileCount: filesOnDisk.length,
|
||||
origin,
|
||||
};
|
||||
});
|
||||
locals.push(...parsed);
|
||||
}
|
||||
catch (error) {
|
||||
parsingLoader?.fail(formatError(error));
|
||||
throw error;
|
||||
}
|
||||
finally {
|
||||
parsingLoader?.stop();
|
||||
}
|
||||
const candidatesLoader = jsonMode ? null : createCrabLoader("Checking registry sync state");
|
||||
const candidates = [];
|
||||
try {
|
||||
let done = 0;
|
||||
const resolved = await mapWithConcurrency(locals, Math.min(concurrency, 16), async (skill) => {
|
||||
try {
|
||||
return await checkRegistrySyncState(registry, skill, ownerHandle, token);
|
||||
}
|
||||
finally {
|
||||
done += 1;
|
||||
if (candidatesLoader) {
|
||||
candidatesLoader.text = `Checking registry sync state ${done}/${locals.length}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
candidates.push(...resolved);
|
||||
}
|
||||
catch (error) {
|
||||
candidatesLoader?.fail(formatError(error));
|
||||
throw error;
|
||||
}
|
||||
finally {
|
||||
candidatesLoader?.stop();
|
||||
}
|
||||
const synced = candidates.filter((candidate) => candidate.status === "synced");
|
||||
const actionable = candidates.filter((candidate) => candidate.status !== "synced");
|
||||
const bump = options.bump ?? "patch";
|
||||
if (actionable.length === 0) {
|
||||
writeNoActionOutput({
|
||||
jsonMode,
|
||||
dryRun: Boolean(options.dryRun),
|
||||
registry,
|
||||
roots: outputRoots,
|
||||
owner: ownerHandle,
|
||||
duplicates: deduped.duplicates,
|
||||
synced,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!jsonMode) {
|
||||
printSection("To sync", formatBulletList(actionable.map((candidate) => formatActionableLine(candidate, bump)), 20));
|
||||
}
|
||||
if (!jsonMode && synced.length > 0) {
|
||||
printSection("Already synced", formatSyncedDisplay(synced));
|
||||
}
|
||||
if (!options.dryRun && !options.all && !allowPrompt) {
|
||||
fail("Pass --all to publish every detected local skill without prompting.");
|
||||
}
|
||||
const selected = await selectToUpload(actionable, {
|
||||
allowPrompt,
|
||||
all: Boolean(options.all),
|
||||
bump,
|
||||
});
|
||||
if (selected.length === 0) {
|
||||
writeNoActionOutput({
|
||||
jsonMode,
|
||||
dryRun: Boolean(options.dryRun),
|
||||
registry,
|
||||
roots: outputRoots,
|
||||
owner: ownerHandle,
|
||||
duplicates: deduped.duplicates,
|
||||
synced,
|
||||
outroText: "Nothing selected.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const plannedPublishes = selected.map((skill) => {
|
||||
const source = buildSourceProvenance(opts, skill, options, outputRoots);
|
||||
return { skill, source };
|
||||
});
|
||||
if (options.dryRun) {
|
||||
const wouldPublish = plannedPublishes.map(({ skill, source }) => {
|
||||
const { publishVersion } = resolvePublishMeta(skill, {
|
||||
bump,
|
||||
changelogFlag: options.changelog,
|
||||
});
|
||||
return formatPublishJson(skill, publishVersion, source);
|
||||
});
|
||||
if (jsonMode) {
|
||||
writeSyncJson(buildSyncJsonOutput({
|
||||
ok: true,
|
||||
dryRun: true,
|
||||
registry,
|
||||
roots: outputRoots,
|
||||
owner: ownerHandle,
|
||||
duplicates: deduped.duplicates,
|
||||
alreadySynced: synced.map(formatSyncedJson),
|
||||
wouldPublish,
|
||||
published: [],
|
||||
failed: [],
|
||||
}));
|
||||
return;
|
||||
}
|
||||
outro(`Dry run: would publish ${selected.length} skill(s).`);
|
||||
return;
|
||||
}
|
||||
const tags = options.tags ?? "latest";
|
||||
const failedUploads = [];
|
||||
const published = [];
|
||||
const racedNoOps = [];
|
||||
for (const { skill, source } of plannedPublishes) {
|
||||
const { publishVersion, changelog } = resolvePublishMeta(skill, {
|
||||
bump,
|
||||
changelogFlag: options.changelog,
|
||||
});
|
||||
const forkOf = buildForkOf(skill, registry, ownerHandle);
|
||||
try {
|
||||
const previousExitCode = process.exitCode;
|
||||
const result = await cmdPublish(opts, skill.folder, {
|
||||
slug: skill.slug,
|
||||
name: skill.displayName,
|
||||
owner: ownerHandle,
|
||||
version: publishVersion,
|
||||
changelog,
|
||||
tags,
|
||||
forkOf,
|
||||
quiet: jsonMode,
|
||||
...(source
|
||||
? {
|
||||
sourceRepo: options.sourceRepo,
|
||||
sourceCommit: options.sourceCommit,
|
||||
sourceRef: options.sourceRef,
|
||||
sourcePath: source.path,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
const publishExitCode = process.exitCode;
|
||||
if (isNonZeroExitCode(publishExitCode) && publishExitCode !== previousExitCode) {
|
||||
process.exitCode = previousExitCode;
|
||||
failedUploads.push({
|
||||
slug: skill.slug,
|
||||
message: `Publish command exited with code ${String(publishExitCode)}`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (result?.status === "unchanged") {
|
||||
racedNoOps.push({
|
||||
slug: skill.slug,
|
||||
folder: skill.folder,
|
||||
version: result.version ?? publishVersion,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
published.push({
|
||||
slug: skill.slug,
|
||||
folder: skill.folder,
|
||||
version: result?.version ?? publishVersion,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
failedUploads.push({ slug: skill.slug, message: formatError(error) });
|
||||
}
|
||||
}
|
||||
if (failedUploads.length > 0) {
|
||||
if (jsonMode) {
|
||||
writeSyncJson(buildSyncJsonOutput({
|
||||
ok: false,
|
||||
dryRun: false,
|
||||
registry,
|
||||
roots: outputRoots,
|
||||
owner: ownerHandle,
|
||||
duplicates: deduped.duplicates,
|
||||
alreadySynced: [...synced.map(formatSyncedJson), ...racedNoOps],
|
||||
wouldPublish: [],
|
||||
published,
|
||||
failed: failedUploads,
|
||||
}));
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
printSection("Failed to publish", formatBulletList(failedUploads.map((failure) => `${failure.slug}: ${failure.message}`), 20));
|
||||
outro(`Published ${published.length} of ${selected.length} skill(s). ${failedUploads.length} failed.`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
if (jsonMode) {
|
||||
writeSyncJson(buildSyncJsonOutput({
|
||||
ok: true,
|
||||
dryRun: false,
|
||||
registry,
|
||||
roots: outputRoots,
|
||||
owner: ownerHandle,
|
||||
duplicates: deduped.duplicates,
|
||||
alreadySynced: [...synced.map(formatSyncedJson), ...racedNoOps],
|
||||
wouldPublish: [],
|
||||
published,
|
||||
failed: [],
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (racedNoOps.length > 0) {
|
||||
outro(`Published ${published.length} of ${selected.length} skill(s). ${racedNoOps.length} already synced.`);
|
||||
}
|
||||
else {
|
||||
outro(`Published ${published.length} skill(s).`);
|
||||
}
|
||||
}
|
||||
function writeNoActionOutput(params) {
|
||||
if (params.jsonMode) {
|
||||
writeSyncJson(buildSyncJsonOutput({
|
||||
ok: true,
|
||||
dryRun: params.dryRun,
|
||||
registry: params.registry,
|
||||
roots: params.roots,
|
||||
owner: params.owner,
|
||||
duplicates: params.duplicates,
|
||||
alreadySynced: params.synced.map(formatSyncedJson),
|
||||
wouldPublish: [],
|
||||
published: [],
|
||||
failed: [],
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (params.synced.length > 0) {
|
||||
printSection("Already synced", formatCommaList(params.synced.map(formatSyncedSummary), 16));
|
||||
}
|
||||
outro(params.outroText ?? "Nothing to sync.");
|
||||
}
|
||||
function normalizeRegistry(value) {
|
||||
return value.trim().replace(/\/+$/, "").toLowerCase();
|
||||
}
|
||||
function buildForkOf(skill, registry, ownerHandle) {
|
||||
const origin = skill.origin;
|
||||
if (!origin || normalizeRegistry(origin.registry) !== normalizeRegistry(registry))
|
||||
return undefined;
|
||||
const originOwner = normalizeOwner(origin.ownerHandle);
|
||||
const publishOwner = normalizeOwner(ownerHandle);
|
||||
if (origin.slug === skill.slug && originOwner === publishOwner)
|
||||
return undefined;
|
||||
const ref = `${origin.slug}@${origin.installedVersion}`;
|
||||
return originOwner ? `@${originOwner}/${ref}` : ref;
|
||||
}
|
||||
function isNonZeroExitCode(value) {
|
||||
if (typeof value === "number")
|
||||
return value !== 0;
|
||||
if (typeof value === "string")
|
||||
return value.trim() !== "" && value.trim() !== "0";
|
||||
return false;
|
||||
}
|
||||
function normalizeOwner(value) {
|
||||
return value?.trim().replace(/^@+/, "") || undefined;
|
||||
}
|
||||
async function resolveSyncOwnerHandle(registry, token, owner) {
|
||||
const explicitOwner = normalizeOwner(owner);
|
||||
if (explicitOwner || !token)
|
||||
return explicitOwner;
|
||||
return await resolveDefaultOwnerHandle(registry, token);
|
||||
}
|
||||
function formatSyncedJson(candidate) {
|
||||
return {
|
||||
slug: candidate.slug,
|
||||
folder: candidate.folder,
|
||||
version: candidate.matchVersion ?? candidate.latestVersion ?? "unknown",
|
||||
};
|
||||
}
|
||||
function formatPublishJson(candidate, version, source) {
|
||||
return {
|
||||
slug: candidate.slug,
|
||||
displayName: candidate.displayName,
|
||||
folder: candidate.folder,
|
||||
status: candidate.status,
|
||||
version,
|
||||
latestVersion: candidate.latestVersion,
|
||||
fileCount: candidate.fileCount,
|
||||
fingerprint: candidate.fingerprint,
|
||||
...(source ? { source } : {}),
|
||||
};
|
||||
}
|
||||
function buildSourceProvenance(opts, skill, options, scanRoots) {
|
||||
const rawRepo = options.sourceRepo?.trim();
|
||||
const commit = options.sourceCommit?.trim();
|
||||
if (!rawRepo && !commit && !options.sourceRef?.trim())
|
||||
return undefined;
|
||||
if (!rawRepo || !commit)
|
||||
fail("--source-repo and --source-commit must be provided together");
|
||||
const repo = normalizeGitHubRepo(rawRepo);
|
||||
if (!repo)
|
||||
fail("--source-repo must be a GitHub repo or URL");
|
||||
return {
|
||||
kind: "github",
|
||||
url: `https://github.com/${repo}`,
|
||||
repo,
|
||||
ref: options.sourceRef?.trim() || commit,
|
||||
commit,
|
||||
path: sourcePathForSkill(opts, skill.folder, scanRoots),
|
||||
};
|
||||
}
|
||||
function sourcePathForSkill(opts, folder, scanRoots) {
|
||||
const bases = Array.from(new Set([opts.workdir, opts.dir, ...scanRoots, process.cwd()].map(normalizeSourcePathBase)));
|
||||
for (const base of bases) {
|
||||
const rel = relativeInside(base, folder);
|
||||
if (rel)
|
||||
return rel;
|
||||
}
|
||||
fail("Source provenance requires each skill folder to be inside the current directory, --workdir, configured skills directory, or a --root directory.");
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
function normalizeSourcePathBase(value) {
|
||||
return value.replace(/\/+$/, "") || "/";
|
||||
}
|
||||
function relativeInside(base, target) {
|
||||
const rel = relative(base, target);
|
||||
if (!rel)
|
||||
return ".";
|
||||
if (rel.startsWith("..") || isAbsolute(rel))
|
||||
return null;
|
||||
return normalizeSourcePath(rel);
|
||||
}
|
||||
function normalizeSourcePath(value) {
|
||||
const normalized = value
|
||||
.replaceAll("\\", "/")
|
||||
.replace(/^\.\/+/, "")
|
||||
.replace(/\/+$/, "");
|
||||
return normalized || ".";
|
||||
}
|
||||
function buildSyncJsonOutput(params) {
|
||||
const skipped = params.duplicates.map((duplicate) => ({
|
||||
slug: duplicate.replace(/\s+\(\d+\)$/, ""),
|
||||
reason: "duplicate-slug",
|
||||
detail: duplicate,
|
||||
}));
|
||||
return {
|
||||
ok: params.ok,
|
||||
dryRun: params.dryRun,
|
||||
registry: params.registry,
|
||||
roots: params.roots,
|
||||
...(params.owner ? { owner: params.owner } : {}),
|
||||
summary: {
|
||||
wouldPublish: params.wouldPublish.length,
|
||||
published: params.published.length,
|
||||
alreadySynced: params.alreadySynced.length,
|
||||
skipped: skipped.length,
|
||||
failed: params.failed.length,
|
||||
},
|
||||
wouldPublish: params.wouldPublish,
|
||||
published: params.published,
|
||||
alreadySynced: params.alreadySynced,
|
||||
skipped,
|
||||
failed: params.failed,
|
||||
};
|
||||
}
|
||||
function writeSyncJson(value) {
|
||||
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
||||
}
|
||||
//# sourceMappingURL=sync.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+36
@@ -0,0 +1,36 @@
|
||||
import { type SkillFolder } from "../scanSkills.js";
|
||||
import type { GlobalOpts } from "../types.js";
|
||||
import type { Candidate, LocalSkill } from "./syncTypes.js";
|
||||
export declare function buildScanRoots(opts: GlobalOpts, extraRoots: string[] | undefined): string[];
|
||||
export declare function normalizeConcurrency(value: number | undefined): number;
|
||||
export declare function mapWithConcurrency<T, R>(items: T[], limit: number, fn: (item: T) => Promise<R>): Promise<R[]>;
|
||||
export declare function checkRegistrySyncState(registry: string, skill: LocalSkill, ownerHandle?: string, token?: string): Promise<Candidate>;
|
||||
export declare function scanRootsWithLabels(roots: string[]): Promise<{
|
||||
roots: string[];
|
||||
skillsByRoot: Record<string, SkillFolder[]>;
|
||||
skills: SkillFolder[];
|
||||
rootsWithSkills: string[];
|
||||
}>;
|
||||
export declare function selectToUpload(candidates: Candidate[], params: {
|
||||
allowPrompt: boolean;
|
||||
all: boolean;
|
||||
bump: "patch" | "minor" | "major";
|
||||
}): Promise<Candidate[]>;
|
||||
export declare function resolvePublishMeta(skill: Candidate, params: {
|
||||
bump: "patch" | "minor" | "major";
|
||||
changelogFlag?: string;
|
||||
}): {
|
||||
publishVersion: string;
|
||||
changelog: string;
|
||||
};
|
||||
export declare function formatList(values: string[], max: number): string;
|
||||
export declare function printSection(title: string, body?: string): void;
|
||||
export declare function dedupeSkillsBySlug(skills: SkillFolder[]): {
|
||||
skills: SkillFolder[];
|
||||
duplicates: string[];
|
||||
};
|
||||
export declare function formatActionableLine(candidate: Candidate, bump: "patch" | "minor" | "major"): string;
|
||||
export declare function formatSyncedSummary(candidate: Candidate): string;
|
||||
export declare function formatBulletList(lines: string[], max: number): string;
|
||||
export declare function formatSyncedDisplay(synced: Candidate[]): string;
|
||||
export declare function formatCommaList(values: string[], max: number): string;
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
import { realpath } from "node:fs/promises";
|
||||
import { isAbsolute, resolve } from "node:path";
|
||||
import { isCancel, multiselect } from "@clack/prompts";
|
||||
import semver from "semver";
|
||||
import { resolveHome } from "../../homedir.js";
|
||||
import { apiRequest } from "../../http.js";
|
||||
import { ApiRoutes, ApiV1SkillResolveResponseSchema } from "../../schema/index.js";
|
||||
import { findSkillFolders } from "../scanSkills.js";
|
||||
import { fail, formatError } from "../ui.js";
|
||||
export function buildScanRoots(opts, extraRoots) {
|
||||
const roots = [opts.workdir, opts.dir, ...(extraRoots ?? [])];
|
||||
return Array.from(new Set(roots.map((root) => resolveScanRoot(opts, root))));
|
||||
}
|
||||
function resolveScanRoot(opts, root) {
|
||||
return isAbsolute(root) ? resolve(root) : resolve(opts.workdir, root);
|
||||
}
|
||||
export function normalizeConcurrency(value) {
|
||||
const raw = typeof value === "number" ? value : 4;
|
||||
const rounded = Number.isFinite(raw) ? Math.round(raw) : 4;
|
||||
return Math.min(32, Math.max(1, rounded));
|
||||
}
|
||||
export async function mapWithConcurrency(items, limit, fn) {
|
||||
const results = Array.from({ length: items.length });
|
||||
let nextIndex = 0;
|
||||
const workerCount = Math.min(Math.max(1, limit), items.length || 1);
|
||||
async function worker() {
|
||||
while (true) {
|
||||
const index = nextIndex;
|
||||
nextIndex += 1;
|
||||
if (index >= items.length)
|
||||
return;
|
||||
results[index] = await fn(items[index]);
|
||||
}
|
||||
}
|
||||
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
||||
return results;
|
||||
}
|
||||
export async function checkRegistrySyncState(registry, skill, ownerHandle, token) {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
slug: skill.slug,
|
||||
hash: skill.fingerprint,
|
||||
});
|
||||
if (ownerHandle)
|
||||
params.set("ownerHandle", ownerHandle);
|
||||
const resolved = await apiRequest(registry, {
|
||||
method: "GET",
|
||||
path: `${ApiRoutes.resolve}?${params.toString()}`,
|
||||
token,
|
||||
}, ApiV1SkillResolveResponseSchema);
|
||||
const latestVersion = resolved.latestVersion?.version ?? null;
|
||||
const matchVersion = resolved.match?.version ?? null;
|
||||
if (!latestVersion) {
|
||||
return { ...skill, status: "new", matchVersion: null, latestVersion: null };
|
||||
}
|
||||
return {
|
||||
...skill,
|
||||
status: matchVersion ? "synced" : "update",
|
||||
matchVersion,
|
||||
latestVersion,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
const message = formatError(error);
|
||||
if (/skill not found/i.test(message) || /HTTP 404/i.test(message)) {
|
||||
return { ...skill, status: "new", matchVersion: null, latestVersion: null };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
export async function scanRootsWithLabels(roots) {
|
||||
const all = [];
|
||||
const rootsWithSkills = [];
|
||||
const uniqueRoots = await dedupeRoots(roots);
|
||||
const skillsByRoot = {};
|
||||
for (const root of uniqueRoots) {
|
||||
const found = await findSkillFolders(root);
|
||||
skillsByRoot[root] = found;
|
||||
if (found.length > 0)
|
||||
rootsWithSkills.push(root);
|
||||
all.push(...found);
|
||||
}
|
||||
const byFolder = new Map();
|
||||
for (const folder of all) {
|
||||
byFolder.set(folder.folder, folder);
|
||||
}
|
||||
return {
|
||||
roots: uniqueRoots,
|
||||
skillsByRoot,
|
||||
skills: Array.from(byFolder.values()),
|
||||
rootsWithSkills,
|
||||
};
|
||||
}
|
||||
async function dedupeRoots(roots) {
|
||||
const seen = new Set();
|
||||
const unique = [];
|
||||
for (const root of roots) {
|
||||
const resolved = resolve(root);
|
||||
const canonical = await realpath(resolved).catch(() => null);
|
||||
const key = canonical ?? resolved;
|
||||
if (seen.has(key))
|
||||
continue;
|
||||
seen.add(key);
|
||||
unique.push(key);
|
||||
}
|
||||
return unique;
|
||||
}
|
||||
export async function selectToUpload(candidates, params) {
|
||||
if (params.all || !params.allowPrompt)
|
||||
return candidates;
|
||||
const valueByKey = new Map();
|
||||
const choices = candidates.map((candidate) => {
|
||||
const key = candidate.folder;
|
||||
valueByKey.set(key, candidate);
|
||||
return {
|
||||
value: key,
|
||||
label: `${candidate.slug} ${formatActionableStatus(candidate, params.bump)}`,
|
||||
hint: `${abbreviatePath(candidate.folder)} | ${candidate.fileCount} files`,
|
||||
};
|
||||
});
|
||||
const picked = await multiselect({
|
||||
message: "Select skills to publish",
|
||||
options: choices,
|
||||
initialValues: choices.map((choice) => choice.value),
|
||||
required: false,
|
||||
});
|
||||
if (isCancel(picked))
|
||||
fail("Canceled");
|
||||
return picked.map((key) => valueByKey.get(key)).filter(Boolean);
|
||||
}
|
||||
export function resolvePublishMeta(skill, params) {
|
||||
if (skill.status === "new") {
|
||||
return { publishVersion: "1.0.0", changelog: "" };
|
||||
}
|
||||
const latest = skill.latestVersion;
|
||||
if (!latest)
|
||||
fail(`Could not resolve latest version for ${skill.slug}`);
|
||||
const publishVersion = semver.inc(latest, params.bump);
|
||||
if (!publishVersion)
|
||||
fail(`Could not bump version for ${skill.slug}`);
|
||||
const fromFlag = params.changelogFlag?.trim();
|
||||
return { publishVersion, changelog: fromFlag ?? "" };
|
||||
}
|
||||
export function formatList(values, max) {
|
||||
if (values.length === 0)
|
||||
return "";
|
||||
const shown = values.map(abbreviatePath);
|
||||
if (shown.length <= max)
|
||||
return shown.join("\n");
|
||||
const head = shown.slice(0, Math.max(1, max - 1));
|
||||
const rest = values.length - head.length;
|
||||
return [...head, `... +${rest} more`].join("\n");
|
||||
}
|
||||
export function printSection(title, body) {
|
||||
const trimmed = body?.trim();
|
||||
if (!trimmed) {
|
||||
console.log(title);
|
||||
return;
|
||||
}
|
||||
if (trimmed.includes("\n")) {
|
||||
console.log(`\n${title}\n${trimmed}`);
|
||||
return;
|
||||
}
|
||||
console.log(`${title}: ${trimmed}`);
|
||||
}
|
||||
function abbreviatePath(value) {
|
||||
const home = resolveHome();
|
||||
if (value.startsWith(home))
|
||||
return `~${value.slice(home.length)}`;
|
||||
return value;
|
||||
}
|
||||
export function dedupeSkillsBySlug(skills) {
|
||||
const bySlug = new Map();
|
||||
for (const skill of skills) {
|
||||
const existing = bySlug.get(skill.slug);
|
||||
if (existing)
|
||||
existing.push(skill);
|
||||
else
|
||||
bySlug.set(skill.slug, [skill]);
|
||||
}
|
||||
const unique = [];
|
||||
const duplicates = [];
|
||||
for (const [slug, entries] of bySlug.entries()) {
|
||||
unique.push(entries[0]);
|
||||
if (entries.length > 1)
|
||||
duplicates.push(`${slug} (${entries.length})`);
|
||||
}
|
||||
return { skills: unique, duplicates };
|
||||
}
|
||||
function formatActionableStatus(candidate, bump) {
|
||||
if (candidate.status === "new")
|
||||
return "NEW (publish 1.0.0)";
|
||||
const latest = candidate.latestVersion;
|
||||
const next = latest ? semver.inc(latest, bump) : null;
|
||||
if (latest && next)
|
||||
return `LOCAL CHANGES latest ${latest}; publish ${next}`;
|
||||
return "LOCAL CHANGES";
|
||||
}
|
||||
export function formatActionableLine(candidate, bump) {
|
||||
return `${candidate.slug} ${formatActionableStatus(candidate, bump)} (${candidate.fileCount} files)`;
|
||||
}
|
||||
function formatSyncedLine(candidate) {
|
||||
const version = candidate.matchVersion ?? candidate.latestVersion ?? "unknown";
|
||||
return `${candidate.slug} synced (${version})`;
|
||||
}
|
||||
export function formatSyncedSummary(candidate) {
|
||||
const version = candidate.matchVersion ?? candidate.latestVersion;
|
||||
return version ? `${candidate.slug}@${version}` : candidate.slug;
|
||||
}
|
||||
export function formatBulletList(lines, max) {
|
||||
if (lines.length <= max)
|
||||
return lines.map((line) => `- ${line}`).join("\n");
|
||||
const head = lines.slice(0, max);
|
||||
const rest = lines.length - head.length;
|
||||
return [...head, `... +${rest} more`].map((line) => `- ${line}`).join("\n");
|
||||
}
|
||||
export function formatSyncedDisplay(synced) {
|
||||
const lines = synced.map(formatSyncedLine);
|
||||
if (lines.length <= 12)
|
||||
return formatBulletList(lines, 12);
|
||||
return formatCommaList(synced.map(formatSyncedSummary), 24);
|
||||
}
|
||||
export function formatCommaList(values, max) {
|
||||
if (values.length === 0)
|
||||
return "";
|
||||
if (values.length <= max)
|
||||
return values.join(", ");
|
||||
const head = values.slice(0, Math.max(1, max - 1));
|
||||
const rest = values.length - head.length;
|
||||
return `${head.join(", ")}, ... +${rest} more`;
|
||||
}
|
||||
//# sourceMappingURL=syncHelpers.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+26
@@ -0,0 +1,26 @@
|
||||
import type { SkillOrigin } from "../../skills.js";
|
||||
import type { SkillFolder } from "../scanSkills.js";
|
||||
export type SyncOptions = {
|
||||
root?: string[];
|
||||
all?: boolean;
|
||||
dryRun?: boolean;
|
||||
json?: boolean;
|
||||
owner?: string;
|
||||
bump?: "patch" | "minor" | "major";
|
||||
changelog?: string;
|
||||
tags?: string;
|
||||
concurrency?: number;
|
||||
sourceRepo?: string;
|
||||
sourceCommit?: string;
|
||||
sourceRef?: string;
|
||||
};
|
||||
export type LocalSkill = SkillFolder & {
|
||||
fingerprint: string;
|
||||
fileCount: number;
|
||||
origin: SkillOrigin | null;
|
||||
};
|
||||
export type Candidate = LocalSkill & {
|
||||
status: "synced" | "new" | "update";
|
||||
matchVersion: string | null;
|
||||
latestVersion: string | null;
|
||||
};
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=syncTypes.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"syncTypes.js","sourceRoot":"","sources":["../../../src/cli/commands/syncTypes.ts"],"names":[],"mappings":""}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import type { GlobalOpts } from "../types.js";
|
||||
type ConfirmOptions = {
|
||||
yes?: boolean;
|
||||
};
|
||||
export declare function cmdTransferRequest(opts: GlobalOpts, slugArg: string, toHandleArg: string, options: ConfirmOptions & {
|
||||
message?: string;
|
||||
}, inputAllowed: boolean): Promise<{
|
||||
ok: true;
|
||||
transferId?: string | undefined;
|
||||
toUserHandle?: string | undefined;
|
||||
toPublisherHandle?: string | undefined;
|
||||
skillSlug?: string | undefined;
|
||||
expiresAt?: number | undefined;
|
||||
transferred?: boolean | undefined;
|
||||
} | undefined>;
|
||||
export declare function cmdTransferList(opts: GlobalOpts, options: {
|
||||
outgoing?: boolean;
|
||||
}): Promise<{
|
||||
transfers: {
|
||||
_id: string;
|
||||
skill: {
|
||||
_id: string;
|
||||
slug: string;
|
||||
displayName: string;
|
||||
};
|
||||
requestedAt: number;
|
||||
expiresAt: number;
|
||||
fromUser?: {
|
||||
_id: string;
|
||||
handle: string | null;
|
||||
displayName: string | null;
|
||||
} | undefined;
|
||||
toUser?: {
|
||||
_id: string;
|
||||
handle: string | null;
|
||||
displayName: string | null;
|
||||
} | undefined;
|
||||
message?: string | undefined;
|
||||
}[];
|
||||
}>;
|
||||
export declare function cmdTransferAccept(opts: GlobalOpts, slugArg: string, options: ConfirmOptions, inputAllowed: boolean): Promise<{
|
||||
ok: true;
|
||||
skillSlug?: string | undefined;
|
||||
} | undefined>;
|
||||
export declare function cmdTransferReject(opts: GlobalOpts, slugArg: string, options: ConfirmOptions, inputAllowed: boolean): Promise<{
|
||||
ok: true;
|
||||
skillSlug?: string | undefined;
|
||||
} | undefined>;
|
||||
export declare function cmdTransferCancel(opts: GlobalOpts, slugArg: string, options: ConfirmOptions, inputAllowed: boolean): Promise<{
|
||||
ok: true;
|
||||
skillSlug?: string | undefined;
|
||||
} | undefined>;
|
||||
export {};
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
import { apiRequest, registryUrl } from "../../http.js";
|
||||
import { ApiRoutes, ApiV1TransferDecisionResponseSchema, ApiV1TransferListResponseSchema, ApiV1TransferRequestResponseSchema, parseArk, } from "../../schema/index.js";
|
||||
import { requireAuthToken } from "../authToken.js";
|
||||
import { getRegistry } from "../registry.js";
|
||||
import { createCrabLoader, fail, formatError, isInteractive, promptConfirm } from "../ui.js";
|
||||
const DECISION_SPECS = {
|
||||
accept: {
|
||||
verb: "Accept",
|
||||
progress: "Accepting",
|
||||
success: "Transfer accepted",
|
||||
action: "accept",
|
||||
},
|
||||
reject: {
|
||||
verb: "Reject",
|
||||
progress: "Rejecting",
|
||||
success: "Transfer rejected",
|
||||
action: "reject",
|
||||
},
|
||||
cancel: {
|
||||
verb: "Cancel",
|
||||
progress: "Cancelling",
|
||||
success: "Transfer cancelled",
|
||||
action: "cancel",
|
||||
},
|
||||
};
|
||||
function normalizeSlug(slugArg) {
|
||||
const slug = slugArg.trim().toLowerCase();
|
||||
if (!slug)
|
||||
fail("Skill required");
|
||||
return slug;
|
||||
}
|
||||
function parseSkillRef(skillArg) {
|
||||
const value = skillArg.trim().toLowerCase();
|
||||
if (!value)
|
||||
fail("Skill required");
|
||||
const slashIndex = value.indexOf("/");
|
||||
if (slashIndex < 0)
|
||||
return { slug: normalizeSlug(value), ownerHandle: undefined };
|
||||
if (value.indexOf("/", slashIndex + 1) >= 0)
|
||||
fail(`Invalid skill ref: ${skillArg}`);
|
||||
const ownerHandle = value.slice(0, slashIndex).replace(/^@+/, "");
|
||||
const slug = normalizeSlug(value.slice(slashIndex + 1));
|
||||
if (!ownerHandle)
|
||||
fail(`Invalid skill ref: ${skillArg}`);
|
||||
return { slug, ownerHandle };
|
||||
}
|
||||
function ownerScopedTransferUrl(registry, slug, ownerHandle, suffix = "") {
|
||||
const path = `${ApiRoutes.skills}/${encodeURIComponent(slug)}/transfer${suffix}`;
|
||||
if (!ownerHandle)
|
||||
return null;
|
||||
const url = registryUrl(path, registry);
|
||||
url.searchParams.set("ownerHandle", ownerHandle);
|
||||
return url.toString();
|
||||
}
|
||||
function transferRequestArgs(registry, slug, ownerHandle, suffix, token) {
|
||||
const path = `${ApiRoutes.skills}/${encodeURIComponent(slug)}/transfer${suffix}`;
|
||||
const url = ownerScopedTransferUrl(registry, slug, ownerHandle, suffix);
|
||||
return url ? { method: "POST", url, token } : { method: "POST", path, token };
|
||||
}
|
||||
function canPrompt(inputAllowed) {
|
||||
return isInteractive() && inputAllowed !== false;
|
||||
}
|
||||
async function requireYesOrConfirm(options, inputAllowed, prompt) {
|
||||
if (options.yes)
|
||||
return true;
|
||||
if (!canPrompt(inputAllowed))
|
||||
fail("Pass --yes (no input)");
|
||||
return promptConfirm(prompt);
|
||||
}
|
||||
export async function cmdTransferRequest(opts, slugArg, toHandleArg, options, inputAllowed) {
|
||||
const requested = parseSkillRef(slugArg);
|
||||
const slug = requested.slug;
|
||||
const toHandle = toHandleArg.trim().replace(/^@+/, "").toLowerCase();
|
||||
if (!toHandle)
|
||||
fail("Recipient handle required (e.g., @username)");
|
||||
const confirmed = await requireYesOrConfirm(options, inputAllowed, `Transfer ${slug} to @${toHandle}? User transfers require recipient acceptance; org transfers apply immediately if you are an admin of both owners.`);
|
||||
if (!confirmed)
|
||||
return undefined;
|
||||
const token = await requireAuthToken();
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
const spinner = createCrabLoader(`Requesting transfer of ${slug} to @${toHandle}`);
|
||||
try {
|
||||
const result = await apiRequest(registry, {
|
||||
...transferRequestArgs(registry, slug, requested.ownerHandle, "", token),
|
||||
body: {
|
||||
toUserHandle: toHandle,
|
||||
message: options.message,
|
||||
},
|
||||
}, ApiV1TransferRequestResponseSchema);
|
||||
const parsed = parseArk(ApiV1TransferRequestResponseSchema, result, "Transfer request response");
|
||||
if (parsed.transferred) {
|
||||
spinner.succeed(`Transferred ${slug} to @${parsed.toPublisherHandle ?? toHandle}`);
|
||||
}
|
||||
else {
|
||||
spinner.succeed(`Transfer requested for ${slug} to @${parsed.toUserHandle ?? toHandle}`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(formatError(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
export async function cmdTransferList(opts, options) {
|
||||
const token = await requireAuthToken();
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
const spinner = createCrabLoader("Fetching transfers");
|
||||
try {
|
||||
const path = options.outgoing
|
||||
? `${ApiRoutes.transfers}/outgoing`
|
||||
: `${ApiRoutes.transfers}/incoming`;
|
||||
const result = await apiRequest(registry, { method: "GET", path, token }, ApiV1TransferListResponseSchema);
|
||||
const parsed = parseArk(ApiV1TransferListResponseSchema, result, "Transfer list response");
|
||||
spinner.stop();
|
||||
if (parsed.transfers.length === 0) {
|
||||
console.log(options.outgoing ? "No outgoing transfers." : "No incoming transfers.");
|
||||
return parsed;
|
||||
}
|
||||
console.log(options.outgoing ? "Outgoing transfers:" : "Incoming transfers:");
|
||||
for (const transfer of parsed.transfers) {
|
||||
const otherHandle = options.outgoing ? transfer.toUser?.handle : transfer.fromUser?.handle;
|
||||
const other = otherHandle ? `@${otherHandle.replace(/^@+/, "")}` : "(unknown user)";
|
||||
const expiresInDays = Math.max(0, Math.ceil((transfer.expiresAt - Date.now()) / (24 * 60 * 60 * 1000)));
|
||||
console.log(` ${transfer.skill.slug} -> ${other} (expires in ${expiresInDays}d)`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(formatError(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async function runTransferDecision(opts, slugArg, options, inputAllowed, spec) {
|
||||
const requested = parseSkillRef(slugArg);
|
||||
const slug = requested.slug;
|
||||
const confirmed = await requireYesOrConfirm(options, inputAllowed, `${spec.verb} transfer of ${slug}?`);
|
||||
if (!confirmed)
|
||||
return undefined;
|
||||
const token = await requireAuthToken();
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
const spinner = createCrabLoader(`${spec.progress} transfer of ${slug}`);
|
||||
try {
|
||||
const result = await apiRequest(registry, transferRequestArgs(registry, slug, requested.ownerHandle, `/${spec.action}`, token), ApiV1TransferDecisionResponseSchema);
|
||||
const parsed = parseArk(ApiV1TransferDecisionResponseSchema, result, "Transfer response");
|
||||
spinner.succeed(`${spec.success} (${slug})`);
|
||||
return parsed;
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(formatError(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
export function cmdTransferAccept(opts, slugArg, options, inputAllowed) {
|
||||
return runTransferDecision(opts, slugArg, options, inputAllowed, DECISION_SPECS.accept);
|
||||
}
|
||||
export function cmdTransferReject(opts, slugArg, options, inputAllowed) {
|
||||
return runTransferDecision(opts, slugArg, options, inputAllowed, DECISION_SPECS.reject);
|
||||
}
|
||||
export function cmdTransferCancel(opts, slugArg, options, inputAllowed) {
|
||||
return runTransferDecision(opts, slugArg, options, inputAllowed, DECISION_SPECS.cancel);
|
||||
}
|
||||
//# sourceMappingURL=transfer.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+8
@@ -0,0 +1,8 @@
|
||||
import type { GlobalOpts } from "../types.js";
|
||||
export declare function cmdUnstarSkill(opts: GlobalOpts, slugArg: string, options: {
|
||||
yes?: boolean;
|
||||
}, inputAllowed: boolean): Promise<{
|
||||
ok: true;
|
||||
unstarred: boolean;
|
||||
alreadyUnstarred: boolean;
|
||||
} | undefined>;
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { apiRequest, registryUrl } from "../../http.js";
|
||||
import { ApiRoutes, ApiV1UnstarResponseSchema } from "../../schema/index.js";
|
||||
import { requireAuthToken } from "../authToken.js";
|
||||
import { getRegistry } from "../registry.js";
|
||||
import { createCrabLoader, fail, formatError, isInteractive, promptConfirm } from "../ui.js";
|
||||
function parseSkillRef(skillArg) {
|
||||
const value = skillArg.trim().toLowerCase();
|
||||
if (!value)
|
||||
fail("Skill required");
|
||||
const slashIndex = value.indexOf("/");
|
||||
if (slashIndex < 0)
|
||||
return { slug: value, ownerHandle: undefined };
|
||||
if (value.indexOf("/", slashIndex + 1) >= 0)
|
||||
fail(`Invalid skill ref: ${skillArg}`);
|
||||
const ownerHandle = value.slice(0, slashIndex).replace(/^@+/, "");
|
||||
const slug = value.slice(slashIndex + 1);
|
||||
if (!ownerHandle || !slug)
|
||||
fail(`Invalid skill ref: ${skillArg}`);
|
||||
return { slug, ownerHandle };
|
||||
}
|
||||
function unstarRequestArgs(registry, slug, ownerHandle, token) {
|
||||
const path = `${ApiRoutes.stars}/${encodeURIComponent(slug)}`;
|
||||
if (!ownerHandle)
|
||||
return { method: "DELETE", path, token };
|
||||
const url = registryUrl(path, registry);
|
||||
url.searchParams.set("ownerHandle", ownerHandle);
|
||||
return { method: "DELETE", url: url.toString(), token };
|
||||
}
|
||||
export async function cmdUnstarSkill(opts, slugArg, options, inputAllowed) {
|
||||
const requested = parseSkillRef(slugArg);
|
||||
const slug = requested.slug;
|
||||
const allowPrompt = isInteractive() && inputAllowed !== false;
|
||||
if (!options.yes) {
|
||||
if (!allowPrompt)
|
||||
fail("Pass --yes (no input)");
|
||||
const ok = await promptConfirm(`Unstar ${slug}?`);
|
||||
if (!ok)
|
||||
return undefined;
|
||||
}
|
||||
const token = await requireAuthToken();
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
const spinner = createCrabLoader(`Unstarring ${slug}`);
|
||||
try {
|
||||
const result = await apiRequest(registry, unstarRequestArgs(registry, slug, requested.ownerHandle, token), ApiV1UnstarResponseSchema);
|
||||
spinner.succeed(result.alreadyUnstarred ? `OK. ${slug} already unstarred.` : `OK. Unstarred ${slug}`);
|
||||
return result;
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(formatError(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=unstar.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"unstar.js","sourceRoot":"","sources":["../../../src/cli/commands/unstar.ts"],"names":[],"mappings":"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"}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
export declare function styleTitle(value: string): string;
|
||||
export declare function configureCommanderHelp(program: {
|
||||
configureHelp: (config: Record<string, (value: string) => string>) => unknown;
|
||||
}): void;
|
||||
export declare function styleEnvBlock(value: string): string;
|
||||
export declare function styleError(value: string): string;
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
function wrap(start, end = "\x1b[0m") {
|
||||
return (value) => `${start}${value}${end}`;
|
||||
}
|
||||
const ansi = {
|
||||
reset: "\x1b[0m",
|
||||
bold: wrap("\x1b[1m"),
|
||||
dim: wrap("\x1b[2m"),
|
||||
red: wrap("\x1b[31m"),
|
||||
white: wrap("\x1b[37m"),
|
||||
};
|
||||
function isColorEnabled() {
|
||||
if (!process.stdout.isTTY)
|
||||
return false;
|
||||
if (process.env.NO_COLOR)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
function isErrorColorEnabled() {
|
||||
if (!process.stderr.isTTY)
|
||||
return false;
|
||||
if (process.env.NO_COLOR)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
export function styleTitle(value) {
|
||||
if (!isColorEnabled())
|
||||
return value;
|
||||
return `${ansi.bold(ansi.red(value))}${ansi.reset}`;
|
||||
}
|
||||
export function configureCommanderHelp(program) {
|
||||
if (!isColorEnabled())
|
||||
return;
|
||||
program.configureHelp({
|
||||
styleTitle: (value) => ansi.bold(ansi.white(value)),
|
||||
styleCommandText: (value) => ansi.white(value),
|
||||
styleSubcommandText: (value) => ansi.bold(ansi.red(value)),
|
||||
styleOptionText: (value) => ansi.white(value),
|
||||
styleOptionTerm: (value) => ansi.white(value),
|
||||
styleArgumentText: (value) => ansi.white(value),
|
||||
styleArgumentTerm: (value) => ansi.white(value),
|
||||
});
|
||||
}
|
||||
export function styleEnvBlock(value) {
|
||||
if (!isColorEnabled())
|
||||
return value;
|
||||
return `${ansi.dim(value)}${ansi.reset}`;
|
||||
}
|
||||
export function styleError(value) {
|
||||
if (!isErrorColorEnabled())
|
||||
return value;
|
||||
return value.replace(/^error:/i, ansi.bold(ansi.red("error:")));
|
||||
}
|
||||
//# sourceMappingURL=helpStyle.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"helpStyle.js","sourceRoot":"","sources":["../../src/cli/helpStyle.ts"],"names":[],"mappings":"AAEA,SAAS,IAAI,CAAC,KAAa,EAAE,GAAG,GAAG,SAAS;IAC1C,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,GAAG,KAAK,GAAG,GAAG,EAAE,CAAC;AAC7C,CAAC;AAED,MAAM,IAAI,GAAG;IACX,KAAK,EAAE,SAAS;IAChB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;IACrB,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC;IACpB,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC;IACrB,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC;CACxB,CAAC;AAEF,SAAS,cAAc;IACrB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACxC,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IACvC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,mBAAmB;IAC1B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACxC,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IACvC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,IAAI,CAAC,cAAc,EAAE;QAAE,OAAO,KAAK,CAAC;IACpC,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,OAEtC;IACC,IAAI,CAAC,cAAc,EAAE;QAAE,OAAO;IAC9B,OAAO,CAAC,aAAa,CAAC;QACpB,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACnD,gBAAgB,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;QAC9C,mBAAmB,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAC1D,eAAe,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;QAC7C,eAAe,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;QAC7C,iBAAiB,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;QAC/C,iBAAiB,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;KAChD,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,IAAI,CAAC,cAAc,EAAE;QAAE,OAAO,KAAK,CAAC;IACpC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,IAAI,CAAC,mBAAmB,EAAE;QAAE,OAAO,KAAK,CAAC;IACzC,OAAO,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;AAClE,CAAC"}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import type { GlobalOpts } from "./types.js";
|
||||
export declare const DEFAULT_SITE = "https://clawhub.ai";
|
||||
export declare const DEFAULT_REGISTRY = "https://clawhub.ai";
|
||||
export declare function resolveRegistry(opts: GlobalOpts): Promise<string>;
|
||||
export declare function getRegistry(opts: GlobalOpts, params?: {
|
||||
cache?: boolean;
|
||||
}): Promise<string>;
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { readGlobalConfig, writeGlobalConfig } from "../config.js";
|
||||
import { discoverRegistryFromSite } from "../discovery.js";
|
||||
export const DEFAULT_SITE = "https://clawhub.ai";
|
||||
export const DEFAULT_REGISTRY = "https://clawhub.ai";
|
||||
const LEGACY_REGISTRY_HOSTS = new Set([
|
||||
"auth.clawdhub.com",
|
||||
"auth.clawhub.com",
|
||||
"auth.clawhub.ai",
|
||||
"registry.clawhub.ai",
|
||||
]);
|
||||
export async function resolveRegistry(opts) {
|
||||
const explicit = opts.registrySource !== "default" ? opts.registry.trim() : "";
|
||||
if (explicit)
|
||||
return explicit;
|
||||
const discovery = await discoverRegistryFromSite(opts.site).catch(() => null);
|
||||
const discovered = discovery?.apiBase?.trim();
|
||||
if (discovered)
|
||||
return discovered;
|
||||
const cfg = await readGlobalConfig();
|
||||
const cached = cfg?.registry?.trim();
|
||||
if (cached && !isLegacyRegistry(cached))
|
||||
return cached;
|
||||
return DEFAULT_REGISTRY;
|
||||
}
|
||||
export async function getRegistry(opts, params) {
|
||||
const cache = params?.cache !== false;
|
||||
const registry = await resolveRegistry(opts);
|
||||
if (!cache)
|
||||
return registry;
|
||||
const cfg = await readGlobalConfig();
|
||||
const cached = cfg?.registry?.trim();
|
||||
const shouldUpdate = !cached ||
|
||||
isLegacyRegistry(cached) ||
|
||||
(cached === DEFAULT_REGISTRY && registry !== DEFAULT_REGISTRY);
|
||||
if (shouldUpdate)
|
||||
await writeGlobalConfig({ registry, token: cfg?.token });
|
||||
return registry;
|
||||
}
|
||||
function isLegacyRegistry(registry) {
|
||||
try {
|
||||
return LEGACY_REGISTRY_HOSTS.has(new URL(registry).hostname);
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=registry.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/cli/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAG3D,MAAM,CAAC,MAAM,YAAY,GAAG,oBAAoB,CAAC;AACjD,MAAM,CAAC,MAAM,gBAAgB,GAAG,oBAAoB,CAAC;AACrD,MAAM,qBAAqB,GAAG,IAAI,GAAG,CAAC;IACpC,mBAAmB;IACnB,kBAAkB;IAClB,iBAAiB;IACjB,qBAAqB;CACtB,CAAC,CAAC;AAEH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAgB;IACpD,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC/E,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAE9B,MAAM,SAAS,GAAG,MAAM,wBAAwB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IAC9E,MAAM,UAAU,GAAG,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC9C,IAAI,UAAU;QAAE,OAAO,UAAU,CAAC;IAElC,MAAM,GAAG,GAAG,MAAM,gBAAgB,EAAE,CAAC;IACrC,MAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACrC,IAAI,MAAM,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC;IACvD,OAAO,gBAAgB,CAAC;AAC1B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,IAAgB,EAAE,MAA4B;IAC9E,MAAM,KAAK,GAAG,MAAM,EAAE,KAAK,KAAK,KAAK,CAAC;IACtC,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC,CAAC;IAC7C,IAAI,CAAC,KAAK;QAAE,OAAO,QAAQ,CAAC;IAC5B,MAAM,GAAG,GAAG,MAAM,gBAAgB,EAAE,CAAC;IACrC,MAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACrC,MAAM,YAAY,GAChB,CAAC,MAAM;QACP,gBAAgB,CAAC,MAAM,CAAC;QACxB,CAAC,MAAM,KAAK,gBAAgB,IAAI,QAAQ,KAAK,gBAAgB,CAAC,CAAC;IACjE,IAAI,YAAY;QAAE,MAAM,iBAAiB,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3E,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,gBAAgB,CAAC,QAAgB;IACxC,IAAI,CAAC;QACH,OAAO,qBAAqB,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
export type SkillFolder = {
|
||||
folder: string;
|
||||
slug: string;
|
||||
displayName: string;
|
||||
};
|
||||
export declare function findSkillFolders(root: string): Promise<SkillFolder[]>;
|
||||
export declare function getFallbackSkillRoots(workdir: string): string[];
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { readdir, stat } from "node:fs/promises";
|
||||
import { basename, join, resolve } from "node:path";
|
||||
import { resolveHome } from "../homedir.js";
|
||||
import { sanitizeSlug, titleCase } from "./slug.js";
|
||||
export async function findSkillFolders(root) {
|
||||
const absRoot = resolve(root);
|
||||
const rootStat = await stat(absRoot).catch(() => null);
|
||||
if (!rootStat || !rootStat.isDirectory())
|
||||
return [];
|
||||
const direct = await isSkillFolder(absRoot);
|
||||
if (direct)
|
||||
return [direct];
|
||||
const entries = await readdir(absRoot, { withFileTypes: true }).catch(() => []);
|
||||
const folders = entries
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => join(absRoot, entry.name));
|
||||
const results = [];
|
||||
for (const folder of folders) {
|
||||
const found = await isSkillFolder(folder);
|
||||
if (found)
|
||||
results.push(found);
|
||||
}
|
||||
return results.sort((a, b) => a.slug.localeCompare(b.slug));
|
||||
}
|
||||
export function getFallbackSkillRoots(workdir) {
|
||||
const home = resolveHome();
|
||||
const roots = [
|
||||
resolve(workdir, "..", "openclaw", "skills"),
|
||||
resolve(workdir, "..", "openclaw", "Skills"),
|
||||
resolve(home, ".openclaw", "skills"),
|
||||
resolve(home, ".openclaw", "Skills"),
|
||||
resolve(home, "openclaw", "skills"),
|
||||
resolve(home, "openclaw", "Skills"),
|
||||
resolve(home, "Library", "Application Support", "openclaw", "skills"),
|
||||
resolve(home, "Library", "Application Support", "openclaw", "Skills"),
|
||||
];
|
||||
return Array.from(new Set(roots));
|
||||
}
|
||||
async function isSkillFolder(folder) {
|
||||
const marker = await findSkillMarker(folder);
|
||||
if (!marker)
|
||||
return null;
|
||||
const base = basename(folder);
|
||||
const slug = sanitizeSlug(base);
|
||||
if (!slug)
|
||||
return null;
|
||||
const displayName = titleCase(base);
|
||||
return { folder, slug, displayName };
|
||||
}
|
||||
async function findSkillMarker(folder) {
|
||||
const candidates = ["SKILL.md", "skill.md"];
|
||||
for (const name of candidates) {
|
||||
const path = join(folder, name);
|
||||
const st = await stat(path).catch(() => null);
|
||||
if (st?.isFile())
|
||||
return path;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
//# sourceMappingURL=scanSkills.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"scanSkills.js","sourceRoot":"","sources":["../../src/cli/scanSkills.ts"],"names":[],"mappings":"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"}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export declare function sanitizeSlug(value: string): string;
|
||||
export declare function titleCase(value: string): string;
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
export function sanitizeSlug(value) {
|
||||
const raw = value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]+/g, "-");
|
||||
const cleaned = raw.replace(/^-+/, "").replace(/-+$/, "").replace(/--+/g, "-");
|
||||
return cleaned;
|
||||
}
|
||||
export function titleCase(value) {
|
||||
return value
|
||||
.trim()
|
||||
.replace(/[-_]+/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/\b\w/g, (char) => char.toUpperCase());
|
||||
}
|
||||
//# sourceMappingURL=slug.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"slug.js","sourceRoot":"","sources":["../../src/cli/slug.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,MAAM,GAAG,GAAG,KAAK;SACd,IAAI,EAAE;SACN,WAAW,EAAE;SACb,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;IAChC,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC/E,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,OAAO,KAAK;SACT,IAAI,EAAE;SACN,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC;SACtB,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;AACpD,CAAC"}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
export type GlobalOpts = {
|
||||
workdir: string;
|
||||
dir: string;
|
||||
site: string;
|
||||
registry: string;
|
||||
registrySource: "cli" | "env" | "default";
|
||||
};
|
||||
export type ResolveResult = {
|
||||
match: {
|
||||
version: string;
|
||||
} | null;
|
||||
latestVersion: {
|
||||
version: string;
|
||||
} | null;
|
||||
};
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=types.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/cli/types.ts"],"names":[],"mappings":""}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
export declare function promptHidden(prompt: string): Promise<string>;
|
||||
export declare function promptConfirm(prompt: string): Promise<boolean>;
|
||||
export declare function openInBrowser(url: string): void;
|
||||
export declare function isInteractive(): boolean;
|
||||
type CrabLoader = {
|
||||
text: string;
|
||||
readonly isSpinning: boolean;
|
||||
start(text?: string): CrabLoader;
|
||||
stop(): CrabLoader;
|
||||
succeed(text?: string): CrabLoader;
|
||||
fail(text?: string): CrabLoader;
|
||||
};
|
||||
export declare function createCrabLoader(text: string): CrabLoader;
|
||||
export declare function formatError(error: unknown): string;
|
||||
declare const textStyles: {
|
||||
brand: string;
|
||||
strong: string;
|
||||
muted: string;
|
||||
};
|
||||
export declare function styleText(value: string, style: keyof typeof textStyles): string;
|
||||
export declare function fail(message: string): never;
|
||||
export {};
|
||||
+274
@@ -0,0 +1,274 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { stdin } from "node:process";
|
||||
import { confirm, isCancel } from "@clack/prompts";
|
||||
import ora from "ora";
|
||||
export async function promptHidden(prompt) {
|
||||
if (!stdin.isTTY)
|
||||
return "";
|
||||
process.stdout.write(prompt);
|
||||
const chunks = [];
|
||||
stdin.setRawMode(true);
|
||||
stdin.resume();
|
||||
return new Promise((resolvePromise) => {
|
||||
function onData(data) {
|
||||
const text = data.toString("utf8");
|
||||
if (text === "\r" || text === "\n") {
|
||||
stdin.setRawMode(false);
|
||||
stdin.pause();
|
||||
stdin.off("data", onData);
|
||||
process.stdout.write("\n");
|
||||
resolvePromise(Buffer.concat(chunks).toString("utf8").trim());
|
||||
return;
|
||||
}
|
||||
if (text === "\u0003") {
|
||||
stdin.setRawMode(false);
|
||||
stdin.pause();
|
||||
stdin.off("data", onData);
|
||||
process.stdout.write("\n");
|
||||
fail("Canceled");
|
||||
}
|
||||
if (text === "\u007f") {
|
||||
chunks.pop();
|
||||
return;
|
||||
}
|
||||
chunks.push(data);
|
||||
}
|
||||
stdin.on("data", onData);
|
||||
});
|
||||
}
|
||||
export async function promptConfirm(prompt) {
|
||||
const answer = await confirm({ message: prompt });
|
||||
if (isCancel(answer))
|
||||
return false;
|
||||
return answer;
|
||||
}
|
||||
export function openInBrowser(url) {
|
||||
const args = process.platform === "darwin"
|
||||
? ["open", url]
|
||||
: process.platform === "win32"
|
||||
? ["explorer", url]
|
||||
: ["xdg-open", url];
|
||||
const [command, ...commandArgs] = args;
|
||||
if (!command)
|
||||
return;
|
||||
const child = spawn(command, commandArgs, { stdio: "ignore", detached: true });
|
||||
child.on("error", (err) => {
|
||||
if (err.code === "ENOENT") {
|
||||
console.log("");
|
||||
console.log("Could not open browser automatically.");
|
||||
console.log("Please open this URL manually:");
|
||||
console.log("");
|
||||
console.log(` ${url}`);
|
||||
console.log("");
|
||||
}
|
||||
});
|
||||
child.unref();
|
||||
}
|
||||
export function isInteractive() {
|
||||
return process.stdout.isTTY && stdin.isTTY;
|
||||
}
|
||||
const clawhubCrab = {
|
||||
interval: 110,
|
||||
frames: [
|
||||
"🦀 ",
|
||||
" 🦀 ",
|
||||
" 🦀 ",
|
||||
" 🦀 ",
|
||||
" 🦀 ",
|
||||
" 🦀 ",
|
||||
" 🦀",
|
||||
" 🦀 ",
|
||||
" 🦀 ",
|
||||
" 🦀 ",
|
||||
" 🦀 ",
|
||||
" 🦀 ",
|
||||
],
|
||||
};
|
||||
function createNonInteractiveCrabLoader(text) {
|
||||
let currentText = text;
|
||||
const loader = {
|
||||
get text() {
|
||||
return currentText;
|
||||
},
|
||||
set text(value) {
|
||||
currentText = value;
|
||||
},
|
||||
get isSpinning() {
|
||||
return false;
|
||||
},
|
||||
start(value) {
|
||||
if (value)
|
||||
currentText = value;
|
||||
return loader;
|
||||
},
|
||||
stop() {
|
||||
return loader;
|
||||
},
|
||||
succeed(value) {
|
||||
if (value)
|
||||
console.log(value);
|
||||
return loader;
|
||||
},
|
||||
fail(value) {
|
||||
if (value)
|
||||
console.error(value);
|
||||
return loader;
|
||||
},
|
||||
};
|
||||
return loader;
|
||||
}
|
||||
export function createCrabLoader(text) {
|
||||
if (!isInteractive())
|
||||
return createNonInteractiveCrabLoader(text);
|
||||
return ora({ text, spinner: clawhubCrab, color: "red" }).start();
|
||||
}
|
||||
export function formatError(error) {
|
||||
if (error instanceof Error) {
|
||||
const diagnostic = formatTransportError(error);
|
||||
if (diagnostic)
|
||||
return diagnostic;
|
||||
return redactSensitiveText(error.message);
|
||||
}
|
||||
return redactSensitiveText(String(error));
|
||||
}
|
||||
function isStdoutColorEnabled() {
|
||||
if (!process.stdout.isTTY)
|
||||
return false;
|
||||
if (process.env.NO_COLOR)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
const textStyles = {
|
||||
brand: "\x1b[1m\x1b[31m",
|
||||
strong: "\x1b[1m",
|
||||
muted: "\x1b[2m",
|
||||
};
|
||||
export function styleText(value, style) {
|
||||
if (!isStdoutColorEnabled())
|
||||
return value;
|
||||
return `${textStyles[style]}${value}\x1b[0m`;
|
||||
}
|
||||
function isErrorColorEnabled() {
|
||||
if (!process.stderr.isTTY)
|
||||
return false;
|
||||
if (process.env.NO_COLOR)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
function formatErrorLabel() {
|
||||
if (!isErrorColorEnabled())
|
||||
return "Error:";
|
||||
return "\x1b[1m\x1b[31mError:\x1b[0m";
|
||||
}
|
||||
export function fail(message) {
|
||||
console.error(`${formatErrorLabel()} ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
function formatTransportError(error) {
|
||||
const chain = errorChain(error);
|
||||
const messages = chain
|
||||
.map((entry) => entry.message)
|
||||
.filter((message) => message.trim().length > 0);
|
||||
const codes = chain.map((entry) => entry.code).filter((code) => Boolean(code));
|
||||
const combined = [...messages, ...codes].join("\n");
|
||||
if (!isTransportError(combined))
|
||||
return null;
|
||||
const sanitized = redactSensitiveText(messages.join(": "));
|
||||
const reason = classifyTransportError(combined);
|
||||
const detail = sanitized.length > 0 ? ` ${sanitized}` : "";
|
||||
return `${reason}${detail}\nCheck the registry URL, network connection, proxy settings (HTTPS_PROXY, HTTP_PROXY, NO_PROXY), and TLS/certificate configuration.`;
|
||||
}
|
||||
function errorChain(error) {
|
||||
const entries = [];
|
||||
const seen = new Set();
|
||||
let current = error;
|
||||
while (current instanceof Error && !seen.has(current)) {
|
||||
seen.add(current);
|
||||
entries.push({ message: current.message, code: errorCode(current) });
|
||||
current = current.cause;
|
||||
}
|
||||
if (current !== undefined && current !== null && !seen.has(current)) {
|
||||
entries.push({ message: formatUnknownCause(current) });
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
function formatUnknownCause(value) {
|
||||
if (typeof value === "string")
|
||||
return value;
|
||||
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
||||
return String(value);
|
||||
}
|
||||
if (typeof value === "symbol")
|
||||
return value.description ?? "symbol";
|
||||
if (typeof value === "function")
|
||||
return value.name ? `function: ${value.name}` : "function";
|
||||
if (typeof value === "object") {
|
||||
const fields = safeObjectCauseFields(Object(value));
|
||||
if (fields.length > 0)
|
||||
return fields.join(", ");
|
||||
return "[object cause omitted]";
|
||||
}
|
||||
return "[cause omitted]";
|
||||
}
|
||||
const objectCauseFields = [
|
||||
"name",
|
||||
"message",
|
||||
"code",
|
||||
"errno",
|
||||
"syscall",
|
||||
"hostname",
|
||||
"host",
|
||||
"address",
|
||||
"port",
|
||||
];
|
||||
function safeObjectCauseFields(value) {
|
||||
return objectCauseFields.flatMap((field) => {
|
||||
const property = value[field];
|
||||
if (typeof property === "string" && property.trim().length > 0) {
|
||||
return [`${field}: ${redactSensitiveText(property)}`];
|
||||
}
|
||||
if (typeof property === "number" ||
|
||||
typeof property === "boolean" ||
|
||||
typeof property === "bigint") {
|
||||
return [`${field}: ${String(property)}`];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
function errorCode(error) {
|
||||
const code = error.code;
|
||||
return typeof code === "string" ? code : undefined;
|
||||
}
|
||||
function isTransportError(value) {
|
||||
return /fetch failed|failed to fetch|curl failed|request timed out|aborted|aborterror|enotfound|eai_again|econnrefused|econnreset|etimedout|ehostunreach|enetunreach|cert_|certificate|tls|ssl|proxy|connect tunnel|could not resolve host|operation timed out/i.test(value);
|
||||
}
|
||||
function classifyTransportError(value) {
|
||||
if (/enotfound|eai_again|could not resolve host/i.test(value)) {
|
||||
return "Network request failed: DNS lookup failed.";
|
||||
}
|
||||
if (/request timed out|etimedout|operation timed out|aborterror|aborted/i.test(value)) {
|
||||
return "Network request failed: the request timed out.";
|
||||
}
|
||||
if (/cert_|certificate|tls|ssl/i.test(value)) {
|
||||
return "Network request failed: TLS or certificate validation failed.";
|
||||
}
|
||||
if (/proxy|connect tunnel/i.test(value)) {
|
||||
return "Network request failed: proxy connection failed.";
|
||||
}
|
||||
if (/econnrefused|ehostunreach|enetunreach/i.test(value)) {
|
||||
return "Network request failed: the registry was unreachable.";
|
||||
}
|
||||
return "Network request failed.";
|
||||
}
|
||||
function redactSensitiveText(value) {
|
||||
return value
|
||||
.replace(/(["']?(?:authorization|set-cookie|cookie|api[_-]?key|secret|token)["']?\s*:\s*["'])[^"'\n\r]+/gi, "$1[redacted]")
|
||||
.replace(/(authorization:\s*bearer\s+)[^\s,;]+/gi, "$1[redacted]")
|
||||
.replace(/(authorization:\s*)(?!bearer\s+)(?:[^\s,;]+\s+)?[^\s,;]+/gi, "$1[redacted]")
|
||||
.replace(/(cookie:\s*)[^\n\r]+/gi, "$1[redacted]")
|
||||
.replace(/((?:api[_-]?key|secret|token):\s*)[^\s,;]+/gi, "$1[redacted]")
|
||||
.replace(/(clh_[A-Za-z0-9._-]+)/g, "[redacted]")
|
||||
.replace(/([?&](?:access_token|api[_-]?key|auth|authorization|callback|callback_url|code|key|redirect_uri|return_to|secret|state|token)=)[^&#\s]+/gi, "$1[redacted]")
|
||||
.replace(/:\/\/[^/\s@]+@/g, "://[redacted]@");
|
||||
}
|
||||
//# sourceMappingURL=ui.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user