thecatapi-cli 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -0
- package/bun.lock +32 -0
- package/dist/index.js +2840 -0
- package/dist/thecatapi-cli.js +2840 -0
- package/package.json +21 -0
- package/skills/thecatapi-cli/SKILL.md +80 -0
- package/src/commands/auth.ts +53 -0
- package/src/index.ts +41 -0
- package/src/lib/auth.ts +54 -0
- package/src/lib/client.ts +107 -0
- package/src/lib/config.ts +29 -0
- package/src/lib/errors.ts +101 -0
- package/src/lib/logger.ts +41 -0
- package/src/lib/output.ts +169 -0
- package/src/resources/breeds.ts +103 -0
- package/src/resources/example.ts +125 -0
- package/src/resources/facts.ts +35 -0
- package/src/resources/favourites.ts +83 -0
- package/src/resources/images.ts +153 -0
- package/src/resources/votes.ts +88 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { globalFlags } from "./config.js";
|
|
3
|
+
|
|
4
|
+
interface JsonEnvelope {
|
|
5
|
+
ok: true;
|
|
6
|
+
data: unknown;
|
|
7
|
+
meta?: { total?: number; page?: number };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function output(
|
|
11
|
+
data: unknown,
|
|
12
|
+
opts: { json?: boolean; format?: string; fields?: string[]; noHeader?: boolean } = {},
|
|
13
|
+
): void {
|
|
14
|
+
const isJson = opts.json ?? globalFlags.json;
|
|
15
|
+
const format = isJson ? "json" : (opts.format ?? globalFlags.format);
|
|
16
|
+
|
|
17
|
+
switch (format) {
|
|
18
|
+
case "json":
|
|
19
|
+
printJson(data);
|
|
20
|
+
break;
|
|
21
|
+
case "csv":
|
|
22
|
+
printCsv(data, opts.fields, opts.noHeader ?? globalFlags.noHeader);
|
|
23
|
+
break;
|
|
24
|
+
case "yaml":
|
|
25
|
+
printYaml(data, 0);
|
|
26
|
+
break;
|
|
27
|
+
default:
|
|
28
|
+
printText(data, opts.fields, opts.noHeader ?? globalFlags.noHeader);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function printJson(data: unknown): void {
|
|
33
|
+
const envelope: JsonEnvelope = { ok: true, data };
|
|
34
|
+
if (Array.isArray(data)) {
|
|
35
|
+
envelope.meta = { total: data.length };
|
|
36
|
+
}
|
|
37
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function printText(data: unknown, fields?: string[], noHeader?: boolean): void {
|
|
41
|
+
if (Array.isArray(data)) {
|
|
42
|
+
if (data.length === 0) {
|
|
43
|
+
console.log(pc.dim("(no results)"));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
printTable(data as Record<string, unknown>[], fields, noHeader);
|
|
47
|
+
console.log(pc.dim(`\n${data.length} result${data.length === 1 ? "" : "s"}`));
|
|
48
|
+
} else if (typeof data === "object" && data !== null) {
|
|
49
|
+
printKeyValue(data as Record<string, unknown>);
|
|
50
|
+
} else {
|
|
51
|
+
console.log(String(data));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function printKeyValue(obj: Record<string, unknown>): void {
|
|
56
|
+
const maxKey = Math.max(...Object.keys(obj).map((k) => k.length));
|
|
57
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
58
|
+
const label = pc.bold(k.padEnd(maxKey));
|
|
59
|
+
const val = formatValue(v);
|
|
60
|
+
console.log(` ${label} ${val}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function formatValue(v: unknown): string {
|
|
65
|
+
if (v === null || v === undefined) return pc.dim("-");
|
|
66
|
+
if (typeof v === "boolean") return v ? pc.green("true") : pc.red("false");
|
|
67
|
+
if (typeof v === "number") return pc.cyan(String(v));
|
|
68
|
+
if (typeof v === "object") return pc.dim(JSON.stringify(v));
|
|
69
|
+
const s = String(v);
|
|
70
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(s)) return pc.dim(s);
|
|
71
|
+
if (/^https?:\/\//.test(s)) return pc.underline(pc.cyan(s));
|
|
72
|
+
return s;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatCell(v: unknown): string {
|
|
76
|
+
if (v === null || v === undefined) return pc.dim("-");
|
|
77
|
+
if (typeof v === "boolean") return v ? pc.green("yes") : pc.red("no");
|
|
78
|
+
if (typeof v === "number") return pc.cyan(String(v));
|
|
79
|
+
if (typeof v === "object") return pc.dim(JSON.stringify(v));
|
|
80
|
+
const s = String(v);
|
|
81
|
+
if (/^\d{4}-\d{2}-\d{2}T/.test(s)) {
|
|
82
|
+
return pc.dim(s.replace("T", " ").replace(/\.\d+Z$/, "Z"));
|
|
83
|
+
}
|
|
84
|
+
return s;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function printTable(
|
|
88
|
+
rows: Record<string, unknown>[],
|
|
89
|
+
fields?: string[],
|
|
90
|
+
noHeader?: boolean,
|
|
91
|
+
): void {
|
|
92
|
+
const cols = fields ?? Object.keys(rows[0] ?? {});
|
|
93
|
+
const widths = cols.map((col) => {
|
|
94
|
+
const values = rows.map((r) => stripAnsi(formatCell(r[col])).length);
|
|
95
|
+
return Math.min(Math.max(col.length, ...values), 40);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (!noHeader) {
|
|
99
|
+
const header = cols.map((c, i) => pc.bold(pc.white(c.padEnd(widths[i] ?? 10)))).join(" ");
|
|
100
|
+
console.log(header);
|
|
101
|
+
console.log(pc.dim(widths.map((w) => "\u2500".repeat(w)).join(" ")));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const row of rows) {
|
|
105
|
+
const line = cols.map((c, i) => {
|
|
106
|
+
const formatted = formatCell(row[c]);
|
|
107
|
+
const raw = stripAnsi(formatted);
|
|
108
|
+
const w = widths[i] ?? 10;
|
|
109
|
+
if (raw.length > w) {
|
|
110
|
+
return formatted.slice(0, formatted.length - (raw.length - w) - 1) + pc.dim("\u2026");
|
|
111
|
+
}
|
|
112
|
+
const padding = w - raw.length;
|
|
113
|
+
return formatted + " ".repeat(padding > 0 ? padding : 0);
|
|
114
|
+
});
|
|
115
|
+
console.log(line.join(" "));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function stripAnsi(s: string): string {
|
|
120
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function printCsv(data: unknown, fields?: string[], noHeader?: boolean): void {
|
|
124
|
+
if (!Array.isArray(data)) {
|
|
125
|
+
console.log(JSON.stringify(data));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (data.length === 0) return;
|
|
129
|
+
|
|
130
|
+
const cols = fields ?? Object.keys((data[0] as Record<string, unknown>) ?? {});
|
|
131
|
+
|
|
132
|
+
if (!noHeader) {
|
|
133
|
+
console.log(cols.join(","));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const row of data as Record<string, unknown>[]) {
|
|
137
|
+
console.log(cols.map((c) => csvEscape(String(row[c] ?? ""))).join(","));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function csvEscape(val: string): string {
|
|
142
|
+
if (val.includes(",") || val.includes('"') || val.includes("\n")) {
|
|
143
|
+
return `"${val.replace(/"/g, '""')}"`;
|
|
144
|
+
}
|
|
145
|
+
return val;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function printYaml(data: unknown, indent: number): void {
|
|
149
|
+
const pad = " ".repeat(indent);
|
|
150
|
+
if (Array.isArray(data)) {
|
|
151
|
+
for (const item of data) {
|
|
152
|
+
if (typeof item === "object" && item !== null) {
|
|
153
|
+
console.log(`${pad}-`);
|
|
154
|
+
printYaml(item, indent + 1);
|
|
155
|
+
} else {
|
|
156
|
+
console.log(`${pad}- ${String(item)}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} else if (typeof data === "object" && data !== null) {
|
|
160
|
+
for (const [k, v] of Object.entries(data)) {
|
|
161
|
+
if (typeof v === "object" && v !== null) {
|
|
162
|
+
console.log(`${pad}${k}:`);
|
|
163
|
+
printYaml(v, indent + 1);
|
|
164
|
+
} else {
|
|
165
|
+
console.log(`${pad}${k}: ${String(v)}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { client } from "../lib/client.js";
|
|
3
|
+
import { output } from "../lib/output.js";
|
|
4
|
+
import { handleError } from "../lib/errors.js";
|
|
5
|
+
|
|
6
|
+
interface ActionOpts {
|
|
7
|
+
json?: boolean;
|
|
8
|
+
format?: string;
|
|
9
|
+
fields?: string;
|
|
10
|
+
limit?: string;
|
|
11
|
+
page?: string;
|
|
12
|
+
order?: string;
|
|
13
|
+
attachImage?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const breedsResource = new Command("breeds")
|
|
17
|
+
.description("Browse and search cat breeds");
|
|
18
|
+
|
|
19
|
+
// ── LIST ───────────────────────────────────────────
|
|
20
|
+
breedsResource
|
|
21
|
+
.command("list")
|
|
22
|
+
.description("List all cat breeds")
|
|
23
|
+
.option("--limit <n>", "Results per page")
|
|
24
|
+
.option("--page <n>", "Page number")
|
|
25
|
+
.option("--fields <cols>", "Comma-separated columns to display")
|
|
26
|
+
.option("--json", "Output as JSON")
|
|
27
|
+
.option("--format <fmt>", "Output format: text, json, csv, yaml")
|
|
28
|
+
.addHelpText("after", "\nExamples:\n thecatapi-cli breeds list\n thecatapi-cli breeds list --limit 5 --json")
|
|
29
|
+
.action(async (opts: ActionOpts) => {
|
|
30
|
+
try {
|
|
31
|
+
const params: Record<string, string> = {};
|
|
32
|
+
if (opts.limit) params.limit = opts.limit;
|
|
33
|
+
if (opts.page) params.page = opts.page;
|
|
34
|
+
const data = await client.get("/breeds", params);
|
|
35
|
+
const fields = opts.fields?.split(",");
|
|
36
|
+
output(data, { json: opts.json, format: opts.format, fields });
|
|
37
|
+
} catch (err) {
|
|
38
|
+
handleError(err, opts.json);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ── GET ────────────────────────────────────────────
|
|
43
|
+
breedsResource
|
|
44
|
+
.command("get")
|
|
45
|
+
.description("Get a specific breed by ID")
|
|
46
|
+
.argument("<id>", "Breed ID (e.g. abys, beng)")
|
|
47
|
+
.option("--json", "Output as JSON")
|
|
48
|
+
.option("--format <fmt>", "Output format: text, json, csv, yaml")
|
|
49
|
+
.addHelpText("after", "\nExample:\n thecatapi-cli breeds get abys")
|
|
50
|
+
.action(async (id: string, opts: ActionOpts) => {
|
|
51
|
+
try {
|
|
52
|
+
const data = await client.get(`/breeds/${id}`);
|
|
53
|
+
output(data, { json: opts.json, format: opts.format });
|
|
54
|
+
} catch (err) {
|
|
55
|
+
handleError(err, opts.json);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ── SEARCH ─────────────────────────────────────────
|
|
60
|
+
breedsResource
|
|
61
|
+
.command("search")
|
|
62
|
+
.description("Search breeds by name")
|
|
63
|
+
.argument("<query>", "Search term")
|
|
64
|
+
.option("--attach-image", "Include reference image in results")
|
|
65
|
+
.option("--fields <cols>", "Comma-separated columns to display")
|
|
66
|
+
.option("--json", "Output as JSON")
|
|
67
|
+
.option("--format <fmt>", "Output format: text, json, csv, yaml")
|
|
68
|
+
.addHelpText("after", "\nExample:\n thecatapi-cli breeds search bengal")
|
|
69
|
+
.action(async (query: string, opts: ActionOpts) => {
|
|
70
|
+
try {
|
|
71
|
+
const params: Record<string, string> = { q: query };
|
|
72
|
+
if (opts.attachImage) params.attach_image = "1";
|
|
73
|
+
const data = await client.get("/breeds/search", params);
|
|
74
|
+
const fields = opts.fields?.split(",");
|
|
75
|
+
output(data, { json: opts.json, format: opts.format, fields });
|
|
76
|
+
} catch (err) {
|
|
77
|
+
handleError(err, opts.json);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ── FACTS ──────────────────────────────────────────
|
|
82
|
+
breedsResource
|
|
83
|
+
.command("facts")
|
|
84
|
+
.description("Get facts about a specific breed")
|
|
85
|
+
.argument("<id>", "Breed ID")
|
|
86
|
+
.option("--limit <n>", "Number of facts")
|
|
87
|
+
.option("--page <n>", "Page number")
|
|
88
|
+
.option("--order <order>", "Order: ASC, DESC, RAND")
|
|
89
|
+
.option("--json", "Output as JSON")
|
|
90
|
+
.option("--format <fmt>", "Output format: text, json, csv, yaml")
|
|
91
|
+
.addHelpText("after", "\nExample:\n thecatapi-cli breeds facts abys")
|
|
92
|
+
.action(async (id: string, opts: ActionOpts) => {
|
|
93
|
+
try {
|
|
94
|
+
const params: Record<string, string> = {};
|
|
95
|
+
if (opts.limit) params.limit = opts.limit;
|
|
96
|
+
if (opts.page) params.page = opts.page;
|
|
97
|
+
if (opts.order) params.order = opts.order;
|
|
98
|
+
const data = await client.get(`/breeds/${id}/facts`, params);
|
|
99
|
+
output(data, { json: opts.json, format: opts.format });
|
|
100
|
+
} catch (err) {
|
|
101
|
+
handleError(err, opts.json);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example resource file.
|
|
3
|
+
* Copy this and adapt for each API resource.
|
|
4
|
+
* Pattern: one file per resource (drafts.ts, links.ts, accounts.ts, etc.)
|
|
5
|
+
*/
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { client } from "../lib/client.js";
|
|
8
|
+
import { output } from "../lib/output.js";
|
|
9
|
+
import { handleError } from "../lib/errors.js";
|
|
10
|
+
|
|
11
|
+
interface ActionOpts {
|
|
12
|
+
json?: boolean;
|
|
13
|
+
format?: string;
|
|
14
|
+
fields?: string;
|
|
15
|
+
limit?: string;
|
|
16
|
+
page?: string;
|
|
17
|
+
sort?: string;
|
|
18
|
+
filter?: string;
|
|
19
|
+
name?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const exampleResource = new Command("examples")
|
|
24
|
+
.description("Manage examples (replace with your resource)");
|
|
25
|
+
|
|
26
|
+
// ── LIST ──────────────────────────────────────────────
|
|
27
|
+
exampleResource
|
|
28
|
+
.command("list")
|
|
29
|
+
.description("List all examples")
|
|
30
|
+
.option("--limit <n>", "Max results to return", "20")
|
|
31
|
+
.option("--page <n>", "Page number", "1")
|
|
32
|
+
.option("--sort <field>", "Sort by field (e.g. created_at:desc)")
|
|
33
|
+
.option("--filter <expr>", "Filter expression (e.g. status=active)")
|
|
34
|
+
.option("--fields <cols>", "Comma-separated columns to display")
|
|
35
|
+
.option("--json", "Output as JSON")
|
|
36
|
+
.option("--format <fmt>", "Output format: text, json, csv, yaml")
|
|
37
|
+
.addHelpText("after", "\nExamples:\n thecatapi-cli examples list\n thecatapi-cli examples list --limit 5 --json")
|
|
38
|
+
.action(async (opts: ActionOpts) => {
|
|
39
|
+
try {
|
|
40
|
+
const data = await client.get("/examples", {
|
|
41
|
+
limit: opts.limit ?? "20",
|
|
42
|
+
page: opts.page ?? "1",
|
|
43
|
+
...(opts.sort && { sort: opts.sort }),
|
|
44
|
+
...(opts.filter && { filter: opts.filter }),
|
|
45
|
+
});
|
|
46
|
+
const fields = opts.fields?.split(",");
|
|
47
|
+
output(data, { json: opts.json, format: opts.format, fields });
|
|
48
|
+
} catch (err) {
|
|
49
|
+
handleError(err, opts.json);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ── GET ───────────────────────────────────────────────
|
|
54
|
+
exampleResource
|
|
55
|
+
.command("get")
|
|
56
|
+
.description("Get a specific example by ID")
|
|
57
|
+
.argument("<id>", "Example ID")
|
|
58
|
+
.option("--json", "Output as JSON")
|
|
59
|
+
.option("--format <fmt>", "Output format: text, json, csv, yaml")
|
|
60
|
+
.addHelpText("after", "\nExample:\n thecatapi-cli examples get abc123")
|
|
61
|
+
.action(async (id: string, opts: ActionOpts) => {
|
|
62
|
+
try {
|
|
63
|
+
const data = await client.get(`/examples/${id}`);
|
|
64
|
+
output(data, { json: opts.json, format: opts.format });
|
|
65
|
+
} catch (err) {
|
|
66
|
+
handleError(err, opts.json);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ── CREATE ────────────────────────────────────────────
|
|
71
|
+
exampleResource
|
|
72
|
+
.command("create")
|
|
73
|
+
.description("Create a new example")
|
|
74
|
+
.requiredOption("--name <name>", "Name for the example")
|
|
75
|
+
.option("--description <desc>", "Optional description")
|
|
76
|
+
.option("--json", "Output as JSON")
|
|
77
|
+
.addHelpText("after", '\nExample:\n thecatapi-cli examples create --name "My Example"')
|
|
78
|
+
.action(async (opts: ActionOpts) => {
|
|
79
|
+
try {
|
|
80
|
+
const data = await client.post("/examples", {
|
|
81
|
+
name: opts.name,
|
|
82
|
+
...(opts.description && { description: opts.description }),
|
|
83
|
+
});
|
|
84
|
+
output(data, { json: opts.json });
|
|
85
|
+
} catch (err) {
|
|
86
|
+
handleError(err, opts.json);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ── UPDATE ────────────────────────────────────────────
|
|
91
|
+
exampleResource
|
|
92
|
+
.command("update")
|
|
93
|
+
.description("Update an existing example")
|
|
94
|
+
.argument("<id>", "Example ID")
|
|
95
|
+
.option("--name <name>", "New name")
|
|
96
|
+
.option("--description <desc>", "New description")
|
|
97
|
+
.option("--json", "Output as JSON")
|
|
98
|
+
.addHelpText("after", '\nExample:\n thecatapi-cli examples update abc123 --name "Updated"')
|
|
99
|
+
.action(async (id: string, opts: ActionOpts) => {
|
|
100
|
+
try {
|
|
101
|
+
const body: Record<string, unknown> = {};
|
|
102
|
+
if (opts.name) body.name = opts.name;
|
|
103
|
+
if (opts.description) body.description = opts.description;
|
|
104
|
+
const data = await client.patch(`/examples/${id}`, body);
|
|
105
|
+
output(data, { json: opts.json });
|
|
106
|
+
} catch (err) {
|
|
107
|
+
handleError(err, opts.json);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ── DELETE ────────────────────────────────────────────
|
|
112
|
+
exampleResource
|
|
113
|
+
.command("delete")
|
|
114
|
+
.description("Delete an example")
|
|
115
|
+
.argument("<id>", "Example ID")
|
|
116
|
+
.option("--json", "Output as JSON")
|
|
117
|
+
.addHelpText("after", "\nExample:\n thecatapi-cli examples delete abc123")
|
|
118
|
+
.action(async (id: string, opts: ActionOpts) => {
|
|
119
|
+
try {
|
|
120
|
+
await client.delete(`/examples/${id}`);
|
|
121
|
+
output({ deleted: true, id }, { json: opts.json });
|
|
122
|
+
} catch (err) {
|
|
123
|
+
handleError(err, opts.json);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { client } from "../lib/client.js";
|
|
3
|
+
import { output } from "../lib/output.js";
|
|
4
|
+
import { handleError } from "../lib/errors.js";
|
|
5
|
+
|
|
6
|
+
interface ActionOpts {
|
|
7
|
+
json?: boolean;
|
|
8
|
+
format?: string;
|
|
9
|
+
fields?: string;
|
|
10
|
+
limit?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const factsResource = new Command("facts")
|
|
14
|
+
.description("Get random cat facts");
|
|
15
|
+
|
|
16
|
+
// ── LIST ───────────────────────────────────────────
|
|
17
|
+
factsResource
|
|
18
|
+
.command("list")
|
|
19
|
+
.description("Get random cat facts")
|
|
20
|
+
.option("--limit <n>", "Number of facts to return", "5")
|
|
21
|
+
.option("--fields <cols>", "Comma-separated columns to display")
|
|
22
|
+
.option("--json", "Output as JSON")
|
|
23
|
+
.option("--format <fmt>", "Output format: text, json, csv, yaml")
|
|
24
|
+
.addHelpText("after", "\nExamples:\n thecatapi-cli facts list\n thecatapi-cli facts list --limit 10 --json")
|
|
25
|
+
.action(async (opts: ActionOpts) => {
|
|
26
|
+
try {
|
|
27
|
+
const data = await client.get("/facts", {
|
|
28
|
+
limit: opts.limit ?? "5",
|
|
29
|
+
});
|
|
30
|
+
const fields = opts.fields?.split(",");
|
|
31
|
+
output(data, { json: opts.json, format: opts.format, fields });
|
|
32
|
+
} catch (err) {
|
|
33
|
+
handleError(err, opts.json);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { client } from "../lib/client.js";
|
|
3
|
+
import { output } from "../lib/output.js";
|
|
4
|
+
import { handleError } from "../lib/errors.js";
|
|
5
|
+
|
|
6
|
+
interface ActionOpts {
|
|
7
|
+
json?: boolean;
|
|
8
|
+
format?: string;
|
|
9
|
+
fields?: string;
|
|
10
|
+
imageId?: string;
|
|
11
|
+
subId?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const favouritesResource = new Command("favourites")
|
|
15
|
+
.description("Manage your favourite cat images");
|
|
16
|
+
|
|
17
|
+
// ── LIST ───────────────────────────────────────────
|
|
18
|
+
favouritesResource
|
|
19
|
+
.command("list")
|
|
20
|
+
.description("List your favourited images")
|
|
21
|
+
.option("--fields <cols>", "Comma-separated columns to display")
|
|
22
|
+
.option("--json", "Output as JSON")
|
|
23
|
+
.option("--format <fmt>", "Output format: text, json, csv, yaml")
|
|
24
|
+
.addHelpText("after", "\nExample:\n thecatapi-cli favourites list")
|
|
25
|
+
.action(async (opts: ActionOpts) => {
|
|
26
|
+
try {
|
|
27
|
+
const data = await client.get("/favourites");
|
|
28
|
+
const fields = opts.fields?.split(",");
|
|
29
|
+
output(data, { json: opts.json, format: opts.format, fields });
|
|
30
|
+
} catch (err) {
|
|
31
|
+
handleError(err, opts.json);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// ── GET ────────────────────────────────────────────
|
|
36
|
+
favouritesResource
|
|
37
|
+
.command("get")
|
|
38
|
+
.description("Get a specific favourite")
|
|
39
|
+
.argument("<id>", "Favourite ID")
|
|
40
|
+
.option("--json", "Output as JSON")
|
|
41
|
+
.option("--format <fmt>", "Output format: text, json, csv, yaml")
|
|
42
|
+
.action(async (id: string, opts: ActionOpts) => {
|
|
43
|
+
try {
|
|
44
|
+
const data = await client.get(`/favourites/${id}`);
|
|
45
|
+
output(data, { json: opts.json, format: opts.format });
|
|
46
|
+
} catch (err) {
|
|
47
|
+
handleError(err, opts.json);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ── CREATE ─────────────────────────────────────────
|
|
52
|
+
favouritesResource
|
|
53
|
+
.command("create")
|
|
54
|
+
.description("Add an image to your favourites")
|
|
55
|
+
.requiredOption("--image-id <id>", "Image ID to favourite")
|
|
56
|
+
.option("--sub-id <id>", "User segment identifier")
|
|
57
|
+
.option("--json", "Output as JSON")
|
|
58
|
+
.addHelpText("after", "\nExample:\n thecatapi-cli favourites create --image-id abc123")
|
|
59
|
+
.action(async (opts: ActionOpts) => {
|
|
60
|
+
try {
|
|
61
|
+
const body: Record<string, unknown> = { image_id: opts.imageId };
|
|
62
|
+
if (opts.subId) body.sub_id = opts.subId;
|
|
63
|
+
const data = await client.post("/favourites", body);
|
|
64
|
+
output(data, { json: opts.json });
|
|
65
|
+
} catch (err) {
|
|
66
|
+
handleError(err, opts.json);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ── DELETE ─────────────────────────────────────────
|
|
71
|
+
favouritesResource
|
|
72
|
+
.command("delete")
|
|
73
|
+
.description("Remove an image from your favourites")
|
|
74
|
+
.argument("<id>", "Favourite ID")
|
|
75
|
+
.option("--json", "Output as JSON")
|
|
76
|
+
.action(async (id: string, opts: ActionOpts) => {
|
|
77
|
+
try {
|
|
78
|
+
await client.delete(`/favourites/${id}`);
|
|
79
|
+
output({ deleted: true, id }, { json: opts.json });
|
|
80
|
+
} catch (err) {
|
|
81
|
+
handleError(err, opts.json);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { client } from "../lib/client.js";
|
|
3
|
+
import { output } from "../lib/output.js";
|
|
4
|
+
import { handleError } from "../lib/errors.js";
|
|
5
|
+
import { buildAuthHeaders } from "../lib/auth.js";
|
|
6
|
+
import { BASE_URL } from "../lib/config.js";
|
|
7
|
+
import { CliError } from "../lib/errors.js";
|
|
8
|
+
|
|
9
|
+
interface ActionOpts {
|
|
10
|
+
json?: boolean;
|
|
11
|
+
format?: string;
|
|
12
|
+
fields?: string;
|
|
13
|
+
limit?: string;
|
|
14
|
+
page?: string;
|
|
15
|
+
size?: string;
|
|
16
|
+
mimeTypes?: string;
|
|
17
|
+
hasBreeds?: boolean;
|
|
18
|
+
order?: string;
|
|
19
|
+
subId?: string;
|
|
20
|
+
breedIds?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const imagesResource = new Command("images")
|
|
24
|
+
.description("Search, list, upload, and manage cat images");
|
|
25
|
+
|
|
26
|
+
// ── SEARCH ─────────────────────────────────────────
|
|
27
|
+
imagesResource
|
|
28
|
+
.command("search")
|
|
29
|
+
.description("Search for random or filtered cat images")
|
|
30
|
+
.option("--limit <n>", "Results per page (max 25)", "10")
|
|
31
|
+
.option("--page <n>", "Page number", "0")
|
|
32
|
+
.option("--size <size>", "Image size: thumb, small, med, full")
|
|
33
|
+
.option("--mime-types <types>", "Filter by mime type: jpg, png, gif")
|
|
34
|
+
.option("--has-breeds", "Only images with breed data")
|
|
35
|
+
.option("--order <order>", "Order: RANDOM, ASC, DESC", "RANDOM")
|
|
36
|
+
.option("--fields <cols>", "Comma-separated columns to display")
|
|
37
|
+
.option("--json", "Output as JSON")
|
|
38
|
+
.option("--format <fmt>", "Output format: text, json, csv, yaml")
|
|
39
|
+
.addHelpText("after", "\nExamples:\n thecatapi-cli images search\n thecatapi-cli images search --limit 5 --has-breeds --json")
|
|
40
|
+
.action(async (opts: ActionOpts) => {
|
|
41
|
+
try {
|
|
42
|
+
const params: Record<string, string> = {
|
|
43
|
+
limit: opts.limit ?? "10",
|
|
44
|
+
page: opts.page ?? "0",
|
|
45
|
+
order: opts.order ?? "RANDOM",
|
|
46
|
+
};
|
|
47
|
+
if (opts.size) params.size = opts.size;
|
|
48
|
+
if (opts.mimeTypes) params.mime_types = opts.mimeTypes;
|
|
49
|
+
if (opts.hasBreeds) params.has_breeds = "1";
|
|
50
|
+
const data = await client.get("/images/search", params);
|
|
51
|
+
const fields = opts.fields?.split(",");
|
|
52
|
+
output(data, { json: opts.json, format: opts.format, fields });
|
|
53
|
+
} catch (err) {
|
|
54
|
+
handleError(err, opts.json);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ── LIST (user's uploaded images) ──────────────────
|
|
59
|
+
imagesResource
|
|
60
|
+
.command("list")
|
|
61
|
+
.description("List your uploaded images")
|
|
62
|
+
.option("--limit <n>", "Results per page (1-10)", "10")
|
|
63
|
+
.option("--page <n>", "Page number", "0")
|
|
64
|
+
.option("--order <order>", "Order: ASC, DESC", "DESC")
|
|
65
|
+
.option("--fields <cols>", "Comma-separated columns to display")
|
|
66
|
+
.option("--json", "Output as JSON")
|
|
67
|
+
.option("--format <fmt>", "Output format: text, json, csv, yaml")
|
|
68
|
+
.action(async (opts: ActionOpts) => {
|
|
69
|
+
try {
|
|
70
|
+
const data = await client.get("/images", {
|
|
71
|
+
limit: opts.limit ?? "10",
|
|
72
|
+
page: opts.page ?? "0",
|
|
73
|
+
order: opts.order ?? "DESC",
|
|
74
|
+
});
|
|
75
|
+
const fields = opts.fields?.split(",");
|
|
76
|
+
output(data, { json: opts.json, format: opts.format, fields });
|
|
77
|
+
} catch (err) {
|
|
78
|
+
handleError(err, opts.json);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ── GET ────────────────────────────────────────────
|
|
83
|
+
imagesResource
|
|
84
|
+
.command("get")
|
|
85
|
+
.description("Get a specific image by ID")
|
|
86
|
+
.argument("<id>", "Image ID")
|
|
87
|
+
.option("--json", "Output as JSON")
|
|
88
|
+
.option("--format <fmt>", "Output format: text, json, csv, yaml")
|
|
89
|
+
.action(async (id: string, opts: ActionOpts) => {
|
|
90
|
+
try {
|
|
91
|
+
const data = await client.get(`/images/${id}`);
|
|
92
|
+
output(data, { json: opts.json, format: opts.format });
|
|
93
|
+
} catch (err) {
|
|
94
|
+
handleError(err, opts.json);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ── UPLOAD ─────────────────────────────────────────
|
|
99
|
+
imagesResource
|
|
100
|
+
.command("upload")
|
|
101
|
+
.description("Upload a cat image")
|
|
102
|
+
.argument("<file>", "Path to image file")
|
|
103
|
+
.option("--sub-id <id>", "User segment identifier")
|
|
104
|
+
.option("--breed-ids <ids>", "Comma-separated breed IDs")
|
|
105
|
+
.option("--json", "Output as JSON")
|
|
106
|
+
.action(async (file: string, opts: ActionOpts) => {
|
|
107
|
+
try {
|
|
108
|
+
const fs = await import("fs");
|
|
109
|
+
const path = await import("path");
|
|
110
|
+
const resolved = path.resolve(file);
|
|
111
|
+
if (!fs.existsSync(resolved)) {
|
|
112
|
+
throw new CliError(2, `File not found: ${resolved}`);
|
|
113
|
+
}
|
|
114
|
+
const fileData = fs.readFileSync(resolved);
|
|
115
|
+
const fileName = path.basename(resolved);
|
|
116
|
+
|
|
117
|
+
const formData = new FormData();
|
|
118
|
+
formData.append("file", new Blob([fileData]), fileName);
|
|
119
|
+
if (opts.subId) formData.append("sub_id", opts.subId);
|
|
120
|
+
if (opts.breedIds) formData.append("breed_ids", opts.breedIds);
|
|
121
|
+
|
|
122
|
+
const headers = buildAuthHeaders();
|
|
123
|
+
const res = await fetch(`${BASE_URL}/images/upload`, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: { ...headers, Accept: "application/json" },
|
|
126
|
+
body: formData,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const data = await res.json().catch(() => null);
|
|
130
|
+
if (!res.ok) {
|
|
131
|
+
const msg = (data as Record<string, unknown>)?.message ?? res.statusText;
|
|
132
|
+
throw new CliError(res.status, `${res.status}: ${String(msg)}`);
|
|
133
|
+
}
|
|
134
|
+
output(data, { json: opts.json });
|
|
135
|
+
} catch (err) {
|
|
136
|
+
handleError(err, opts.json);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ── DELETE ─────────────────────────────────────────
|
|
141
|
+
imagesResource
|
|
142
|
+
.command("delete")
|
|
143
|
+
.description("Delete an uploaded image")
|
|
144
|
+
.argument("<id>", "Image ID")
|
|
145
|
+
.option("--json", "Output as JSON")
|
|
146
|
+
.action(async (id: string, opts: ActionOpts) => {
|
|
147
|
+
try {
|
|
148
|
+
await client.delete(`/images/${id}`);
|
|
149
|
+
output({ deleted: true, id }, { json: opts.json });
|
|
150
|
+
} catch (err) {
|
|
151
|
+
handleError(err, opts.json);
|
|
152
|
+
}
|
|
153
|
+
});
|