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.
Files changed (53) hide show
  1. package/README.md +10 -1
  2. package/bin/commands/doctor.js +195 -0
  3. package/bin/commands/doctor.ts +202 -0
  4. package/bin/ima2.js +3 -105
  5. package/bin/ima2.ts +3 -109
  6. package/config.js +1 -0
  7. package/config.ts +5 -0
  8. package/docs/CLI.md +36 -0
  9. package/docs/FAQ.ko.md +82 -2
  10. package/docs/FAQ.md +85 -2
  11. package/docs/PROMPT_STUDIO.ko.md +111 -0
  12. package/docs/PROMPT_STUDIO.md +115 -0
  13. package/docs/README.ko.md +8 -1
  14. package/docs/migration/runtime-test-inventory.md +6 -1
  15. package/lib/agentRuntime.js +9 -2
  16. package/lib/agentRuntime.ts +8 -2
  17. package/lib/errorClassify.js +1 -1
  18. package/lib/errorClassify.ts +1 -1
  19. package/lib/generationErrors.js +121 -23
  20. package/lib/generationErrors.ts +100 -13
  21. package/lib/responsesDoctor.js +386 -0
  22. package/lib/responsesDoctor.ts +456 -0
  23. package/lib/responsesErrors.js +57 -0
  24. package/lib/responsesErrors.ts +83 -0
  25. package/lib/responsesFallback.js +72 -0
  26. package/lib/responsesFallback.ts +114 -0
  27. package/lib/responsesImageAdapter.js +121 -174
  28. package/lib/responsesImageAdapter.ts +136 -211
  29. package/lib/responsesParse.js +324 -0
  30. package/lib/responsesParse.ts +452 -0
  31. package/lib/responsesTools.js +15 -0
  32. package/lib/responsesTools.ts +28 -0
  33. package/package.json +1 -1
  34. package/routes/edit.js +26 -1
  35. package/routes/edit.ts +26 -1
  36. package/routes/generate.js +40 -0
  37. package/routes/generate.ts +47 -0
  38. package/ui/dist/.vite/manifest.json +12 -12
  39. package/ui/dist/assets/{AgentWorkspace-BJe9yxPA.js → AgentWorkspace-B6YNOZHi.js} +1 -1
  40. package/ui/dist/assets/{CardNewsWorkspace-BBLdwzYU.js → CardNewsWorkspace-EFVeg4l_.js} +1 -1
  41. package/ui/dist/assets/{NodeCanvas-BSZ527J4.js → NodeCanvas-iM6yjHvO.js} +1 -1
  42. package/ui/dist/assets/{PromptBuilderPanel-Y2VygFc0.js → PromptBuilderPanel-C3GdLDCl.js} +1 -1
  43. package/ui/dist/assets/{PromptImportDialog-C6lFV-LL.js → PromptImportDialog-DS9vrc_w.js} +2 -2
  44. package/ui/dist/assets/{PromptImportDiscoverySection-D8YJFhND.js → PromptImportDiscoverySection-DHFEt_FA.js} +1 -1
  45. package/ui/dist/assets/{PromptImportFolderSection-ywfcQolW.js → PromptImportFolderSection-BQxb1zs5.js} +1 -1
  46. package/ui/dist/assets/{PromptLibraryPanel-fk4KmrGy.js → PromptLibraryPanel-NhMKVGfU.js} +2 -2
  47. package/ui/dist/assets/{SettingsWorkspace-DL5vhAHQ.js → SettingsWorkspace-FjKjaDqj.js} +1 -1
  48. package/ui/dist/assets/index-BAN6lKgf.js +28 -0
  49. package/ui/dist/assets/{index-BLx55BOg.js → index-BbFZyM92.js} +1 -1
  50. package/ui/dist/assets/index-DK1faG9Z.css +1 -0
  51. package/ui/dist/index.html +2 -2
  52. package/ui/dist/assets/index-ByViUJfx.css +0 -1
  53. 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
  ![Multimode sequence with four candidate slots generating from one prompt and active job history in the sidebar.](assets/screenshots/multimode-sequence.png)
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 { buildHardeningDoctorLines } from "./lib/doctor-checks.js";
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();