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
package/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# memes-business
|
|
2
|
+
|
|
3
|
+
CLI for [memes.business](https://memes.business) — generate memes from your terminal.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g memes-business
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Authentication
|
|
12
|
+
|
|
13
|
+
Get your API key from [memes.business/settings](https://memes.business/settings), then:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
memes login --api-key <your-api-key>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Verify you're logged in:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
memes whoami
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### Generate Memes with AI
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
memes generate --ai "make a meme about Monday meetings"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Save the generated meme to a file:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
memes generate --ai "distracted boyfriend meme about coding vs debugging" --save meme.png
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Generate Memes Manually
|
|
40
|
+
|
|
41
|
+
Search for a template:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
memes search "drake"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Generate with specific text:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
memes generate <template-id> --text "Top text" --text "Bottom text"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Manage Your Memes
|
|
54
|
+
|
|
55
|
+
List your saved memes:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
memes library list
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Show details of a specific meme:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
memes library show <meme-id>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Download a meme:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
memes library download <meme-id> ./my-meme.png
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Favorites
|
|
74
|
+
|
|
75
|
+
List your favorites:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
memes favorites
|
|
79
|
+
memes favorites --templates # only templates
|
|
80
|
+
memes favorites --memes # only memes
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Toggle favorite on a template:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
memes favorite <template-id>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Account Status
|
|
90
|
+
|
|
91
|
+
Check your usage and subscription:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
memes status
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Open billing page:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
memes upgrade
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Commands
|
|
104
|
+
|
|
105
|
+
| Command | Description |
|
|
106
|
+
|---------|-------------|
|
|
107
|
+
| `memes login --api-key <key>` | Authenticate with your API key |
|
|
108
|
+
| `memes logout` | Remove stored credentials |
|
|
109
|
+
| `memes whoami` | Display current user info |
|
|
110
|
+
| `memes search <query>` | Search for meme templates |
|
|
111
|
+
| `memes generate --ai <prompt>` | Generate meme with AI |
|
|
112
|
+
| `memes generate <id> --text <text>` | Generate meme from template |
|
|
113
|
+
| `memes library list` | List your saved memes |
|
|
114
|
+
| `memes library show <id>` | Show meme details |
|
|
115
|
+
| `memes library download <id> [path]` | Download meme image |
|
|
116
|
+
| `memes favorites` | List favorited items |
|
|
117
|
+
| `memes favorite <template-id>` | Toggle favorite on template |
|
|
118
|
+
| `memes status` | Show subscription status |
|
|
119
|
+
| `memes upgrade` | Open billing page |
|
|
120
|
+
|
|
121
|
+
## JSON Output
|
|
122
|
+
|
|
123
|
+
Most commands support `--json` for scripting:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
memes search "cat" --json | jq '.data[0].id'
|
|
127
|
+
memes status --json
|
|
128
|
+
memes whoami --json
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT
|
|
134
|
+
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { setApiKey, clearApiKey, getApiKey, CONFIG_DIR } from "../lib/config.js";
|
|
5
|
+
import { getCurrentSession, verifyApiKey } from "../lib/api.js";
|
|
6
|
+
export function createLoginCommand() {
|
|
7
|
+
const login = new Command("login")
|
|
8
|
+
.description("Authenticate with memes.business")
|
|
9
|
+
.option("--api-key <key>", "API key for authentication")
|
|
10
|
+
.action(async (options) => {
|
|
11
|
+
if (!options.apiKey) {
|
|
12
|
+
console.log(chalk.yellow("Please provide an API key:"));
|
|
13
|
+
console.log(chalk.dim(" memes login --api-key <your-api-key>"));
|
|
14
|
+
console.log();
|
|
15
|
+
console.log(chalk.dim("Get your API key at: https://memes.business/settings"));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const spinner = ora("Validating API key...").start();
|
|
19
|
+
const result = await verifyApiKey(options.apiKey);
|
|
20
|
+
if (result.error) {
|
|
21
|
+
spinner.fail(chalk.red(result.error.message));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
await setApiKey(options.apiKey);
|
|
25
|
+
spinner.succeed(chalk.green("Logged in successfully!"));
|
|
26
|
+
console.log();
|
|
27
|
+
console.log(chalk.dim(` User: ${result.data?.user.name}`));
|
|
28
|
+
console.log(chalk.dim(` Email: ${result.data?.user.email}`));
|
|
29
|
+
console.log();
|
|
30
|
+
console.log(chalk.dim(`Credentials saved to ${CONFIG_DIR}`));
|
|
31
|
+
});
|
|
32
|
+
return login;
|
|
33
|
+
}
|
|
34
|
+
export function createLogoutCommand() {
|
|
35
|
+
const logout = new Command("logout")
|
|
36
|
+
.description("Clear stored credentials")
|
|
37
|
+
.action(async () => {
|
|
38
|
+
const apiKey = await getApiKey();
|
|
39
|
+
if (!apiKey) {
|
|
40
|
+
console.log(chalk.yellow("Not currently logged in."));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const spinner = ora("Logging out...").start();
|
|
44
|
+
await clearApiKey();
|
|
45
|
+
spinner.succeed(chalk.green("Logged out successfully!"));
|
|
46
|
+
console.log(chalk.dim(`Credentials cleared from ${CONFIG_DIR}`));
|
|
47
|
+
});
|
|
48
|
+
return logout;
|
|
49
|
+
}
|
|
50
|
+
export function createWhoamiCommand() {
|
|
51
|
+
const whoami = new Command("whoami")
|
|
52
|
+
.description("Display current user info")
|
|
53
|
+
.option("--json", "Output as JSON")
|
|
54
|
+
.action(async (options) => {
|
|
55
|
+
const apiKey = await getApiKey();
|
|
56
|
+
if (!apiKey) {
|
|
57
|
+
if (options.json) {
|
|
58
|
+
console.log(JSON.stringify({ error: "Not logged in" }));
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
console.log(chalk.yellow("Not logged in."));
|
|
62
|
+
console.log(chalk.dim("Run 'memes login --api-key <key>' to authenticate."));
|
|
63
|
+
}
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
const spinner = ora("Fetching user info...").start();
|
|
67
|
+
const result = await getCurrentSession();
|
|
68
|
+
if (result.error) {
|
|
69
|
+
spinner.fail(chalk.red(result.error.message));
|
|
70
|
+
if (options.json) {
|
|
71
|
+
console.log(JSON.stringify({ error: result.error }));
|
|
72
|
+
}
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
spinner.stop();
|
|
76
|
+
const user = result.data?.user;
|
|
77
|
+
if (options.json) {
|
|
78
|
+
console.log(JSON.stringify(user, null, 2));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
console.log(chalk.bold("Current User"));
|
|
82
|
+
console.log();
|
|
83
|
+
console.log(` ${chalk.dim("Name:")} ${user?.name}`);
|
|
84
|
+
console.log(` ${chalk.dim("Email:")} ${user?.email}`);
|
|
85
|
+
console.log(` ${chalk.dim("ID:")} ${user?.id}`);
|
|
86
|
+
if (user?.role) {
|
|
87
|
+
console.log(` ${chalk.dim("Role:")} ${user.role}`);
|
|
88
|
+
}
|
|
89
|
+
console.log(` ${chalk.dim("Verified:")} ${user?.emailVerified ? chalk.green("Yes") : chalk.yellow("No")}`);
|
|
90
|
+
});
|
|
91
|
+
return whoami;
|
|
92
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
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 createFavoritesCommand() {
|
|
6
|
+
const favorites = new Command("favorites")
|
|
7
|
+
.description("List favorited templates and memes")
|
|
8
|
+
.option("--templates", "show only templates")
|
|
9
|
+
.option("--memes", "show only memes")
|
|
10
|
+
.option("--json", "output as JSON for scripting")
|
|
11
|
+
.action(async (options) => {
|
|
12
|
+
// Check for mutually exclusive flags
|
|
13
|
+
if (options.templates && options.memes) {
|
|
14
|
+
const errorMessage = "Error: --templates and --memes are mutually exclusive. Use one or the other.";
|
|
15
|
+
if (options.json) {
|
|
16
|
+
console.log(JSON.stringify({ error: { message: errorMessage } }, null, 2));
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
console.error(chalk.red(errorMessage));
|
|
20
|
+
}
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const spinner = options.json ? null : ora("Fetching favorites...").start();
|
|
24
|
+
const params = new URLSearchParams();
|
|
25
|
+
if (options.templates)
|
|
26
|
+
params.set("entity_type", "template");
|
|
27
|
+
if (options.memes)
|
|
28
|
+
params.set("entity_type", "user_meme");
|
|
29
|
+
const queryString = params.toString();
|
|
30
|
+
const path = `/api/favorites/list${queryString ? `?${queryString}` : ""}`;
|
|
31
|
+
const result = await apiRequest(path);
|
|
32
|
+
if (result.error) {
|
|
33
|
+
if (spinner)
|
|
34
|
+
spinner.fail(chalk.red(result.error.message));
|
|
35
|
+
if (options.json) {
|
|
36
|
+
console.log(JSON.stringify({ error: result.error }, null, 2));
|
|
37
|
+
}
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const response = result.data;
|
|
41
|
+
if (!response || !response.data) {
|
|
42
|
+
if (spinner)
|
|
43
|
+
spinner.fail(chalk.red("Invalid response from server"));
|
|
44
|
+
if (options.json) {
|
|
45
|
+
console.log(JSON.stringify({ error: { message: "Invalid response" } }, null, 2));
|
|
46
|
+
}
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
if (spinner)
|
|
50
|
+
spinner.stop();
|
|
51
|
+
if (options.json) {
|
|
52
|
+
console.log(JSON.stringify(response, null, 2));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const items = response.data;
|
|
56
|
+
if (items.length === 0) {
|
|
57
|
+
const filter = options.templates ? "templates" : options.memes ? "memes" : "items";
|
|
58
|
+
console.log(chalk.yellow(`No favorited ${filter} found.`));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
console.log(chalk.bold(`${response.meta.total_count} favorite${response.meta.total_count !== 1 ? "s" : ""}`));
|
|
62
|
+
console.log();
|
|
63
|
+
items.forEach((item, index) => {
|
|
64
|
+
const num = chalk.dim(`${(index + 1).toString().padStart(2)}.`);
|
|
65
|
+
const name = chalk.bold(item.name);
|
|
66
|
+
const type = chalk.cyan(`[${item.type}]`);
|
|
67
|
+
console.log(`${num} ${name} ${type}`);
|
|
68
|
+
console.log(chalk.dim(` ID: ${item.id}`));
|
|
69
|
+
console.log();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
return favorites;
|
|
73
|
+
}
|
|
74
|
+
export function createFavoriteCommand() {
|
|
75
|
+
const favorite = new Command("favorite")
|
|
76
|
+
.description("Toggle favorite status on a template")
|
|
77
|
+
.argument("<template-id>", "template ID to favorite/unfavorite")
|
|
78
|
+
.action(async (templateId) => {
|
|
79
|
+
const spinner = ora("Toggling favorite...").start();
|
|
80
|
+
const result = await apiRequest("/api/favorites", {
|
|
81
|
+
method: "POST",
|
|
82
|
+
body: JSON.stringify({ entity_type: "template", entity_id: templateId }),
|
|
83
|
+
});
|
|
84
|
+
if (result.error) {
|
|
85
|
+
spinner.fail(chalk.red(result.error.message));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
const response = result.data;
|
|
89
|
+
if (!response || !response.data) {
|
|
90
|
+
spinner.fail(chalk.red("Invalid response from server"));
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
const { isFavorited, message } = response.data;
|
|
94
|
+
if (isFavorited) {
|
|
95
|
+
spinner.succeed(chalk.green(message || `Added template ${templateId} to favorites`));
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
spinner.succeed(chalk.yellow(message || `Removed template ${templateId} from favorites`));
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
return favorite;
|
|
102
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
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 { apiRequest } from "../lib/api.js";
|
|
7
|
+
import { getApiKey, getApiUrl } from "../lib/config.js";
|
|
8
|
+
/**
|
|
9
|
+
* Make a streaming request for AI generation
|
|
10
|
+
*/
|
|
11
|
+
async function streamAIGeneration(prompt, onMeme, onError) {
|
|
12
|
+
const baseUrl = await getApiUrl();
|
|
13
|
+
const apiKey = await getApiKey();
|
|
14
|
+
if (!apiKey) {
|
|
15
|
+
throw new Error("Not authenticated. Run 'memes login' first.");
|
|
16
|
+
}
|
|
17
|
+
const response = await fetch(`${baseUrl}/api/generate`, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: {
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
"x-api-key": apiKey,
|
|
22
|
+
},
|
|
23
|
+
body: JSON.stringify({ request: prompt }),
|
|
24
|
+
});
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
const errorData = await response.json().catch(() => ({}));
|
|
27
|
+
throw new Error(errorData.error?.message ||
|
|
28
|
+
`HTTP ${response.status}: ${response.statusText}`);
|
|
29
|
+
}
|
|
30
|
+
const reader = response.body?.getReader();
|
|
31
|
+
if (!reader) {
|
|
32
|
+
throw new Error("No response body");
|
|
33
|
+
}
|
|
34
|
+
const decoder = new TextDecoder();
|
|
35
|
+
let buffer = "";
|
|
36
|
+
while (true) {
|
|
37
|
+
const { done, value } = await reader.read();
|
|
38
|
+
if (done)
|
|
39
|
+
break;
|
|
40
|
+
buffer += decoder.decode(value, { stream: true });
|
|
41
|
+
const lines = buffer.split("\n");
|
|
42
|
+
buffer = lines.pop() || "";
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
if (!line.trim())
|
|
45
|
+
continue;
|
|
46
|
+
try {
|
|
47
|
+
let jsonStr;
|
|
48
|
+
// Handle server's numbered format: "0: {...}"
|
|
49
|
+
const numberedMatch = line.match(/^\d+:\s*(.+)$/);
|
|
50
|
+
if (numberedMatch) {
|
|
51
|
+
jsonStr = numberedMatch[1];
|
|
52
|
+
}
|
|
53
|
+
else if (line.startsWith("data:")) {
|
|
54
|
+
// Handle SSE format: "data: {...}"
|
|
55
|
+
jsonStr = line.slice(5).trim();
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!jsonStr || jsonStr === "[DONE]")
|
|
61
|
+
continue;
|
|
62
|
+
const event = JSON.parse(jsonStr);
|
|
63
|
+
// Handle server's generatedMeme format
|
|
64
|
+
if (event.generatedMeme) {
|
|
65
|
+
const meme = event.generatedMeme;
|
|
66
|
+
onMeme({
|
|
67
|
+
name: meme.title || meme.template_name || "Meme",
|
|
68
|
+
texts: meme.generation_data?.texts || [],
|
|
69
|
+
imageUrl: `${baseUrl}/api/memes/${meme.id}/image`,
|
|
70
|
+
templateId: meme.generation_data?.template_id,
|
|
71
|
+
memeId: meme.id,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// Handle legacy SSE format
|
|
75
|
+
else if (event.type === "meme" && (event.data || event.meme)) {
|
|
76
|
+
onMeme(event.data || event.meme);
|
|
77
|
+
}
|
|
78
|
+
else if (event.type === "error" || event.error) {
|
|
79
|
+
onError(event.error || "Unknown error");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Skip unparseable lines
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Process any remaining content in buffer after stream ends
|
|
88
|
+
if (buffer.trim()) {
|
|
89
|
+
try {
|
|
90
|
+
let jsonStr;
|
|
91
|
+
// Handle server's numbered format: "0: {...}"
|
|
92
|
+
const numberedMatch = buffer.match(/^\d+:\s*(.+)$/);
|
|
93
|
+
if (numberedMatch) {
|
|
94
|
+
jsonStr = numberedMatch[1];
|
|
95
|
+
}
|
|
96
|
+
else if (buffer.startsWith("data:")) {
|
|
97
|
+
// Handle SSE format: "data: {...}"
|
|
98
|
+
jsonStr = buffer.slice(5).trim();
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
jsonStr = "";
|
|
102
|
+
}
|
|
103
|
+
if (jsonStr && jsonStr !== "[DONE]") {
|
|
104
|
+
const event = JSON.parse(jsonStr);
|
|
105
|
+
// Handle server's generatedMeme format
|
|
106
|
+
if (event.generatedMeme) {
|
|
107
|
+
const meme = event.generatedMeme;
|
|
108
|
+
onMeme({
|
|
109
|
+
name: meme.title || meme.template_name || "Meme",
|
|
110
|
+
texts: meme.generation_data?.texts || [],
|
|
111
|
+
imageUrl: `${baseUrl}/api/memes/${meme.id}/image`,
|
|
112
|
+
templateId: meme.generation_data?.template_id,
|
|
113
|
+
memeId: meme.id,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// Handle legacy SSE format
|
|
117
|
+
else if (event.type === "meme" && (event.data || event.meme)) {
|
|
118
|
+
onMeme(event.data || event.meme);
|
|
119
|
+
}
|
|
120
|
+
else if (event.type === "error" || event.error) {
|
|
121
|
+
onError(event.error || "Unknown error");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Skip unparseable content
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Check if a path is a directory
|
|
132
|
+
*/
|
|
133
|
+
async function isDirectory(path) {
|
|
134
|
+
try {
|
|
135
|
+
const stats = await stat(path);
|
|
136
|
+
return stats.isDirectory();
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get file extension from content type
|
|
144
|
+
*/
|
|
145
|
+
function getExtensionFromContentType(contentType) {
|
|
146
|
+
if (!contentType)
|
|
147
|
+
return ".jpg";
|
|
148
|
+
if (contentType.includes("png"))
|
|
149
|
+
return ".png";
|
|
150
|
+
if (contentType.includes("gif"))
|
|
151
|
+
return ".gif";
|
|
152
|
+
if (contentType.includes("webp"))
|
|
153
|
+
return ".webp";
|
|
154
|
+
return ".jpg";
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Download an image from URL
|
|
158
|
+
*/
|
|
159
|
+
async function downloadImage(imageUrl, savePath, memeId) {
|
|
160
|
+
const baseUrl = await getApiUrl();
|
|
161
|
+
const apiKey = await getApiKey();
|
|
162
|
+
// Handle relative URLs
|
|
163
|
+
const fullUrl = imageUrl.startsWith("http")
|
|
164
|
+
? imageUrl
|
|
165
|
+
: `${baseUrl}${imageUrl}`;
|
|
166
|
+
const response = await fetch(fullUrl, {
|
|
167
|
+
headers: apiKey ? { "x-api-key": apiKey } : {},
|
|
168
|
+
});
|
|
169
|
+
if (!response.ok) {
|
|
170
|
+
throw new Error(`Failed to download: ${response.statusText}`);
|
|
171
|
+
}
|
|
172
|
+
const contentType = response.headers.get("content-type");
|
|
173
|
+
const ext = getExtensionFromContentType(contentType);
|
|
174
|
+
// Resolve the save path (handle directory case)
|
|
175
|
+
let resolvedPath = savePath;
|
|
176
|
+
if (await isDirectory(savePath)) {
|
|
177
|
+
const filename = memeId ? `${memeId}${ext}` : `meme-${Date.now()}${ext}`;
|
|
178
|
+
resolvedPath = join(savePath, filename);
|
|
179
|
+
}
|
|
180
|
+
const buffer = await response.arrayBuffer();
|
|
181
|
+
await writeFile(resolvedPath, Buffer.from(buffer));
|
|
182
|
+
return resolvedPath;
|
|
183
|
+
}
|
|
184
|
+
export function createGenerateCommand() {
|
|
185
|
+
const generate = new Command("generate")
|
|
186
|
+
.description("Generate a meme from a template or AI prompt")
|
|
187
|
+
.argument("[template]", "template ID (for manual generation)")
|
|
188
|
+
.option("-t, --text <text...>", "text for each text box (manual mode)")
|
|
189
|
+
.option("-a, --ai <prompt>", "AI prompt for generation")
|
|
190
|
+
.option("-s, --save <path>", "save the meme image to a file")
|
|
191
|
+
.action(async (template, options) => {
|
|
192
|
+
const { text, ai, save } = options;
|
|
193
|
+
// Validate arguments
|
|
194
|
+
if (!template && !ai) {
|
|
195
|
+
console.log(chalk.red("Error: Provide a template ID or use --ai"));
|
|
196
|
+
console.log();
|
|
197
|
+
console.log(chalk.dim("Manual: memes generate <template-id> --text \"Top\" --text \"Bottom\""));
|
|
198
|
+
console.log(chalk.dim("AI: memes generate --ai \"make a meme about Monday\""));
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
if (template && ai) {
|
|
202
|
+
console.log(chalk.red("Error: Cannot use both template and --ai"));
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
if (ai) {
|
|
206
|
+
await handleAIGeneration(ai, save);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
await handleManualGeneration(template, text || [], save);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
return generate;
|
|
213
|
+
}
|
|
214
|
+
async function handleManualGeneration(templateId, texts, savePath) {
|
|
215
|
+
if (texts.length === 0) {
|
|
216
|
+
console.log(chalk.red("Error: At least one --text is required"));
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
const spinner = ora("Generating meme...").start();
|
|
220
|
+
const result = await apiRequest("/api/memes", {
|
|
221
|
+
method: "POST",
|
|
222
|
+
body: JSON.stringify({
|
|
223
|
+
templateId,
|
|
224
|
+
texts,
|
|
225
|
+
}),
|
|
226
|
+
});
|
|
227
|
+
if (result.error) {
|
|
228
|
+
spinner.fail(chalk.red(result.error.message));
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
const meme = result.data?.data;
|
|
232
|
+
if (!meme) {
|
|
233
|
+
spinner.fail(chalk.red("No meme data in response"));
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
spinner.succeed(chalk.green("Meme generated!"));
|
|
237
|
+
console.log();
|
|
238
|
+
console.log(` ${chalk.dim("Title:")} ${meme.title}`);
|
|
239
|
+
console.log(` ${chalk.dim("URL:")} ${meme.imageUrl}`);
|
|
240
|
+
if (savePath) {
|
|
241
|
+
const saveSpinner = ora("Saving image...").start();
|
|
242
|
+
try {
|
|
243
|
+
const resolvedPath = await downloadImage(meme.imageUrl, savePath, meme.id);
|
|
244
|
+
saveSpinner.succeed(chalk.green(`Saved to ${resolvedPath}`));
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
248
|
+
saveSpinner.fail(chalk.red(`Failed to save: ${msg}`));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async function handleAIGeneration(prompt, savePath) {
|
|
253
|
+
const spinner = ora("Generating meme with AI...").start();
|
|
254
|
+
const memes = [];
|
|
255
|
+
let hasError = false;
|
|
256
|
+
let errorMsg = "";
|
|
257
|
+
try {
|
|
258
|
+
await streamAIGeneration(prompt, (meme) => {
|
|
259
|
+
memes.push(meme);
|
|
260
|
+
spinner.text = `Generated ${memes.length} meme(s)...`;
|
|
261
|
+
}, (error) => {
|
|
262
|
+
hasError = true;
|
|
263
|
+
errorMsg = error;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
spinner.fail(chalk.red(err instanceof Error ? err.message : "Generation failed"));
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
if (hasError) {
|
|
271
|
+
spinner.fail(chalk.red(errorMsg));
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
if (memes.length === 0) {
|
|
275
|
+
spinner.fail(chalk.red("No memes generated"));
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
spinner.succeed(chalk.green(`Generated ${memes.length} meme(s)!`));
|
|
279
|
+
console.log();
|
|
280
|
+
for (const meme of memes) {
|
|
281
|
+
console.log(` ${chalk.bold(meme.name)}`);
|
|
282
|
+
console.log(` ${chalk.dim("Texts:")} ${meme.texts.join(" | ")}`);
|
|
283
|
+
console.log(` ${chalk.dim("URL:")} ${meme.imageUrl}`);
|
|
284
|
+
console.log();
|
|
285
|
+
}
|
|
286
|
+
// Save memes if --save is provided
|
|
287
|
+
if (savePath && memes.length > 0) {
|
|
288
|
+
const isDir = await isDirectory(savePath);
|
|
289
|
+
const memesToSave = isDir ? memes : [memes[0]];
|
|
290
|
+
const saveSpinner = ora(`Saving ${memesToSave.length} image(s)...`).start();
|
|
291
|
+
const savedPaths = [];
|
|
292
|
+
const errors = [];
|
|
293
|
+
for (const meme of memesToSave) {
|
|
294
|
+
try {
|
|
295
|
+
const resolvedPath = await downloadImage(meme.imageUrl, savePath, meme.memeId);
|
|
296
|
+
savedPaths.push(resolvedPath);
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
300
|
+
errors.push(`${meme.name}: ${msg}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (savedPaths.length > 0) {
|
|
304
|
+
saveSpinner.succeed(chalk.green(`Saved ${savedPaths.length} image(s) to ${isDir ? savePath : savedPaths[0]}`));
|
|
305
|
+
}
|
|
306
|
+
if (errors.length > 0) {
|
|
307
|
+
for (const error of errors) {
|
|
308
|
+
console.log(chalk.red(` ✖ ${error}`));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|