自动同步: 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
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2026 Peter Steinberger
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+180
View File
@@ -0,0 +1,180 @@
# `clawhub`
ClawHub CLI — install, update, search, and publish agent skills plus OpenClaw packages.
## Install
```bash
# From this repo (shortcut script at repo root)
bun clawhub --help
# Once published to npm
# npm i -g clawhub
```
## Auth (publish)
```bash
clawhub login
# or
clawhub auth login
# Remote/headless browser approval
clawhub login --device
# or (token paste / headless)
clawhub login --token clh_...
# print the stored token for CI setup
clawhub token
```
Notes:
- Browser login opens `https://clawhub.ai/cli/auth` and completes via a loopback callback.
- Device login prints a one-time code and waits while you approve it at `https://clawhub.ai/cli/device`.
- Default config path:
- macOS: `~/Library/Application Support/clawhub/config.json`
- Linux/XDG: `$XDG_CONFIG_HOME/clawhub/config.json` or `~/.config/clawhub/config.json`
- Windows: `%APPDATA%\\clawhub\\config.json`
- Legacy fallback: if `clawhub/config.json` does not exist yet but `clawdhub/config.json` does, the CLI reuses the legacy path.
- Override via `CLAWHUB_CONFIG_PATH` (legacy `CLAWDHUB_CONFIG_PATH`).
## Examples
```bash
clawhub search "postgres backups"
clawhub install @openclaw/demo
clawhub pin bear-notes --reason "scanner-flagged while awaiting moderation"
clawhub update --all
clawhub update --all --no-input --force
clawhub unpin bear-notes
clawhub skill publish ./my-skill-pack --slug my-skill-pack --name "My Skill Pack" --changelog "Fixes + docs"
clawhub skill publish ./org-skill --owner openclaw --changelog "Org publish"
clawhub sync --all --dry-run
clawhub sync --all
clawhub package explore --family skill
clawhub package explore --family code-plugin
clawhub package inspect @openclaw/example-plugin
clawhub package download @openclaw/example-plugin --tag latest
clawhub package verify ./example-plugin-1.0.0.tgz --package @openclaw/example-plugin --version 1.0.0
clawhub package validate ./example-plugin
clawhub package publish openclaw/example-plugin
clawhub package publish openclaw/example-plugin@v1.0.0
clawhub package publish https://github.com/openclaw/example-plugin --dry-run
clawhub package publish ./example-plugin-1.0.0.tgz --dry-run
clawhub package publish ./example-plugin
```
## Publish code plugins
For ClawPack publish, create the npm-pack tarball yourself and upload that
exact `.tgz`:
```bash
npm pack
clawhub package publish ./my-plugin-1.0.0.tgz --family code-plugin --dry-run
clawhub package publish ./my-plugin-1.0.0.tgz --family code-plugin
```
For local plugin folders, start with a dry run:
```bash
clawhub package publish ./my-plugin --family code-plugin --dry-run
clawhub package publish ./my-plugin --family code-plugin
```
For code plugins, folder publish builds and uploads a ClawPack artifact from
the package folder. Bundle-plugin folders still use the extracted-file publish
path.
Use `clawhub package download` to resolve the published artifact through
ClawHub's explicit artifact route. ClawPack downloads are verified against npm
integrity/shasum plus ClawHub SHA-256; legacy package versions still download
as ZIPs.
`code-plugin` packages must declare these `package.json` fields:
- `openclaw.compat.pluginApi`
- `openclaw.build.openclawVersion`
Minimal example:
```json
{
"name": "@myorg/openclaw-my-plugin",
"version": "1.0.0",
"type": "module",
"openclaw": {
"extensions": ["./index.ts"],
"compat": {
"pluginApi": ">=2026.3.24-beta.2"
},
"build": {
"openclawVersion": "2026.3.24-beta.2"
}
}
}
```
`package.json.version` does not replace these OpenClaw-specific fields. Add
`openclaw.compat.minGatewayVersion` and
`openclaw.build.pluginSdkVersion` when you want richer compatibility metadata,
but they are not required for publish.
## GitHub Actions
This repo also provides an official reusable workflow for plugin repos:
- [`.github/workflows/package-publish.yml`](../../.github/workflows/package-publish.yml)
Use `dry_run: true` on pull requests and reserve real publishes for trusted events
such as `workflow_dispatch` or tag pushes with a `CLAWHUB_TOKEN` secret.
For monorepos, pass `source_path` to publish the plugin package folder, for
example `source_path: extensions/codex`.
Package trusted publishing starts after the first normal authenticated publish
creates the package row. Then a package manager can attach GitHub Actions OIDC
config for future supported publishes:
```bash
clawhub package trusted-publisher set @openclaw/example-plugin \
--repository openclaw/example-plugin \
--workflow-filename package-publish.yml \
--environment release
clawhub package trusted-publisher get @openclaw/example-plugin
clawhub package trusted-publisher delete @openclaw/example-plugin
```
`--environment` is optional and exact-match sensitive. If configured, the
GitHub Actions environment in the OIDC claim must match. Tag-push real publishes
still need `clawhub_token` unless the reusable workflow adds tag OIDC support.
## Maintainers
The `clawhub` npm package is released separately from the ClawHub app deploy.
- Release workflow: [`.github/workflows/clawhub-cli-npm-release.yml`](../../.github/workflows/clawhub-cli-npm-release.yml)
- Release model: manual-only, stable tags only (`vX.Y.Z`), with a preflight run before the real publish
- Publish auth: npm trusted publishing through the `npm-release` GitHub environment
## Development
The supported verification flow for this package is package-local:
```bash
bun run --cwd packages/clawhub test
bun run --cwd packages/clawhub verify:build
bun run --cwd packages/clawhub test:artifact
bun run --cwd packages/clawhub verify
```
`test` runs source tests only. `test:artifact` builds `dist/` and runs a small smoke suite against the built CLI entrypoint.
## Defaults
- Site: `https://clawhub.ai` (override via `--site` or `CLAWHUB_SITE`, legacy `CLAWDHUB_SITE`)
- Registry: discovered from `/.well-known/clawhub.json` on the site (legacy `/.well-known/clawdhub.json`; override via `--registry` or `CLAWHUB_REGISTRY`)
- Workdir: current directory (falls back to Clawdbot workspace if configured; override via `--workdir` or `CLAWHUB_WORKDIR`)
- Install dir: `./skills` under workdir (override via `--dir`)
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env node
try {
await import("../dist/cli.js");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`clawhub: failed to load CLI: ${message}`);
process.exitCode = 1;
}
+13
View File
@@ -0,0 +1,13 @@
type ClawPackEntry = {
path: string;
bytes: Uint8Array;
};
type ParsedClawPack = {
packageName: string;
packageVersion: string;
entries: ClawPackEntry[];
packageJson: Record<string, unknown>;
pluginManifest: Record<string, unknown>;
};
export declare function parseClawPack(bytes: Uint8Array): ParsedClawPack;
export {};
+127
View File
@@ -0,0 +1,127 @@
import { gunzipSync } from "fflate";
const TAR_BLOCK_SIZE = 512;
function isRecord(value) {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function textFromBytes(bytes) {
return new TextDecoder().decode(bytes);
}
function readTarString(block, offset, length) {
const slice = block.subarray(offset, offset + length);
const end = slice.indexOf(0);
return textFromBytes(end === -1 ? slice : slice.subarray(0, end)).trim();
}
function readTarSize(block) {
const raw = readTarString(block, 124, 12).split("\0").join("").trim();
if (!raw)
return 0;
const size = Number.parseInt(raw, 8);
if (!Number.isFinite(size) || size < 0)
throw new Error("Invalid tar entry size");
return size;
}
function normalizeTarPath(path) {
const normalized = path.replaceAll("\\", "/").replace(/^\.\/+/, "");
if (!normalized || normalized.startsWith("/") || normalized.includes("\0"))
return null;
const segments = normalized.split("/").filter(Boolean);
if (segments.length === 0 || segments.some((segment) => segment === "." || segment === "..")) {
return null;
}
return segments.join("/");
}
function isZeroBlock(block) {
return block.every((byte) => byte === 0);
}
function nextTarOffset(offset, size) {
return offset + Math.ceil(size / TAR_BLOCK_SIZE) * TAR_BLOCK_SIZE;
}
function parseTarEntries(bytes) {
const entries = [];
let offset = 0;
while (offset + TAR_BLOCK_SIZE <= bytes.byteLength) {
const header = bytes.subarray(offset, offset + TAR_BLOCK_SIZE);
if (isZeroBlock(header))
break;
const name = readTarString(header, 0, 100);
const prefix = readTarString(header, 345, 155);
const path = normalizeTarPath(prefix ? `${prefix}/${name}` : name);
if (!path)
throw new Error("ClawPack contains an unsafe tar path");
const size = readTarSize(header);
const payloadOffset = offset + TAR_BLOCK_SIZE;
const payloadEnd = payloadOffset + size;
if (payloadEnd > bytes.byteLength)
throw new Error("ClawPack tar entry is truncated");
const typeflag = String.fromCharCode(header[156] ?? 0).replace("\0", "");
if (typeflag === "" || typeflag === "0") {
if (!path.startsWith("package/")) {
throw new Error("ClawPack entries must be rooted under package/");
}
const relPath = path.slice("package/".length);
if (relPath) {
entries.push({
path: relPath,
bytes: Uint8Array.from(bytes.subarray(payloadOffset, payloadEnd)),
});
}
}
else if (typeflag !== "5") {
throw new Error("ClawPack may only contain regular files and directories");
}
offset = nextTarOffset(payloadOffset, size);
}
if (entries.length === 0)
throw new Error("ClawPack contains no files");
return entries;
}
export function parseClawPack(bytes) {
let tarBytes;
try {
tarBytes = gunzipSync(bytes);
}
catch {
throw new Error("ClawPack must be a gzip-compressed npm pack tarball");
}
const entries = parseTarEntries(tarBytes);
const packageJsonEntry = entries.find((entry) => entry.path === "package.json");
if (!packageJsonEntry)
throw new Error("ClawPack must contain package/package.json");
const pluginManifestEntry = entries.find((entry) => entry.path === "openclaw.plugin.json");
if (!pluginManifestEntry) {
throw new Error("ClawPack must contain package/openclaw.plugin.json");
}
let packageJson;
try {
packageJson = JSON.parse(textFromBytes(packageJsonEntry.bytes));
}
catch {
throw new Error("ClawPack package.json is invalid JSON");
}
if (!isRecord(packageJson))
throw new Error("ClawPack package.json must be an object");
const packageName = typeof packageJson.name === "string" ? packageJson.name.trim() : "";
const packageVersion = typeof packageJson.version === "string" ? packageJson.version.trim() : "";
if (!packageName)
throw new Error("ClawPack package.json must declare a name");
if (!packageVersion)
throw new Error("ClawPack package.json must declare a version");
let pluginManifest;
try {
pluginManifest = JSON.parse(textFromBytes(pluginManifestEntry.bytes));
}
catch {
throw new Error("ClawPack openclaw.plugin.json is invalid JSON");
}
if (!isRecord(pluginManifest)) {
throw new Error("ClawPack openclaw.plugin.json must be an object");
}
return {
packageName,
packageVersion,
entries,
packageJson,
pluginManifest,
};
}
//# sourceMappingURL=clawpack.js.map
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env node
export {};
+822
View File
@@ -0,0 +1,822 @@
#!/usr/bin/env node
import { stat } from "node:fs/promises";
import { join, resolve } from "node:path";
import { Command, Option } from "commander";
import { getCliBuildLabel, getCliVersion } from "./cli/buildInfo.js";
import { resolveClawdbotDefaultWorkspace } from "./cli/clawdbotConfig.js";
import { cmdLoginFlow, cmdLogout, cmdToken, cmdWhoami } from "./cli/commands/auth.js";
import { cmdDeleteSkill, cmdHideSkill, cmdUndeleteSkill, cmdUnhideSkill, } from "./cli/commands/delete.js";
import { cmdInspect, cmdVerifySkill } from "./cli/commands/inspect.js";
import { cmdMergeSkill, cmdRenameSkill } from "./cli/commands/ownership.js";
import { cmdDeletePackage, cmdDeletePackageTrustedPublisher, cmdDownloadPackage, cmdExplorePackages, cmdGetPackageTrustedPublisher, cmdInspectPackage, cmdPackageModerationStatus, cmdPackageMigrationStatus, cmdPackageReadiness, cmdPackPackage, cmdPublishPackage, cmdReportPackage, cmdSetPackageTrustedPublisher, cmdTransferPackage, cmdUndeletePackage, cmdValidatePackage, cmdVerifyPackage, } from "./cli/commands/packages.js";
import { cmdPublish } from "./cli/commands/publish.js";
import { cmdCreatePublisher } from "./cli/commands/publishers.js";
import { cmdScan, cmdScanDownload } from "./cli/commands/scan.js";
import { cmdExplore, cmdInstall, cmdList, cmdPin, cmdSearch, cmdUninstall, cmdUnpin, cmdUpdate, } from "./cli/commands/skills.js";
import { cmdStarSkill } from "./cli/commands/star.js";
import { cmdSync } from "./cli/commands/sync.js";
import { cmdTransferAccept, cmdTransferCancel, cmdTransferList, cmdTransferReject, cmdTransferRequest, } from "./cli/commands/transfer.js";
import { cmdUnstarSkill } from "./cli/commands/unstar.js";
import { configureCommanderHelp, styleEnvBlock, styleError, styleTitle } from "./cli/helpStyle.js";
import { DEFAULT_REGISTRY, DEFAULT_SITE } from "./cli/registry.js";
import { fail, formatError } from "./cli/ui.js";
const CLI_HELP_HEADER = styleTitle(`🦞 ClawHub CLI ${getCliBuildLabel()}`);
const HELP_DESCRIPTION = "Display help for command";
const program = new Command()
.name("clawhub")
.description(styleEnvBlock("install, update, search, and publish skills plus OpenClaw packages."))
.version(getCliVersion(), "-V, --cli-version", "Show CLI version")
.helpOption("-h, --help", HELP_DESCRIPTION)
.option("--workdir <dir>", "Working directory (default: cwd)")
.option("--dir <dir>", "Skills directory (relative to workdir, default: skills)")
.option("--site <url>", "Site base URL (for browser login)")
.option("--registry <url>", "Registry API base URL")
.option("--no-input", "Disable prompts")
.showHelpAfterError()
.showSuggestionAfterError()
.addHelpCommand("help [command]", HELP_DESCRIPTION)
.addHelpText("after", styleEnvBlock("\nEnv:\n CLAWHUB_SITE\n CLAWHUB_REGISTRY\n CLAWHUB_WORKDIR\n (CLAWDHUB_* supported)\n"));
configureCommanderHelp(program);
addCliHelpHeader(program);
program.configureOutput({
outputError: (message, write) => write(styleError(message)),
});
function addCliHelpHeader(command) {
command.addHelpText("beforeAll", `${CLI_HELP_HEADER}\n`);
}
function registerCommand(parent, path) {
const command = parent.command(path.at(-1) ?? "").helpOption("-h, --help", HELP_DESCRIPTION);
configureCommanderHelp(command);
return command;
}
function registerCommandGroup(parent, path) {
const command = parent.command(path.at(-1) ?? "").helpOption("-h, --help", HELP_DESCRIPTION);
configureCommanderHelp(command);
return command;
}
function applyCommandHelpGroups(parent, groups) {
for (const command of parent.commands) {
const group = groups[command.name()];
if (group)
command.helpGroup(group);
}
}
function validateTopLevelCommand(args) {
if (hasTerminalGlobalFlag(args))
return;
const commandName = findFirstTopLevelOperand(args);
if (!commandName)
return;
const knownCommands = new Set([
"help",
...program.commands.flatMap((command) => [command.name(), ...command.aliases()]),
]);
if (knownCommands.has(commandName))
return;
program.error(`error: unknown command '${commandName}'`, { code: "commander.unknownCommand" });
}
function hasTerminalGlobalFlag(args) {
return args.some((arg) => arg === "--help" || arg === "-h" || arg === "--cli-version" || arg === "-V");
}
function findFirstTopLevelOperand(args) {
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (!arg)
continue;
if (arg === "--")
return args[index + 1];
if (arg.startsWith("--")) {
if (arg === "--workdir" || arg === "--dir" || arg === "--site" || arg === "--registry") {
index += 1;
continue;
}
if (arg.startsWith("--workdir=") ||
arg.startsWith("--dir=") ||
arg.startsWith("--site=") ||
arg.startsWith("--registry=")) {
continue;
}
if (arg === "--help" || arg === "--cli-version")
return undefined;
if (arg === "--no-input")
continue;
return undefined;
}
if (arg.startsWith("-")) {
if (arg === "-h" || arg === "-V")
return undefined;
return undefined;
}
return arg;
}
return undefined;
}
async function resolveGlobalOpts() {
const raw = program.opts();
const workdir = await resolveWorkdir(raw.workdir);
const dir = resolve(workdir, raw.dir ?? "skills");
const site = raw.site ?? process.env.CLAWHUB_SITE ?? process.env.CLAWDHUB_SITE ?? DEFAULT_SITE;
const registrySource = raw.registry
? "cli"
: process.env.CLAWHUB_REGISTRY || process.env.CLAWDHUB_REGISTRY
? "env"
: "default";
const registry = raw.registry ??
process.env.CLAWHUB_REGISTRY ??
process.env.CLAWDHUB_REGISTRY ??
DEFAULT_REGISTRY;
return { workdir, dir, site, registry, registrySource };
}
function isInputAllowed() {
const globalFlags = program.opts();
return globalFlags.input !== false;
}
async function resolveWorkdir(explicit) {
if (explicit?.trim())
return resolve(explicit.trim());
const envWorkdir = process.env.CLAWHUB_WORKDIR?.trim() ?? process.env.CLAWDHUB_WORKDIR?.trim();
if (envWorkdir)
return resolve(envWorkdir);
const cwd = resolve(process.cwd());
const hasMarker = await hasClawhubMarker(cwd);
if (hasMarker)
return cwd;
const clawdbotWorkspace = await resolveClawdbotDefaultWorkspace();
return clawdbotWorkspace ? resolve(clawdbotWorkspace) : cwd;
}
async function hasClawhubMarker(workdir) {
const lockfile = join(workdir, ".clawhub", "lock.json");
if (await pathExists(lockfile))
return true;
const markerDir = join(workdir, ".clawhub");
if (await pathExists(markerDir))
return true;
const legacyLockfile = join(workdir, ".clawdhub", "lock.json");
if (await pathExists(legacyLockfile))
return true;
const legacyMarkerDir = join(workdir, ".clawdhub");
return pathExists(legacyMarkerDir);
}
async function pathExists(path) {
try {
await stat(path);
return true;
}
catch {
return false;
}
}
registerCommand(program, ["login"])
.description("Log in with device flow or store a token")
.option("--token <token>", "API token")
.option("--label <label>", "Token label", "CLI device login")
.option("--no-browser", "Do not open browser (device flow prints a verification URL)")
.option("--device", "Use device flow (default)")
.action(async (options) => {
const opts = await resolveGlobalOpts();
await cmdLoginFlow(opts, options, isInputAllowed());
});
registerCommand(program, ["logout"])
.description("Remove stored token")
.action(async () => {
const opts = await resolveGlobalOpts();
await cmdLogout(opts);
});
registerCommand(program, ["whoami"])
.description("Validate token")
.action(async () => {
const opts = await resolveGlobalOpts();
await cmdWhoami(opts);
});
registerCommand(program, ["token"])
.description("Print stored API token")
.action(async () => {
await cmdToken();
});
const auth = registerCommandGroup(program, ["auth"])
.description("Authentication commands")
.showHelpAfterError()
.showSuggestionAfterError();
registerCommand(auth, ["auth", "login"])
.description("Log in with device flow or store a token")
.option("--token <token>", "API token")
.option("--label <label>", "Token label", "CLI device login")
.option("--no-browser", "Do not open browser (device flow prints a verification URL)")
.option("--device", "Use device flow (default)")
.action(async (options) => {
const opts = await resolveGlobalOpts();
await cmdLoginFlow(opts, options, isInputAllowed());
});
registerCommand(auth, ["auth", "logout"])
.description("Remove stored token")
.action(async () => {
const opts = await resolveGlobalOpts();
await cmdLogout(opts);
});
registerCommand(auth, ["auth", "whoami"])
.description("Validate token")
.action(async () => {
const opts = await resolveGlobalOpts();
await cmdWhoami(opts);
});
registerCommand(program, ["search"])
.description("Vector search skills")
.argument("<query...>", "Query string")
.option("--limit <n>", "Max results", (value) => Number.parseInt(value, 10))
.action(async (queryParts, options) => {
const opts = await resolveGlobalOpts();
const query = queryParts.join(" ").trim();
await cmdSearch(opts, query, options.limit);
});
registerCommand(program, ["install"])
.description("Install a skill into <dir>")
.argument("<skill>", "Skill to install, e.g. @openclaw/demo")
.option("--version <version>", "Version to install")
.option("--force", "Overwrite existing folder")
.option("--force-install", "Install a pending GitHub-backed skill before ClawHub scan completes")
.action(async (slug, options) => {
const opts = await resolveGlobalOpts();
await cmdInstall(opts, slug, options.version, options.force, options.forceInstall);
});
registerCommand(program, ["update"])
.description("Update installed skills")
.argument("[skill]", "Skill to update, e.g. @openclaw/demo")
.option("--all", "Update all installed skills")
.option("--version <version>", "Update to specific version (single slug only)")
.option("--force", "Overwrite when local files do not match any version")
.option("--force-install", "Install a pending GitHub-backed skill before ClawHub scan completes")
.action(async (slug, options) => {
const opts = await resolveGlobalOpts();
await cmdUpdate(opts, slug, options, isInputAllowed());
});
registerCommand(program, ["uninstall"])
.description("Uninstall a skill")
.argument("<skill>", "Installed skill to remove")
.option("--yes", "Skip confirmation")
.action(async (slug, options) => {
const opts = await resolveGlobalOpts();
await cmdUninstall(opts, slug, options, isInputAllowed());
});
registerCommand(program, ["list"])
.description("List installed skills (tracked and manually installed)")
.action(async () => {
const opts = await resolveGlobalOpts();
await cmdList(opts);
});
registerCommand(program, ["pin"])
.description("Pin an installed skill so update commands skip it")
.argument("<skill>", "Installed skill to pin")
.option("--reason <text>", "Optional pin reason")
.action(async (slug, options) => {
const opts = await resolveGlobalOpts();
await cmdPin(opts, slug, options);
});
registerCommand(program, ["unpin"])
.description("Remove a skill pin so updates can change it again")
.argument("<skill>", "Installed skill to unpin")
.action(async (slug) => {
const opts = await resolveGlobalOpts();
await cmdUnpin(opts, slug);
});
registerCommand(program, ["explore"])
.description("Browse latest updated skills from the registry")
.option("--limit <n>", "Number of skills to show (max 200)", (value) => Number.parseInt(value, 10), 25)
.option("--sort <order>", "Sort by newest, rating, downloads, or trending", "newest")
.option("--json", "Output JSON")
.action(async (options) => {
const opts = await resolveGlobalOpts();
const limit = typeof options.limit === "number" && Number.isFinite(options.limit) ? options.limit : 25;
await cmdExplore(opts, { limit, sort: options.sort, json: options.json });
});
registerCommand(program, ["inspect"])
.description("Fetch skill metadata and files without installing")
.argument("<skill>", "Skill to inspect, e.g. @openclaw/demo")
.option("--version <version>", "Version to inspect")
.option("--tag <tag>", "Tag to inspect (default: latest)")
.option("--versions", "List version history (first page)")
.option("--limit <n>", "Max versions to list (1-200)", (value) => Number.parseInt(value, 10))
.option("--files", "List files for the selected version")
.option("--file <path>", "Fetch raw file content (text <= 200KB)")
.option("--json", "Output JSON")
.action(async (slug, options) => {
const opts = await resolveGlobalOpts();
await cmdInspect(opts, slug, options);
});
registerCommand(program, ["publish"])
.description("Legacy alias: publish a skill from folder")
.argument("<path>", "Skill folder path")
.option("--slug <slug>", "Published skill URL name")
.option("--name <name>", "Display name")
.option("--owner <handle>", "Publish under an org/user publisher handle")
.option("--source-owner <handle>", "Source owner handle when migrating an existing skill")
.option("--migrate-owner", "Move an existing skill to the selected owner when republishing")
.option("--version <version>", "Explicit version (defaults to 1.0.0 or next patch)")
.option("--fork-of <slug[@version]>", "Mark as a fork of an existing skill")
.option("--changelog <text>", "Changelog text")
.option("--tags <tags>", "Comma-separated tags", "latest")
.option("--categories <slugs>", "Comma-separated category slugs")
.option("--topics <topics>", "Comma-separated topics")
.option("--dry-run", "Preview without publishing")
.option("--json", "Output JSON")
.option("--source-repo <repo>", "GitHub source repository")
.option("--source-commit <sha>", "GitHub source commit")
.option("--source-ref <ref>", "GitHub source ref")
.option("--source-path <path>", "Path to the skill within the source repository")
.action(async (folder, options) => {
const opts = await resolveGlobalOpts();
await cmdPublish(opts, folder, options);
});
const scanCmd = registerCommand(program, ["scan"])
.description("Run or download ClawHub scan reports")
.argument("[path]", "Deprecated local skill folder path")
.option("--slug <slug>", "Published skill slug to scan")
.option("--version <version>", "Published skill version to scan")
.option("--update", "Write published scan results back to the selected version")
.option("-o, --output <path>", "Write the full report ZIP to a file")
.option("--json", "Output scan report JSON")
.action(async (folder, options) => {
const opts = await resolveGlobalOpts();
await cmdScan(opts, folder, options);
});
registerCommand(scanCmd, ["scan", "download"])
.description("Download stored scan results for a submitted skill or plugin version")
.argument("<name>", "Skill slug or plugin package name")
.option("--version <version>", "Submitted version to download scan results for")
.option("--kind <kind>", "Artifact kind: skill or plugin", "skill")
.option("-o, --output <path>", "Output ZIP file")
.action(async (name, options) => {
const opts = await resolveGlobalOpts();
const parentOptions = scanCmd.opts();
await cmdScanDownload(opts, name, {
...options,
version: options.version ?? parentOptions.version,
output: options.output ?? parentOptions.output,
});
});
registerCommand(program, ["delete"])
.description("Soft-delete a skill or permanently delete one version")
.argument("<skill>", "Skill ref")
.option("--version <version>", "Permanently delete one version; cannot be restored or republished; publish a replacement first if deleting the current latest version")
.option("--reason <text>", "Whole-skill moderation note/reason")
.option("--note <text>", "Alias for --reason")
.option("--yes", "Skip confirmation")
.action(async (slug, options) => {
const opts = await resolveGlobalOpts();
await cmdDeleteSkill(opts, slug, options, isInputAllowed());
});
registerCommand(program, ["hide"])
.description("Hide one of your skills")
.argument("<skill>", "Skill to hide")
.option("--reason <text>", "Moderation note/reason")
.option("--note <text>", "Alias for --reason")
.option("--yes", "Skip confirmation")
.action(async (slug, options) => {
const opts = await resolveGlobalOpts();
await cmdHideSkill(opts, slug, options, isInputAllowed());
});
registerCommand(program, ["undelete"])
.description("Restore one of your hidden skills")
.argument("<skill>", "Skill to restore")
.option("--reason <text>", "Moderation note/reason")
.option("--note <text>", "Alias for --reason")
.option("--yes", "Skip confirmation")
.action(async (slug, options) => {
const opts = await resolveGlobalOpts();
await cmdUndeleteSkill(opts, slug, options, isInputAllowed());
});
registerCommand(program, ["unhide"])
.description("Unhide one of your skills")
.argument("<skill>", "Skill to unhide")
.option("--reason <text>", "Moderation note/reason")
.option("--note <text>", "Alias for --reason")
.option("--yes", "Skip confirmation")
.action(async (slug, options) => {
const opts = await resolveGlobalOpts();
await cmdUnhideSkill(opts, slug, options, isInputAllowed());
});
const skill = registerCommandGroup(program, ["skill"]).description("Manage published skills");
registerCommand(skill, ["skill", "publish"])
.description("Publish a skill from folder")
.argument("<path>", "Skill folder path")
.option("--slug <slug>", "Published skill URL name")
.option("--name <name>", "Display name")
.option("--owner <handle>", "Publish under an org/user publisher handle")
.option("--source-owner <handle>", "Source owner handle when migrating an existing skill")
.option("--migrate-owner", "Move an existing skill to the selected owner when republishing")
.option("--version <version>", "Explicit version (defaults to 1.0.0 or next patch)")
.option("--fork-of <slug[@version]>", "Mark as a fork of an existing skill")
.option("--changelog <text>", "Changelog text")
.option("--tags <tags>", "Comma-separated tags", "latest")
.option("--categories <slugs>", "Comma-separated category slugs")
.option("--topics <topics>", "Comma-separated topics")
.option("--dry-run", "Preview without publishing")
.option("--json", "Output JSON")
.option("--source-repo <repo>", "GitHub source repository")
.option("--source-commit <sha>", "GitHub source commit")
.option("--source-ref <ref>", "GitHub source ref")
.option("--source-path <path>", "Path to the skill within the source repository")
.action(async (folder, options) => {
const opts = await resolveGlobalOpts();
await cmdPublish(opts, folder, options);
});
registerCommand(skill, ["skill", "verify"])
.description("Verify a published skill using ClawHub security evidence")
.argument("<slug>", "Skill slug")
.option("--version <version>", "Version to verify")
.option("--tag <tag>", "Tag to verify")
.option("--card", "Output generated skill-card.md Markdown")
.addOption(new Option("--json", "Output JSON").hideHelp())
.action(async (slug, options) => {
const opts = await resolveGlobalOpts();
await cmdVerifySkill(opts, slug, options);
});
const publisherCmd = registerCommandGroup(program, ["publisher"])
.description("Publisher organization commands")
.showHelpAfterError()
.showSuggestionAfterError();
registerCommand(publisherCmd, ["publisher", "create"])
.description("Create an org publisher you own")
.argument("<handle>", "Publisher handle, for example example.tools")
.option("--display-name <name>", "Publisher display name")
.option("--json", "Output JSON")
.action(async (handle, options) => {
const opts = await resolveGlobalOpts();
await cmdCreatePublisher(opts, handle, options);
});
const packageCmd = registerCommandGroup(program, ["package"]).description("Browse and publish OpenClaw packages");
registerCommand(packageCmd, ["package", "explore"])
.description("Browse published packages and plugins")
.argument("[query...]", "Optional search query")
.option("--family <family>", "skill|code-plugin|bundle-plugin")
.option("--official", "Only official packages")
.option("--executes-code", "Only packages that execute code")
.option("--target <target>", "Filter by host target, e.g. darwin-arm64")
.option("--os <os>", "Filter by host OS, e.g. darwin, linux, win32")
.option("--arch <arch>", "Filter by host architecture, e.g. arm64 or x64")
.option("--libc <libc>", "Filter by libc, e.g. glibc or musl")
.option("--requires-browser", "Only packages that require a browser")
.option("--requires-desktop", "Only packages that require local desktop access")
.option("--requires-native-deps", "Only packages with native dependency requirements")
.option("--requires-external-service", "Only packages that require an external service")
.option("--external-service <name>", "Filter by named external service")
.option("--binary <name>", "Filter by required local binary")
.option("--os-permission <name>", "Filter by required OS permission")
.option("--artifact-kind <kind>", "legacy-zip|npm-pack")
.option("--npm-mirror", "Only packages available through the npm mirror")
.option("--limit <n>", "Number of packages to show (max 100)", (value) => Number.parseInt(value, 10), 25)
.option("--json", "Output JSON")
.action(async (queryParts, options) => {
const opts = await resolveGlobalOpts();
const query = Array.isArray(queryParts) ? queryParts.join(" ").trim() : "";
await cmdExplorePackages(opts, query, options);
});
registerCommand(packageCmd, ["package", "inspect"])
.description("Fetch package metadata and files without installing")
.argument("<name>", "Package name")
.option("--version <version>", "Version to inspect")
.option("--tag <tag>", "Tag to inspect (default: latest)")
.option("--versions", "List version history (first page)")
.option("--limit <n>", "Max versions to list (1-100)", (value) => Number.parseInt(value, 10))
.option("--files", "List files for the selected version")
.option("--file <path>", "Fetch raw file content (text only)")
.option("--json", "Output JSON")
.action(async (name, options) => {
const opts = await resolveGlobalOpts();
await cmdInspectPackage(opts, name, options);
});
registerCommand(packageCmd, ["package", "download"])
.description("Download a package artifact and verify its published digests")
.argument("<name>", "Package name")
.option("--version <version>", "Version to download")
.option("--tag <tag>", "Tag to download (default: latest)")
.option("-o, --output <path>", "Output file or directory")
.option("--force", "Overwrite existing output file")
.option("--json", "Output JSON")
.action(async (name, options) => {
const opts = await resolveGlobalOpts();
await cmdDownloadPackage(opts, name, options);
});
registerCommand(packageCmd, ["package", "verify"])
.description("Verify a local package artifact against ClawHub or expected digests")
.argument("<file>", "Artifact file")
.option("--package <name>", "Package name to resolve expected artifact metadata")
.option("--version <version>", "Package version to resolve")
.option("--tag <tag>", "Package tag to resolve")
.option("--sha256 <hex>", "Expected ClawHub SHA-256")
.option("--npm-integrity <sri>", "Expected npm sha512 integrity")
.option("--npm-shasum <sha1>", "Expected npm shasum")
.option("--json", "Output JSON")
.action(async (file, options) => {
const opts = await resolveGlobalOpts();
await cmdVerifyPackage(opts, file, {
...options,
packageName: options.package,
});
});
registerCommand(packageCmd, ["package", "validate"])
.description("Validate a local plugin package with the bundled Plugin Inspector")
.argument("<source>", "Package folder path")
.option("--out <dir>", "Directory for Plugin Inspector reports", "reports")
.option("--openclaw <path>", "Optional local OpenClaw checkout to inspect against")
.option("--runtime", "Enable runtime capture; imports plugin code")
.option("--allow-execute", "Allow runtime capture in an isolated workspace")
.option("--no-mock-sdk", "Disable mocked OpenClaw SDK during runtime capture")
.option("--json", "Output JSON")
.action(async (source, options) => {
const opts = await resolveGlobalOpts();
await cmdValidatePackage(opts, source, options);
});
registerCommand(packageCmd, ["package", "delete"])
.description("Soft-delete a package or permanently delete one version")
.argument("<name>", "Package name")
.option("--version <version>", "Permanently delete one version; cannot be restored or republished; publish a replacement first if deleting the current latest version")
.option("--yes", "Skip confirmation")
.option("--json", "Output JSON")
.action(async (name, options) => {
const opts = await resolveGlobalOpts();
await cmdDeletePackage(opts, name, options, isInputAllowed());
});
registerCommand(packageCmd, ["package", "undelete"])
.description("Restore a soft-deleted package and releases")
.argument("<name>", "Package name")
.option("--yes", "Skip confirmation")
.option("--json", "Output JSON")
.action(async (name, options) => {
const opts = await resolveGlobalOpts();
await cmdUndeletePackage(opts, name, options, isInputAllowed());
});
registerCommand(packageCmd, ["package", "transfer"])
.description("Transfer a plugin package to another publisher")
.argument("<name>", "Package name")
.requiredOption("--to <owner>", "Destination publisher handle")
.option("--reason <text>", "Audit reason")
.option("--json", "Output JSON")
.action(async (name, options) => {
const opts = await resolveGlobalOpts();
await cmdTransferPackage(opts, name, options);
});
registerCommand(packageCmd, ["package", "report"])
.description("Report a package for moderator review")
.argument("<name>", "Package name")
.option("--version <version>", "Package version")
.requiredOption("--reason <text>", "Report reason")
.option("--json", "Output JSON")
.action(async (name, options) => {
const opts = await resolveGlobalOpts();
await cmdReportPackage(opts, name, options);
});
registerCommand(packageCmd, ["package", "moderation-status"])
.description("Show package moderation status")
.argument("<name>", "Package name")
.option("--json", "Output JSON")
.action(async (name, options) => {
const opts = await resolveGlobalOpts();
await cmdPackageModerationStatus(opts, name, options);
});
registerCommand(packageCmd, ["package", "readiness"])
.description("Check package readiness for future OpenClaw consumption")
.argument("<name>", "Package name")
.option("--json", "Output JSON")
.action(async (name, options) => {
const opts = await resolveGlobalOpts();
await cmdPackageReadiness(opts, name, options);
});
registerCommand(packageCmd, ["package", "migration-status"])
.description("Show package migration status for future OpenClaw consumption")
.argument("<name>", "Package name")
.option("--json", "Output JSON")
.action(async (name, options) => {
const opts = await resolveGlobalOpts();
await cmdPackageMigrationStatus(opts, name, options);
});
registerCommand(packageCmd, ["package", "pack"])
.description("Create a ClawPack npm tarball from a plugin package folder")
.argument("<source>", "Package folder path")
.option("--pack-destination <dir>", "Directory for the generated .tgz (default: workdir)")
.option("--json", "Output JSON")
.action(async (source, options) => {
const opts = await resolveGlobalOpts();
await cmdPackPackage(opts, source, options);
});
registerCommand(packageCmd, ["package", "publish"])
.description("Publish a code plugin or bundle plugin from a folder or GitHub source")
.argument("<source>", "Package folder path, GitHub repo (owner/repo[@ref]), or URL")
.option("--family <family>", "code-plugin|bundle-plugin")
.option("--name <name>", "Package name")
.option("--display-name <name>", "Display name")
.option("--owner <handle>", "Publish under this owner/publisher handle")
.option("--version <version>", "Version")
.option("--changelog <text>", "Changelog text")
.option("--manual-override-reason <reason>", "Required for manual publish when trusted publisher config exists")
.option("--tags <tags>", "Comma-separated tags", "latest")
.option("--categories <slugs>", "Comma-separated category slugs")
.option("--topics <topics>", "Comma-separated topics")
.option("--bundle-format <format>", "Bundle format")
.option("--host-targets <targets>", "Comma-separated bundle host targets")
.option("--source-repo <repo>", "GitHub repo (owner/repo or URL)")
.option("--source-commit <sha>", "Git commit SHA")
.option("--source-ref <ref>", "Git ref/tag/branch")
.option("--source-path <path>", "Repo subpath")
.option("--dry-run", "Preview what would be published without uploading")
.option("--json", "Output JSON (for CI pipelines)")
.action(async (source, options) => {
const opts = await resolveGlobalOpts();
await cmdPublishPackage(opts, source, options);
});
const trustedPublisherCmd = registerCommandGroup(packageCmd, [
"package",
"trusted-publisher",
]).description("Manage package trusted publisher config");
registerCommand(trustedPublisherCmd, ["package", "trusted-publisher", "get"])
.description("Show trusted publisher config for a package")
.argument("<name>", "Package name")
.option("--json", "Output JSON")
.action(async (name, options) => {
const opts = await resolveGlobalOpts();
await cmdGetPackageTrustedPublisher(opts, name, options);
});
registerCommand(trustedPublisherCmd, ["package", "trusted-publisher", "set"])
.description("Set trusted publisher config for a package")
.argument("<name>", "Package name")
.requiredOption("--repository <repo>", "GitHub repository, for example openclaw/openclaw")
.requiredOption("--workflow-filename <file>", "GitHub Actions workflow filename")
.option("--environment <name>", "GitHub Actions environment name")
.option("--json", "Output JSON")
.action(async (name, options) => {
const opts = await resolveGlobalOpts();
await cmdSetPackageTrustedPublisher(opts, name, options);
});
registerCommand(trustedPublisherCmd, ["package", "trusted-publisher", "delete"])
.description("Delete trusted publisher config for a package")
.argument("<name>", "Package name")
.option("--json", "Output JSON")
.action(async (name, options) => {
const opts = await resolveGlobalOpts();
await cmdDeletePackageTrustedPublisher(opts, name, options);
});
registerCommand(skill, ["skill", "rename"])
.description("Rename a published skill and keep the old slug as a redirect")
.argument("<skill>", "Current skill")
.argument("<new-slug>", "New canonical slug")
.option("--yes", "Skip confirmation")
.action(async (slug, newSlug, options) => {
const opts = await resolveGlobalOpts();
await cmdRenameSkill(opts, slug, newSlug, options, isInputAllowed());
});
registerCommand(skill, ["skill", "merge"])
.description("Merge one owned skill into another and redirect the old slug")
.argument("<source>", "Source skill")
.argument("<target>", "Target skill")
.option("--yes", "Skip confirmation")
.action(async (sourceSlug, targetSlug, options) => {
const opts = await resolveGlobalOpts();
await cmdMergeSkill(opts, sourceSlug, targetSlug, options, isInputAllowed());
});
const transfer = registerCommandGroup(program, ["transfer"]).description("Transfer skill ownership");
registerCommand(transfer, ["transfer", "request"])
.description("Request skill transfer to another user")
.argument("<skill>", "Skill to transfer")
.argument("<handle>", "Recipient handle (e.g., @username)")
.option("--message <text>", "Optional message for recipient")
.option("--yes", "Skip confirmation")
.action(async (slug, handle, options) => {
const opts = await resolveGlobalOpts();
await cmdTransferRequest(opts, slug, handle, options, isInputAllowed());
});
registerCommand(transfer, ["transfer", "list"])
.description("List pending transfer requests")
.option("--outgoing", "Show outgoing transfer requests")
.action(async (options) => {
const opts = await resolveGlobalOpts();
await cmdTransferList(opts, options);
});
registerCommand(transfer, ["transfer", "accept"])
.description("Accept incoming transfer for a skill")
.argument("<skill>", "Skill to accept")
.option("--yes", "Skip confirmation")
.action(async (slug, options) => {
const opts = await resolveGlobalOpts();
await cmdTransferAccept(opts, slug, options, isInputAllowed());
});
registerCommand(transfer, ["transfer", "reject"])
.description("Reject incoming transfer for a skill")
.argument("<skill>", "Skill to reject")
.option("--yes", "Skip confirmation")
.action(async (slug, options) => {
const opts = await resolveGlobalOpts();
await cmdTransferReject(opts, slug, options, isInputAllowed());
});
registerCommand(transfer, ["transfer", "cancel"])
.description("Cancel outgoing transfer for a skill")
.argument("<skill>", "Skill to cancel")
.option("--yes", "Skip confirmation")
.action(async (slug, options) => {
const opts = await resolveGlobalOpts();
await cmdTransferCancel(opts, slug, options, isInputAllowed());
});
registerCommand(program, ["star"])
.description("Add a skill to your highlights")
.argument("<skill>", "Skill to highlight")
.option("--yes", "Skip confirmation")
.action(async (slug, options) => {
const opts = await resolveGlobalOpts();
await cmdStarSkill(opts, slug, options, isInputAllowed());
});
registerCommand(program, ["unstar"])
.description("Remove a skill from your highlights")
.argument("<skill>", "Skill to remove from highlights")
.option("--yes", "Skip confirmation")
.action(async (slug, options) => {
const opts = await resolveGlobalOpts();
await cmdUnstarSkill(opts, slug, options, isInputAllowed());
});
registerCommand(program, ["sync"])
.description("Scan local skills and publish new or changed ones")
.option("--root <dir...>", "Extra scan roots (one or more)")
.option("--all", "Publish all new or changed skills without prompting")
.option("--dry-run", "Show what would be published")
.option("--json", "Output JSON")
.option("--owner <handle>", "Publish under an org/user publisher handle")
.option("--bump <type>", "Version bump for updates (patch|minor|major)", "patch")
.option("--changelog <text>", "Changelog to use for updates")
.option("--tags <tags>", "Comma-separated tags", "latest")
.option("--concurrency <n>", "Concurrent registry/file checks", (value) => Number.parseInt(value, 10))
.option("--source-repo <repo>", "GitHub repo URL or owner/name for source provenance")
.option("--source-commit <sha>", "Git commit SHA for source provenance")
.option("--source-ref <ref>", "Git ref for source provenance")
.action(async (options) => {
const opts = await resolveGlobalOpts();
const bump = options.bump === "patch" || options.bump === "minor" || options.bump === "major"
? options.bump
: fail("--bump must be patch, minor, or major");
const concurrency = options.concurrency ?? 6;
if (concurrency < 1 || concurrency > 32)
fail("--concurrency must be between 1 and 32");
await cmdSync(opts, {
root: options.root,
all: options.all,
dryRun: options.dryRun,
json: options.json,
owner: options.owner,
bump,
changelog: options.changelog,
tags: options.tags,
concurrency,
sourceRepo: options.sourceRepo,
sourceCommit: options.sourceCommit,
sourceRef: options.sourceRef,
}, isInputAllowed());
});
applyCommandHelpGroups(program, {
login: "Auth:",
logout: "Auth:",
whoami: "Auth:",
auth: "Auth:",
search: "Skills:",
install: "Skills:",
update: "Skills:",
uninstall: "Skills:",
list: "Skills:",
pin: "Skills:",
unpin: "Skills:",
explore: "Skills:",
inspect: "Skills:",
star: "Skills:",
unstar: "Skills:",
publish: "Publishing:",
sync: "Publishing:",
skill: "Publishing:",
publisher: "Publishing:",
package: "Packages:",
delete: "Moderation:",
hide: "Moderation:",
undelete: "Moderation:",
unhide: "Moderation:",
transfer: "Moderation:",
help: "Help:",
});
applyCommandHelpGroups(packageCmd, {
explore: "Discovery:",
inspect: "Discovery:",
download: "Artifacts:",
verify: "Artifacts:",
pack: "Publishing:",
publish: "Publishing:",
"trusted-publisher": "Publishing:",
delete: "Moderation:",
undelete: "Moderation:",
transfer: "Moderation:",
report: "Moderation:",
"moderation-status": "Moderation:",
readiness: "Operations:",
"migration-status": "Operations:",
});
program.action(() => {
program.outputHelp();
process.exitCode = 0;
});
validateTopLevelCommand(process.argv.slice(2));
void program.parseAsync(process.argv).catch((error) => {
fail(formatError(error));
});
//# sourceMappingURL=cli.js.map
File diff suppressed because one or more lines are too long
+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":"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"}
+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":"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"}
+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
+3
View File
@@ -0,0 +1,3 @@
import { type GlobalConfig } from "./schema/index.js";
export declare function readGlobalConfig(): Promise<GlobalConfig | null>;
export declare function writeGlobalConfig(config: GlobalConfig): Promise<void>;
+77
View File
@@ -0,0 +1,77 @@
import { existsSync } from "node:fs";
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { resolveHome } from "./homedir.js";
import { GlobalConfigSchema, parseArk } from "./schema/index.js";
/**
* Resolve config path with legacy fallback.
* Checks for 'clawhub' first, falls back to legacy 'clawdhub' if it exists.
*/
function resolveConfigPath(baseDir) {
const clawhubPath = join(baseDir, "clawhub", "config.json");
const clawdhubPath = join(baseDir, "clawdhub", "config.json");
if (existsSync(clawhubPath))
return clawhubPath;
if (existsSync(clawdhubPath))
return clawdhubPath;
return clawhubPath;
}
function isNonFatalChmodError(error) {
if (!(error instanceof Error))
return false;
const code = error.code;
return code === "EPERM" || code === "ENOTSUP" || code === "EOPNOTSUPP" || code === "EINVAL";
}
function getGlobalConfigPath() {
const override = process.env.CLAWHUB_CONFIG_PATH?.trim() ?? process.env.CLAWDHUB_CONFIG_PATH?.trim();
if (override)
return resolve(override);
const home = resolveHome();
if (process.platform === "darwin") {
return resolveConfigPath(join(home, "Library", "Application Support"));
}
const xdg = process.env.XDG_CONFIG_HOME;
if (xdg) {
return resolveConfigPath(xdg);
}
if (process.platform === "win32") {
const appData = process.env.APPDATA;
if (appData) {
return resolveConfigPath(appData);
}
}
return resolveConfigPath(join(home, ".config"));
}
export async function readGlobalConfig() {
try {
const raw = await readFile(getGlobalConfigPath(), "utf8");
const parsed = JSON.parse(raw);
return parseArk(GlobalConfigSchema, parsed, "Global config");
}
catch {
return null;
}
}
export async function writeGlobalConfig(config) {
const path = getGlobalConfigPath();
const dir = dirname(path);
// Create directory with restricted permissions (owner only)
await mkdir(dir, { recursive: true, mode: 0o700 });
// Write file with restricted permissions (owner read/write only)
// This protects API tokens from being read by other users
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, {
encoding: "utf8",
mode: 0o600,
});
// Ensure permissions on existing files (writeFile mode only applies on create)
if (process.platform !== "win32") {
try {
await chmod(path, 0o600);
}
catch (error) {
if (!isNonFatalChmodError(error))
throw error;
}
}
}
//# sourceMappingURL=config.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
+46
View File
@@ -0,0 +1,46 @@
/**
* GitHub Device Flow authentication for headless environments.
*
* Implements RFC 8628 / GitHub's Device Flow:
* https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow
*
* This allows CLI authentication without a browser redirect to localhost,
* enabling headless agents and remote servers to authenticate.
*/
type DeviceCodeResponse = {
device_code: string;
user_code: string;
verification_uri: string;
expires_in: number;
interval: number;
};
type DeviceTokenResponse = {
access_token: string;
token_type: string;
scope: string;
};
type DeviceFlowConfig = {
/** The ClawHub API URL that exposes device flow endpoints */
apiUrl: string;
/** The ClawHub site URL that hosts the verification page */
siteUrl: string;
/** Token label to show during approval and store on the created token */
label?: string;
/** Client ID for the OAuth app (provided by ClawHub) */
clientId?: string;
/** Scope to request */
scope?: string;
};
/**
* Request a device code from the ClawHub device flow endpoint.
*/
export declare function requestDeviceCode(config: DeviceFlowConfig): Promise<DeviceCodeResponse>;
/**
* Poll for the device flow token until the user completes authorization,
* the code expires, or an unrecoverable error occurs.
*/
export declare function pollForDeviceToken(config: DeviceFlowConfig, deviceCode: string, options: {
interval: number;
expiresIn: number;
}): Promise<DeviceTokenResponse>;
export {};
+153
View File
@@ -0,0 +1,153 @@
/**
* GitHub Device Flow authentication for headless environments.
*
* Implements RFC 8628 / GitHub's Device Flow:
* https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow
*
* This allows CLI authentication without a browser redirect to localhost,
* enabling headless agents and remote servers to authenticate.
*/
const DEFAULT_SCOPE = "read write";
/**
* Request a device code from the ClawHub device flow endpoint.
*/
export async function requestDeviceCode(config) {
const url = new URL("/api/cli/device/code", config.apiUrl);
const body = {
scope: config.scope ?? DEFAULT_SCOPE,
site_url: config.siteUrl,
};
if (config.label) {
body.label = config.label;
}
if (config.clientId) {
body.client_id = config.clientId;
}
const response = await fetch(url.toString(), {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(body),
});
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(`Device code request failed (${response.status}): ${text || response.statusText}`);
}
const data = await response.json().catch(() => null);
if (!isDeviceCodeResponse(data)) {
throw new Error("Invalid device code response from server");
}
return data;
}
/**
* Poll for the device flow token until the user completes authorization,
* the code expires, or an unrecoverable error occurs.
*/
export async function pollForDeviceToken(config, deviceCode, options) {
const url = new URL("/api/cli/device/token", config.apiUrl);
const deadline = Date.now() + options.expiresIn * 1000;
const expirationMessage = "Device code expired (timeout). Please try again.";
let interval = options.interval * 1000;
const body = {
device_code: deviceCode,
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
};
if (config.clientId) {
body.client_id = config.clientId;
}
while (Date.now() < deadline) {
await sleep(interval);
const remainingMs = deadline - Date.now();
if (remainingMs <= 0) {
break;
}
const controller = new AbortController();
let timeout;
let response;
try {
response = await new Promise((resolve, reject) => {
timeout = setTimeout(() => {
controller.abort();
reject(new Error(expirationMessage));
}, remainingMs);
void fetch(url.toString(), {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(body),
signal: controller.signal,
}).then(resolve, reject);
});
}
catch (error) {
if (controller.signal.aborted) {
throw new Error(expirationMessage, { cause: error });
}
throw error;
}
finally {
if (timeout !== undefined) {
clearTimeout(timeout);
}
}
// Parse JSON once to avoid "body already read" errors
const data = await response.json().catch(() => null);
if (response.ok) {
if (!isDeviceTokenResponse(data)) {
throw new Error("Invalid device token response from server");
}
return data;
}
if (!isRecord(data)) {
throw new Error("Invalid device token response from server");
}
const errorData = data;
switch (errorData.error) {
case "authorization_pending":
// User hasn't completed auth yet — keep polling
break;
case "slow_down":
// Server requests longer interval
interval = (errorData.interval ?? Math.ceil(interval / 1000) + 5) * 1000;
break;
case "expired_token":
throw new Error("Device code expired. Please try again.");
case "access_denied":
throw new Error("Authorization denied by user.");
default:
throw new Error(`Device flow error: ${errorData.error_description || errorData.error || "unknown error"}`);
}
}
throw new Error(expirationMessage);
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isRecord(value) {
return typeof value === "object" && value !== null;
}
function isNonEmptyString(value) {
return typeof value === "string" && value.trim().length > 0;
}
function isPositiveFiniteNumber(value) {
return typeof value === "number" && Number.isFinite(value) && value > 0;
}
function isDeviceCodeResponse(value) {
return (isRecord(value) &&
isNonEmptyString(value.device_code) &&
isNonEmptyString(value.user_code) &&
isNonEmptyString(value.verification_uri) &&
isPositiveFiniteNumber(value.expires_in) &&
isPositiveFiniteNumber(value.interval));
}
function isDeviceTokenResponse(value) {
return (isRecord(value) &&
isNonEmptyString(value.access_token) &&
isNonEmptyString(value.token_type) &&
isNonEmptyString(value.scope));
}
//# sourceMappingURL=deviceAuth.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"deviceAuth.js","sourceRoot":"","sources":["../src/deviceAuth.ts"],"names":[],"mappings":"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"}
+5
View File
@@ -0,0 +1,5 @@
export declare function discoverRegistryFromSite(siteUrl: string): Promise<{
apiBase: string;
authBase: string | undefined;
minCliVersion: string | undefined;
} | null>;
+25
View File
@@ -0,0 +1,25 @@
import { parseArk, WellKnownConfigSchema } from "./schema/index.js";
export async function discoverRegistryFromSite(siteUrl) {
const paths = ["/.well-known/clawhub.json", "/.well-known/clawdhub.json"];
for (const path of paths) {
const url = new URL(path, siteUrl);
const response = await fetch(url.toString(), {
method: "GET",
headers: { Accept: "application/json" },
});
if (!response.ok)
continue;
const raw = (await response.json());
const parsed = parseArk(WellKnownConfigSchema, raw, "WellKnown config");
const apiBase = "apiBase" in parsed ? parsed.apiBase : parsed.registry;
if (!apiBase)
return null;
return {
apiBase,
authBase: parsed.authBase,
minCliVersion: parsed.minCliVersion,
};
}
return null;
}
//# sourceMappingURL=discovery.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"discovery.js","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAEpE,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,OAAe;IAC5D,MAAM,KAAK,GAAG,CAAC,2BAA2B,EAAE,4BAA4B,CAAC,CAAC;IAC1E,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACnC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE;YAC3C,MAAM,EAAE,KAAK;YACb,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE;SACxC,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,SAAS;QAC3B,MAAM,GAAG,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAY,CAAC;QAC/C,MAAM,MAAM,GAAG,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE,kBAAkB,CAAC,CAAC;QACxE,MAAM,OAAO,GAAG,SAAS,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC;QACvE,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAC;QAC1B,OAAO;YACL,OAAO;YACP,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,aAAa,EAAE,MAAM,CAAC,aAAa;SACpC,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
+7
View File
@@ -0,0 +1,7 @@
/**
* Resolve the user's home directory, preferring environment variables over
* os.homedir(). On Linux, os.homedir() reads from /etc/passwd which can
* return a stale path after a user rename (usermod -l). The $HOME env var
* is set by the login process and reflects the current session.
*/
export declare function resolveHome(): string;

Some files were not shown because too many files have changed in this diff Show More