ima2-gen 1.0.1 → 1.0.3
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 +211 -40
- package/assets/screenshot.png +0 -0
- package/bin/commands/edit.js +70 -0
- package/bin/commands/gen.js +136 -0
- package/bin/commands/ls.js +49 -0
- package/bin/commands/ping.js +28 -0
- package/bin/commands/ps.js +46 -0
- package/bin/commands/show.js +57 -0
- package/bin/ima2.js +206 -12
- package/bin/lib/args.js +73 -0
- package/bin/lib/client.js +97 -0
- package/bin/lib/files.js +39 -0
- package/bin/lib/output.js +48 -0
- package/package.json +11 -4
- package/server.js +639 -83
- package/ui/dist/assets/index-CGvmo0q2.js +16 -0
- package/ui/dist/assets/index-CGvmo0q2.js.map +1 -0
- package/ui/dist/assets/index-Dr1O_KZg.css +1 -0
- package/ui/dist/index.html +24 -0
- package/public/index.html +0 -1008
package/bin/ima2.js
CHANGED
|
@@ -4,12 +4,20 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
|
4
4
|
import { join, dirname } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { spawn, execSync } from "child_process";
|
|
7
|
+
import { networkInterfaces, homedir } from "os";
|
|
7
8
|
|
|
8
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
const ROOT = join(__dirname, "..");
|
|
11
|
+
const HOME = homedir();
|
|
10
12
|
const CONFIG_DIR = join(ROOT, ".ima2");
|
|
11
13
|
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
12
14
|
|
|
15
|
+
// Load package.json for version
|
|
16
|
+
let pkg = { version: "?", name: "ima2-gen" };
|
|
17
|
+
try {
|
|
18
|
+
pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8"));
|
|
19
|
+
} catch {}
|
|
20
|
+
|
|
13
21
|
function loadConfig() {
|
|
14
22
|
if (existsSync(CONFIG_FILE)) {
|
|
15
23
|
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
@@ -52,8 +60,8 @@ async function setup() {
|
|
|
52
60
|
|
|
53
61
|
// Check if codex auth exists
|
|
54
62
|
const hasAuth =
|
|
55
|
-
existsSync(join(
|
|
56
|
-
existsSync(join(
|
|
63
|
+
existsSync(join(HOME, ".codex", "auth.json")) ||
|
|
64
|
+
existsSync(join(HOME, ".chatgpt-local", "auth.json"));
|
|
57
65
|
|
|
58
66
|
if (!hasAuth) {
|
|
59
67
|
console.log(" Running 'codex login' — follow the browser prompt.\n");
|
|
@@ -83,6 +91,25 @@ async function serve() {
|
|
|
83
91
|
config = await setup();
|
|
84
92
|
}
|
|
85
93
|
|
|
94
|
+
// Ensure ui/dist exists — if missing, auto-build (dev) or error (installed pkg)
|
|
95
|
+
const distIndex = join(ROOT, "ui", "dist", "index.html");
|
|
96
|
+
if (!existsSync(distIndex)) {
|
|
97
|
+
const hasUiSrc = existsSync(join(ROOT, "ui", "package.json"));
|
|
98
|
+
if (hasUiSrc) {
|
|
99
|
+
console.log("\n ui/dist missing — running 'npm run build' first...\n");
|
|
100
|
+
try {
|
|
101
|
+
execSync("npm run build", { stdio: "inherit", cwd: ROOT });
|
|
102
|
+
} catch {
|
|
103
|
+
console.log("\n Build failed. Try: cd ui && npm install && npm run build\n");
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
console.log("\n ui/dist not found and ui/ source is missing.");
|
|
108
|
+
console.log(" This installation appears broken. Reinstall: npm i -g ima2-gen\n");
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
86
113
|
const env = { ...process.env };
|
|
87
114
|
|
|
88
115
|
if (config.provider === "api" && config.apiKey) {
|
|
@@ -102,10 +129,161 @@ async function serve() {
|
|
|
102
129
|
process.on("SIGTERM", () => child.kill("SIGTERM"));
|
|
103
130
|
}
|
|
104
131
|
|
|
132
|
+
async function showStatus() {
|
|
133
|
+
const config = loadConfig();
|
|
134
|
+
console.log(`\n ${pkg.name} v${pkg.version}\n`);
|
|
135
|
+
console.log(` Config file: ${CONFIG_FILE}`);
|
|
136
|
+
console.log(` Exists: ${existsSync(CONFIG_FILE) ? "yes" : "no"}\n`);
|
|
137
|
+
|
|
138
|
+
if (config.provider) {
|
|
139
|
+
console.log(` Provider: ${config.provider}`);
|
|
140
|
+
if (config.provider === "api") {
|
|
141
|
+
const key = config.apiKey || "";
|
|
142
|
+
console.log(` API Key: ${key ? key.slice(0, 8) + "..." + key.slice(-4) : "not set"}`);
|
|
143
|
+
}
|
|
144
|
+
console.log("");
|
|
145
|
+
} else {
|
|
146
|
+
console.log(" Status: not configured");
|
|
147
|
+
console.log(" Run 'ima2 setup' to configure.\n");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Check OAuth auth files
|
|
151
|
+
const hasCodexAuth = existsSync(join(HOME, ".codex", "auth.json"));
|
|
152
|
+
const hasChatgptAuth = existsSync(join(HOME, ".chatgpt-local", "auth.json"));
|
|
153
|
+
console.log(` OAuth sessions:`);
|
|
154
|
+
console.log(` ~/.codex/auth.json ${hasCodexAuth ? "✓" : "✗"}`);
|
|
155
|
+
console.log(` ~/.chatgpt-local/auth.json ${hasChatgptAuth ? "✓" : "✗"}`);
|
|
156
|
+
console.log("");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function doctor() {
|
|
160
|
+
console.log(`\n ${pkg.name} v${pkg.version} — Doctor\n`);
|
|
161
|
+
|
|
162
|
+
let ok = 0;
|
|
163
|
+
let fail = 0;
|
|
164
|
+
|
|
165
|
+
// Node version
|
|
166
|
+
const nodeVersion = process.version;
|
|
167
|
+
const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0]);
|
|
168
|
+
if (nodeMajor >= 18) {
|
|
169
|
+
console.log(` ✓ Node.js ${nodeVersion} (>= 18)`);
|
|
170
|
+
ok++;
|
|
171
|
+
} else {
|
|
172
|
+
console.log(` ✗ Node.js ${nodeVersion} (requires >= 18)`);
|
|
173
|
+
fail++;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// package.json exists
|
|
177
|
+
if (existsSync(join(ROOT, "package.json"))) {
|
|
178
|
+
console.log(" ✓ package.json found");
|
|
179
|
+
ok++;
|
|
180
|
+
} else {
|
|
181
|
+
console.log(" ✗ package.json missing");
|
|
182
|
+
fail++;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// node_modules
|
|
186
|
+
if (existsSync(join(ROOT, "node_modules"))) {
|
|
187
|
+
console.log(" ✓ node_modules installed");
|
|
188
|
+
ok++;
|
|
189
|
+
} else {
|
|
190
|
+
console.log(" ✗ node_modules missing — run 'npm install'");
|
|
191
|
+
fail++;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// .env
|
|
195
|
+
if (existsSync(join(ROOT, ".env"))) {
|
|
196
|
+
console.log(" ✓ .env file exists");
|
|
197
|
+
ok++;
|
|
198
|
+
} else {
|
|
199
|
+
console.log(" ⚠ .env file not found (optional — copy from .env.example)");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Config
|
|
203
|
+
const config = loadConfig();
|
|
204
|
+
if (config.provider) {
|
|
205
|
+
console.log(` ✓ Configured: ${config.provider}`);
|
|
206
|
+
ok++;
|
|
207
|
+
} else {
|
|
208
|
+
console.log(" ⚠ Not configured — run 'ima2 setup'");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Port availability (simple check)
|
|
212
|
+
const port = process.env.PORT || 3333;
|
|
213
|
+
console.log(` ℹ Default port: ${port}`);
|
|
214
|
+
|
|
215
|
+
console.log(`\n ${ok} passed, ${fail} failed\n`);
|
|
216
|
+
process.exit(fail > 0 ? 1 : 0);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function openBrowser() {
|
|
220
|
+
const port = process.env.PORT || 3333;
|
|
221
|
+
const url = `http://localhost:${port}`;
|
|
222
|
+
|
|
223
|
+
const platform = process.platform;
|
|
224
|
+
let cmd;
|
|
225
|
+
if (platform === "darwin") cmd = "open";
|
|
226
|
+
else if (platform === "win32") cmd = "start";
|
|
227
|
+
else cmd = "xdg-open";
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
execSync(`${cmd} ${url}`, { stdio: "ignore" });
|
|
231
|
+
console.log(`\n Opening ${url} ...\n`);
|
|
232
|
+
} catch {
|
|
233
|
+
console.log(`\n Could not open browser. Visit: ${url}\n`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function showHelp() {
|
|
238
|
+
console.log(`
|
|
239
|
+
${pkg.name} v${pkg.version} — GPT Image 2 Generator
|
|
240
|
+
|
|
241
|
+
Usage: ima2 <command> [options]
|
|
242
|
+
|
|
243
|
+
Server commands:
|
|
244
|
+
serve Start the image generation server
|
|
245
|
+
setup, login Configure API key or OAuth (interactive)
|
|
246
|
+
status Show current configuration status
|
|
247
|
+
doctor Diagnose environment and setup
|
|
248
|
+
open Open web UI in browser
|
|
249
|
+
reset Reset configuration
|
|
250
|
+
|
|
251
|
+
Client commands (require a running 'ima2 serve'):
|
|
252
|
+
gen <prompt> Generate image(s) from prompt (ima2 gen --help)
|
|
253
|
+
edit <file> Edit an existing image (ima2 edit --help)
|
|
254
|
+
ls List recent history (ima2 ls --help)
|
|
255
|
+
show <name> Show one history item (ima2 show --help)
|
|
256
|
+
ps List active jobs (ima2 ps --help)
|
|
257
|
+
ping Ping running server / check health
|
|
258
|
+
|
|
259
|
+
Options:
|
|
260
|
+
-v, --version Show version
|
|
261
|
+
-h, --help Show help
|
|
262
|
+
|
|
263
|
+
Examples:
|
|
264
|
+
ima2 serve Start server
|
|
265
|
+
ima2 gen "a shiba in space" Generate from CLI
|
|
266
|
+
ima2 gen "merge" --ref a.png --ref b.png -q high -o out.png
|
|
267
|
+
ima2 ls -n 10 Last 10 generations
|
|
268
|
+
ima2 ping Health check
|
|
269
|
+
`);
|
|
270
|
+
}
|
|
271
|
+
|
|
105
272
|
// ── CLI ──
|
|
106
273
|
const args = process.argv.slice(2);
|
|
107
274
|
const command = args[0];
|
|
108
275
|
|
|
276
|
+
if (args.includes("-v") || args.includes("--version")) {
|
|
277
|
+
console.log(pkg.version);
|
|
278
|
+
process.exit(0);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if ((!command || args.includes("-h") || args.includes("--help"))
|
|
282
|
+
&& !["gen", "edit", "ls", "show", "ps", "ping"].includes(command)) {
|
|
283
|
+
showHelp();
|
|
284
|
+
process.exit(command ? 0 : 1);
|
|
285
|
+
}
|
|
286
|
+
|
|
109
287
|
switch (command) {
|
|
110
288
|
case "serve":
|
|
111
289
|
serve();
|
|
@@ -114,21 +292,37 @@ switch (command) {
|
|
|
114
292
|
case "login":
|
|
115
293
|
setup().then(() => console.log(" Done. Run 'ima2 serve' to start."));
|
|
116
294
|
break;
|
|
295
|
+
case "status":
|
|
296
|
+
showStatus();
|
|
297
|
+
break;
|
|
298
|
+
case "doctor":
|
|
299
|
+
doctor();
|
|
300
|
+
break;
|
|
301
|
+
case "open":
|
|
302
|
+
openBrowser();
|
|
303
|
+
break;
|
|
117
304
|
case "reset":
|
|
118
305
|
if (existsSync(CONFIG_FILE)) {
|
|
119
306
|
writeFileSync(CONFIG_FILE, "{}");
|
|
120
307
|
console.log(" Config reset. Run 'ima2 serve' to reconfigure.");
|
|
308
|
+
} else {
|
|
309
|
+
console.log(" No config to reset.");
|
|
121
310
|
}
|
|
122
311
|
break;
|
|
312
|
+
case "gen":
|
|
313
|
+
case "edit":
|
|
314
|
+
case "ls":
|
|
315
|
+
case "show":
|
|
316
|
+
case "ps":
|
|
317
|
+
case "ping": {
|
|
318
|
+
const { setCliVersion } = await import("./lib/client.js");
|
|
319
|
+
setCliVersion(pkg.version);
|
|
320
|
+
const mod = await import(`./commands/${command}.js`);
|
|
321
|
+
await mod.default(args.slice(1));
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
123
324
|
default:
|
|
124
|
-
console.log(`
|
|
125
|
-
ima2
|
|
126
|
-
|
|
127
|
-
Usage:
|
|
128
|
-
ima2 serve Start the image generation server
|
|
129
|
-
ima2 setup Configure API key or OAuth (interactive)
|
|
130
|
-
ima2 reset Reset configuration
|
|
131
|
-
|
|
132
|
-
First run of 'ima2 serve' will prompt for setup automatically.
|
|
133
|
-
`);
|
|
325
|
+
console.log(` Unknown command: "${command}"`);
|
|
326
|
+
console.log(" Run 'ima2 --help' for usage.\n");
|
|
327
|
+
process.exit(1);
|
|
134
328
|
}
|
package/bin/lib/args.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Tiny argv parser — no dependencies.
|
|
2
|
+
// Supports: --long, --long=val, --long val, -s, -s val, repeatable flags, positional, --.
|
|
3
|
+
|
|
4
|
+
export function parseArgs(argv, spec = {}) {
|
|
5
|
+
const shortMap = {};
|
|
6
|
+
for (const [name, def] of Object.entries(spec.flags || {})) {
|
|
7
|
+
if (def.short) shortMap[def.short] = name;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const out = { positional: [], _unknown: [] };
|
|
11
|
+
for (const [name, def] of Object.entries(spec.flags || {})) {
|
|
12
|
+
if (def.repeatable) out[name] = [];
|
|
13
|
+
else if ("default" in def) out[name] = def.default;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let i = 0;
|
|
17
|
+
let doubleDashSeen = false;
|
|
18
|
+
while (i < argv.length) {
|
|
19
|
+
const a = argv[i];
|
|
20
|
+
if (doubleDashSeen) {
|
|
21
|
+
out.positional.push(a);
|
|
22
|
+
i++;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (a === "--") {
|
|
26
|
+
doubleDashSeen = true;
|
|
27
|
+
i++;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (a.startsWith("--")) {
|
|
31
|
+
const eq = a.indexOf("=");
|
|
32
|
+
const name = eq > -1 ? a.slice(2, eq) : a.slice(2);
|
|
33
|
+
const def = (spec.flags || {})[name];
|
|
34
|
+
if (!def) {
|
|
35
|
+
out._unknown.push(a);
|
|
36
|
+
i++;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (def.type === "boolean") {
|
|
40
|
+
out[name] = true;
|
|
41
|
+
i++;
|
|
42
|
+
} else {
|
|
43
|
+
const val = eq > -1 ? a.slice(eq + 1) : argv[i + 1];
|
|
44
|
+
if (eq === -1) i++;
|
|
45
|
+
if (def.repeatable) out[name].push(val);
|
|
46
|
+
else out[name] = val;
|
|
47
|
+
i++;
|
|
48
|
+
}
|
|
49
|
+
} else if (a.startsWith("-") && a.length > 1 && !/^-\d/.test(a)) {
|
|
50
|
+
const short = a.slice(1);
|
|
51
|
+
const name = shortMap[short];
|
|
52
|
+
if (!name) {
|
|
53
|
+
out._unknown.push(a);
|
|
54
|
+
i++;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const def = spec.flags[name];
|
|
58
|
+
if (def.type === "boolean") {
|
|
59
|
+
out[name] = true;
|
|
60
|
+
i++;
|
|
61
|
+
} else {
|
|
62
|
+
const val = argv[i + 1];
|
|
63
|
+
if (def.repeatable) out[name].push(val);
|
|
64
|
+
else out[name] = val;
|
|
65
|
+
i += 2;
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
out.positional.push(a);
|
|
69
|
+
i++;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_PORT = 3333;
|
|
6
|
+
|
|
7
|
+
function readAdvertise() {
|
|
8
|
+
const p = join(homedir(), ".ima2", "server.json");
|
|
9
|
+
if (!existsSync(p)) return null;
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(readFileSync(p, "utf-8"));
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function probe(base, timeoutMs = 600) {
|
|
18
|
+
try {
|
|
19
|
+
const r = await fetch(`${base}/api/health`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
20
|
+
if (!r.ok) return null;
|
|
21
|
+
return await r.json();
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function resolveServer({ serverFlag } = {}) {
|
|
28
|
+
if (serverFlag) {
|
|
29
|
+
const base = serverFlag.replace(/\/$/, "");
|
|
30
|
+
const health = await probe(base);
|
|
31
|
+
if (health) return { base, health };
|
|
32
|
+
const err = new Error(`server unreachable at ${base}`);
|
|
33
|
+
err.code = "SERVER_UNREACHABLE";
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
const candidates = [];
|
|
37
|
+
if (process.env.IMA2_SERVER) candidates.push(process.env.IMA2_SERVER.replace(/\/$/, ""));
|
|
38
|
+
const adv = readAdvertise();
|
|
39
|
+
if (adv?.port) candidates.push(`http://localhost:${adv.port}`);
|
|
40
|
+
candidates.push(`http://localhost:${DEFAULT_PORT}`);
|
|
41
|
+
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
const uniq = candidates.filter((c) => !seen.has(c) && seen.add(c));
|
|
44
|
+
|
|
45
|
+
for (const base of uniq) {
|
|
46
|
+
const health = await probe(base);
|
|
47
|
+
if (health) return { base, health };
|
|
48
|
+
}
|
|
49
|
+
const err = new Error("server unreachable — is 'ima2 serve' running?");
|
|
50
|
+
err.code = "SERVER_UNREACHABLE";
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function request(base, path, { method = "GET", body, timeoutMs = 180_000 } = {}) {
|
|
55
|
+
const res = await fetch(base + path, {
|
|
56
|
+
method,
|
|
57
|
+
headers: {
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
"X-ima2-client": `cli/${CLI_VERSION}`,
|
|
60
|
+
},
|
|
61
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
62
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
63
|
+
});
|
|
64
|
+
const text = await res.text();
|
|
65
|
+
let json = null;
|
|
66
|
+
try { json = JSON.parse(text); } catch {}
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
const err = new Error(json?.error || `HTTP ${res.status}`);
|
|
69
|
+
err.status = res.status;
|
|
70
|
+
err.code = json?.code || null;
|
|
71
|
+
err.body = json || text;
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
return json;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function normalizeGenerate(resp) {
|
|
78
|
+
if (!resp) return { images: [], elapsed: null, requestId: null };
|
|
79
|
+
if (Array.isArray(resp.images)) {
|
|
80
|
+
return {
|
|
81
|
+
images: resp.images.map((it) => ({ image: it.image, filename: it.filename })),
|
|
82
|
+
elapsed: resp.elapsed ?? null,
|
|
83
|
+
requestId: resp.requestId ?? null,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (resp.image) {
|
|
87
|
+
return {
|
|
88
|
+
images: [{ image: resp.image, filename: resp.filename || null }],
|
|
89
|
+
elapsed: resp.elapsed ?? null,
|
|
90
|
+
requestId: resp.requestId ?? null,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return { images: [], elapsed: resp.elapsed ?? null, requestId: resp.requestId ?? null };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export let CLI_VERSION = "dev";
|
|
97
|
+
export function setCliVersion(v) { CLI_VERSION = v; }
|
package/bin/lib/files.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { dirname, extname, basename, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const MIME = {
|
|
5
|
+
png: "image/png",
|
|
6
|
+
jpg: "image/jpeg",
|
|
7
|
+
jpeg: "image/jpeg",
|
|
8
|
+
webp: "image/webp",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export async function fileToDataUri(path) {
|
|
12
|
+
const b64 = (await readFile(path)).toString("base64");
|
|
13
|
+
const ext = extname(path).slice(1).toLowerCase();
|
|
14
|
+
const mime = MIME[ext] || "image/png";
|
|
15
|
+
return `data:${mime};base64,${b64}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function dataUriToFile(dataUri, outPath) {
|
|
19
|
+
const m = dataUri.match(/^data:([^;]+);base64,(.+)$/);
|
|
20
|
+
const raw = m ? m[2] : dataUri;
|
|
21
|
+
await mkdir(dirname(outPath) || ".", { recursive: true });
|
|
22
|
+
await writeFile(outPath, Buffer.from(raw, "base64"));
|
|
23
|
+
return outPath;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function defaultOutName(index, total, ext = "png") {
|
|
27
|
+
const now = new Date();
|
|
28
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
29
|
+
const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
30
|
+
if (total <= 1) return `ima2-${stamp}.${ext}`;
|
|
31
|
+
return `ima2-${stamp}-${index}.${ext}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function readStdin() {
|
|
35
|
+
if (process.stdin.isTTY) return "";
|
|
36
|
+
const chunks = [];
|
|
37
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
38
|
+
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
39
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const isTty = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
2
|
+
|
|
3
|
+
export const color = {
|
|
4
|
+
dim: (s) => (isTty ? `\x1b[2m${s}\x1b[0m` : s),
|
|
5
|
+
bold: (s) => (isTty ? `\x1b[1m${s}\x1b[0m` : s),
|
|
6
|
+
red: (s) => (isTty ? `\x1b[31m${s}\x1b[0m` : s),
|
|
7
|
+
green: (s) => (isTty ? `\x1b[32m${s}\x1b[0m` : s),
|
|
8
|
+
yellow: (s) => (isTty ? `\x1b[33m${s}\x1b[0m` : s),
|
|
9
|
+
cyan: (s) => (isTty ? `\x1b[36m${s}\x1b[0m` : s),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function out(msg = "") { process.stdout.write(msg + "\n"); }
|
|
13
|
+
export function err(msg = "") { process.stderr.write(msg + "\n"); }
|
|
14
|
+
|
|
15
|
+
export function die(code, msg) {
|
|
16
|
+
if (msg) err(color.red("✗ ") + msg);
|
|
17
|
+
process.exit(code);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function json(obj) {
|
|
21
|
+
process.stdout.write(JSON.stringify(obj) + "\n");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function table(rows, columns) {
|
|
25
|
+
if (rows.length === 0) return;
|
|
26
|
+
const widths = columns.map((c) =>
|
|
27
|
+
Math.max(c.label.length, ...rows.map((r) => {
|
|
28
|
+
const v = c.format ? c.format(r[c.key], r) : r[c.key];
|
|
29
|
+
return String(v ?? "").length;
|
|
30
|
+
})),
|
|
31
|
+
);
|
|
32
|
+
const pad = (s, w) => String(s ?? "").padEnd(w);
|
|
33
|
+
out(color.dim(columns.map((c, i) => pad(c.label, widths[i])).join(" ")));
|
|
34
|
+
out(color.dim(widths.map((w) => "─".repeat(w)).join(" ")));
|
|
35
|
+
for (const r of rows) {
|
|
36
|
+
out(columns.map((c, i) => pad(c.format ? c.format(r[c.key], r) : r[c.key], widths[i])).join(" "));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function exitCodeForError(e) {
|
|
41
|
+
if (e.code === "SERVER_UNREACHABLE") return 3;
|
|
42
|
+
if (e.code === "APIKEY_DISABLED") return 4;
|
|
43
|
+
if (e.code === "SAFETY_REFUSAL") return 7;
|
|
44
|
+
if (e.name === "TimeoutError" || /abort/i.test(e.message || "")) return 8;
|
|
45
|
+
if (e.status >= 500) return 6;
|
|
46
|
+
if (e.status >= 400) return 5;
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ima2-gen",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "GPT Image 2 generator with OAuth & API key support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,8 +9,13 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/ima2.js serve",
|
|
11
11
|
"dev": "node --watch server.js",
|
|
12
|
+
"ui:install": "cd ui && npm install",
|
|
13
|
+
"ui:dev": "cd ui && npm run dev",
|
|
14
|
+
"ui:build": "cd ui && npm run build",
|
|
15
|
+
"build": "npm run ui:build",
|
|
16
|
+
"test": "node --test tests/**/*.test.js",
|
|
12
17
|
"setup": "node bin/ima2.js setup",
|
|
13
|
-
"prepublishOnly": "npm run lint:pkg",
|
|
18
|
+
"prepublishOnly": "npm run build && npm run lint:pkg",
|
|
14
19
|
"lint:pkg": "node -e \"const p=require('./package.json'); if(!p.name||!p.version||!p.bin) throw new Error('missing fields')\"",
|
|
15
20
|
"release:patch": "npm version patch && npm publish && git push origin main --tags",
|
|
16
21
|
"release:minor": "npm version minor && npm publish && git push origin main --tags",
|
|
@@ -30,7 +35,7 @@
|
|
|
30
35
|
},
|
|
31
36
|
"files": [
|
|
32
37
|
"bin/",
|
|
33
|
-
"
|
|
38
|
+
"ui/dist/",
|
|
34
39
|
"assets/",
|
|
35
40
|
"server.js",
|
|
36
41
|
".env.example",
|
|
@@ -40,9 +45,11 @@
|
|
|
40
45
|
"node": ">=18"
|
|
41
46
|
},
|
|
42
47
|
"dependencies": {
|
|
48
|
+
"better-sqlite3": "^12.9.0",
|
|
43
49
|
"dotenv": "^17.4.2",
|
|
44
50
|
"express": "^5.1.0",
|
|
45
51
|
"openai": "^5.8.2",
|
|
46
|
-
"openai-oauth": "^1.0.2"
|
|
52
|
+
"openai-oauth": "^1.0.2",
|
|
53
|
+
"ulid": "^3.0.2"
|
|
47
54
|
}
|
|
48
55
|
}
|