自动同步: 2026-07-01 02:10

This commit is contained in:
2026-07-01 02:10:07 +08:00
parent d20bd4cfab
commit 489a0835e9
4162 changed files with 1010609 additions and 12590 deletions
+2
View File
@@ -0,0 +1,2 @@
export declare function getOptionalAuthToken(): Promise<string | undefined>;
export declare function requireAuthToken(): Promise<string>;
+13
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,2 @@
export declare function getCliVersion(): string;
export declare function getCliBuildLabel(): string;
+128
View File
@@ -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
View File
@@ -0,0 +1 @@
{"version":3,"file":"buildInfo.js","sourceRoot":"","sources":["../../src/cli/buildInfo.ts"],"names":[],"mappings":"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"}
+1
View File
@@ -0,0 +1 @@
export declare function resolveClawdbotDefaultWorkspace(): Promise<string | null>;
+90
View File
@@ -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
@@ -0,0 +1 @@
{"version":3,"file":"clawdbotConfig.js","sourceRoot":"","sources":["../../src/cli/clawdbotConfig.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAc5C,MAAM,CAAC,KAAK,UAAU,+BAA+B;IACnD,MAAM,MAAM,GAAG,MAAM,kBAAkB,EAAE,CAAC;IAC1C,MAAM,cAAc,GAAG,MAAM,kBAAkB,EAAE,CAAC;IAClD,IAAI,CAAC,MAAM,IAAI,CAAC,cAAc;QAAE,OAAO,IAAI,CAAC;IAE5C,MAAM,iBAAiB,GAAG,eAAe,CACvC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,IAAI,MAAM,EAAE,KAAK,EAAE,SAAS,IAAI,EAAE,CACtE,CAAC;IACF,IAAI,iBAAiB;QAAE,OAAO,iBAAiB,CAAC;IAEhD,MAAM,YAAY,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IAC1C,MAAM,YAAY,GAChB,YAAY,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC;QAC3C,YAAY,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,MAAM,CAAC,CAAC;IACpD,MAAM,aAAa,GAAG,eAAe,CAAC,YAAY,EAAE,SAAS,IAAI,EAAE,CAAC,CAAC;IACrE,IAAI,aAAa;QAAE,OAAO,aAAa,CAAC;IAExC,IAAI,CAAC,cAAc;QAAE,OAAO,IAAI,CAAC;IACjC,MAAM,gBAAgB,GAAG,eAAe,CACtC,cAAc,CAAC,MAAM,EAAE,QAAQ,EAAE,SAAS,IAAI,cAAc,CAAC,KAAK,EAAE,SAAS,IAAI,EAAE,CACpF,CAAC;IACF,IAAI,gBAAgB;QAAE,OAAO,gBAAgB,CAAC;IAC9C,MAAM,cAAc,GAAG,YAAY,CAAC,cAAc,CAAC,CAAC;IACpD,MAAM,oBAAoB,GACxB,cAAc,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC;QAC7C,cAAc,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,MAAM,CAAC,CAAC;IACtD,MAAM,iBAAiB,GAAG,eAAe,CAAC,oBAAoB,EAAE,SAAS,IAAI,EAAE,CAAC,CAAC;IACjF,OAAO,iBAAiB,IAAI,IAAI,CAAC;AACnC,CAAC;AAED,SAAS,uBAAuB;IAC9B,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,IAAI,EAAE,CAAC;IACxD,IAAI,QAAQ;QAAE,OAAO,eAAe,CAAC,QAAQ,CAAC,CAAC;IAC/C,OAAO,IAAI,CAAC,WAAW,EAAE,EAAE,WAAW,CAAC,CAAC;AAC1C,CAAC;AAED,SAAS,yBAAyB;IAChC,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,IAAI,EAAE,CAAC;IAC1D,IAAI,QAAQ;QAAE,OAAO,eAAe,CAAC,QAAQ,CAAC,CAAC;IAC/C,OAAO,IAAI,CAAC,uBAAuB,EAAE,EAAE,eAAe,CAAC,CAAC;AAC1D,CAAC;AAED,SAAS,uBAAuB;IAC9B,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,IAAI,EAAE,CAAC;IACxD,IAAI,QAAQ;QAAE,OAAO,eAAe,CAAC,QAAQ,CAAC,CAAC;IAC/C,OAAO,IAAI,CAAC,WAAW,EAAE,EAAE,WAAW,CAAC,CAAC;AAC1C,CAAC;AAED,SAAS,yBAAyB;IAChC,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,IAAI,EAAE,CAAC;IAC1D,IAAI,QAAQ;QAAE,OAAO,eAAe,CAAC,QAAQ,CAAC,CAAC;IAC/C,OAAO,IAAI,CAAC,uBAAuB,EAAE,EAAE,eAAe,CAAC,CAAC;AAC1D,CAAC;AAED,SAAS,eAAe,CAAC,KAAc;IACrC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IACzC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACxB,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5B,OAAO,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,eAAe,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,OAAO,CAAC,OAAO,CAAC,CAAC;AAC1B,CAAC;AAED,SAAS,YAAY,CAAC,MAA6B;IACjD,MAAM,IAAI,GAAG,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC;IAClC,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAC1D,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,KAAK,UAAU,kBAAkB;IAC/B,OAAO,cAAc,CAAC,yBAAyB,EAAE,CAAC,CAAC;AACrD,CAAC;AAED,KAAK,UAAU,kBAAkB;IAC/B,OAAO,cAAc,CAAC,yBAAyB,EAAE,CAAC,CAAC;AACrD,CAAC;AAED,KAAK,UAAU,cAAc,CAAC,IAAY;IACxC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACzC,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QACvD,OAAO,MAAwB,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
+17
View File
@@ -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
View File
@@ -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
@@ -0,0 +1 @@
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../src/cli/commands/auth.ts"],"names":[],"mappings":"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"}
+32
View File
@@ -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
View File
@@ -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
File diff suppressed because one or more lines are too long
+40
View File
@@ -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
View File
@@ -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
File diff suppressed because one or more lines are too long
+18
View File
@@ -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
View File
@@ -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
File diff suppressed because one or more lines are too long
@@ -0,0 +1,6 @@
export declare function reportInstalledSkillsTelemetryIfEnabled(params: {
token: string | undefined;
registry: string;
slug: string;
version?: string | null;
}): Promise<void>;
@@ -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
@@ -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"}
@@ -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 {};
@@ -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
@@ -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"}
@@ -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
View File
@@ -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
File diff suppressed because one or more lines are too long
+132
View File
@@ -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 {};
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+40
View File
@@ -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
View File
@@ -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
File diff suppressed because one or more lines are too long
@@ -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 {};
@@ -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
@@ -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
View File
@@ -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
View File
@@ -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
File diff suppressed because one or more lines are too long
+55
View File
@@ -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
View File
@@ -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
File diff suppressed because one or more lines are too long
+8
View File
@@ -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
View File
@@ -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
@@ -0,0 +1 @@
{"version":3,"file":"star.js","sourceRoot":"","sources":["../../../src/cli/commands/star.ts"],"names":[],"mappings":"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"}
+3
View File
@@ -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
View File
@@ -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
File diff suppressed because one or more lines are too long
@@ -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;
@@ -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
File diff suppressed because one or more lines are too long
@@ -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;
};
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=syncTypes.js.map
@@ -0,0 +1 @@
{"version":3,"file":"syncTypes.js","sourceRoot":"","sources":["../../../src/cli/commands/syncTypes.ts"],"names":[],"mappings":""}
@@ -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
View File
@@ -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
File diff suppressed because one or more lines are too long
@@ -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
View File
@@ -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
@@ -0,0 +1 @@
{"version":3,"file":"unstar.js","sourceRoot":"","sources":["../../../src/cli/commands/unstar.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAC7E,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE7F,SAAS,aAAa,CAAC,QAAgB;IACrC,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC5C,IAAI,CAAC,KAAK;QAAE,IAAI,CAAC,gBAAgB,CAAC,CAAC;IACnC,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACtC,IAAI,UAAU,GAAG,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC;IACnE,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,UAAU,GAAG,CAAC,CAAC,IAAI,CAAC;QAAE,IAAI,CAAC,sBAAsB,QAAQ,EAAE,CAAC,CAAC;IACpF,MAAM,WAAW,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAClE,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC;IACzC,IAAI,CAAC,WAAW,IAAI,CAAC,IAAI;QAAE,IAAI,CAAC,sBAAsB,QAAQ,EAAE,CAAC,CAAC;IAClE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;AAC/B,CAAC;AAED,SAAS,iBAAiB,CACxB,QAAgB,EAChB,IAAY,EACZ,WAA+B,EAC/B,KAAa;IAEb,MAAM,IAAI,GAAG,GAAG,SAAS,CAAC,KAAK,IAAI,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;IAC9D,IAAI,CAAC,WAAW;QAAE,OAAO,EAAE,MAAM,EAAE,QAAiB,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IACpE,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IACxC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;IACjD,OAAO,EAAE,MAAM,EAAE,QAAiB,EAAE,GAAG,EAAE,GAAG,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,CAAC;AACnE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,IAAgB,EAChB,OAAe,EACf,OAA0B,EAC1B,YAAqB;IAErB,MAAM,SAAS,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACzC,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC;IAC5B,MAAM,WAAW,GAAG,aAAa,EAAE,IAAI,YAAY,KAAK,KAAK,CAAC;IAE9D,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;QACjB,IAAI,CAAC,WAAW;YAAE,IAAI,CAAC,uBAAuB,CAAC,CAAC;QAChD,MAAM,EAAE,GAAG,MAAM,aAAa,CAAC,UAAU,IAAI,GAAG,CAAC,CAAC;QAClD,IAAI,CAAC,EAAE;YAAE,OAAO,SAAS,CAAC;IAC5B,CAAC;IAED,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,OAAO,GAAG,gBAAgB,CAAC,cAAc,IAAI,EAAE,CAAC,CAAC;IACvD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,UAAU,CAC7B,QAAQ,EACR,iBAAiB,CAAC,QAAQ,EAAE,IAAI,EAAE,SAAS,CAAC,WAAW,EAAE,KAAK,CAAC,EAC/D,yBAAyB,CAC1B,CAAC;QACF,OAAO,CAAC,OAAO,CACb,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC,OAAO,IAAI,qBAAqB,CAAC,CAAC,CAAC,iBAAiB,IAAI,EAAE,CACrF,CAAC;QACF,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;QACjC,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1 @@
{"version":3,"file":"scanSkills.js","sourceRoot":"","sources":["../../src/cli/scanSkills.ts"],"names":[],"mappings":"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"}
+2
View File
@@ -0,0 +1,2 @@
export declare function sanitizeSlug(value: string): string;
export declare function titleCase(value: string): string;
+16
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=types.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/cli/types.ts"],"names":[],"mappings":""}
+22
View File
@@ -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
View File
@@ -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
File diff suppressed because one or more lines are too long