memes-business 0.0.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 +134 -0
- package/dist/commands/auth.d.ts +4 -0
- package/dist/commands/auth.js +92 -0
- package/dist/commands/favorites.d.ts +3 -0
- package/dist/commands/favorites.js +102 -0
- package/dist/commands/generate.d.ts +2 -0
- package/dist/commands/generate.js +312 -0
- package/dist/commands/memes.d.ts +2 -0
- package/dist/commands/memes.js +236 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +68 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.js +89 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +30 -0
- package/dist/lib/api.d.ts +45 -0
- package/dist/lib/api.js +127 -0
- package/dist/lib/config.d.ts +36 -0
- package/dist/lib/config.js +92 -0
- package/package.json +50 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { writeFile, stat } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { confirm } from "@inquirer/prompts";
|
|
7
|
+
import { apiRequest } from "../lib/api.js";
|
|
8
|
+
import { getApiKey, getApiUrl } from "../lib/config.js";
|
|
9
|
+
/**
|
|
10
|
+
* Check if a path is a directory
|
|
11
|
+
*/
|
|
12
|
+
async function isDirectory(path) {
|
|
13
|
+
try {
|
|
14
|
+
const stats = await stat(path);
|
|
15
|
+
return stats.isDirectory();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Download an image from URL
|
|
23
|
+
*/
|
|
24
|
+
async function downloadImage(imageUrl, savePath, memeId) {
|
|
25
|
+
const baseUrl = await getApiUrl();
|
|
26
|
+
const apiKey = await getApiKey();
|
|
27
|
+
const fullUrl = imageUrl.startsWith("http")
|
|
28
|
+
? imageUrl
|
|
29
|
+
: `${baseUrl}${imageUrl}`;
|
|
30
|
+
const response = await fetch(fullUrl, {
|
|
31
|
+
headers: apiKey ? { "x-api-key": apiKey } : {},
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
throw new Error(`Failed to download: ${response.statusText}`);
|
|
35
|
+
}
|
|
36
|
+
// Resolve the save path (handle directory case)
|
|
37
|
+
let resolvedPath = savePath;
|
|
38
|
+
if (await isDirectory(savePath)) {
|
|
39
|
+
const filename = memeId ? `${memeId}.png` : `meme-${Date.now()}.png`;
|
|
40
|
+
resolvedPath = join(savePath, filename);
|
|
41
|
+
}
|
|
42
|
+
const buffer = await response.arrayBuffer();
|
|
43
|
+
await writeFile(resolvedPath, Buffer.from(buffer));
|
|
44
|
+
return resolvedPath;
|
|
45
|
+
}
|
|
46
|
+
export function createMemesCommand() {
|
|
47
|
+
const memes = new Command("library")
|
|
48
|
+
.description("Manage your saved memes");
|
|
49
|
+
// List subcommand
|
|
50
|
+
memes
|
|
51
|
+
.command("list")
|
|
52
|
+
.description("List your saved memes")
|
|
53
|
+
.option("-p, --page <number>", "page number", "1")
|
|
54
|
+
.option("-l, --limit <number>", "results per page", "10")
|
|
55
|
+
.option("--json", "output as JSON for scripting")
|
|
56
|
+
.action(async (options) => {
|
|
57
|
+
const page = parseInt(options.page, 10);
|
|
58
|
+
const limit = parseInt(options.limit, 10);
|
|
59
|
+
if (isNaN(page) || page < 1) {
|
|
60
|
+
console.error(chalk.red("Error: --page must be a positive number"));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
if (isNaN(limit) || limit < 1) {
|
|
64
|
+
console.error(chalk.red("Error: --limit must be a positive number"));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
const spinner = options.json ? null : ora("Fetching memes...").start();
|
|
68
|
+
const result = await apiRequest(`/api/memes?page=${page}&perPage=${limit}`);
|
|
69
|
+
if (result.error) {
|
|
70
|
+
if (spinner)
|
|
71
|
+
spinner.fail(chalk.red(result.error.message));
|
|
72
|
+
if (options.json)
|
|
73
|
+
console.log(JSON.stringify({ error: result.error }, null, 2));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
const response = result.data;
|
|
77
|
+
if (!response?.data) {
|
|
78
|
+
if (spinner)
|
|
79
|
+
spinner.fail(chalk.red("Invalid response from server"));
|
|
80
|
+
if (options.json)
|
|
81
|
+
console.log(JSON.stringify({ error: { message: "Invalid response" } }, null, 2));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
if (spinner)
|
|
85
|
+
spinner.stop();
|
|
86
|
+
if (options.json) {
|
|
87
|
+
console.log(JSON.stringify(response, null, 2));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const memesList = response.data;
|
|
91
|
+
const { pagination } = response.meta;
|
|
92
|
+
if (memesList.length === 0) {
|
|
93
|
+
console.log(chalk.yellow("No memes found."));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
console.log(chalk.bold(`Your Memes (Page ${pagination.currentPage}/${pagination.totalPages})`));
|
|
97
|
+
console.log(chalk.dim(`Showing ${memesList.length} of ${pagination.totalItems} total`));
|
|
98
|
+
console.log();
|
|
99
|
+
memesList.forEach((meme, index) => {
|
|
100
|
+
const num = chalk.dim(`${((pagination.currentPage - 1) * pagination.perPage + index + 1).toString().padStart(2)}.`);
|
|
101
|
+
const title = chalk.bold(meme.title);
|
|
102
|
+
const vis = meme.isPublic ? chalk.green("public") : chalk.dim("private");
|
|
103
|
+
const date = new Date(meme.createdAt).toLocaleDateString();
|
|
104
|
+
console.log(`${num} ${title} ${vis}`);
|
|
105
|
+
console.log(chalk.dim(` ID: ${meme.id}`));
|
|
106
|
+
if (meme.templateName)
|
|
107
|
+
console.log(chalk.dim(` Template: ${meme.templateName}`));
|
|
108
|
+
console.log(chalk.dim(` Created: ${date}`));
|
|
109
|
+
console.log();
|
|
110
|
+
});
|
|
111
|
+
if (pagination.hasNext) {
|
|
112
|
+
console.log(chalk.dim(`Next page: memes library list --page ${pagination.currentPage + 1}`));
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
// Show subcommand
|
|
116
|
+
memes
|
|
117
|
+
.command("show")
|
|
118
|
+
.description("Show details of a specific meme")
|
|
119
|
+
.argument("<id>", "meme ID")
|
|
120
|
+
.option("--json", "output as JSON for scripting")
|
|
121
|
+
.action(async (id, options) => {
|
|
122
|
+
const spinner = options.json ? null : ora("Fetching meme details...").start();
|
|
123
|
+
const result = await apiRequest(`/api/memes/${id}`);
|
|
124
|
+
if (result.error) {
|
|
125
|
+
if (spinner)
|
|
126
|
+
spinner.fail(chalk.red(result.error.message));
|
|
127
|
+
if (options.json)
|
|
128
|
+
console.log(JSON.stringify({ error: result.error }, null, 2));
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
const meme = result.data;
|
|
132
|
+
if (!meme) {
|
|
133
|
+
if (spinner)
|
|
134
|
+
spinner.fail(chalk.red("Meme not found"));
|
|
135
|
+
if (options.json)
|
|
136
|
+
console.log(JSON.stringify({ error: { message: "Meme not found" } }, null, 2));
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
if (spinner)
|
|
140
|
+
spinner.stop();
|
|
141
|
+
if (options.json) {
|
|
142
|
+
console.log(JSON.stringify(meme, null, 2));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
console.log(chalk.bold(meme.title));
|
|
146
|
+
console.log();
|
|
147
|
+
console.log(` ${chalk.dim("ID:")} ${meme.id}`);
|
|
148
|
+
if (meme.description)
|
|
149
|
+
console.log(` ${chalk.dim("Desc:")} ${meme.description}`);
|
|
150
|
+
console.log(` ${chalk.dim("Image:")} ${meme.imageUrl}`);
|
|
151
|
+
if (meme.templateName)
|
|
152
|
+
console.log(` ${chalk.dim("Template:")} ${meme.templateName}`);
|
|
153
|
+
console.log(` ${chalk.dim("Visibility:")} ${meme.isPublic ? chalk.green("public") : chalk.dim("private")}`);
|
|
154
|
+
if (meme.width && meme.height)
|
|
155
|
+
console.log(` ${chalk.dim("Size:")} ${meme.width}x${meme.height}`);
|
|
156
|
+
if (meme.fileSize)
|
|
157
|
+
console.log(` ${chalk.dim("File:")} ${(meme.fileSize / 1024).toFixed(1)} KB`);
|
|
158
|
+
console.log(` ${chalk.dim("Created:")} ${new Date(meme.createdAt).toLocaleString()}`);
|
|
159
|
+
console.log(` ${chalk.dim("Updated:")} ${new Date(meme.updatedAt).toLocaleString()}`);
|
|
160
|
+
});
|
|
161
|
+
// Download subcommand
|
|
162
|
+
memes
|
|
163
|
+
.command("download")
|
|
164
|
+
.description("Download a meme image to disk")
|
|
165
|
+
.argument("<id>", "meme ID")
|
|
166
|
+
.argument("[path]", "output file path", "./meme.png")
|
|
167
|
+
.action(async (id, savePath) => {
|
|
168
|
+
const spinner = ora("Fetching meme info...").start();
|
|
169
|
+
const result = await apiRequest(`/api/memes/${id}`);
|
|
170
|
+
if (result.error) {
|
|
171
|
+
spinner.fail(chalk.red(result.error.message));
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
const meme = result.data;
|
|
175
|
+
if (!meme) {
|
|
176
|
+
spinner.fail(chalk.red("Meme not found"));
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
spinner.text = "Downloading image...";
|
|
180
|
+
try {
|
|
181
|
+
const resolvedPath = await downloadImage(meme.imageUrl, savePath, meme.id);
|
|
182
|
+
spinner.succeed(chalk.green(`Saved to ${resolvedPath}`));
|
|
183
|
+
console.log(chalk.dim(` Title: ${meme.title}`));
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
187
|
+
spinner.fail(chalk.red(`Failed to download: ${msg}`));
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
// Delete subcommand
|
|
192
|
+
memes
|
|
193
|
+
.command("delete")
|
|
194
|
+
.description("Delete a meme")
|
|
195
|
+
.argument("<id>", "meme ID")
|
|
196
|
+
.option("-y, --yes", "skip confirmation prompt")
|
|
197
|
+
.action(async (id, options) => {
|
|
198
|
+
// First fetch the meme to show what we're deleting
|
|
199
|
+
const infoSpinner = ora("Fetching meme info...").start();
|
|
200
|
+
const infoResult = await apiRequest(`/api/memes/${id}`);
|
|
201
|
+
if (infoResult.error) {
|
|
202
|
+
infoSpinner.fail(chalk.red(infoResult.error.message));
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
const meme = infoResult.data;
|
|
206
|
+
if (!meme) {
|
|
207
|
+
infoSpinner.fail(chalk.red("Meme not found"));
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
infoSpinner.stop();
|
|
211
|
+
console.log(chalk.yellow(`About to delete: ${chalk.bold(meme.title)}`));
|
|
212
|
+
console.log(chalk.dim(` ID: ${meme.id}`));
|
|
213
|
+
console.log();
|
|
214
|
+
// Confirm deletion unless --yes flag provided
|
|
215
|
+
if (!options.yes) {
|
|
216
|
+
const confirmed = await confirm({
|
|
217
|
+
message: "Are you sure you want to delete this meme?",
|
|
218
|
+
default: false,
|
|
219
|
+
});
|
|
220
|
+
if (!confirmed) {
|
|
221
|
+
console.log(chalk.dim("Cancelled."));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const deleteSpinner = ora("Deleting meme...").start();
|
|
226
|
+
const deleteResult = await apiRequest(`/api/memes/${id}`, {
|
|
227
|
+
method: "DELETE",
|
|
228
|
+
});
|
|
229
|
+
if (deleteResult.error) {
|
|
230
|
+
deleteSpinner.fail(chalk.red(deleteResult.error.message));
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
deleteSpinner.succeed(chalk.green("Meme deleted successfully!"));
|
|
234
|
+
});
|
|
235
|
+
return memes;
|
|
236
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { apiRequest } from "../lib/api.js";
|
|
5
|
+
export function createSearchCommand() {
|
|
6
|
+
const search = new Command("search")
|
|
7
|
+
.description("Search for meme templates")
|
|
8
|
+
.argument("<query>", "search query")
|
|
9
|
+
.option("-l, --limit <number>", "maximum number of results", "10")
|
|
10
|
+
.option("--json", "output as JSON for scripting")
|
|
11
|
+
.action(async (query, options) => {
|
|
12
|
+
const limit = parseInt(options.limit, 10);
|
|
13
|
+
if (isNaN(limit) || limit < 1) {
|
|
14
|
+
console.error(chalk.red("Error: --limit must be a positive number"));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
const spinner = options.json ? null : ora("Searching templates...").start();
|
|
18
|
+
const result = await apiRequest(`/api/templates/search?q=${encodeURIComponent(query)}&limit=${limit}`);
|
|
19
|
+
if (result.error) {
|
|
20
|
+
if (spinner)
|
|
21
|
+
spinner.fail(chalk.red(result.error.message));
|
|
22
|
+
if (options.json) {
|
|
23
|
+
console.log(JSON.stringify({ error: result.error }, null, 2));
|
|
24
|
+
}
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
const response = result.data;
|
|
28
|
+
if (!response || !response.data) {
|
|
29
|
+
if (spinner)
|
|
30
|
+
spinner.fail(chalk.red("Invalid response from server"));
|
|
31
|
+
if (options.json) {
|
|
32
|
+
console.log(JSON.stringify({ error: { message: "Invalid response" } }, null, 2));
|
|
33
|
+
}
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
if (spinner)
|
|
37
|
+
spinner.stop();
|
|
38
|
+
// JSON output for scripting
|
|
39
|
+
if (options.json) {
|
|
40
|
+
console.log(JSON.stringify(response, null, 2));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Human-readable output
|
|
44
|
+
const templates = response.data;
|
|
45
|
+
if (templates.length === 0) {
|
|
46
|
+
console.log(chalk.yellow(`No templates found for "${query}"`));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
console.log(chalk.bold(`Found ${response.meta.totalResults} template${response.meta.totalResults !== 1 ? "s" : ""} for "${query}"`));
|
|
50
|
+
console.log();
|
|
51
|
+
templates.forEach((template, index) => {
|
|
52
|
+
const num = chalk.dim(`${(index + 1).toString().padStart(2)}.`);
|
|
53
|
+
const name = chalk.bold(template.name);
|
|
54
|
+
const score = chalk.cyan(`${(template.similarity * 100).toFixed(1)}%`);
|
|
55
|
+
const boxes = chalk.dim(`(${template.numTextBoxes} text box${template.numTextBoxes !== 1 ? "es" : ""})`);
|
|
56
|
+
console.log(`${num} ${name} ${boxes} ${score}`);
|
|
57
|
+
if (template.description) {
|
|
58
|
+
const desc = template.description.length > 80
|
|
59
|
+
? template.description.slice(0, 77) + "..."
|
|
60
|
+
: template.description;
|
|
61
|
+
console.log(chalk.dim(` ${desc}`));
|
|
62
|
+
}
|
|
63
|
+
console.log(chalk.dim(` ID: ${template.id}`));
|
|
64
|
+
console.log();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
return search;
|
|
68
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import open from "open";
|
|
5
|
+
import { apiRequest } from "../lib/api.js";
|
|
6
|
+
import { getApiKey, getApiUrl } from "../lib/config.js";
|
|
7
|
+
export function createStatusCommand() {
|
|
8
|
+
const status = new Command("status")
|
|
9
|
+
.description("Show subscription status and usage")
|
|
10
|
+
.option("--json", "Output as JSON")
|
|
11
|
+
.action(async (options) => {
|
|
12
|
+
const apiKey = await getApiKey();
|
|
13
|
+
if (!apiKey) {
|
|
14
|
+
if (options.json) {
|
|
15
|
+
console.log(JSON.stringify({ error: "Not logged in" }));
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
console.log(chalk.yellow("Not logged in."));
|
|
19
|
+
console.log(chalk.dim("Run 'memes login --api-key <key>' to authenticate."));
|
|
20
|
+
}
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const spinner = options.json ? null : ora("Fetching usage status...").start();
|
|
24
|
+
const result = await apiRequest("/api/generations/remaining");
|
|
25
|
+
if (result.error) {
|
|
26
|
+
if (spinner)
|
|
27
|
+
spinner.fail(chalk.red(result.error.message));
|
|
28
|
+
if (options.json) {
|
|
29
|
+
console.log(JSON.stringify({ error: result.error }));
|
|
30
|
+
}
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
if (spinner)
|
|
34
|
+
spinner.stop();
|
|
35
|
+
const data = result.data?.data;
|
|
36
|
+
if (!data) {
|
|
37
|
+
if (spinner)
|
|
38
|
+
spinner.fail(chalk.red("Invalid response from server"));
|
|
39
|
+
if (options.json) {
|
|
40
|
+
console.log(JSON.stringify({ error: { message: "Invalid response" } }));
|
|
41
|
+
}
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
if (options.json) {
|
|
45
|
+
console.log(JSON.stringify(data, null, 2));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const remaining = data.generationLimit - data.currentGenerations;
|
|
49
|
+
const planType = data.generationLimit > 10 ? "Pro" : "Free";
|
|
50
|
+
console.log(chalk.bold("Subscription Status"));
|
|
51
|
+
console.log();
|
|
52
|
+
console.log(` ${chalk.dim("Plan:")} ${planType === "Pro" ? chalk.green(planType) : chalk.yellow(planType)}`);
|
|
53
|
+
console.log(` ${chalk.dim("Generations:")} ${data.currentGenerations} / ${data.generationLimit} used`);
|
|
54
|
+
console.log(` ${chalk.dim("Remaining:")} ${remaining > 0 ? chalk.green(remaining) : chalk.red(remaining)}`);
|
|
55
|
+
console.log();
|
|
56
|
+
if (remaining <= 0) {
|
|
57
|
+
console.log(chalk.yellow("You've used all your generations for this period."));
|
|
58
|
+
console.log(chalk.dim("Run 'memes upgrade' to get more generations."));
|
|
59
|
+
}
|
|
60
|
+
else if (remaining <= 5) {
|
|
61
|
+
console.log(chalk.yellow(`Only ${remaining} generation(s) remaining.`));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
return status;
|
|
65
|
+
}
|
|
66
|
+
export function createUpgradeCommand() {
|
|
67
|
+
const upgrade = new Command("upgrade")
|
|
68
|
+
.description("Open billing page to upgrade or manage subscription")
|
|
69
|
+
.action(async () => {
|
|
70
|
+
const apiKey = await getApiKey();
|
|
71
|
+
if (!apiKey) {
|
|
72
|
+
console.log(chalk.yellow("Not logged in."));
|
|
73
|
+
console.log(chalk.dim("Run 'memes login --api-key <key>' to authenticate."));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
const baseUrl = await getApiUrl();
|
|
77
|
+
const accountUrl = `${baseUrl}/account`;
|
|
78
|
+
console.log(chalk.dim(`Opening ${accountUrl}...`));
|
|
79
|
+
try {
|
|
80
|
+
await open(accountUrl);
|
|
81
|
+
console.log(chalk.green("Opened billing page in your browser."));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
console.log(chalk.yellow("Could not open browser automatically."));
|
|
85
|
+
console.log(chalk.dim(`Visit: ${accountUrl}`));
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return upgrade;
|
|
89
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { createLoginCommand, createLogoutCommand, createWhoamiCommand, } from "./commands/auth.js";
|
|
4
|
+
import { createFavoriteCommand, createFavoritesCommand } from "./commands/favorites.js";
|
|
5
|
+
import { createGenerateCommand } from "./commands/generate.js";
|
|
6
|
+
import { createSearchCommand } from "./commands/search.js";
|
|
7
|
+
import { createMemesCommand } from "./commands/memes.js";
|
|
8
|
+
import { createStatusCommand, createUpgradeCommand } from "./commands/status.js";
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program
|
|
11
|
+
.name("memes")
|
|
12
|
+
.description("CLI for memes.business")
|
|
13
|
+
.version("0.0.1");
|
|
14
|
+
// Auth commands
|
|
15
|
+
program.addCommand(createLoginCommand());
|
|
16
|
+
program.addCommand(createLogoutCommand());
|
|
17
|
+
program.addCommand(createWhoamiCommand());
|
|
18
|
+
// Search command
|
|
19
|
+
program.addCommand(createSearchCommand());
|
|
20
|
+
// Generate command
|
|
21
|
+
program.addCommand(createGenerateCommand());
|
|
22
|
+
// Favorites commands
|
|
23
|
+
program.addCommand(createFavoritesCommand());
|
|
24
|
+
program.addCommand(createFavoriteCommand());
|
|
25
|
+
// Memes library command
|
|
26
|
+
program.addCommand(createMemesCommand());
|
|
27
|
+
// Status commands
|
|
28
|
+
program.addCommand(createStatusCommand());
|
|
29
|
+
program.addCommand(createUpgradeCommand());
|
|
30
|
+
program.parse();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface ApiError {
|
|
2
|
+
error: {
|
|
3
|
+
code: string;
|
|
4
|
+
message: string;
|
|
5
|
+
details?: Record<string, unknown>;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export interface ApiResponse<T> {
|
|
9
|
+
data?: T;
|
|
10
|
+
error?: ApiError["error"];
|
|
11
|
+
}
|
|
12
|
+
export interface User {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
email: string;
|
|
16
|
+
emailVerified: boolean;
|
|
17
|
+
image?: string | null;
|
|
18
|
+
role?: string | null;
|
|
19
|
+
createdAt: string;
|
|
20
|
+
updatedAt: string;
|
|
21
|
+
}
|
|
22
|
+
export interface Session {
|
|
23
|
+
id: string;
|
|
24
|
+
userId: string;
|
|
25
|
+
token: string;
|
|
26
|
+
expiresAt: string;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
updatedAt: string;
|
|
29
|
+
}
|
|
30
|
+
export interface SessionResponse {
|
|
31
|
+
user: User;
|
|
32
|
+
session: Session;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Make an authenticated API request
|
|
36
|
+
*/
|
|
37
|
+
export declare function apiRequest<T>(path: string, options?: RequestInit): Promise<ApiResponse<T>>;
|
|
38
|
+
/**
|
|
39
|
+
* Get the current user session
|
|
40
|
+
*/
|
|
41
|
+
export declare function getCurrentSession(): Promise<ApiResponse<SessionResponse>>;
|
|
42
|
+
/**
|
|
43
|
+
* Verify if an API key is valid by attempting to get session
|
|
44
|
+
*/
|
|
45
|
+
export declare function verifyApiKey(apiKey: string): Promise<ApiResponse<SessionResponse>>;
|
package/dist/lib/api.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { getApiKey, getApiUrl } from "./config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Create headers for API requests with authentication
|
|
4
|
+
*/
|
|
5
|
+
async function createHeaders() {
|
|
6
|
+
const headers = new Headers({
|
|
7
|
+
"Content-Type": "application/json",
|
|
8
|
+
});
|
|
9
|
+
const apiKey = await getApiKey();
|
|
10
|
+
if (apiKey) {
|
|
11
|
+
// better-auth API key plugin expects the key in x-api-key header
|
|
12
|
+
headers.set("x-api-key", apiKey);
|
|
13
|
+
}
|
|
14
|
+
return headers;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Make an authenticated API request
|
|
18
|
+
*/
|
|
19
|
+
export async function apiRequest(path, options = {}) {
|
|
20
|
+
const baseUrl = await getApiUrl();
|
|
21
|
+
const url = `${baseUrl}${path}`;
|
|
22
|
+
const headers = await createHeaders();
|
|
23
|
+
// Merge provided headers with auth headers
|
|
24
|
+
if (options.headers) {
|
|
25
|
+
const providedHeaders = new Headers(options.headers);
|
|
26
|
+
providedHeaders.forEach((value, key) => {
|
|
27
|
+
headers.set(key, value);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const response = await fetch(url, {
|
|
32
|
+
...options,
|
|
33
|
+
headers,
|
|
34
|
+
});
|
|
35
|
+
const contentType = response.headers.get("content-type");
|
|
36
|
+
if (!contentType?.includes("application/json")) {
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
return {
|
|
39
|
+
error: {
|
|
40
|
+
code: "HTTP_ERROR",
|
|
41
|
+
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return { data: undefined };
|
|
46
|
+
}
|
|
47
|
+
const data = (await response.json());
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
if (data.error) {
|
|
50
|
+
return { error: data.error };
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
error: {
|
|
54
|
+
code: "HTTP_ERROR",
|
|
55
|
+
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return { data };
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
63
|
+
return {
|
|
64
|
+
error: {
|
|
65
|
+
code: "NETWORK_ERROR",
|
|
66
|
+
message: `Failed to connect: ${message}`,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get the current user session
|
|
73
|
+
*/
|
|
74
|
+
export async function getCurrentSession() {
|
|
75
|
+
return apiRequest("/api/auth/get-session");
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Verify if an API key is valid by attempting to get session
|
|
79
|
+
*/
|
|
80
|
+
export async function verifyApiKey(apiKey) {
|
|
81
|
+
const baseUrl = await getApiUrl();
|
|
82
|
+
const url = `${baseUrl}/api/auth/get-session`;
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch(url, {
|
|
85
|
+
headers: {
|
|
86
|
+
"Content-Type": "application/json",
|
|
87
|
+
"x-api-key": apiKey,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
const contentType = response.headers.get("content-type");
|
|
91
|
+
if (!contentType?.includes("application/json")) {
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
return {
|
|
94
|
+
error: {
|
|
95
|
+
code: "HTTP_ERROR",
|
|
96
|
+
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
error: {
|
|
102
|
+
code: "INVALID_API_KEY",
|
|
103
|
+
message: "Invalid API key or API key not found",
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const data = (await response.json());
|
|
108
|
+
if (!response.ok || !data?.user) {
|
|
109
|
+
return {
|
|
110
|
+
error: {
|
|
111
|
+
code: "INVALID_API_KEY",
|
|
112
|
+
message: "Invalid API key or API key not found",
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return { data: data };
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
120
|
+
return {
|
|
121
|
+
error: {
|
|
122
|
+
code: "NETWORK_ERROR",
|
|
123
|
+
message: `Failed to connect: ${message}`,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
apiUrl?: string;
|
|
4
|
+
}
|
|
5
|
+
declare const CONFIG_DIR: string;
|
|
6
|
+
declare const CONFIG_FILE: string;
|
|
7
|
+
/**
|
|
8
|
+
* Load configuration from disk
|
|
9
|
+
*/
|
|
10
|
+
export declare function loadConfig(): Promise<Config>;
|
|
11
|
+
/**
|
|
12
|
+
* Save configuration to disk
|
|
13
|
+
* Sets restrictive permissions (0600) since config contains API keys
|
|
14
|
+
*/
|
|
15
|
+
export declare function saveConfig(config: Config): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Get the API key from config
|
|
18
|
+
*/
|
|
19
|
+
export declare function getApiKey(): Promise<string | undefined>;
|
|
20
|
+
/**
|
|
21
|
+
* Set the API key in config
|
|
22
|
+
*/
|
|
23
|
+
export declare function setApiKey(apiKey: string): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Clear the API key from config
|
|
26
|
+
*/
|
|
27
|
+
export declare function clearApiKey(): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Get the API URL from config
|
|
30
|
+
*/
|
|
31
|
+
export declare function getApiUrl(): Promise<string>;
|
|
32
|
+
/**
|
|
33
|
+
* Check if user is authenticated
|
|
34
|
+
*/
|
|
35
|
+
export declare function isAuthenticated(): Promise<boolean>;
|
|
36
|
+
export { CONFIG_DIR, CONFIG_FILE };
|