grok-image-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.
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/dist/main.mjs +426 -0
- package/dist/main.mjs.map +1 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 cyberash
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# grok-img
|
|
2
|
+
|
|
3
|
+
CLI for generating and editing images with Grok API, powered by `@ai-sdk/xai`.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g grok-image-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### From source
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git clone https://github.com/cyberash/grok-image-cli.git
|
|
15
|
+
cd grok-image-cli
|
|
16
|
+
npm install
|
|
17
|
+
npm run build
|
|
18
|
+
npm link
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Authentication
|
|
22
|
+
|
|
23
|
+
The CLI stores your xAI API key securely in the macOS Keychain. Alternatively, set the `XAI_API_KEY` environment variable.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Save API key to keychain
|
|
27
|
+
grok-img auth login
|
|
28
|
+
|
|
29
|
+
# Check authentication status
|
|
30
|
+
grok-img auth status
|
|
31
|
+
|
|
32
|
+
# Remove API key from keychain
|
|
33
|
+
grok-img auth logout
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Image Generation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Generate a single image
|
|
40
|
+
grok-img generate "A futuristic city skyline at night"
|
|
41
|
+
|
|
42
|
+
# Generate multiple images with specific aspect ratio
|
|
43
|
+
grok-img generate "Mountain landscape at sunrise" -n 4 -a 16:9
|
|
44
|
+
|
|
45
|
+
# Specify output directory
|
|
46
|
+
grok-img generate "A serene Japanese garden" -o ./my-images
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Options
|
|
50
|
+
|
|
51
|
+
| Option | Description | Default |
|
|
52
|
+
|--------|-------------|---------|
|
|
53
|
+
| `-a, --aspect-ratio <ratio>` | Aspect ratio (`1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `3:2`, `2:3`, `2:1`, `1:2`, `19.5:9`, `9:19.5`, `20:9`, `9:20`, `auto`) | `auto` |
|
|
54
|
+
| `-n, --count <number>` | Number of images (1-10) | `1` |
|
|
55
|
+
| `-o, --output <dir>` | Output directory | `./grok-images` |
|
|
56
|
+
|
|
57
|
+
## Image Editing
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Edit a local image
|
|
61
|
+
grok-img edit "Make it look like a watercolor painting" -i ./photo.jpg
|
|
62
|
+
|
|
63
|
+
# Edit using a URL
|
|
64
|
+
grok-img edit "Change the sky to sunset colors" -i https://example.com/photo.jpg
|
|
65
|
+
|
|
66
|
+
# Specify aspect ratio and output directory
|
|
67
|
+
grok-img edit "Add a vintage film grain effect" -i ./photo.jpg -a 3:2 -o ./edited
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Options
|
|
71
|
+
|
|
72
|
+
| Option | Description | Default |
|
|
73
|
+
|--------|-------------|---------|
|
|
74
|
+
| `-i, --image <path>` | Source image (local path or URL) | **required** |
|
|
75
|
+
| `-a, --aspect-ratio <ratio>` | Aspect ratio | `auto` |
|
|
76
|
+
| `-o, --output <dir>` | Output directory | `./grok-images` |
|
|
77
|
+
|
|
78
|
+
## Development
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npm install
|
|
82
|
+
npm run dev # watch mode
|
|
83
|
+
npm run build # production build
|
|
84
|
+
npm run lint # check linting
|
|
85
|
+
npm run lint:fix # auto-fix lint issues
|
|
86
|
+
npm run format # format code
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Architecture
|
|
90
|
+
|
|
91
|
+
This project follows Clean Architecture principles:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
src/
|
|
95
|
+
main.ts # Composition root
|
|
96
|
+
domain/ # Entities & port interfaces (zero deps)
|
|
97
|
+
application/ # Use cases (depends on domain only)
|
|
98
|
+
infrastructure/ # Adapters (@ai-sdk/xai, keychain, fs)
|
|
99
|
+
presentation/ # CLI commands (commander)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Requirements
|
|
103
|
+
|
|
104
|
+
- Node.js >= 20.19.0
|
|
105
|
+
- macOS (for Keychain support) or `XAI_API_KEY` environment variable
|
|
106
|
+
- xAI API key from [console.x.ai](https://console.x.ai)
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
package/dist/main.mjs
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { createXai } from "@ai-sdk/xai";
|
|
5
|
+
import { NoImageGeneratedError, generateImage } from "ai";
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import { createInterface } from "node:readline";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import ora from "ora";
|
|
12
|
+
|
|
13
|
+
//#region src/domain/errors.ts
|
|
14
|
+
var ApiKeyMissingError = class extends Error {
|
|
15
|
+
constructor() {
|
|
16
|
+
super("API key not found. Run `grok-img auth login` or set XAI_API_KEY environment variable.");
|
|
17
|
+
this.name = "ApiKeyMissingError";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var ApiError = class extends Error {
|
|
21
|
+
constructor(message, cause) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.cause = cause;
|
|
24
|
+
this.name = "ApiError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var ImageNotFoundError = class extends Error {
|
|
28
|
+
constructor(path) {
|
|
29
|
+
super(`Image not found: ${path}`);
|
|
30
|
+
this.name = "ImageNotFoundError";
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/application/usecases/edit-image.usecase.ts
|
|
36
|
+
var EditImageUseCase = class {
|
|
37
|
+
constructor(imageGenerator, keyStore, fileStorage) {
|
|
38
|
+
this.imageGenerator = imageGenerator;
|
|
39
|
+
this.keyStore = keyStore;
|
|
40
|
+
this.fileStorage = fileStorage;
|
|
41
|
+
}
|
|
42
|
+
async execute(params, outputDir) {
|
|
43
|
+
const apiKey = await this.keyStore.get();
|
|
44
|
+
if (!apiKey) throw new ApiKeyMissingError();
|
|
45
|
+
let imageSource = params.imageSource;
|
|
46
|
+
if (typeof imageSource === "string" && !imageSource.startsWith("http")) imageSource = await this.fileStorage.readImage(imageSource);
|
|
47
|
+
this.fileStorage.ensureDir(outputDir);
|
|
48
|
+
const result = await this.imageGenerator.edit({
|
|
49
|
+
...params,
|
|
50
|
+
imageSource
|
|
51
|
+
}, apiKey);
|
|
52
|
+
const outputPath = this.fileStorage.generateOutputPath(outputDir, 0, result.mediaType);
|
|
53
|
+
return this.fileStorage.saveImage(result.uint8Array, outputPath);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/application/usecases/generate-image.usecase.ts
|
|
59
|
+
var GenerateImageUseCase = class {
|
|
60
|
+
constructor(imageGenerator, keyStore, fileStorage) {
|
|
61
|
+
this.imageGenerator = imageGenerator;
|
|
62
|
+
this.keyStore = keyStore;
|
|
63
|
+
this.fileStorage = fileStorage;
|
|
64
|
+
}
|
|
65
|
+
async execute(params, outputDir) {
|
|
66
|
+
const apiKey = await this.keyStore.get();
|
|
67
|
+
if (!apiKey) throw new ApiKeyMissingError();
|
|
68
|
+
this.fileStorage.ensureDir(outputDir);
|
|
69
|
+
const results = await this.imageGenerator.generate(params, apiKey);
|
|
70
|
+
const savedPaths = [];
|
|
71
|
+
for (let i = 0; i < results.length; i++) {
|
|
72
|
+
const result = results[i];
|
|
73
|
+
const outputPath = this.fileStorage.generateOutputPath(outputDir, i, result.mediaType);
|
|
74
|
+
const saved = await this.fileStorage.saveImage(result.uint8Array, outputPath);
|
|
75
|
+
savedPaths.push(saved);
|
|
76
|
+
}
|
|
77
|
+
return savedPaths;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/application/usecases/get-auth-status.usecase.ts
|
|
83
|
+
var GetAuthStatusUseCase = class {
|
|
84
|
+
constructor(keyStore) {
|
|
85
|
+
this.keyStore = keyStore;
|
|
86
|
+
}
|
|
87
|
+
async execute() {
|
|
88
|
+
const key = await this.keyStore.get();
|
|
89
|
+
if (!key) return {
|
|
90
|
+
authenticated: false,
|
|
91
|
+
maskedKey: null,
|
|
92
|
+
source: null
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
authenticated: true,
|
|
96
|
+
maskedKey: key.length > 8 ? `${key.slice(0, 4)}${"*".repeat(key.length - 8)}${key.slice(-4)}` : "****",
|
|
97
|
+
source: process.env.XAI_API_KEY === key ? "env" : "keychain"
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/application/usecases/login.usecase.ts
|
|
104
|
+
var LoginUseCase = class {
|
|
105
|
+
constructor(keyStore) {
|
|
106
|
+
this.keyStore = keyStore;
|
|
107
|
+
}
|
|
108
|
+
async execute(apiKey) {
|
|
109
|
+
await this.keyStore.save(apiKey);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/application/usecases/logout.usecase.ts
|
|
115
|
+
var LogoutUseCase = class {
|
|
116
|
+
constructor(keyStore) {
|
|
117
|
+
this.keyStore = keyStore;
|
|
118
|
+
}
|
|
119
|
+
async execute() {
|
|
120
|
+
await this.keyStore.remove();
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region src/infrastructure/adapters/file-storage.adapter.ts
|
|
126
|
+
const MEDIA_TYPE_EXT = {
|
|
127
|
+
"image/png": "png",
|
|
128
|
+
"image/jpeg": "jpg",
|
|
129
|
+
"image/webp": "webp",
|
|
130
|
+
"image/gif": "gif"
|
|
131
|
+
};
|
|
132
|
+
var FileStorageAdapter = class {
|
|
133
|
+
async saveImage(data, outputPath) {
|
|
134
|
+
writeFileSync(outputPath, data);
|
|
135
|
+
return outputPath;
|
|
136
|
+
}
|
|
137
|
+
async readImage(filePath) {
|
|
138
|
+
if (!existsSync(filePath)) throw new ImageNotFoundError(filePath);
|
|
139
|
+
return new Uint8Array(readFileSync(filePath));
|
|
140
|
+
}
|
|
141
|
+
ensureDir(dir) {
|
|
142
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
generateOutputPath(dir, index, mediaType) {
|
|
145
|
+
const ext = MEDIA_TYPE_EXT[mediaType] ?? "png";
|
|
146
|
+
return join(dir, `grok-img-${Date.now()}-${index}.${ext}`);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/infrastructure/adapters/grok-api.adapter.ts
|
|
152
|
+
const MODEL = "grok-imagine-image";
|
|
153
|
+
var GrokApiAdapter = class {
|
|
154
|
+
async generate(params, apiKey) {
|
|
155
|
+
const xai = createXai({ apiKey });
|
|
156
|
+
try {
|
|
157
|
+
const { images } = await generateImage({
|
|
158
|
+
model: xai.image(MODEL),
|
|
159
|
+
prompt: params.prompt,
|
|
160
|
+
aspectRatio: params.aspectRatio,
|
|
161
|
+
n: params.count
|
|
162
|
+
});
|
|
163
|
+
return images.map((img) => ({
|
|
164
|
+
base64: img.base64,
|
|
165
|
+
uint8Array: img.uint8Array,
|
|
166
|
+
mediaType: img.mediaType
|
|
167
|
+
}));
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (NoImageGeneratedError.isInstance(error)) throw new ApiError("Image generation failed: the model could not produce an image.", error);
|
|
170
|
+
if (error instanceof Error) throw new ApiError(`API request failed: ${error.message}`, error);
|
|
171
|
+
throw new ApiError("An unexpected error occurred during image generation.", error);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async edit(params, apiKey) {
|
|
175
|
+
const xai = createXai({ apiKey });
|
|
176
|
+
try {
|
|
177
|
+
const { image } = await generateImage({
|
|
178
|
+
model: xai.image(MODEL),
|
|
179
|
+
prompt: {
|
|
180
|
+
text: params.prompt,
|
|
181
|
+
images: typeof params.imageSource === "string" ? [new URL(params.imageSource)] : [params.imageSource]
|
|
182
|
+
},
|
|
183
|
+
aspectRatio: params.aspectRatio
|
|
184
|
+
});
|
|
185
|
+
return {
|
|
186
|
+
base64: image.base64,
|
|
187
|
+
uint8Array: image.uint8Array,
|
|
188
|
+
mediaType: image.mediaType
|
|
189
|
+
};
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (NoImageGeneratedError.isInstance(error)) throw new ApiError("Image editing failed: the model could not produce an image.", error);
|
|
192
|
+
if (error instanceof Error) throw new ApiError(`API request failed: ${error.message}`, error);
|
|
193
|
+
throw new ApiError("An unexpected error occurred during image editing.", error);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/infrastructure/adapters/keychain.adapter.ts
|
|
200
|
+
const execFileAsync = promisify(execFile);
|
|
201
|
+
const SERVICE = "grok-image-cli";
|
|
202
|
+
const ACCOUNT = "api-key";
|
|
203
|
+
var KeychainAdapter = class {
|
|
204
|
+
async save(key) {
|
|
205
|
+
await execFileAsync("security", [
|
|
206
|
+
"add-generic-password",
|
|
207
|
+
"-U",
|
|
208
|
+
"-s",
|
|
209
|
+
SERVICE,
|
|
210
|
+
"-a",
|
|
211
|
+
ACCOUNT,
|
|
212
|
+
"-w",
|
|
213
|
+
key
|
|
214
|
+
]);
|
|
215
|
+
}
|
|
216
|
+
async get() {
|
|
217
|
+
try {
|
|
218
|
+
const { stdout } = await execFileAsync("security", [
|
|
219
|
+
"find-generic-password",
|
|
220
|
+
"-s",
|
|
221
|
+
SERVICE,
|
|
222
|
+
"-a",
|
|
223
|
+
ACCOUNT,
|
|
224
|
+
"-w"
|
|
225
|
+
]);
|
|
226
|
+
const key = stdout.trim();
|
|
227
|
+
if (key) return key;
|
|
228
|
+
} catch {}
|
|
229
|
+
return process.env.XAI_API_KEY ?? null;
|
|
230
|
+
}
|
|
231
|
+
async remove() {
|
|
232
|
+
try {
|
|
233
|
+
await execFileAsync("security", [
|
|
234
|
+
"delete-generic-password",
|
|
235
|
+
"-s",
|
|
236
|
+
SERVICE,
|
|
237
|
+
"-a",
|
|
238
|
+
ACCOUNT
|
|
239
|
+
]);
|
|
240
|
+
} catch {}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region src/presentation/commands/auth.command.ts
|
|
246
|
+
function readInput(prompt) {
|
|
247
|
+
const rl = createInterface({
|
|
248
|
+
input: process.stdin,
|
|
249
|
+
output: process.stdout
|
|
250
|
+
});
|
|
251
|
+
return new Promise((resolve) => {
|
|
252
|
+
rl.question(prompt, (answer) => {
|
|
253
|
+
rl.close();
|
|
254
|
+
resolve(answer.trim());
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
function createAuthCommand(useCases) {
|
|
259
|
+
const auth = new Command("auth").description("Manage API key authentication");
|
|
260
|
+
auth.command("login").description("Store your xAI API key in the system keychain").action(async () => {
|
|
261
|
+
try {
|
|
262
|
+
const apiKey = await readInput(chalk.cyan("Enter your xAI API key: "));
|
|
263
|
+
if (!apiKey) {
|
|
264
|
+
console.log(chalk.red("No API key provided."));
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
await useCases.login.execute(apiKey);
|
|
268
|
+
console.log(chalk.green("API key saved to keychain successfully."));
|
|
269
|
+
} catch (error) {
|
|
270
|
+
console.error(chalk.red(`Failed to save API key: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
auth.command("logout").description("Remove your xAI API key from the system keychain").action(async () => {
|
|
275
|
+
try {
|
|
276
|
+
await useCases.logout.execute();
|
|
277
|
+
console.log(chalk.green("API key removed from keychain."));
|
|
278
|
+
} catch (error) {
|
|
279
|
+
console.error(chalk.red(`Failed to remove API key: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
auth.command("status").description("Check current authentication status").action(async () => {
|
|
284
|
+
try {
|
|
285
|
+
const status = await useCases.getStatus.execute();
|
|
286
|
+
if (status.authenticated) {
|
|
287
|
+
console.log(chalk.green("Authenticated"));
|
|
288
|
+
console.log(chalk.dim(` Key: ${status.maskedKey}`));
|
|
289
|
+
console.log(chalk.dim(` Source: ${status.source}`));
|
|
290
|
+
} else {
|
|
291
|
+
console.log(chalk.yellow("Not authenticated"));
|
|
292
|
+
console.log(chalk.dim(" Run `grok-img auth login` or set XAI_API_KEY environment variable."));
|
|
293
|
+
}
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error(chalk.red(`Failed to check status: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
return auth;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region src/presentation/commands/edit.command.ts
|
|
304
|
+
const VALID_RATIOS$1 = [
|
|
305
|
+
"1:1",
|
|
306
|
+
"16:9",
|
|
307
|
+
"9:16",
|
|
308
|
+
"4:3",
|
|
309
|
+
"3:4",
|
|
310
|
+
"3:2",
|
|
311
|
+
"2:3",
|
|
312
|
+
"2:1",
|
|
313
|
+
"1:2",
|
|
314
|
+
"19.5:9",
|
|
315
|
+
"9:19.5",
|
|
316
|
+
"20:9",
|
|
317
|
+
"9:20",
|
|
318
|
+
"auto"
|
|
319
|
+
];
|
|
320
|
+
function createEditCommand(editUseCase) {
|
|
321
|
+
return new Command("edit").description("Edit an existing image with a text prompt").argument("<prompt>", "Text prompt describing the edit to apply").requiredOption("-i, --image <path>", "Source image (local file path or URL)").option("-a, --aspect-ratio <ratio>", "Aspect ratio", "auto").option("-o, --output <dir>", "Output directory", "./grok-images").action(async (prompt, options) => {
|
|
322
|
+
if (!VALID_RATIOS$1.includes(options.aspectRatio)) {
|
|
323
|
+
console.error(chalk.red(`Invalid aspect ratio. Valid options: ${VALID_RATIOS$1.join(", ")}`));
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
const imageSource = options.image.startsWith("http") ? options.image : resolve(options.image);
|
|
327
|
+
const outputDir = resolve(options.output);
|
|
328
|
+
const spinner = ora("Editing image...").start();
|
|
329
|
+
try {
|
|
330
|
+
const savedPath = await editUseCase.execute({
|
|
331
|
+
prompt,
|
|
332
|
+
imageSource,
|
|
333
|
+
aspectRatio: options.aspectRatio
|
|
334
|
+
}, outputDir);
|
|
335
|
+
spinner.succeed(chalk.green("Image edited successfully:"));
|
|
336
|
+
console.log(chalk.dim(` ${savedPath}`));
|
|
337
|
+
} catch (error) {
|
|
338
|
+
spinner.fail();
|
|
339
|
+
if (error instanceof ApiKeyMissingError) console.error(chalk.red(error.message));
|
|
340
|
+
else if (error instanceof ImageNotFoundError) console.error(chalk.red(error.message));
|
|
341
|
+
else if (error instanceof ApiError) console.error(chalk.red(`API Error: ${error.message}`));
|
|
342
|
+
else console.error(chalk.red(`Unexpected error: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
//#endregion
|
|
349
|
+
//#region src/presentation/commands/generate.command.ts
|
|
350
|
+
const VALID_RATIOS = [
|
|
351
|
+
"1:1",
|
|
352
|
+
"16:9",
|
|
353
|
+
"9:16",
|
|
354
|
+
"4:3",
|
|
355
|
+
"3:4",
|
|
356
|
+
"3:2",
|
|
357
|
+
"2:3",
|
|
358
|
+
"2:1",
|
|
359
|
+
"1:2",
|
|
360
|
+
"19.5:9",
|
|
361
|
+
"9:19.5",
|
|
362
|
+
"20:9",
|
|
363
|
+
"9:20",
|
|
364
|
+
"auto"
|
|
365
|
+
];
|
|
366
|
+
function createGenerateCommand(generateUseCase) {
|
|
367
|
+
return new Command("generate").description("Generate images from a text prompt").argument("<prompt>", "Text prompt describing the image to generate").option("-a, --aspect-ratio <ratio>", "Aspect ratio", "auto").option("-n, --count <number>", "Number of images (1-10)", "1").option("-o, --output <dir>", "Output directory", "./grok-images").action(async (prompt, options) => {
|
|
368
|
+
const count = parseInt(options.count, 10);
|
|
369
|
+
if (Number.isNaN(count) || count < 1 || count > 10) {
|
|
370
|
+
console.error(chalk.red("Count must be a number between 1 and 10."));
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
|
+
if (!VALID_RATIOS.includes(options.aspectRatio)) {
|
|
374
|
+
console.error(chalk.red(`Invalid aspect ratio. Valid options: ${VALID_RATIOS.join(", ")}`));
|
|
375
|
+
process.exit(1);
|
|
376
|
+
}
|
|
377
|
+
const outputDir = resolve(options.output);
|
|
378
|
+
const spinner = ora(`Generating ${count} image${count > 1 ? "s" : ""}...`).start();
|
|
379
|
+
try {
|
|
380
|
+
const paths = await generateUseCase.execute({
|
|
381
|
+
prompt,
|
|
382
|
+
count,
|
|
383
|
+
aspectRatio: options.aspectRatio
|
|
384
|
+
}, outputDir);
|
|
385
|
+
spinner.succeed(chalk.green(`Generated ${paths.length} image${paths.length > 1 ? "s" : ""}:`));
|
|
386
|
+
for (const p of paths) console.log(chalk.dim(` ${p}`));
|
|
387
|
+
} catch (error) {
|
|
388
|
+
spinner.fail();
|
|
389
|
+
if (error instanceof ApiKeyMissingError) console.error(chalk.red(error.message));
|
|
390
|
+
else if (error instanceof ApiError) console.error(chalk.red(`API Error: ${error.message}`));
|
|
391
|
+
else console.error(chalk.red(`Unexpected error: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
//#endregion
|
|
398
|
+
//#region src/presentation/cli.ts
|
|
399
|
+
function createCli(useCases) {
|
|
400
|
+
const program = new Command().name("grok-img").description("CLI for generating and editing images with Grok API").version("1.0.0");
|
|
401
|
+
program.addCommand(createAuthCommand({
|
|
402
|
+
login: useCases.login,
|
|
403
|
+
logout: useCases.logout,
|
|
404
|
+
getStatus: useCases.getAuthStatus
|
|
405
|
+
}));
|
|
406
|
+
program.addCommand(createGenerateCommand(useCases.generateImage));
|
|
407
|
+
program.addCommand(createEditCommand(useCases.editImage));
|
|
408
|
+
return program;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
//#endregion
|
|
412
|
+
//#region src/main.ts
|
|
413
|
+
const keyStore = new KeychainAdapter();
|
|
414
|
+
const imageGenerator = new GrokApiAdapter();
|
|
415
|
+
const fileStorage = new FileStorageAdapter();
|
|
416
|
+
createCli({
|
|
417
|
+
generateImage: new GenerateImageUseCase(imageGenerator, keyStore, fileStorage),
|
|
418
|
+
editImage: new EditImageUseCase(imageGenerator, keyStore, fileStorage),
|
|
419
|
+
login: new LoginUseCase(keyStore),
|
|
420
|
+
logout: new LogoutUseCase(keyStore),
|
|
421
|
+
getAuthStatus: new GetAuthStatusUseCase(keyStore)
|
|
422
|
+
}).parse();
|
|
423
|
+
|
|
424
|
+
//#endregion
|
|
425
|
+
export { };
|
|
426
|
+
//# sourceMappingURL=main.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.mjs","names":["VALID_RATIOS"],"sources":["../src/domain/errors.ts","../src/application/usecases/edit-image.usecase.ts","../src/application/usecases/generate-image.usecase.ts","../src/application/usecases/get-auth-status.usecase.ts","../src/application/usecases/login.usecase.ts","../src/application/usecases/logout.usecase.ts","../src/infrastructure/adapters/file-storage.adapter.ts","../src/infrastructure/adapters/grok-api.adapter.ts","../src/infrastructure/adapters/keychain.adapter.ts","../src/presentation/commands/auth.command.ts","../src/presentation/commands/edit.command.ts","../src/presentation/commands/generate.command.ts","../src/presentation/cli.ts","../src/main.ts"],"sourcesContent":["export class ApiKeyMissingError extends Error {\n constructor() {\n super(\n \"API key not found. Run `grok-img auth login` or set XAI_API_KEY environment variable.\",\n )\n this.name = \"ApiKeyMissingError\"\n }\n}\n\nexport class ApiError extends Error {\n constructor(\n message: string,\n public readonly cause?: unknown,\n ) {\n super(message)\n this.name = \"ApiError\"\n }\n}\n\nexport class ImageNotFoundError extends Error {\n constructor(path: string) {\n super(`Image not found: ${path}`)\n this.name = \"ImageNotFoundError\"\n }\n}\n","import type { EditParams } from \"../../domain/entities/edit-params.js\"\nimport { ApiKeyMissingError } from \"../../domain/errors.js\"\nimport type { FileStoragePort } from \"../../domain/ports/file-storage.port.js\"\nimport type { ImageGeneratorPort } from \"../../domain/ports/image-generator.port.js\"\nimport type { KeyStorePort } from \"../../domain/ports/key-store.port.js\"\n\nexport class EditImageUseCase {\n constructor(\n private readonly imageGenerator: ImageGeneratorPort,\n private readonly keyStore: KeyStorePort,\n private readonly fileStorage: FileStoragePort,\n ) {}\n\n async execute(params: EditParams, outputDir: string): Promise<string> {\n const apiKey = await this.keyStore.get()\n if (!apiKey) throw new ApiKeyMissingError()\n\n let imageSource = params.imageSource\n if (typeof imageSource === \"string\" && !imageSource.startsWith(\"http\")) {\n imageSource = await this.fileStorage.readImage(imageSource)\n }\n\n this.fileStorage.ensureDir(outputDir)\n\n const result = await this.imageGenerator.edit({ ...params, imageSource }, apiKey)\n\n const outputPath = this.fileStorage.generateOutputPath(outputDir, 0, result.mediaType)\n return this.fileStorage.saveImage(result.uint8Array, outputPath)\n }\n}\n","import type { GenerateParams } from \"../../domain/entities/generate-params.js\"\nimport { ApiKeyMissingError } from \"../../domain/errors.js\"\nimport type { FileStoragePort } from \"../../domain/ports/file-storage.port.js\"\nimport type { ImageGeneratorPort } from \"../../domain/ports/image-generator.port.js\"\nimport type { KeyStorePort } from \"../../domain/ports/key-store.port.js\"\n\nexport class GenerateImageUseCase {\n constructor(\n private readonly imageGenerator: ImageGeneratorPort,\n private readonly keyStore: KeyStorePort,\n private readonly fileStorage: FileStoragePort,\n ) {}\n\n async execute(params: GenerateParams, outputDir: string): Promise<string[]> {\n const apiKey = await this.keyStore.get()\n if (!apiKey) throw new ApiKeyMissingError()\n\n this.fileStorage.ensureDir(outputDir)\n\n const results = await this.imageGenerator.generate(params, apiKey)\n\n const savedPaths: string[] = []\n for (let i = 0; i < results.length; i++) {\n const result = results[i]\n const outputPath = this.fileStorage.generateOutputPath(\n outputDir,\n i,\n result.mediaType,\n )\n const saved = await this.fileStorage.saveImage(result.uint8Array, outputPath)\n savedPaths.push(saved)\n }\n\n return savedPaths\n }\n}\n","import type { KeyStorePort } from \"../../domain/ports/key-store.port.js\"\n\nexport type AuthStatus = {\n authenticated: boolean\n maskedKey: string | null\n source: \"keychain\" | \"env\" | null\n}\n\nexport class GetAuthStatusUseCase {\n constructor(private readonly keyStore: KeyStorePort) {}\n\n async execute(): Promise<AuthStatus> {\n const key = await this.keyStore.get()\n\n if (!key) {\n return { authenticated: false, maskedKey: null, source: null }\n }\n\n const masked =\n key.length > 8\n ? `${key.slice(0, 4)}${\"*\".repeat(key.length - 8)}${key.slice(-4)}`\n : \"****\"\n\n const source = process.env.XAI_API_KEY === key ? \"env\" : \"keychain\"\n\n return { authenticated: true, maskedKey: masked, source }\n }\n}\n","import type { KeyStorePort } from \"../../domain/ports/key-store.port.js\"\n\nexport class LoginUseCase {\n constructor(private readonly keyStore: KeyStorePort) {}\n\n async execute(apiKey: string): Promise<void> {\n await this.keyStore.save(apiKey)\n }\n}\n","import type { KeyStorePort } from \"../../domain/ports/key-store.port.js\"\n\nexport class LogoutUseCase {\n constructor(private readonly keyStore: KeyStorePort) {}\n\n async execute(): Promise<void> {\n await this.keyStore.remove()\n }\n}\n","import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\"\nimport { join } from \"node:path\"\nimport { ImageNotFoundError } from \"../../domain/errors.js\"\nimport type { FileStoragePort } from \"../../domain/ports/file-storage.port.js\"\n\nconst MEDIA_TYPE_EXT: Record<string, string> = {\n \"image/png\": \"png\",\n \"image/jpeg\": \"jpg\",\n \"image/webp\": \"webp\",\n \"image/gif\": \"gif\",\n}\n\nexport class FileStorageAdapter implements FileStoragePort {\n async saveImage(data: Uint8Array, outputPath: string): Promise<string> {\n writeFileSync(outputPath, data)\n return outputPath\n }\n\n async readImage(filePath: string): Promise<Uint8Array> {\n if (!existsSync(filePath)) {\n throw new ImageNotFoundError(filePath)\n }\n return new Uint8Array(readFileSync(filePath))\n }\n\n ensureDir(dir: string): void {\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true })\n }\n }\n\n generateOutputPath(dir: string, index: number, mediaType: string): string {\n const ext = MEDIA_TYPE_EXT[mediaType] ?? \"png\"\n const timestamp = Date.now()\n return join(dir, `grok-img-${timestamp}-${index}.${ext}`)\n }\n}\n","import { createXai } from \"@ai-sdk/xai\"\nimport { generateImage, NoImageGeneratedError } from \"ai\"\nimport type { EditParams } from \"../../domain/entities/edit-params.js\"\nimport type { GenerateParams } from \"../../domain/entities/generate-params.js\"\nimport type { ImageResult } from \"../../domain/entities/image-result.js\"\nimport { ApiError } from \"../../domain/errors.js\"\nimport type { ImageGeneratorPort } from \"../../domain/ports/image-generator.port.js\"\n\nconst MODEL = \"grok-imagine-image\"\n\nexport class GrokApiAdapter implements ImageGeneratorPort {\n async generate(params: GenerateParams, apiKey: string): Promise<ImageResult[]> {\n const xai = createXai({ apiKey })\n\n try {\n const { images } = await generateImage({\n model: xai.image(MODEL),\n prompt: params.prompt,\n aspectRatio: params.aspectRatio,\n n: params.count,\n })\n\n return images.map((img) => ({\n base64: img.base64,\n uint8Array: img.uint8Array,\n mediaType: img.mediaType,\n }))\n } catch (error) {\n if (NoImageGeneratedError.isInstance(error)) {\n throw new ApiError(\n \"Image generation failed: the model could not produce an image.\",\n error,\n )\n }\n if (error instanceof Error) {\n throw new ApiError(`API request failed: ${error.message}`, error)\n }\n throw new ApiError(\"An unexpected error occurred during image generation.\", error)\n }\n }\n\n async edit(params: EditParams, apiKey: string): Promise<ImageResult> {\n const xai = createXai({ apiKey })\n\n try {\n const { image } = await generateImage({\n model: xai.image(MODEL),\n prompt: {\n text: params.prompt,\n images:\n typeof params.imageSource === \"string\"\n ? [new URL(params.imageSource)]\n : [params.imageSource],\n },\n aspectRatio: params.aspectRatio,\n })\n\n return {\n base64: image.base64,\n uint8Array: image.uint8Array,\n mediaType: image.mediaType,\n }\n } catch (error) {\n if (NoImageGeneratedError.isInstance(error)) {\n throw new ApiError(\n \"Image editing failed: the model could not produce an image.\",\n error,\n )\n }\n if (error instanceof Error) {\n throw new ApiError(`API request failed: ${error.message}`, error)\n }\n throw new ApiError(\"An unexpected error occurred during image editing.\", error)\n }\n }\n}\n","import { execFile } from \"node:child_process\"\nimport { promisify } from \"node:util\"\nimport type { KeyStorePort } from \"../../domain/ports/key-store.port.js\"\n\nconst execFileAsync = promisify(execFile)\n\nconst SERVICE = \"grok-image-cli\"\nconst ACCOUNT = \"api-key\"\n\nexport class KeychainAdapter implements KeyStorePort {\n async save(key: string): Promise<void> {\n await execFileAsync(\"security\", [\n \"add-generic-password\",\n \"-U\",\n \"-s\",\n SERVICE,\n \"-a\",\n ACCOUNT,\n \"-w\",\n key,\n ])\n }\n\n async get(): Promise<string | null> {\n try {\n const { stdout } = await execFileAsync(\"security\", [\n \"find-generic-password\",\n \"-s\",\n SERVICE,\n \"-a\",\n ACCOUNT,\n \"-w\",\n ])\n const key = stdout.trim()\n if (key) return key\n } catch {\n // not found in keychain\n }\n\n return process.env.XAI_API_KEY ?? null\n }\n\n async remove(): Promise<void> {\n try {\n await execFileAsync(\"security\", [\n \"delete-generic-password\",\n \"-s\",\n SERVICE,\n \"-a\",\n ACCOUNT,\n ])\n } catch {\n // already removed or never existed\n }\n }\n}\n","import { createInterface } from \"node:readline\"\nimport chalk from \"chalk\"\nimport { Command } from \"commander\"\nimport type { GetAuthStatusUseCase } from \"../../application/usecases/get-auth-status.usecase.js\"\nimport type { LoginUseCase } from \"../../application/usecases/login.usecase.js\"\nimport type { LogoutUseCase } from \"../../application/usecases/logout.usecase.js\"\n\nfunction readInput(prompt: string): Promise<string> {\n const rl = createInterface({\n input: process.stdin,\n output: process.stdout,\n })\n\n return new Promise((resolve) => {\n rl.question(prompt, (answer) => {\n rl.close()\n resolve(answer.trim())\n })\n })\n}\n\nexport function createAuthCommand(useCases: {\n login: LoginUseCase\n logout: LogoutUseCase\n getStatus: GetAuthStatusUseCase\n}): Command {\n const auth = new Command(\"auth\").description(\"Manage API key authentication\")\n\n auth\n .command(\"login\")\n .description(\"Store your xAI API key in the system keychain\")\n .action(async () => {\n try {\n const apiKey = await readInput(chalk.cyan(\"Enter your xAI API key: \"))\n\n if (!apiKey) {\n console.log(chalk.red(\"No API key provided.\"))\n process.exit(1)\n }\n\n await useCases.login.execute(apiKey)\n console.log(chalk.green(\"API key saved to keychain successfully.\"))\n } catch (error) {\n console.error(\n chalk.red(\n `Failed to save API key: ${error instanceof Error ? error.message : \"Unknown error\"}`,\n ),\n )\n process.exit(1)\n }\n })\n\n auth\n .command(\"logout\")\n .description(\"Remove your xAI API key from the system keychain\")\n .action(async () => {\n try {\n await useCases.logout.execute()\n console.log(chalk.green(\"API key removed from keychain.\"))\n } catch (error) {\n console.error(\n chalk.red(\n `Failed to remove API key: ${error instanceof Error ? error.message : \"Unknown error\"}`,\n ),\n )\n process.exit(1)\n }\n })\n\n auth\n .command(\"status\")\n .description(\"Check current authentication status\")\n .action(async () => {\n try {\n const status = await useCases.getStatus.execute()\n\n if (status.authenticated) {\n console.log(chalk.green(\"Authenticated\"))\n console.log(chalk.dim(` Key: ${status.maskedKey}`))\n console.log(chalk.dim(` Source: ${status.source}`))\n } else {\n console.log(chalk.yellow(\"Not authenticated\"))\n console.log(\n chalk.dim(\n \" Run `grok-img auth login` or set XAI_API_KEY environment variable.\",\n ),\n )\n }\n } catch (error) {\n console.error(\n chalk.red(\n `Failed to check status: ${error instanceof Error ? error.message : \"Unknown error\"}`,\n ),\n )\n process.exit(1)\n }\n })\n\n return auth\n}\n","import { resolve } from \"node:path\"\nimport chalk from \"chalk\"\nimport { Command } from \"commander\"\nimport ora from \"ora\"\nimport type { EditImageUseCase } from \"../../application/usecases/edit-image.usecase.js\"\nimport { ApiError, ApiKeyMissingError, ImageNotFoundError } from \"../../domain/errors.js\"\n\nconst VALID_RATIOS = [\n \"1:1\",\n \"16:9\",\n \"9:16\",\n \"4:3\",\n \"3:4\",\n \"3:2\",\n \"2:3\",\n \"2:1\",\n \"1:2\",\n \"19.5:9\",\n \"9:19.5\",\n \"20:9\",\n \"9:20\",\n \"auto\",\n]\n\nexport function createEditCommand(editUseCase: EditImageUseCase): Command {\n return new Command(\"edit\")\n .description(\"Edit an existing image with a text prompt\")\n .argument(\"<prompt>\", \"Text prompt describing the edit to apply\")\n .requiredOption(\"-i, --image <path>\", \"Source image (local file path or URL)\")\n .option(\"-a, --aspect-ratio <ratio>\", \"Aspect ratio\", \"auto\")\n .option(\"-o, --output <dir>\", \"Output directory\", \"./grok-images\")\n .action(async (prompt: string, options) => {\n if (!VALID_RATIOS.includes(options.aspectRatio)) {\n console.error(\n chalk.red(`Invalid aspect ratio. Valid options: ${VALID_RATIOS.join(\", \")}`),\n )\n process.exit(1)\n }\n\n const imageSource = options.image.startsWith(\"http\")\n ? options.image\n : resolve(options.image)\n\n const outputDir = resolve(options.output)\n const spinner = ora(\"Editing image...\").start()\n\n try {\n const savedPath = await editUseCase.execute(\n { prompt, imageSource, aspectRatio: options.aspectRatio },\n outputDir,\n )\n\n spinner.succeed(chalk.green(\"Image edited successfully:\"))\n console.log(chalk.dim(` ${savedPath}`))\n } catch (error) {\n spinner.fail()\n if (error instanceof ApiKeyMissingError) {\n console.error(chalk.red(error.message))\n } else if (error instanceof ImageNotFoundError) {\n console.error(chalk.red(error.message))\n } else if (error instanceof ApiError) {\n console.error(chalk.red(`API Error: ${error.message}`))\n } else {\n console.error(\n chalk.red(\n `Unexpected error: ${error instanceof Error ? error.message : \"Unknown error\"}`,\n ),\n )\n }\n process.exit(1)\n }\n })\n}\n","import { resolve } from \"node:path\"\nimport chalk from \"chalk\"\nimport { Command } from \"commander\"\nimport ora from \"ora\"\nimport type { GenerateImageUseCase } from \"../../application/usecases/generate-image.usecase.js\"\nimport { ApiError, ApiKeyMissingError } from \"../../domain/errors.js\"\n\nconst VALID_RATIOS = [\n \"1:1\",\n \"16:9\",\n \"9:16\",\n \"4:3\",\n \"3:4\",\n \"3:2\",\n \"2:3\",\n \"2:1\",\n \"1:2\",\n \"19.5:9\",\n \"9:19.5\",\n \"20:9\",\n \"9:20\",\n \"auto\",\n]\n\nexport function createGenerateCommand(generateUseCase: GenerateImageUseCase): Command {\n return new Command(\"generate\")\n .description(\"Generate images from a text prompt\")\n .argument(\"<prompt>\", \"Text prompt describing the image to generate\")\n .option(\"-a, --aspect-ratio <ratio>\", \"Aspect ratio\", \"auto\")\n .option(\"-n, --count <number>\", \"Number of images (1-10)\", \"1\")\n .option(\"-o, --output <dir>\", \"Output directory\", \"./grok-images\")\n .action(async (prompt: string, options) => {\n const count = parseInt(options.count, 10)\n if (Number.isNaN(count) || count < 1 || count > 10) {\n console.error(chalk.red(\"Count must be a number between 1 and 10.\"))\n process.exit(1)\n }\n\n if (!VALID_RATIOS.includes(options.aspectRatio)) {\n console.error(\n chalk.red(`Invalid aspect ratio. Valid options: ${VALID_RATIOS.join(\", \")}`),\n )\n process.exit(1)\n }\n\n const outputDir = resolve(options.output)\n const spinner = ora(`Generating ${count} image${count > 1 ? \"s\" : \"\"}...`).start()\n\n try {\n const paths = await generateUseCase.execute(\n { prompt, count, aspectRatio: options.aspectRatio },\n outputDir,\n )\n\n spinner.succeed(\n chalk.green(`Generated ${paths.length} image${paths.length > 1 ? \"s\" : \"\"}:`),\n )\n for (const p of paths) {\n console.log(chalk.dim(` ${p}`))\n }\n } catch (error) {\n spinner.fail()\n if (error instanceof ApiKeyMissingError) {\n console.error(chalk.red(error.message))\n } else if (error instanceof ApiError) {\n console.error(chalk.red(`API Error: ${error.message}`))\n } else {\n console.error(\n chalk.red(\n `Unexpected error: ${error instanceof Error ? error.message : \"Unknown error\"}`,\n ),\n )\n }\n process.exit(1)\n }\n })\n}\n","import { Command } from \"commander\"\nimport type { EditImageUseCase } from \"../application/usecases/edit-image.usecase.js\"\nimport type { GenerateImageUseCase } from \"../application/usecases/generate-image.usecase.js\"\nimport type { GetAuthStatusUseCase } from \"../application/usecases/get-auth-status.usecase.js\"\nimport type { LoginUseCase } from \"../application/usecases/login.usecase.js\"\nimport type { LogoutUseCase } from \"../application/usecases/logout.usecase.js\"\nimport { createAuthCommand } from \"./commands/auth.command.js\"\nimport { createEditCommand } from \"./commands/edit.command.js\"\nimport { createGenerateCommand } from \"./commands/generate.command.js\"\n\nexport type UseCases = {\n generateImage: GenerateImageUseCase\n editImage: EditImageUseCase\n login: LoginUseCase\n logout: LogoutUseCase\n getAuthStatus: GetAuthStatusUseCase\n}\n\nexport function createCli(useCases: UseCases): Command {\n const program = new Command()\n .name(\"grok-img\")\n .description(\"CLI for generating and editing images with Grok API\")\n .version(\"1.0.0\")\n\n program.addCommand(\n createAuthCommand({\n login: useCases.login,\n logout: useCases.logout,\n getStatus: useCases.getAuthStatus,\n }),\n )\n\n program.addCommand(createGenerateCommand(useCases.generateImage))\n program.addCommand(createEditCommand(useCases.editImage))\n\n return program\n}\n","import { EditImageUseCase } from \"./application/usecases/edit-image.usecase.js\"\nimport { GenerateImageUseCase } from \"./application/usecases/generate-image.usecase.js\"\nimport { GetAuthStatusUseCase } from \"./application/usecases/get-auth-status.usecase.js\"\nimport { LoginUseCase } from \"./application/usecases/login.usecase.js\"\nimport { LogoutUseCase } from \"./application/usecases/logout.usecase.js\"\nimport { FileStorageAdapter } from \"./infrastructure/adapters/file-storage.adapter.js\"\nimport { GrokApiAdapter } from \"./infrastructure/adapters/grok-api.adapter.js\"\nimport { KeychainAdapter } from \"./infrastructure/adapters/keychain.adapter.js\"\nimport { createCli } from \"./presentation/cli.js\"\n\nconst keyStore = new KeychainAdapter()\nconst imageGenerator = new GrokApiAdapter()\nconst fileStorage = new FileStorageAdapter()\n\nconst generateImageUseCase = new GenerateImageUseCase(\n imageGenerator,\n keyStore,\n fileStorage,\n)\nconst editImageUseCase = new EditImageUseCase(imageGenerator, keyStore, fileStorage)\nconst loginUseCase = new LoginUseCase(keyStore)\nconst logoutUseCase = new LogoutUseCase(keyStore)\nconst getAuthStatusUseCase = new GetAuthStatusUseCase(keyStore)\n\nconst program = createCli({\n generateImage: generateImageUseCase,\n editImage: editImageUseCase,\n login: loginUseCase,\n logout: logoutUseCase,\n getAuthStatus: getAuthStatusUseCase,\n})\n\nprogram.parse()\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "grok-image-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for generating and editing images with Grok API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"grok-img": "./dist/main.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsdown",
|
|
16
|
+
"dev": "tsdown --watch",
|
|
17
|
+
"start": "node dist/main.mjs",
|
|
18
|
+
"lint": "biome check .",
|
|
19
|
+
"lint:fix": "biome check --write .",
|
|
20
|
+
"format": "biome format --write .",
|
|
21
|
+
"prepublishOnly": "npm run lint && npm run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"grok",
|
|
25
|
+
"xai",
|
|
26
|
+
"image",
|
|
27
|
+
"cli",
|
|
28
|
+
"ai",
|
|
29
|
+
"image-generation",
|
|
30
|
+
"image-editing"
|
|
31
|
+
],
|
|
32
|
+
"author": "",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/cyberash-dev/grok-image-cli.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/cyberash-dev/grok-image-cli#readme",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/cyberash-dev/grok-image-cli/issues"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20.19.0"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@ai-sdk/xai": "3.0.53",
|
|
47
|
+
"ai": "6.0.80",
|
|
48
|
+
"chalk": "5.6.2",
|
|
49
|
+
"commander": "13.1.0",
|
|
50
|
+
"ora": "8.2.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@biomejs/biome": "2.3.14",
|
|
54
|
+
"@types/node": "22.19.11",
|
|
55
|
+
"tsdown": "0.20.3",
|
|
56
|
+
"typescript": "5.9.3"
|
|
57
|
+
}
|
|
58
|
+
}
|