ima2-gen 1.1.13 → 1.1.14
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 +10 -1
- package/bin/commands/doctor.js +195 -0
- package/bin/commands/doctor.ts +202 -0
- package/bin/ima2.js +3 -105
- package/bin/ima2.ts +3 -109
- package/config.js +1 -0
- package/config.ts +5 -0
- package/docs/CLI.md +36 -0
- package/docs/FAQ.ko.md +82 -2
- package/docs/FAQ.md +85 -2
- package/docs/PROMPT_STUDIO.ko.md +111 -0
- package/docs/PROMPT_STUDIO.md +115 -0
- package/docs/README.ko.md +8 -1
- package/docs/migration/runtime-test-inventory.md +6 -1
- package/lib/agentRuntime.js +9 -2
- package/lib/agentRuntime.ts +8 -2
- package/lib/errorClassify.js +1 -1
- package/lib/errorClassify.ts +1 -1
- package/lib/generationErrors.js +121 -23
- package/lib/generationErrors.ts +100 -13
- package/lib/responsesDoctor.js +386 -0
- package/lib/responsesDoctor.ts +456 -0
- package/lib/responsesErrors.js +57 -0
- package/lib/responsesErrors.ts +83 -0
- package/lib/responsesFallback.js +72 -0
- package/lib/responsesFallback.ts +114 -0
- package/lib/responsesImageAdapter.js +121 -174
- package/lib/responsesImageAdapter.ts +136 -211
- package/lib/responsesParse.js +324 -0
- package/lib/responsesParse.ts +452 -0
- package/lib/responsesTools.js +15 -0
- package/lib/responsesTools.ts +28 -0
- package/package.json +1 -1
- package/routes/edit.js +26 -1
- package/routes/edit.ts +26 -1
- package/routes/generate.js +40 -0
- package/routes/generate.ts +47 -0
- package/ui/dist/.vite/manifest.json +12 -12
- package/ui/dist/assets/{AgentWorkspace-BJe9yxPA.js → AgentWorkspace-B6YNOZHi.js} +1 -1
- package/ui/dist/assets/{CardNewsWorkspace-BBLdwzYU.js → CardNewsWorkspace-EFVeg4l_.js} +1 -1
- package/ui/dist/assets/{NodeCanvas-BSZ527J4.js → NodeCanvas-iM6yjHvO.js} +1 -1
- package/ui/dist/assets/{PromptBuilderPanel-Y2VygFc0.js → PromptBuilderPanel-C3GdLDCl.js} +1 -1
- package/ui/dist/assets/{PromptImportDialog-C6lFV-LL.js → PromptImportDialog-DS9vrc_w.js} +2 -2
- package/ui/dist/assets/{PromptImportDiscoverySection-D8YJFhND.js → PromptImportDiscoverySection-DHFEt_FA.js} +1 -1
- package/ui/dist/assets/{PromptImportFolderSection-ywfcQolW.js → PromptImportFolderSection-BQxb1zs5.js} +1 -1
- package/ui/dist/assets/{PromptLibraryPanel-fk4KmrGy.js → PromptLibraryPanel-NhMKVGfU.js} +2 -2
- package/ui/dist/assets/{SettingsWorkspace-DL5vhAHQ.js → SettingsWorkspace-FjKjaDqj.js} +1 -1
- package/ui/dist/assets/index-BAN6lKgf.js +28 -0
- package/ui/dist/assets/{index-BLx55BOg.js → index-BbFZyM92.js} +1 -1
- package/ui/dist/assets/index-DK1faG9Z.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-ByViUJfx.css +0 -1
- package/ui/dist/assets/index-Ci36vcFD.js +0 -28
package/README.md
CHANGED
|
@@ -93,6 +93,10 @@ Use Classic when you want one strong result quickly.
|
|
|
93
93
|
4. Generate one image, or enable multimode to fan out several candidate slots from the same prompt.
|
|
94
94
|
5. Copy, download, continue from the result, or send it into Canvas Mode.
|
|
95
95
|
|
|
96
|
+
For a control-by-control guide to Prompt Studio, multimode recipes, Direct mode,
|
|
97
|
+
reasoning effort, and gallery favorite behavior, see the
|
|
98
|
+
[Prompt Studio manual](docs/PROMPT_STUDIO.md).
|
|
99
|
+
|
|
96
100
|

|
|
97
101
|
|
|
98
102
|
### Node Mode
|
|
@@ -143,6 +147,7 @@ The settings workspace keeps account, model, appearance, and language controls a
|
|
|
143
147
|
| `ima2 setup` | Reconfigure saved auth |
|
|
144
148
|
| `ima2 status` | Show config and OAuth status |
|
|
145
149
|
| `ima2 doctor` | Diagnose Node, package, config, and auth |
|
|
150
|
+
| `ima2 doctor image-probe [--json]` | Run sanitized image probes for no-image diagnostics |
|
|
146
151
|
| `ima2 open` | Open the web UI |
|
|
147
152
|
| `ima2 reset` | Remove saved config |
|
|
148
153
|
|
|
@@ -216,6 +221,7 @@ Useful references:
|
|
|
216
221
|
|
|
217
222
|
- [CLI Reference](docs/CLI.md)
|
|
218
223
|
- [API Reference](docs/API.md)
|
|
224
|
+
- [Prompt Studio manual](docs/PROMPT_STUDIO.md)
|
|
219
225
|
- [FAQ](docs/FAQ.md)
|
|
220
226
|
- [Recover old images](docs/RECOVER_OLD_IMAGES.md)
|
|
221
227
|
- [Korean README](docs/README.ko.md)
|
|
@@ -231,11 +237,14 @@ Start `ima2 serve`, then check `~/.ima2/server.json`. You can also run `ima2 pin
|
|
|
231
237
|
Run `npx @openai/codex login`, confirm `ima2 status`, then restart `ima2 serve`.
|
|
232
238
|
|
|
233
239
|
**`fetch failed` repeats on a proxy/VPN network**
|
|
234
|
-
Check that the local OAuth proxy is reachable. On networks that require a proxy, enable your proxy client's TUN/TURN-style mode, then retry `npx openai-oauth --port 10531`. If it still fails, set `HTTP_PROXY` and `HTTPS_PROXY` in the same terminal that runs `ima2 serve` or `openai-oauth`.
|
|
240
|
+
Check that the local OAuth proxy is reachable. On networks that require a proxy, enable your proxy client's TUN/TURN-style mode, then retry `npx openai-oauth --port 10531`. If it still fails, set `HTTP_PROXY` and `HTTPS_PROXY` in the same terminal that runs `ima2 serve` or `openai-oauth`. On Windows, also check for auto-start network interception tools, including DNS/fragmentation bypass tools such as SecretDNS, because they can break OAuth or streaming image responses even when the browser appears connected.
|
|
235
241
|
|
|
236
242
|
**Images fail with `API_KEY_REQUIRED`**
|
|
237
243
|
Set `OPENAI_API_KEY` or configure an API key before using `provider: "api"`. The default OAuth path still works without an API key.
|
|
238
244
|
|
|
245
|
+
**Image generation returns `EMPTY_RESPONSE` or no image data**
|
|
246
|
+
Run `ima2 doctor image-probe --json > ima2-image-probe.json` and attach the safe JSON when opening an issue. For OAuth cases, also capture `ima2 gen "고양이" --no-web-search --json` and `ima2 gen "고양이" --json` while `ima2 serve` is running. Do not share ChatGPT cookies, OAuth token files, API keys, raw upstream responses, prompt history, or generated base64. See the [FAQ support bundle](docs/FAQ.md#what-should-i-share-when-oauth-image-generation-returns-no-image).
|
|
247
|
+
|
|
239
248
|
**A large reference image fails**
|
|
240
249
|
The app compresses large JPEG/PNG references before upload. If a file still fails, convert it to JPEG or PNG at a lower resolution and try again. HEIC/HEIF files are not supported by the browser path.
|
|
241
250
|
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { buildHardeningDoctorLines } from "../lib/doctor-checks.js";
|
|
6
|
+
import { buildStorageDoctorLines } from "../lib/storage-doctor.js";
|
|
7
|
+
import { detectCodexAuth } from "../../lib/codexDetect.js";
|
|
8
|
+
import { runImageDoctorProbe } from "../../lib/responsesDoctor.js";
|
|
9
|
+
import { config as runtimeConfig } from "../../config.js";
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const ROOT = join(__dirname, "../..");
|
|
12
|
+
const requireFromRoot = createRequire(join(ROOT, "package.json"));
|
|
13
|
+
const CONFIG_FILE = runtimeConfig.storage.configFile;
|
|
14
|
+
const LEGACY_CONFIG_FILE = join(ROOT, ".ima2", "config.json");
|
|
15
|
+
let pkg = { version: "?", name: "ima2-gen" };
|
|
16
|
+
try {
|
|
17
|
+
pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8"));
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
function loadConfig() {
|
|
21
|
+
if (existsSync(CONFIG_FILE)) {
|
|
22
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
23
|
+
}
|
|
24
|
+
if (existsSync(LEGACY_CONFIG_FILE)) {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(readFileSync(LEGACY_CONFIG_FILE, "utf-8"));
|
|
27
|
+
}
|
|
28
|
+
catch { }
|
|
29
|
+
}
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
function missingRuntimeDeps() {
|
|
33
|
+
const deps = ["express", "better-sqlite3", "openai", "openai-oauth"];
|
|
34
|
+
return deps.filter((dep) => {
|
|
35
|
+
try {
|
|
36
|
+
requireFromRoot.resolve(dep);
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function valueAfter(args, name) {
|
|
45
|
+
const index = args.indexOf(name);
|
|
46
|
+
if (index === -1)
|
|
47
|
+
return null;
|
|
48
|
+
return args[index + 1] || null;
|
|
49
|
+
}
|
|
50
|
+
function showImageProbeHelp() {
|
|
51
|
+
console.log(`
|
|
52
|
+
Usage: ima2 doctor image-probe [options]
|
|
53
|
+
|
|
54
|
+
Runs live, sanitized Responses probes for EMPTY_RESPONSE diagnosis.
|
|
55
|
+
The output never includes prompt text, auth tokens, URLs with credentials, or base64 image data.
|
|
56
|
+
|
|
57
|
+
Options:
|
|
58
|
+
--json Emit machine-readable JSON
|
|
59
|
+
--matrix Add current-payload web_search/tool_choice probes
|
|
60
|
+
--provider <api|oauth> Override configured provider
|
|
61
|
+
--model <model> Override image-capable Responses model
|
|
62
|
+
--size <size> Default: 1024x1024
|
|
63
|
+
--quality <quality> Default: low
|
|
64
|
+
--moderation <value> Default: low
|
|
65
|
+
--prompt <text> Override built-in cat prompt
|
|
66
|
+
--oauth-url <url> Override OAuth proxy URL
|
|
67
|
+
--timeout-ms <ms> Per-probe timeout
|
|
68
|
+
`);
|
|
69
|
+
}
|
|
70
|
+
async function imageProbe(args) {
|
|
71
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
72
|
+
showImageProbeHelp();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const fileConfig = loadConfig();
|
|
76
|
+
const result = await runImageDoctorProbe({
|
|
77
|
+
provider: valueAfter(args, "--provider") || fileConfig.provider || "oauth",
|
|
78
|
+
apiKey: typeof fileConfig.apiKey === "string" ? fileConfig.apiKey : undefined,
|
|
79
|
+
oauthUrl: valueAfter(args, "--oauth-url") || undefined,
|
|
80
|
+
model: valueAfter(args, "--model") || runtimeConfig.imageModels?.default || "gpt-5.4-mini",
|
|
81
|
+
size: valueAfter(args, "--size") || "1024x1024",
|
|
82
|
+
quality: valueAfter(args, "--quality") || "low",
|
|
83
|
+
moderation: valueAfter(args, "--moderation") || "low",
|
|
84
|
+
prompt: valueAfter(args, "--prompt") || undefined,
|
|
85
|
+
matrix: args.includes("--matrix"),
|
|
86
|
+
timeoutMs: Number(valueAfter(args, "--timeout-ms")) || undefined,
|
|
87
|
+
ctx: { config: runtimeConfig },
|
|
88
|
+
});
|
|
89
|
+
if (args.includes("--json")) {
|
|
90
|
+
console.log(JSON.stringify(result, null, 2));
|
|
91
|
+
process.exit(result.summary.ok ? 0 : 1);
|
|
92
|
+
}
|
|
93
|
+
console.log(`\n ${pkg.name} v${pkg.version} — Image Probe\n`);
|
|
94
|
+
console.log(` Provider: ${result.provider}`);
|
|
95
|
+
console.log(` Model: ${result.model}`);
|
|
96
|
+
console.log(` Prompt: ${result.promptId} (${result.promptChars} chars, redacted)`);
|
|
97
|
+
for (const probe of result.probes) {
|
|
98
|
+
const mark = probe.ok ? "✓" : "✗";
|
|
99
|
+
const reason = probe.diagnosticReason ? ` — ${probe.diagnosticReason}` : "";
|
|
100
|
+
console.log(` ${mark} ${probe.id}${reason}`);
|
|
101
|
+
console.log(` status=${probe.response.httpStatus ?? "n/a"} events=${probe.response.eventCount} images=${probe.response.imageResultCount} textChars=${probe.response.textOutputChars}`);
|
|
102
|
+
}
|
|
103
|
+
console.log(`\n ${result.summary.passed} passed, ${result.summary.failed} failed\n`);
|
|
104
|
+
process.exit(result.summary.ok ? 0 : 1);
|
|
105
|
+
}
|
|
106
|
+
async function standardDoctor() {
|
|
107
|
+
console.log(`\n ${pkg.name} v${pkg.version} — Doctor\n`);
|
|
108
|
+
let ok = 0;
|
|
109
|
+
let fail = 0;
|
|
110
|
+
const nodeVersion = process.version;
|
|
111
|
+
const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0]);
|
|
112
|
+
if (nodeMajor >= 20) {
|
|
113
|
+
console.log(` ✓ Node.js ${nodeVersion} (>= 20)`);
|
|
114
|
+
ok++;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
console.log(` ✗ Node.js ${nodeVersion} (requires >= 20)`);
|
|
118
|
+
fail++;
|
|
119
|
+
}
|
|
120
|
+
if (existsSync(join(ROOT, "package.json"))) {
|
|
121
|
+
console.log(" ✓ package.json found");
|
|
122
|
+
ok++;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
console.log(" ✗ package.json missing");
|
|
126
|
+
fail++;
|
|
127
|
+
}
|
|
128
|
+
const missingDeps = missingRuntimeDeps();
|
|
129
|
+
if (missingDeps.length === 0) {
|
|
130
|
+
console.log(" ✓ runtime dependencies resolvable");
|
|
131
|
+
ok++;
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
console.log(` ✗ missing runtime dependencies: ${missingDeps.join(", ")}`);
|
|
135
|
+
fail++;
|
|
136
|
+
}
|
|
137
|
+
if (existsSync(join(ROOT, ".env"))) {
|
|
138
|
+
console.log(" ✓ .env file exists");
|
|
139
|
+
ok++;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
console.log(" ⚠ .env file not found (optional — copy from .env.example)");
|
|
143
|
+
}
|
|
144
|
+
const fileConfig = loadConfig();
|
|
145
|
+
if (fileConfig.provider) {
|
|
146
|
+
console.log(` ✓ Configured: ${fileConfig.provider}`);
|
|
147
|
+
ok++;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
console.log(" ⚠ Not configured — run 'ima2 setup'");
|
|
151
|
+
}
|
|
152
|
+
const advPath = runtimeConfig.storage.advertiseFile;
|
|
153
|
+
const adv = existsSync(advPath) ? JSON.parse(readFileSync(advPath, "utf-8")) : null;
|
|
154
|
+
console.log(` ℹ Preferred backend port: ${runtimeConfig.server.port}`);
|
|
155
|
+
if (adv?.backend || adv?.port) {
|
|
156
|
+
console.log(` ℹ Backend actual URL: ${adv?.backend?.url || adv?.url || `http://localhost:${adv.port}`}`);
|
|
157
|
+
if (adv?.oauth)
|
|
158
|
+
console.log(` ℹ OAuth actual URL: ${adv.oauth.url} (${adv.oauth.status || "unknown"})`);
|
|
159
|
+
}
|
|
160
|
+
const hardeningLines = await buildHardeningDoctorLines({
|
|
161
|
+
root: ROOT,
|
|
162
|
+
configFile: CONFIG_FILE,
|
|
163
|
+
fileConfig,
|
|
164
|
+
});
|
|
165
|
+
for (const line of hardeningLines) {
|
|
166
|
+
const prefix = line.kind === "pass" ? "✓"
|
|
167
|
+
: line.kind === "fail" ? "✗"
|
|
168
|
+
: line.kind === "warn" ? "⚠"
|
|
169
|
+
: "ℹ";
|
|
170
|
+
console.log(` ${prefix} ${line.text}`);
|
|
171
|
+
if (line.kind === "pass")
|
|
172
|
+
ok++;
|
|
173
|
+
if (line.kind === "fail")
|
|
174
|
+
fail++;
|
|
175
|
+
}
|
|
176
|
+
const storageLines = await buildStorageDoctorLines({
|
|
177
|
+
rootDir: ROOT,
|
|
178
|
+
config: runtimeConfig,
|
|
179
|
+
});
|
|
180
|
+
console.log("");
|
|
181
|
+
for (const line of storageLines)
|
|
182
|
+
console.log(line);
|
|
183
|
+
const auth = detectCodexAuth();
|
|
184
|
+
if (auth.platform === "win32")
|
|
185
|
+
console.log(" ℹ Windows OAuth note: use WSL2 for Codex login.");
|
|
186
|
+
console.log(`\n ${ok} passed, ${fail} failed\n`);
|
|
187
|
+
process.exit(fail > 0 ? 1 : 0);
|
|
188
|
+
}
|
|
189
|
+
export async function doctor(args = []) {
|
|
190
|
+
if (args[0] === "image-probe") {
|
|
191
|
+
await imageProbe(args.slice(1));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
await standardDoctor();
|
|
195
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { buildHardeningDoctorLines } from "../lib/doctor-checks.js";
|
|
6
|
+
import { buildStorageDoctorLines } from "../lib/storage-doctor.js";
|
|
7
|
+
import { detectCodexAuth } from "../../lib/codexDetect.js";
|
|
8
|
+
import { runImageDoctorProbe } from "../../lib/responsesDoctor.js";
|
|
9
|
+
import { config as runtimeConfig } from "../../config.js";
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const ROOT = join(__dirname, "../..");
|
|
13
|
+
const requireFromRoot = createRequire(join(ROOT, "package.json"));
|
|
14
|
+
const CONFIG_FILE = runtimeConfig.storage.configFile;
|
|
15
|
+
const LEGACY_CONFIG_FILE = join(ROOT, ".ima2", "config.json");
|
|
16
|
+
|
|
17
|
+
let pkg = { version: "?", name: "ima2-gen" };
|
|
18
|
+
try {
|
|
19
|
+
pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8"));
|
|
20
|
+
} catch {}
|
|
21
|
+
|
|
22
|
+
function loadConfig() {
|
|
23
|
+
if (existsSync(CONFIG_FILE)) {
|
|
24
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
25
|
+
}
|
|
26
|
+
if (existsSync(LEGACY_CONFIG_FILE)) {
|
|
27
|
+
try { return JSON.parse(readFileSync(LEGACY_CONFIG_FILE, "utf-8")); } catch {}
|
|
28
|
+
}
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function missingRuntimeDeps() {
|
|
33
|
+
const deps = ["express", "better-sqlite3", "openai", "openai-oauth"];
|
|
34
|
+
return deps.filter((dep) => {
|
|
35
|
+
try {
|
|
36
|
+
requireFromRoot.resolve(dep);
|
|
37
|
+
return false;
|
|
38
|
+
} catch {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function valueAfter(args: string[], name: string) {
|
|
45
|
+
const index = args.indexOf(name);
|
|
46
|
+
if (index === -1) return null;
|
|
47
|
+
return args[index + 1] || null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function showImageProbeHelp() {
|
|
51
|
+
console.log(`
|
|
52
|
+
Usage: ima2 doctor image-probe [options]
|
|
53
|
+
|
|
54
|
+
Runs live, sanitized Responses probes for EMPTY_RESPONSE diagnosis.
|
|
55
|
+
The output never includes prompt text, auth tokens, URLs with credentials, or base64 image data.
|
|
56
|
+
|
|
57
|
+
Options:
|
|
58
|
+
--json Emit machine-readable JSON
|
|
59
|
+
--matrix Add current-payload web_search/tool_choice probes
|
|
60
|
+
--provider <api|oauth> Override configured provider
|
|
61
|
+
--model <model> Override image-capable Responses model
|
|
62
|
+
--size <size> Default: 1024x1024
|
|
63
|
+
--quality <quality> Default: low
|
|
64
|
+
--moderation <value> Default: low
|
|
65
|
+
--prompt <text> Override built-in cat prompt
|
|
66
|
+
--oauth-url <url> Override OAuth proxy URL
|
|
67
|
+
--timeout-ms <ms> Per-probe timeout
|
|
68
|
+
`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function imageProbe(args: string[]) {
|
|
72
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
73
|
+
showImageProbeHelp();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const fileConfig = loadConfig();
|
|
77
|
+
const result = await runImageDoctorProbe({
|
|
78
|
+
provider: valueAfter(args, "--provider") || fileConfig.provider || "oauth",
|
|
79
|
+
apiKey: typeof fileConfig.apiKey === "string" ? fileConfig.apiKey : undefined,
|
|
80
|
+
oauthUrl: valueAfter(args, "--oauth-url") || undefined,
|
|
81
|
+
model: valueAfter(args, "--model") || runtimeConfig.imageModels?.default || "gpt-5.4-mini",
|
|
82
|
+
size: valueAfter(args, "--size") || "1024x1024",
|
|
83
|
+
quality: valueAfter(args, "--quality") || "low",
|
|
84
|
+
moderation: valueAfter(args, "--moderation") || "low",
|
|
85
|
+
prompt: valueAfter(args, "--prompt") || undefined,
|
|
86
|
+
matrix: args.includes("--matrix"),
|
|
87
|
+
timeoutMs: Number(valueAfter(args, "--timeout-ms")) || undefined,
|
|
88
|
+
ctx: { config: runtimeConfig },
|
|
89
|
+
});
|
|
90
|
+
if (args.includes("--json")) {
|
|
91
|
+
console.log(JSON.stringify(result, null, 2));
|
|
92
|
+
process.exit(result.summary.ok ? 0 : 1);
|
|
93
|
+
}
|
|
94
|
+
console.log(`\n ${pkg.name} v${pkg.version} — Image Probe\n`);
|
|
95
|
+
console.log(` Provider: ${result.provider}`);
|
|
96
|
+
console.log(` Model: ${result.model}`);
|
|
97
|
+
console.log(` Prompt: ${result.promptId} (${result.promptChars} chars, redacted)`);
|
|
98
|
+
for (const probe of result.probes) {
|
|
99
|
+
const mark = probe.ok ? "✓" : "✗";
|
|
100
|
+
const reason = probe.diagnosticReason ? ` — ${probe.diagnosticReason}` : "";
|
|
101
|
+
console.log(` ${mark} ${probe.id}${reason}`);
|
|
102
|
+
console.log(
|
|
103
|
+
` status=${probe.response.httpStatus ?? "n/a"} events=${probe.response.eventCount} images=${probe.response.imageResultCount} textChars=${probe.response.textOutputChars}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
console.log(`\n ${result.summary.passed} passed, ${result.summary.failed} failed\n`);
|
|
107
|
+
process.exit(result.summary.ok ? 0 : 1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function standardDoctor() {
|
|
111
|
+
console.log(`\n ${pkg.name} v${pkg.version} — Doctor\n`);
|
|
112
|
+
|
|
113
|
+
let ok = 0;
|
|
114
|
+
let fail = 0;
|
|
115
|
+
|
|
116
|
+
const nodeVersion = process.version;
|
|
117
|
+
const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0]);
|
|
118
|
+
if (nodeMajor >= 20) {
|
|
119
|
+
console.log(` ✓ Node.js ${nodeVersion} (>= 20)`);
|
|
120
|
+
ok++;
|
|
121
|
+
} else {
|
|
122
|
+
console.log(` ✗ Node.js ${nodeVersion} (requires >= 20)`);
|
|
123
|
+
fail++;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (existsSync(join(ROOT, "package.json"))) {
|
|
127
|
+
console.log(" ✓ package.json found");
|
|
128
|
+
ok++;
|
|
129
|
+
} else {
|
|
130
|
+
console.log(" ✗ package.json missing");
|
|
131
|
+
fail++;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const missingDeps = missingRuntimeDeps();
|
|
135
|
+
if (missingDeps.length === 0) {
|
|
136
|
+
console.log(" ✓ runtime dependencies resolvable");
|
|
137
|
+
ok++;
|
|
138
|
+
} else {
|
|
139
|
+
console.log(` ✗ missing runtime dependencies: ${missingDeps.join(", ")}`);
|
|
140
|
+
fail++;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (existsSync(join(ROOT, ".env"))) {
|
|
144
|
+
console.log(" ✓ .env file exists");
|
|
145
|
+
ok++;
|
|
146
|
+
} else {
|
|
147
|
+
console.log(" ⚠ .env file not found (optional — copy from .env.example)");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const fileConfig = loadConfig();
|
|
151
|
+
if (fileConfig.provider) {
|
|
152
|
+
console.log(` ✓ Configured: ${fileConfig.provider}`);
|
|
153
|
+
ok++;
|
|
154
|
+
} else {
|
|
155
|
+
console.log(" ⚠ Not configured — run 'ima2 setup'");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const advPath = runtimeConfig.storage.advertiseFile;
|
|
159
|
+
const adv = existsSync(advPath) ? JSON.parse(readFileSync(advPath, "utf-8")) : null;
|
|
160
|
+
console.log(` ℹ Preferred backend port: ${runtimeConfig.server.port}`);
|
|
161
|
+
if (adv?.backend || adv?.port) {
|
|
162
|
+
console.log(` ℹ Backend actual URL: ${adv?.backend?.url || adv?.url || `http://localhost:${adv.port}`}`);
|
|
163
|
+
if (adv?.oauth) console.log(` ℹ OAuth actual URL: ${adv.oauth.url} (${adv.oauth.status || "unknown"})`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const hardeningLines = await buildHardeningDoctorLines({
|
|
167
|
+
root: ROOT,
|
|
168
|
+
configFile: CONFIG_FILE,
|
|
169
|
+
fileConfig,
|
|
170
|
+
});
|
|
171
|
+
for (const line of hardeningLines) {
|
|
172
|
+
const prefix =
|
|
173
|
+
line.kind === "pass" ? "✓"
|
|
174
|
+
: line.kind === "fail" ? "✗"
|
|
175
|
+
: line.kind === "warn" ? "⚠"
|
|
176
|
+
: "ℹ";
|
|
177
|
+
console.log(` ${prefix} ${line.text}`);
|
|
178
|
+
if (line.kind === "pass") ok++;
|
|
179
|
+
if (line.kind === "fail") fail++;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const storageLines = await buildStorageDoctorLines({
|
|
183
|
+
rootDir: ROOT,
|
|
184
|
+
config: runtimeConfig,
|
|
185
|
+
});
|
|
186
|
+
console.log("");
|
|
187
|
+
for (const line of storageLines) console.log(line);
|
|
188
|
+
|
|
189
|
+
const auth = detectCodexAuth();
|
|
190
|
+
if (auth.platform === "win32") console.log(" ℹ Windows OAuth note: use WSL2 for Codex login.");
|
|
191
|
+
|
|
192
|
+
console.log(`\n ${ok} passed, ${fail} failed\n`);
|
|
193
|
+
process.exit(fail > 0 ? 1 : 0);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function doctor(args: string[] = []) {
|
|
197
|
+
if (args[0] === "image-probe") {
|
|
198
|
+
await imageProbe(args.slice(1));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
await standardDoctor();
|
|
202
|
+
}
|
package/bin/ima2.js
CHANGED
|
@@ -3,20 +3,17 @@ import { createInterface } from "readline/promises";
|
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
4
4
|
import { join, dirname } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
|
-
import { createRequire } from "module";
|
|
7
6
|
import { spawn, execSync } from "child_process";
|
|
8
7
|
import { confirmDestructiveAction } from "./lib/destructive-confirm.js";
|
|
9
|
-
import {
|
|
8
|
+
import { doctor } from "./commands/doctor.js";
|
|
10
9
|
import { openUrl, resolveBin } from "./lib/platform.js";
|
|
11
10
|
import { maybePromptGithubStar } from "./lib/star-prompt.js";
|
|
12
|
-
import { buildStorageDoctorLines } from "./lib/storage-doctor.js";
|
|
13
11
|
import { ensureFreshUiDist } from "./lib/ui-build.js";
|
|
14
12
|
import { detectCodexAuth } from "../lib/codexDetect.js";
|
|
15
13
|
import { config as runtimeConfig } from "../config.js";
|
|
16
14
|
import { errInfo } from "../lib/errInfo.js";
|
|
17
15
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
16
|
const ROOT = join(__dirname, "..");
|
|
19
|
-
const requireFromRoot = createRequire(join(ROOT, "package.json"));
|
|
20
17
|
// Config lives under runtimeConfig.storage.configDir (honors IMA2_CONFIG_DIR).
|
|
21
18
|
// Legacy installs that stored config at <packageRoot>/.ima2/config.json will be
|
|
22
19
|
// migrated on first write.
|
|
@@ -61,18 +58,6 @@ function advertisedServerUrl() {
|
|
|
61
58
|
const adv = loadAdvertisement();
|
|
62
59
|
return adv?.backend?.url || adv?.url || (adv?.port ? `http://localhost:${adv.port}` : null);
|
|
63
60
|
}
|
|
64
|
-
function missingRuntimeDeps() {
|
|
65
|
-
const deps = ["express", "better-sqlite3", "openai", "openai-oauth"];
|
|
66
|
-
return deps.filter((dep) => {
|
|
67
|
-
try {
|
|
68
|
-
requireFromRoot.resolve(dep);
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
61
|
async function setup() {
|
|
77
62
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
78
63
|
console.log("\n ima2-gen — GPT Image 2 Generator\n");
|
|
@@ -200,93 +185,6 @@ async function showStatus() {
|
|
|
200
185
|
}
|
|
201
186
|
console.log("");
|
|
202
187
|
}
|
|
203
|
-
async function doctor() {
|
|
204
|
-
console.log(`\n ${pkg.name} v${pkg.version} — Doctor\n`);
|
|
205
|
-
let ok = 0;
|
|
206
|
-
let fail = 0;
|
|
207
|
-
// Node version
|
|
208
|
-
const nodeVersion = process.version;
|
|
209
|
-
const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0]);
|
|
210
|
-
if (nodeMajor >= 20) {
|
|
211
|
-
console.log(` ✓ Node.js ${nodeVersion} (>= 20)`);
|
|
212
|
-
ok++;
|
|
213
|
-
}
|
|
214
|
-
else {
|
|
215
|
-
console.log(` ✗ Node.js ${nodeVersion} (requires >= 20)`);
|
|
216
|
-
fail++;
|
|
217
|
-
}
|
|
218
|
-
// package.json exists
|
|
219
|
-
if (existsSync(join(ROOT, "package.json"))) {
|
|
220
|
-
console.log(" ✓ package.json found");
|
|
221
|
-
ok++;
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
console.log(" ✗ package.json missing");
|
|
225
|
-
fail++;
|
|
226
|
-
}
|
|
227
|
-
// Runtime dependencies may be hoisted by npm, pnpm, or Yarn. Resolve the
|
|
228
|
-
// packages instead of requiring a package-local node_modules folder.
|
|
229
|
-
const missingDeps = missingRuntimeDeps();
|
|
230
|
-
if (missingDeps.length === 0) {
|
|
231
|
-
console.log(" ✓ runtime dependencies resolvable");
|
|
232
|
-
ok++;
|
|
233
|
-
}
|
|
234
|
-
else {
|
|
235
|
-
console.log(` ✗ missing runtime dependencies: ${missingDeps.join(", ")}`);
|
|
236
|
-
fail++;
|
|
237
|
-
}
|
|
238
|
-
// .env
|
|
239
|
-
if (existsSync(join(ROOT, ".env"))) {
|
|
240
|
-
console.log(" ✓ .env file exists");
|
|
241
|
-
ok++;
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
console.log(" ⚠ .env file not found (optional — copy from .env.example)");
|
|
245
|
-
}
|
|
246
|
-
// Config
|
|
247
|
-
const config = loadConfig();
|
|
248
|
-
if (config.provider) {
|
|
249
|
-
console.log(` ✓ Configured: ${config.provider}`);
|
|
250
|
-
ok++;
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
console.log(" ⚠ Not configured — run 'ima2 setup'");
|
|
254
|
-
}
|
|
255
|
-
// Port availability (simple check)
|
|
256
|
-
const adv = loadAdvertisement();
|
|
257
|
-
console.log(` ℹ Preferred backend port: ${runtimeConfig.server.port}`);
|
|
258
|
-
if (adv?.backend || adv?.port) {
|
|
259
|
-
console.log(` ℹ Backend actual URL: ${adv?.backend?.url || adv?.url || `http://localhost:${adv.port}`}`);
|
|
260
|
-
if (adv?.oauth) {
|
|
261
|
-
console.log(` ℹ OAuth actual URL: ${adv.oauth.url} (${adv.oauth.status || "unknown"})`);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
const hardeningLines = await buildHardeningDoctorLines({
|
|
265
|
-
root: ROOT,
|
|
266
|
-
configFile: CONFIG_FILE,
|
|
267
|
-
fileConfig: config,
|
|
268
|
-
});
|
|
269
|
-
for (const line of hardeningLines) {
|
|
270
|
-
const prefix = line.kind === "pass" ? "✓"
|
|
271
|
-
: line.kind === "fail" ? "✗"
|
|
272
|
-
: line.kind === "warn" ? "⚠"
|
|
273
|
-
: "ℹ";
|
|
274
|
-
console.log(` ${prefix} ${line.text}`);
|
|
275
|
-
if (line.kind === "pass")
|
|
276
|
-
ok++;
|
|
277
|
-
if (line.kind === "fail")
|
|
278
|
-
fail++;
|
|
279
|
-
}
|
|
280
|
-
const storageLines = await buildStorageDoctorLines({
|
|
281
|
-
rootDir: ROOT,
|
|
282
|
-
config: runtimeConfig,
|
|
283
|
-
});
|
|
284
|
-
console.log("");
|
|
285
|
-
for (const line of storageLines)
|
|
286
|
-
console.log(line);
|
|
287
|
-
console.log(`\n ${ok} passed, ${fail} failed\n`);
|
|
288
|
-
process.exit(fail > 0 ? 1 : 0);
|
|
289
|
-
}
|
|
290
188
|
function openBrowser() {
|
|
291
189
|
const url = advertisedServerUrl() || `http://localhost:${runtimeConfig.server.port}`;
|
|
292
190
|
const res = openUrl(url);
|
|
@@ -372,7 +270,7 @@ if (args.includes("-v") || args.includes("--version")) {
|
|
|
372
270
|
process.exit(0);
|
|
373
271
|
}
|
|
374
272
|
if ((!command || args.includes("-h") || args.includes("--help"))
|
|
375
|
-
&& !["gen", "edit", "ls", "show", "ps", "cancel", "session", "history", "prompt", "multimode", "node", "annotate", "canvas-versions", "metadata", "comfy", "cardnews", "inflight", "storage", "billing", "providers", "oauth", "config", "defaults", "capabilities", "skill", "ping"].includes(command)) {
|
|
273
|
+
&& !["doctor", "gen", "edit", "ls", "show", "ps", "cancel", "session", "history", "prompt", "multimode", "node", "annotate", "canvas-versions", "metadata", "comfy", "cardnews", "inflight", "storage", "billing", "providers", "oauth", "config", "defaults", "capabilities", "skill", "ping"].includes(command)) {
|
|
376
274
|
showHelp();
|
|
377
275
|
process.exit(command ? 0 : 1);
|
|
378
276
|
}
|
|
@@ -388,7 +286,7 @@ switch (command) {
|
|
|
388
286
|
showStatus();
|
|
389
287
|
break;
|
|
390
288
|
case "doctor":
|
|
391
|
-
await doctor();
|
|
289
|
+
await doctor(args.slice(1));
|
|
392
290
|
break;
|
|
393
291
|
case "open":
|
|
394
292
|
openBrowser();
|