@yoyo-bot/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +160 -0
  2. package/dist/commands/auth.d.ts +5 -0
  3. package/dist/commands/auth.d.ts.map +1 -0
  4. package/dist/commands/auth.js +26 -0
  5. package/dist/commands/auth.js.map +1 -0
  6. package/dist/commands/bind.d.ts +9 -0
  7. package/dist/commands/bind.d.ts.map +1 -0
  8. package/dist/commands/bind.js +132 -0
  9. package/dist/commands/bind.js.map +1 -0
  10. package/dist/commands/init.d.ts +5 -0
  11. package/dist/commands/init.d.ts.map +1 -0
  12. package/dist/commands/init.js +57 -0
  13. package/dist/commands/init.js.map +1 -0
  14. package/dist/commands/list.d.ts +6 -0
  15. package/dist/commands/list.d.ts.map +1 -0
  16. package/dist/commands/list.js +38 -0
  17. package/dist/commands/list.js.map +1 -0
  18. package/dist/commands/publish.d.ts +8 -0
  19. package/dist/commands/publish.d.ts.map +1 -0
  20. package/dist/commands/publish.js +96 -0
  21. package/dist/commands/publish.js.map +1 -0
  22. package/dist/commands/search.d.ts +5 -0
  23. package/dist/commands/search.d.ts.map +1 -0
  24. package/dist/commands/search.js +36 -0
  25. package/dist/commands/search.js.map +1 -0
  26. package/dist/commands/unbind.d.ts +7 -0
  27. package/dist/commands/unbind.d.ts.map +1 -0
  28. package/dist/commands/unbind.js +26 -0
  29. package/dist/commands/unbind.js.map +1 -0
  30. package/dist/commands/whoami.d.ts +4 -0
  31. package/dist/commands/whoami.d.ts.map +1 -0
  32. package/dist/commands/whoami.js +34 -0
  33. package/dist/commands/whoami.js.map +1 -0
  34. package/dist/index.d.ts +3 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +67 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/lib/api.d.ts +60 -0
  39. package/dist/lib/api.d.ts.map +1 -0
  40. package/dist/lib/api.js +54 -0
  41. package/dist/lib/api.js.map +1 -0
  42. package/dist/lib/config.d.ts +11 -0
  43. package/dist/lib/config.d.ts.map +1 -0
  44. package/dist/lib/config.js +38 -0
  45. package/dist/lib/config.js.map +1 -0
  46. package/package.json +26 -0
  47. package/src/commands/auth.ts +30 -0
  48. package/src/commands/bind.ts +147 -0
  49. package/src/commands/init.ts +69 -0
  50. package/src/commands/list.ts +43 -0
  51. package/src/commands/publish.ts +117 -0
  52. package/src/commands/search.ts +40 -0
  53. package/src/commands/unbind.ts +34 -0
  54. package/src/commands/whoami.ts +34 -0
  55. package/src/index.ts +76 -0
  56. package/src/lib/api.ts +118 -0
  57. package/src/lib/config.ts +45 -0
  58. package/tsconfig.json +10 -0
@@ -0,0 +1,43 @@
1
+ import pc from "picocolors";
2
+ import { api } from "../lib/api.js";
3
+
4
+ export async function listCommand(opts: { owner?: string; page?: string; json?: boolean }): Promise<void> {
5
+ const page = Number(opts.page ?? 1);
6
+ const pageSize = 20;
7
+
8
+ let result;
9
+ try {
10
+ result = await api.listBinders(opts.owner, page, pageSize);
11
+ } catch (err) {
12
+ console.error(pc.red(`Failed to list binders: ${String(err)}`));
13
+ process.exit(1);
14
+ }
15
+
16
+ if (opts.json) {
17
+ console.log(JSON.stringify(result, null, 2));
18
+ return;
19
+ }
20
+
21
+ if (result.items.length === 0) {
22
+ console.log(pc.dim("No binders found."));
23
+ return;
24
+ }
25
+
26
+ const title = opts.owner ? `Binders by ${opts.owner}` : "All binders";
27
+ console.log(pc.bold(`\n${title}`) + pc.dim(` (${result.total} total)\n`));
28
+
29
+ for (const b of result.items) {
30
+ const name = pc.cyan(`${b.owner}/${b.slug}`);
31
+ const ver = b.version ? pc.dim(`v${b.version}`) : "";
32
+ const badge =
33
+ b.verification === "official" ? pc.yellow(" ✦") : b.verification === "verified" ? pc.green(" ✓") : "";
34
+ const dl = b.downloads != null ? pc.dim(` · ${b.downloads.toLocaleString()} downloads`) : "";
35
+ console.log(` ${name}${badge} ${ver}${dl}`);
36
+ if (b.description) console.log(` ${pc.dim(b.description.slice(0, 80))}`);
37
+ console.log();
38
+ }
39
+
40
+ if (result.total > page * pageSize) {
41
+ console.log(pc.dim(`Page ${page} of ${Math.ceil(result.total / pageSize)} — use --page to navigate`));
42
+ }
43
+ }
@@ -0,0 +1,117 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { existsSync } from "node:fs";
4
+ import pc from "picocolors";
5
+ import { api, ApiError } from "../lib/api.js";
6
+ import { getApiKey } from "../lib/config.js";
7
+
8
+ interface PublishOptions {
9
+ dir?: string;
10
+ dryRun?: boolean;
11
+ json?: boolean;
12
+ }
13
+
14
+ interface BinderManifest {
15
+ slug: string;
16
+ displayName?: string;
17
+ description?: string;
18
+ version: string;
19
+ tab?: string;
20
+ tags?: string[];
21
+ platforms?: string[];
22
+ license?: string;
23
+ dependencies?: Record<string, string>;
24
+ }
25
+
26
+ export async function publishCommand(opts: PublishOptions): Promise<void> {
27
+ const cwd = opts.dir ? resolve(opts.dir) : process.cwd();
28
+ const manifestPath = join(cwd, "binder.json");
29
+
30
+ if (!existsSync(manifestPath)) {
31
+ console.error(pc.red("No binder.json found. Run: yoyo init"));
32
+ process.exit(1);
33
+ }
34
+
35
+ let manifest: BinderManifest;
36
+ try {
37
+ const raw = await readFile(manifestPath, "utf8");
38
+ manifest = JSON.parse(raw) as BinderManifest;
39
+ } catch {
40
+ console.error(pc.red("Failed to parse binder.json."));
41
+ process.exit(1);
42
+ }
43
+
44
+ if (!manifest.slug) {
45
+ console.error(pc.red("binder.json missing required field: slug"));
46
+ process.exit(1);
47
+ }
48
+ if (!manifest.version) {
49
+ console.error(pc.red("binder.json missing required field: version"));
50
+ process.exit(1);
51
+ }
52
+
53
+ const key = await getApiKey();
54
+ if (!key) {
55
+ console.error(pc.red("Not authenticated. Run: yoyo auth"));
56
+ process.exit(1);
57
+ }
58
+
59
+ // Read page.md as readme if present
60
+ let readme: string | undefined;
61
+ const pagePath = join(cwd, "page.md");
62
+ if (existsSync(pagePath)) {
63
+ readme = await readFile(pagePath, "utf8");
64
+ } else {
65
+ const readmePath = join(cwd, "README.md");
66
+ if (existsSync(readmePath)) readme = await readFile(readmePath, "utf8");
67
+ }
68
+
69
+ if (opts.dryRun || opts.json) {
70
+ const payload = {
71
+ slug: manifest.slug,
72
+ displayName: manifest.displayName,
73
+ description: manifest.description,
74
+ version: manifest.version,
75
+ readme,
76
+ tags: manifest.tags ?? [],
77
+ tab: manifest.tab,
78
+ license: manifest.license,
79
+ platforms: manifest.platforms ?? [],
80
+ dependencies: manifest.dependencies ?? {},
81
+ };
82
+ if (opts.json) {
83
+ console.log(JSON.stringify({ dryRun: opts.dryRun ?? false, payload }, null, 2));
84
+ } else {
85
+ console.log(pc.yellow("Dry run — would publish:"));
86
+ console.log(` ${manifest.slug}@${manifest.version}`);
87
+ }
88
+ return;
89
+ }
90
+
91
+ console.log(pc.dim(`Publishing ${manifest.slug}@${manifest.version}…`));
92
+
93
+ try {
94
+ const result = await api.publish({
95
+ slug: manifest.slug,
96
+ displayName: manifest.displayName,
97
+ description: manifest.description,
98
+ version: manifest.version,
99
+ readme,
100
+ tags: manifest.tags ?? [],
101
+ tab: manifest.tab,
102
+ license: manifest.license,
103
+ platforms: manifest.platforms ?? [],
104
+ dependencies: manifest.dependencies ?? {},
105
+ });
106
+
107
+ console.log(pc.green(`\nPublished ${result.slug}@${result.version}`));
108
+ console.log(pc.dim(`ID: ${result.id}`));
109
+ } catch (err) {
110
+ if (err instanceof ApiError) {
111
+ console.error(pc.red(`Publish failed (${err.status}): ${err.message}`));
112
+ } else {
113
+ console.error(pc.red(`Publish failed: ${String(err)}`));
114
+ }
115
+ process.exit(1);
116
+ }
117
+ }
@@ -0,0 +1,40 @@
1
+ import pc from "picocolors";
2
+ import { api } from "../lib/api.js";
3
+
4
+ export async function searchCommand(query: string, opts: { limit?: string; json?: boolean }): Promise<void> {
5
+ const pageSize = Math.min(Number(opts.limit ?? 10), 50);
6
+
7
+ let result;
8
+ try {
9
+ result = await api.search(query, 1, pageSize);
10
+ } catch (err: unknown) {
11
+ console.error(pc.red("Search failed: " + String(err)));
12
+ process.exit(1);
13
+ }
14
+
15
+ if (opts.json) {
16
+ console.log(JSON.stringify(result, null, 2));
17
+ return;
18
+ }
19
+
20
+ if (result.items.length === 0) {
21
+ console.log(pc.dim(`No results for "${query}"`));
22
+ return;
23
+ }
24
+
25
+ console.log(pc.bold(`\nSearch results for "${query}"`) + pc.dim(` (${result.total} total)\n`));
26
+
27
+ for (const b of result.items) {
28
+ const name = pc.cyan(`${b.owner}/${b.slug}`);
29
+ const ver = b.version ? pc.dim(`v${b.version}`) : "";
30
+ const badge = b.verification === "official" ? pc.yellow(" ✦") : b.verification === "verified" ? pc.green(" ✓") : "";
31
+ console.log(` ${name}${badge} ${ver}`);
32
+ if (b.description) {
33
+ console.log(` ${pc.dim(b.description.slice(0, 80))}`);
34
+ }
35
+ if (b.tags?.length) {
36
+ console.log(` ${pc.dim(b.tags.slice(0, 5).join(", "))}`);
37
+ }
38
+ console.log();
39
+ }
40
+ }
@@ -0,0 +1,34 @@
1
+ import { rm } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { existsSync } from "node:fs";
5
+ import pc from "picocolors";
6
+
7
+ interface UnbindOptions {
8
+ dir?: string;
9
+ force?: boolean;
10
+ }
11
+
12
+ function defaultBindRoot(): string {
13
+ return join(homedir(), ".yoyo", "binders");
14
+ }
15
+
16
+ export async function unbindCommand(ref: string, opts: UnbindOptions): Promise<void> {
17
+ const parts = ref.split("/");
18
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
19
+ console.error(pc.red(`Invalid ref "${ref}". Expected <owner>/<slug>`));
20
+ process.exit(1);
21
+ }
22
+ const [owner, slug] = parts;
23
+ const bindRoot = opts.dir ? resolve(opts.dir) : defaultBindRoot();
24
+ const bindDir = join(bindRoot, owner!, slug!);
25
+
26
+ if (!existsSync(bindDir)) {
27
+ console.error(pc.red(`Not bound: ${ref} (${bindDir} not found)`));
28
+ process.exit(1);
29
+ }
30
+
31
+ console.log(pc.dim(`Removing ${bindDir}…`));
32
+ await rm(bindDir, { recursive: true, force: true });
33
+ console.log(pc.green(`Unbound ${ref}`));
34
+ }
@@ -0,0 +1,34 @@
1
+ import pc from "picocolors";
2
+ import { api, ApiError } from "../lib/api.js";
3
+ import { getApiKey } from "../lib/config.js";
4
+
5
+ export async function whoamiCommand(opts: { json?: boolean }): Promise<void> {
6
+ const key = await getApiKey();
7
+ if (!key) {
8
+ console.error(pc.red("Not authenticated. Run: yoyo auth"));
9
+ process.exit(1);
10
+ }
11
+
12
+ let user;
13
+ try {
14
+ user = await api.whoami();
15
+ } catch (err) {
16
+ if (err instanceof ApiError && err.status === 401) {
17
+ console.error(pc.red("Invalid or expired API key. Run: yoyo auth"));
18
+ } else {
19
+ console.error(pc.red(`Error: ${String(err)}`));
20
+ }
21
+ process.exit(1);
22
+ }
23
+
24
+ if (opts.json) {
25
+ console.log(JSON.stringify(user, null, 2));
26
+ return;
27
+ }
28
+
29
+ console.log();
30
+ console.log(` ${pc.bold(user.displayName ?? user.slug)}`);
31
+ console.log(` ${pc.dim("Slug")} ${user.slug}`);
32
+ if (user.email) console.log(` ${pc.dim("Email")} ${user.email}`);
33
+ console.log();
34
+ }
package/src/index.ts ADDED
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ import { program } from "commander";
3
+ import { searchCommand } from "./commands/search.js";
4
+ import { bindCommand } from "./commands/bind.js";
5
+ import { unbindCommand } from "./commands/unbind.js";
6
+ import { publishCommand } from "./commands/publish.js";
7
+ import { initCommand } from "./commands/init.js";
8
+ import { authCommand } from "./commands/auth.js";
9
+ import { whoamiCommand } from "./commands/whoami.js";
10
+ import { listCommand } from "./commands/list.js";
11
+
12
+ program
13
+ .name("yoyo")
14
+ .description("Yoyo — the AI agent binder registry CLI")
15
+ .version("0.0.1");
16
+
17
+ program
18
+ .command("search <query>")
19
+ .description("Search for binders in the registry")
20
+ .option("-l, --limit <n>", "max results", "10")
21
+ .option("--json", "output JSON")
22
+ .action((query: string, opts) => searchCommand(query, opts));
23
+
24
+ program
25
+ .command("bind <owner/slug>")
26
+ .description("Bind (install) a binder to ~/.yoyo/binders/")
27
+ .option("-v, --version <ver>", "specific version")
28
+ .option("-d, --dir <path>", "bind root directory")
29
+ .option("--dry-run", "preview without writing")
30
+ .option("--json", "output JSON")
31
+ .action((ref: string, opts) => bindCommand(ref, opts));
32
+
33
+ program
34
+ .command("unbind <owner/slug>")
35
+ .description("Remove a bound binder")
36
+ .option("-d, --dir <path>", "bind root directory")
37
+ .option("-f, --force", "skip confirmation")
38
+ .action((ref: string, opts) => unbindCommand(ref, opts));
39
+
40
+ program
41
+ .command("list")
42
+ .description("List binders in the registry")
43
+ .option("--owner <slug>", "filter by owner")
44
+ .option("--page <n>", "page number", "1")
45
+ .option("--json", "output JSON")
46
+ .action((opts) => listCommand(opts));
47
+
48
+ program
49
+ .command("publish")
50
+ .description("Publish binder from current directory")
51
+ .option("-d, --dir <path>", "binder directory (default: cwd)")
52
+ .option("--dry-run", "preview without uploading")
53
+ .option("--json", "output JSON")
54
+ .action((opts) => publishCommand(opts));
55
+
56
+ program
57
+ .command("init")
58
+ .description("Scaffold a new binder in the current directory")
59
+ .option("-d, --dir <path>", "target directory (default: cwd)")
60
+ .option("-y, --yes", "skip prompts, use defaults")
61
+ .action((opts) => initCommand(opts));
62
+
63
+ program
64
+ .command("auth")
65
+ .description("Save your Yoyo API key to ~/.yoyo/config.json")
66
+ .option("-k, --key <apiKey>", "API key (omit to prompt)")
67
+ .option("--url <apiUrl>", "custom API URL")
68
+ .action((opts) => authCommand(opts));
69
+
70
+ program
71
+ .command("whoami")
72
+ .description("Show the currently authenticated user")
73
+ .option("--json", "output JSON")
74
+ .action((opts) => whoamiCommand(opts));
75
+
76
+ program.parseAsync(process.argv);
package/src/lib/api.ts ADDED
@@ -0,0 +1,118 @@
1
+ import { getApiKey, getApiUrl } from "./config.js";
2
+
3
+ export class ApiError extends Error {
4
+ constructor(
5
+ public status: number,
6
+ message: string,
7
+ public body?: unknown
8
+ ) {
9
+ super(message);
10
+ this.name = "ApiError";
11
+ }
12
+ }
13
+
14
+ async function request<T>(
15
+ method: string,
16
+ path: string,
17
+ opts: { body?: unknown; auth?: boolean } = {}
18
+ ): Promise<T> {
19
+ const base = await getApiUrl();
20
+ const url = `${base}${path}`;
21
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
22
+
23
+ if (opts.auth !== false) {
24
+ const key = await getApiKey();
25
+ if (key) headers["Authorization"] = `Bearer ${key}`;
26
+ }
27
+
28
+ const res = await fetch(url, {
29
+ method,
30
+ headers,
31
+ body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
32
+ });
33
+
34
+ if (!res.ok) {
35
+ let detail = "";
36
+ try {
37
+ const j = (await res.json()) as { message?: string; error?: string };
38
+ detail = j.message ?? j.error ?? "";
39
+ } catch {}
40
+ throw new ApiError(res.status, detail || res.statusText);
41
+ }
42
+
43
+ return res.json() as Promise<T>;
44
+ }
45
+
46
+ // ── Types ──────────────────────────────────────────────────────────────────
47
+
48
+ export interface BinderSummary {
49
+ owner: string;
50
+ slug: string;
51
+ displayName?: string;
52
+ description?: string;
53
+ version?: string;
54
+ downloads?: number;
55
+ tab?: string;
56
+ verification?: "official" | "verified" | "none";
57
+ tags?: string[];
58
+ }
59
+
60
+ export interface BinderDetail extends BinderSummary {
61
+ readme?: string;
62
+ latestVersion?: {
63
+ version: string;
64
+ tarballUrl?: string;
65
+ tarballSha256?: string;
66
+ platforms?: string[];
67
+ };
68
+ }
69
+
70
+ export interface SearchResult {
71
+ items: BinderSummary[];
72
+ total: number;
73
+ page: number;
74
+ pageSize: number;
75
+ }
76
+
77
+ export interface WhoamiResult {
78
+ id: string;
79
+ slug: string;
80
+ displayName?: string;
81
+ email?: string;
82
+ }
83
+
84
+ // ── API calls ──────────────────────────────────────────────────────────────
85
+
86
+ export const api = {
87
+ search: (q: string, page = 1, pageSize = 20) =>
88
+ request<SearchResult>("GET", `/binders?q=${encodeURIComponent(q)}&page=${page}&pageSize=${pageSize}`, {
89
+ auth: false,
90
+ }),
91
+
92
+ getBinder: (owner: string, slug: string) =>
93
+ request<BinderDetail>("GET", `/binders/${encodeURIComponent(owner)}/${encodeURIComponent(slug)}`, {
94
+ auth: false,
95
+ }),
96
+
97
+ listBinders: (owner?: string, page = 1, pageSize = 20) => {
98
+ const ownerQ = owner ? `&owner=${encodeURIComponent(owner)}` : "";
99
+ return request<SearchResult>("GET", `/binders?page=${page}&pageSize=${pageSize}${ownerQ}`, {
100
+ auth: false,
101
+ });
102
+ },
103
+
104
+ whoami: () => request<WhoamiResult>("GET", "/auth/me"),
105
+
106
+ publish: (data: {
107
+ slug: string;
108
+ displayName?: string;
109
+ description?: string;
110
+ version: string;
111
+ readme?: string;
112
+ tags?: string[];
113
+ tab?: string;
114
+ license?: string;
115
+ platforms?: string[];
116
+ dependencies?: Record<string, string>;
117
+ }) => request<{ id: string; slug: string; version: string }>("POST", "/binders", { body: data }),
118
+ };
@@ -0,0 +1,45 @@
1
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
2
+ import { join, dirname } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { existsSync } from "node:fs";
5
+
6
+ export interface YoyoConfig {
7
+ apiKey?: string;
8
+ apiUrl?: string;
9
+ }
10
+
11
+ export function configPath(): string {
12
+ return join(homedir(), ".yoyo", "config.json");
13
+ }
14
+
15
+ export function defaultApiUrl(): string {
16
+ return process.env.YOYO_API_URL ?? "https://api.yoyo.bot/v1";
17
+ }
18
+
19
+ export async function loadConfig(): Promise<YoyoConfig> {
20
+ const p = configPath();
21
+ if (!existsSync(p)) return {};
22
+ try {
23
+ const raw = await readFile(p, "utf8");
24
+ return JSON.parse(raw) as YoyoConfig;
25
+ } catch {
26
+ return {};
27
+ }
28
+ }
29
+
30
+ export async function saveConfig(cfg: YoyoConfig): Promise<void> {
31
+ const p = configPath();
32
+ await mkdir(dirname(p), { recursive: true });
33
+ await writeFile(p, JSON.stringify(cfg, null, 2) + "\n", "utf8");
34
+ }
35
+
36
+ export async function getApiKey(): Promise<string | undefined> {
37
+ if (process.env.YOYO_API_KEY) return process.env.YOYO_API_KEY;
38
+ const cfg = await loadConfig();
39
+ return cfg.apiKey;
40
+ }
41
+
42
+ export async function getApiUrl(): Promise<string> {
43
+ const cfg = await loadConfig();
44
+ return process.env.YOYO_API_URL ?? cfg.apiUrl ?? "https://api.yoyo.bot/v1";
45
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist",
6
+ "composite": true
7
+ },
8
+ "include": ["src/**/*"],
9
+ "exclude": ["node_modules", "dist"]
10
+ }