ima2-gen 1.0.10 → 1.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.
@@ -0,0 +1,182 @@
1
+ # ima2-gen
2
+
3
+ [![npm version](https://img.shields.io/npm/v/ima2-gen)](https://www.npmjs.com/package/ima2-gen)
4
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](../LICENSE)
6
+
7
+ > **其他语言**: [English](../README.md) · [한국어](README.ko.md) · [日本語](README.ja.md)
8
+
9
+ `ima2-gen` 是一个本地图像生成工作室,让你像使用小型桌面应用一样使用 ChatGPT/Codex OAuth 图像生成流程。
10
+
11
+ 用 `npx` 启动,登录 Codex OAuth,输入 prompt,然后通过历史记录、参考图、style sheet 和节点分支持续迭代。默认图像生成路径不需要 OpenAI API key。
12
+
13
+ ![显示 prompt 输入区、生成图片、模型标签和结果元数据的 ima2-gen classic 界面](../assets/screenshots/classic-generate-light.png)
14
+
15
+ ## 快速开始
16
+
17
+ ```bash
18
+ npx ima2-gen serve
19
+ ```
20
+
21
+ 然后打开 `http://localhost:3333`。
22
+
23
+ 如果还没有登录 Codex:
24
+
25
+ ```bash
26
+ npx @openai/codex login
27
+ npx ima2-gen serve
28
+ ```
29
+
30
+ 也可以全局安装:
31
+
32
+ ```bash
33
+ npm install -g ima2-gen
34
+ ima2 serve
35
+ ```
36
+
37
+ ## 能做什么
38
+
39
+ - **Classic mode**:快速生成、编辑,并把当前图片继续作为参考图使用。
40
+ - **Node mode**:从一张满意的图出发,向多个方向分支探索。
41
+ - **Local gallery**:把生成结果保存在本机,并按 session 查看历史。
42
+ - **Reference images**:支持拖放、粘贴和文件选择;大图会在上传前自动压缩。
43
+ - **Style sheets**:保存一套视觉方向,并复用到 classic/node prompt。
44
+ - **Observable jobs**:用 request ID 追踪进行中和最近完成的任务。
45
+
46
+ ## 图像生成只走 OAuth
47
+
48
+ 当前图像生成通过本地 Codex/ChatGPT OAuth 路径执行。
49
+
50
+ 即使 env/config 里有 API key,它也只可能用于 billing 检查或 style-sheet 提取等辅助功能。生成接口收到 `provider: "api"` 时会返回 `APIKEY_DISABLED`。
51
+
52
+ 如果设置页显示 **Configured but disabled**,意思是检测到了 API key,但图像生成仍然使用 OAuth。
53
+
54
+ ![显示 OAuth active 与 API key disabled 状态的设置页](../assets/screenshots/settings-oauth-generation.png)
55
+
56
+ ## 模型建议
57
+
58
+ 如果想要稳定、均衡的结果,建议先从 **`gpt-5.4`** 开始。
59
+
60
+ - `gpt-5.4` — 推荐的均衡选择。
61
+ - `gpt-5.4-mini` — 当前应用默认值,适合快速草稿。
62
+ - `gpt-5.5` — 在支持的环境中是质量最强的选择。但它可能消耗更多额度,也可能需要更新 Codex CLI,或依赖账号/后端路径是否开放对应的图像 capability。
63
+
64
+ Quality 支持 `low`, `medium`, `high`;moderation 支持 `auto`, `low`。
65
+
66
+ ## 工作流
67
+
68
+ ### Classic mode
69
+
70
+ 适合快速做出一张图并继续调整。
71
+
72
+ 1. 写 prompt。
73
+ 2. 需要时添加参考图。
74
+ 3. 选择模型、quality、size、format、moderation。
75
+ 4. 生成后复制、下载,或继续从当前结果迭代。
76
+
77
+ ### Node mode
78
+
79
+ 适合把想法分叉后对比。
80
+
81
+ ![显示连接节点、生成卡片和节点元数据的 Node mode 界面](../assets/screenshots/node-graph-branching.png)
82
+
83
+ 每个节点都有自己的 prompt 和结果。根节点可以附加本地参考图;子节点会使用父图作为来源。完成的任务会通过 request ID 重新匹配,因此刷新或 graph version conflict 后也能恢复结果。
84
+
85
+ ### Settings 和 Style sheets
86
+
87
+ Settings workspace 会把账号、模型、主题和语言设置从生成面板中分离出来。
88
+
89
+ ![显示账号区域和生成模型设置的 Settings workspace](../assets/screenshots/settings-workspace.png)
90
+
91
+ Style sheet 用来保存可复用的视觉方向。
92
+
93
+ ![包含 medium、composition、mood、subject、palette、negative 字段的 style sheet editor](../assets/screenshots/style-sheet-editor.png)
94
+
95
+ ## CLI 命令
96
+
97
+ ### Server
98
+
99
+ | Command | Description |
100
+ |---|---|
101
+ | `ima2 serve` | 启动本地 Web server |
102
+ | `ima2 setup` | 重新配置认证 |
103
+ | `ima2 status` | 查看 config 和 OAuth 状态 |
104
+ | `ima2 doctor` | 诊断 Node、package、config、auth |
105
+ | `ima2 open` | 打开 Web UI |
106
+ | `ima2 reset` | 删除已保存的 config |
107
+
108
+ ### Client
109
+
110
+ 这些命令需要先运行 `ima2 serve`。
111
+
112
+ | Command | Description |
113
+ |---|---|
114
+ | `ima2 gen <prompt>` | 从 CLI 生成图片 |
115
+ | `ima2 edit <file> --prompt <text>` | 编辑已有图片 |
116
+ | `ima2 ls` | 查看本地历史 |
117
+ | `ima2 show <name>` | 打开生成文件 |
118
+ | `ima2 ps` | 查看进行中的任务 |
119
+ | `ima2 ping` | 检查 server 是否可用 |
120
+
121
+ Server 端口会写入 `~/.ima2/server.json`。也可以用 `--server <url>` 或 `IMA2_SERVER=http://localhost:3333` 覆盖。
122
+
123
+ ## 配置
124
+
125
+ 优先级:
126
+
127
+ ```text
128
+ environment variables > ~/.ima2/config.json > built-in defaults
129
+ ```
130
+
131
+ | Variable | Default | Description |
132
+ |---|---:|---|
133
+ | `IMA2_PORT` / `PORT` | `3333` | Web server port |
134
+ | `IMA2_OAUTH_PROXY_PORT` / `OAUTH_PORT` | `10531` | OAuth proxy port |
135
+ | `IMA2_SERVER` | — | CLI target override |
136
+ | `IMA2_CONFIG_DIR` | `~/.ima2` | Config 和 SQLite 位置 |
137
+ | `IMA2_GENERATED_DIR` | `~/.ima2/generated` | 生成图片目录 |
138
+ | `IMA2_NO_OAUTH_PROXY` | — | 设为 `1` 时关闭 OAuth proxy 自动启动 |
139
+ | `IMA2_INFLIGHT_TERMINAL_TTL_MS` | `30000` | 调试用 recent job 保留时间 |
140
+ | `OPENAI_API_KEY` | — | 辅助功能用 API key,不用于图像生成 |
141
+
142
+ ## API 文档
143
+
144
+ 接口列表已移到 [API Reference](API.md)。
145
+
146
+ ## 常见问题
147
+
148
+ **`ima2 ping` 提示 server unreachable**
149
+ 先启动 `ima2 serve`,再检查 `~/.ima2/server.json`。也可以运行 `ima2 ping --server http://localhost:3333`。
150
+
151
+ **OAuth 登录失败**
152
+ 运行 `npx @openai/codex login`,用 `ima2 status` 确认状态,然后重启 `ima2 serve`。
153
+
154
+ **生成图片时返回 `APIKEY_DISABLED`**
155
+ 当前 build 需要使用 OAuth 生成。API-key image generation 是有意关闭的。
156
+
157
+ **大参考图上传失败**
158
+ JPEG/PNG 会在上传前自动压缩。如果仍然失败,请转成更低分辨率的 JPEG/PNG。HEIC/HEIF 不支持浏览器路径。
159
+
160
+ **更新后看不到旧图库图片**
161
+ 新版本把生成图片目录从已安装的 package 文件夹移到了 `~/.ima2/generated`。请运行 `ima2 doctor`,并查看[旧图片恢复指南](RECOVER_OLD_IMAGES.md)。
162
+
163
+ **只有 `gpt-5.5` 失败**
164
+ 请先更新 Codex CLI 后再试。如果仍然失败,说明当前账号或后端路径下 `gpt-5.5` 的图像 capability 或额度策略可能还不同;稳定替代方案是使用 `gpt-5.4`。
165
+
166
+ **端口突然变成 `3457`**
167
+ shell 可能继承了其他本地工具的 `PORT=3457`。运行 `unset PORT`,或使用 `IMA2_PORT=3333 ima2 serve`。
168
+
169
+ ## Development
170
+
171
+ ```bash
172
+ git clone https://github.com/lidge-jun/ima2-gen.git
173
+ cd ima2-gen
174
+ npm install
175
+ npm run dev
176
+ npm test
177
+ npm run build
178
+ ```
179
+
180
+ ## License
181
+
182
+ MIT
@@ -0,0 +1,91 @@
1
+ # Recover Old Generated Images
2
+
3
+ `ima2-gen` moved generated images to a safer user-data folder in `v1.0.8`.
4
+
5
+ ## What changed
6
+
7
+ Versions up to `v1.0.7` stored generated images inside the installed package:
8
+
9
+ ```text
10
+ <global node_modules>/ima2-gen/generated
11
+ ```
12
+
13
+ Starting with `v1.0.8`, generated images are stored under your user data folder:
14
+
15
+ ```text
16
+ macOS / Linux: ~/.ima2/generated
17
+ Windows: %USERPROFILE%\.ima2\generated
18
+ ```
19
+
20
+ This prevents future package updates from mixing application code and runtime user files.
21
+
22
+ ## Why images may look missing
23
+
24
+ Sorry for the scare. Older global installs may have kept images inside the package folder. If that old global install folder was replaced during an update, the previous `generated/` folder may no longer be on disk.
25
+
26
+ `ima2-gen` can copy old images only when the old folder still exists. If no old folder can be found, recovery may require a backup.
27
+
28
+ ## First check
29
+
30
+ Run:
31
+
32
+ ```bash
33
+ ima2 doctor
34
+ ```
35
+
36
+ The Storage section shows the current gallery folder and whether legacy folders were found.
37
+
38
+ ## macOS / Linux: find old folders
39
+
40
+ ```bash
41
+ find ~/.ima2 ~/.npm-global ~/.nvm ~/.fnm ~/.volta ~/.bun ~/.config/yarn ~/Library/pnpm ~/.local/share/pnpm ~/.asdf ~/.local/share/mise /usr/local /opt/homebrew \
42
+ -path '*ima2-gen/generated' -type d 2>/dev/null
43
+ ```
44
+
45
+ If you used `npx` or `npm exec`:
46
+
47
+ ```bash
48
+ find "$(npm config get cache)/_npx" \
49
+ -path '*/node_modules/ima2-gen/generated' -type d 2>/dev/null
50
+ ```
51
+
52
+ ## Windows PowerShell: find old folders
53
+
54
+ ```powershell
55
+ $roots = @($env:USERPROFILE, $env:APPDATA, $env:LOCALAPPDATA, $env:NVM_HOME, "C:\Program Files\nodejs")
56
+ foreach ($r in $roots) {
57
+ if (Test-Path $r) {
58
+ Get-ChildItem -Path $r -Recurse -Directory -ErrorAction SilentlyContinue |
59
+ Where-Object { $_.FullName -match 'ima2-gen[\\/]+generated$' } |
60
+ Select-Object -ExpandProperty FullName
61
+ }
62
+ }
63
+ ```
64
+
65
+ If you used `npx` or `npm exec`:
66
+
67
+ ```powershell
68
+ Get-ChildItem "$env:LOCALAPPDATA\npm-cache\_npx" -Recurse -Directory -Filter generated -ErrorAction SilentlyContinue |
69
+ Where-Object { $_.FullName -match 'node_modules\\ima2-gen\\generated$' } |
70
+ Select-Object -ExpandProperty FullName
71
+ ```
72
+
73
+ ## Copy found files into the new location
74
+
75
+ macOS / Linux:
76
+
77
+ ```bash
78
+ mkdir -p ~/.ima2/generated
79
+ cp -n "/path/to/old/ima2-gen/generated/"* ~/.ima2/generated/
80
+ ```
81
+
82
+ Windows PowerShell:
83
+
84
+ ```powershell
85
+ New-Item -ItemType Directory -Force "$env:USERPROFILE\.ima2\generated"
86
+ Copy-Item "C:\path\to\old\ima2-gen\generated\*" "$env:USERPROFILE\.ima2\generated" -Recurse -Force:$false
87
+ ```
88
+
89
+ ## Important
90
+
91
+ Do not delete old global install folders or npm caches until you confirm your images are visible in the app again.
@@ -45,6 +45,7 @@ export async function listHistoryRows(baseDir = config.storage.generatedDir) {
45
45
  quality: meta?.quality || null,
46
46
  size: meta?.size || null,
47
47
  format: meta?.format || name.split(".").pop(),
48
+ model: meta?.model || null,
48
49
  provider: meta?.provider || "oauth",
49
50
  usage: meta?.usage || null,
50
51
  webSearchCalls: meta?.webSearchCalls || 0,
@@ -0,0 +1,32 @@
1
+ const FALLBACK_IMAGE_MODEL = "gpt-5.4-mini";
2
+ const VALID_IMAGE_MODELS = new Set(["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"]);
3
+ const UNSUPPORTED_IMAGE_MODELS = new Set(["gpt-5.3-codex-spark"]);
4
+
5
+ export function normalizeImageModel(ctx, rawModel) {
6
+ const configured = ctx?.config?.imageModels;
7
+ const fallback = configured?.default ?? FALLBACK_IMAGE_MODEL;
8
+ const valid = configured?.valid ?? VALID_IMAGE_MODELS;
9
+ const unsupported = configured?.unsupported ?? UNSUPPORTED_IMAGE_MODELS;
10
+
11
+ if (typeof rawModel !== "string" || rawModel.length === 0) {
12
+ return { model: valid.has(fallback) ? fallback : FALLBACK_IMAGE_MODEL };
13
+ }
14
+
15
+ if (unsupported.has(rawModel)) {
16
+ return {
17
+ error: "model is listed by OAuth but does not support image_generation: gpt-5.3-codex-spark",
18
+ code: "IMAGE_MODEL_UNSUPPORTED",
19
+ status: 400,
20
+ };
21
+ }
22
+
23
+ if (!valid.has(rawModel)) {
24
+ return {
25
+ error: "model must be one of: gpt-5.5, gpt-5.4, gpt-5.4-mini",
26
+ code: "INVALID_IMAGE_MODEL",
27
+ status: 400,
28
+ };
29
+ }
30
+
31
+ return { model: rawModel };
32
+ }
package/lib/oauthProxy.js CHANGED
@@ -155,6 +155,7 @@ export async function generateViaOAuth(
155
155
  options = {},
156
156
  ) {
157
157
  const oauthUrl = getOAuthUrl(ctx);
158
+ const model = options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini";
158
159
  const tools = [
159
160
  { type: "web_search" },
160
161
  {
@@ -181,7 +182,7 @@ export async function generateViaOAuth(
181
182
  method: "POST",
182
183
  headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
183
184
  body: JSON.stringify({
184
- model: "gpt-5.4",
185
+ model,
185
186
  input: [
186
187
  { role: "developer", content: GENERATE_DEVELOPER_PROMPT },
187
188
  { role: "user", content: userContent },
@@ -194,6 +195,7 @@ export async function generateViaOAuth(
194
195
 
195
196
  logEvent("oauth", "response", {
196
197
  requestId,
198
+ model,
197
199
  status: res.status,
198
200
  contentType: res.headers.get("content-type"),
199
201
  });
@@ -236,7 +238,7 @@ export async function generateViaOAuth(
236
238
  method: "POST",
237
239
  headers: { "Content-Type": "application/json" },
238
240
  body: JSON.stringify({
239
- model: "gpt-5.4",
241
+ model,
240
242
  input: [{ role: "user", content: buildUserTextPrompt(prompt, mode) }],
241
243
  tools: [{ type: "image_generation", quality, size, moderation }],
242
244
  stream: false,
@@ -260,15 +262,16 @@ export async function generateViaOAuth(
260
262
  return { b64: imageB64, usage, webSearchCalls, revisedPrompt };
261
263
  }
262
264
 
263
- export async function editViaOAuth(prompt, imageB64, quality, size, moderation = "low", mode = "auto", ctx = {}, requestId = null) {
265
+ export async function editViaOAuth(prompt, imageB64, quality, size, moderation = "low", mode = "auto", ctx = {}, requestId = null, options = {}) {
264
266
  const oauthUrl = getOAuthUrl(ctx);
267
+ const model = options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini";
265
268
  const textPrompt = buildEditTextPrompt(prompt, mode);
266
269
 
267
270
  const res = await fetch(`${oauthUrl}/v1/responses`, {
268
271
  method: "POST",
269
272
  headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
270
273
  body: JSON.stringify({
271
- model: "gpt-5.4",
274
+ model,
272
275
  input: [
273
276
  { role: "developer", content: EDIT_DEVELOPER_PROMPT },
274
277
  {
@@ -287,6 +290,7 @@ export async function editViaOAuth(prompt, imageB64, quality, size, moderation =
287
290
 
288
291
  logEvent("oauth-edit", "response", {
289
292
  requestId,
293
+ model,
290
294
  status: res.status,
291
295
  contentType: res.headers.get("content-type"),
292
296
  });
@@ -0,0 +1,35 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdir } from "node:fs/promises";
3
+
4
+ export async function openDirectory(dir, options = {}) {
5
+ await mkdir(dir, { recursive: true });
6
+ const platform = options.platform || process.platform;
7
+ const spawnImpl = options.spawnImpl || spawn;
8
+ const command =
9
+ platform === "darwin" ? "open"
10
+ : platform === "win32" ? "explorer"
11
+ : "xdg-open";
12
+
13
+ return new Promise((resolve) => {
14
+ try {
15
+ const child = spawnImpl(command, [dir], {
16
+ detached: true,
17
+ stdio: "ignore",
18
+ windowsHide: true,
19
+ });
20
+ let settled = false;
21
+ const done = (result) => {
22
+ if (settled) return;
23
+ settled = true;
24
+ resolve(result);
25
+ };
26
+ child.on("error", (err) => {
27
+ done({ ok: false, error: err.message || String(err) });
28
+ });
29
+ child.unref?.();
30
+ setTimeout(() => done({ ok: true }), 50).unref?.();
31
+ } catch (err) {
32
+ resolve({ ok: false, error: err?.message || String(err) });
33
+ }
34
+ });
35
+ }
@@ -1,9 +1,9 @@
1
- import { mkdir, readdir, copyFile, stat } from "node:fs/promises";
2
- import { existsSync } from "node:fs";
1
+ import { mkdir, readdir, copyFile, stat, constants } from "node:fs/promises";
3
2
  import { dirname, isAbsolute, join, resolve, sep } from "node:path";
4
3
  import { homedir } from "node:os";
5
4
 
6
5
  const PACKAGE_NAME = "ima2-gen";
6
+ const RECOVERY_DOCS_PATH = "docs/RECOVER_OLD_IMAGES.md";
7
7
 
8
8
  function addStats(a, b) {
9
9
  return {
@@ -24,12 +24,13 @@ async function copyMissingTree(srcDir, dstDir) {
24
24
  continue;
25
25
  }
26
26
  if (!entry.isFile()) continue;
27
- if (existsSync(dst)) {
27
+ try {
28
+ await copyFile(src, dst, constants.COPYFILE_EXCL);
29
+ stats.copied += 1;
30
+ } catch (err) {
31
+ if (err?.code !== "EEXIST") throw err;
28
32
  stats.skippedExisting += 1;
29
- continue;
30
33
  }
31
- await copyFile(src, dst);
32
- stats.copied += 1;
33
34
  }
34
35
  return stats;
35
36
  }
@@ -42,7 +43,7 @@ function isSameOrInside(child, parent) {
42
43
 
43
44
  export async function migrateGeneratedStorage(ctx, options = {}) {
44
45
  const targetDir = ctx.config.storage.generatedDir;
45
- const candidates = options.legacyDirs || getLegacyGeneratedCandidates(ctx, options.env);
46
+ const candidates = options.legacyDirs || await getLegacyGeneratedCandidates(ctx, options.env);
46
47
  const result = {
47
48
  copied: 0,
48
49
  skippedExisting: 0,
@@ -75,13 +76,18 @@ export async function migrateGeneratedStorage(ctx, options = {}) {
75
76
  return result;
76
77
  }
77
78
 
78
- export function getLegacyGeneratedCandidates(ctx, env = process.env) {
79
+ export async function getLegacyGeneratedCandidates(ctx, env = process.env) {
79
80
  const home = env.IMA2_TEST_HOME || homedir();
80
81
  const execPath = env.IMA2_TEST_EXEC_PATH || process.execPath;
81
82
  const argv1 = env.IMA2_TEST_ARGV1 || process.argv[1] || "";
82
83
  const nodePrefix = dirname(dirname(execPath));
83
84
  const prefixes = getGlobalPrefixCandidates({ env, execPath, argv1 });
84
85
  const appData = env.APPDATA || join(home, "AppData", "Roaming");
86
+ const localAppData = env.LOCALAPPDATA || join(home, "AppData", "Local");
87
+ const npmCache = env.npm_config_cache || join(home, ".npm");
88
+ const xdgDataHome = env.XDG_DATA_HOME || join(home, ".local", "share");
89
+ const pnpmHome = env.PNPM_HOME || "";
90
+ const nvmHome = env.NVM_HOME || join(appData, "nvm");
85
91
 
86
92
  const candidates = [
87
93
  join(ctx.rootDir, "generated"),
@@ -90,6 +96,12 @@ export function getLegacyGeneratedCandidates(ctx, env = process.env) {
90
96
  join(home, ".nvm", "versions", "node", process.version, "lib", "node_modules", PACKAGE_NAME, "generated"),
91
97
  join(home, ".volta", "tools", "image", "packages", PACKAGE_NAME, "lib", "node_modules", PACKAGE_NAME, "generated"),
92
98
  join(home, ".fnm", "node-versions", process.version, "installation", "lib", "node_modules", PACKAGE_NAME, "generated"),
99
+ join(home, ".bun", "install", "global", "node_modules", PACKAGE_NAME, "generated"),
100
+ join(home, ".config", "yarn", "global", "node_modules", PACKAGE_NAME, "generated"),
101
+ join(localAppData, "Yarn", "Data", "global", "node_modules", PACKAGE_NAME, "generated"),
102
+ join(localAppData, "Volta", "tools", "image", "packages", PACKAGE_NAME, "lib", "node_modules", PACKAGE_NAME, "generated"),
103
+ join(nvmHome, process.version, "node_modules", PACKAGE_NAME, "generated"),
104
+ join(dirname(execPath), "node_modules", PACKAGE_NAME, "generated"),
93
105
  ];
94
106
 
95
107
  for (const prefix of prefixes) {
@@ -98,7 +110,160 @@ export function getLegacyGeneratedCandidates(ctx, env = process.env) {
98
110
  }
99
111
 
100
112
  candidates.push(join(nodePrefix, "lib", "node_modules", PACKAGE_NAME, "generated"));
101
- return Array.from(new Set(candidates.map((p) => resolve(p))));
113
+ candidates.push(
114
+ ...await expandOneLevelCandidates([
115
+ [join(home, ".nvm", "versions", "node"), ["*", "lib", "node_modules", PACKAGE_NAME, "generated"]],
116
+ [join(home, ".fnm", "node-versions"), ["*", "installation", "lib", "node_modules", PACKAGE_NAME, "generated"]],
117
+ [join(home, ".asdf", "installs", "nodejs"), ["*", "lib", "node_modules", PACKAGE_NAME, "generated"]],
118
+ [join(home, ".local", "share", "mise", "installs", "node"), ["*", "lib", "node_modules", PACKAGE_NAME, "generated"]],
119
+ [join(home, "Library", "pnpm", "global"), ["*", "node_modules", PACKAGE_NAME, "generated"]],
120
+ [join(xdgDataHome, "pnpm", "global"), ["*", "node_modules", PACKAGE_NAME, "generated"]],
121
+ [join(localAppData, "pnpm", "global"), ["*", "node_modules", PACKAGE_NAME, "generated"]],
122
+ [pnpmHome ? join(pnpmHome, "global") : "", ["*", "node_modules", PACKAGE_NAME, "generated"]],
123
+ [join(npmCache, "_npx"), ["*", "node_modules", PACKAGE_NAME, "generated"]],
124
+ [join(localAppData, "npm-cache", "_npx"), ["*", "node_modules", PACKAGE_NAME, "generated"]],
125
+ [join(appData, "npm-cache", "_npx"), ["*", "node_modules", PACKAGE_NAME, "generated"]],
126
+ [nvmHome, ["*", "node_modules", PACKAGE_NAME, "generated"]],
127
+ ]),
128
+ );
129
+ return uniqueResolvedCandidates(candidates);
130
+ }
131
+
132
+ export async function inspectGeneratedStorage(ctx, options = {}) {
133
+ const env = options.env || process.env;
134
+ const targetDir = ctx.config.storage.generatedDir;
135
+ try {
136
+ const candidates = options.legacyDirs || await getLegacyGeneratedCandidates(ctx, env);
137
+ const targetFileCount = await countFiles(targetDir);
138
+ const legacySources = [];
139
+
140
+ for (const candidate of candidates) {
141
+ if (isSameOrInside(candidate, targetDir) || isSameOrInside(targetDir, candidate)) continue;
142
+ try {
143
+ const candidateStat = await stat(candidate);
144
+ if (!candidateStat.isDirectory()) continue;
145
+ const fileCount = await countFiles(candidate);
146
+ if (fileCount > 0) legacySources.push({ path: candidate, fileCount });
147
+ } catch (err) {
148
+ if (err?.code !== "ENOENT") {
149
+ console.warn("[storage] legacy candidate inspect skipped:", candidate, err.message);
150
+ }
151
+ }
152
+ }
153
+
154
+ const legacyFilesFound = legacySources.reduce((sum, source) => sum + source.fileCount, 0);
155
+ const state =
156
+ targetFileCount > 0 ? "ok"
157
+ : legacyFilesFound > 0 ? "recoverable"
158
+ : "not_found";
159
+
160
+ return {
161
+ ok: true,
162
+ targetDir,
163
+ generatedDirLabel: labelPath(targetDir, env),
164
+ targetExists: await isDirectory(targetDir),
165
+ targetFileCount,
166
+ legacyCandidatesScanned: candidates.length,
167
+ legacySourcesFound: legacySources.length,
168
+ legacyFilesFound,
169
+ legacySources,
170
+ overrides: {
171
+ generatedDir: Boolean(env.IMA2_GENERATED_DIR),
172
+ configDir: Boolean(env.IMA2_CONFIG_DIR),
173
+ },
174
+ state,
175
+ messageKind: state === "not_found" ? "apology" : state,
176
+ recoveryDocsPath: RECOVERY_DOCS_PATH,
177
+ doctorCommand: "ima2 doctor",
178
+ };
179
+ } catch (err) {
180
+ return {
181
+ ok: false,
182
+ targetDir,
183
+ generatedDirLabel: labelPath(targetDir, env),
184
+ targetExists: false,
185
+ targetFileCount: 0,
186
+ legacyCandidatesScanned: 0,
187
+ legacySourcesFound: 0,
188
+ legacyFilesFound: 0,
189
+ legacySources: [],
190
+ overrides: {
191
+ generatedDir: Boolean(env.IMA2_GENERATED_DIR),
192
+ configDir: Boolean(env.IMA2_CONFIG_DIR),
193
+ },
194
+ state: "unknown",
195
+ messageKind: "unknown",
196
+ recoveryDocsPath: RECOVERY_DOCS_PATH,
197
+ doctorCommand: "ima2 doctor",
198
+ error: err?.message || String(err),
199
+ };
200
+ }
201
+ }
202
+
203
+ async function expandOneLevelCandidates(patterns) {
204
+ const candidates = [];
205
+ for (const [baseDir, segments] of patterns) {
206
+ if (!baseDir) continue;
207
+ candidates.push(...await expandOneLevelPattern(baseDir, segments));
208
+ }
209
+ return candidates;
210
+ }
211
+
212
+ async function expandOneLevelPattern(baseDir, segments) {
213
+ const wildcardIndex = segments.indexOf("*");
214
+ if (wildcardIndex < 0) return [join(baseDir, ...segments)];
215
+
216
+ const before = segments.slice(0, wildcardIndex);
217
+ const after = segments.slice(wildcardIndex + 1);
218
+ const wildcardBase = join(baseDir, ...before);
219
+ try {
220
+ const entries = await readdir(wildcardBase, { withFileTypes: true });
221
+ return entries
222
+ .filter((entry) => entry.isDirectory())
223
+ .map((entry) => join(wildcardBase, entry.name, ...after));
224
+ } catch (err) {
225
+ if (err?.code === "ENOENT") return [];
226
+ console.warn("[storage] legacy candidate scan skipped:", wildcardBase, err.message);
227
+ return [];
228
+ }
229
+ }
230
+
231
+ async function countFiles(dir) {
232
+ try {
233
+ const entries = await readdir(dir, { withFileTypes: true });
234
+ let count = 0;
235
+ for (const entry of entries) {
236
+ if (entry.name === ".trash") continue;
237
+ const fullPath = join(dir, entry.name);
238
+ if (entry.isDirectory()) count += await countFiles(fullPath);
239
+ else if (entry.isFile()) count += 1;
240
+ }
241
+ return count;
242
+ } catch (err) {
243
+ if (err?.code === "ENOENT") return 0;
244
+ throw err;
245
+ }
246
+ }
247
+
248
+ async function isDirectory(dir) {
249
+ try {
250
+ return (await stat(dir)).isDirectory();
251
+ } catch {
252
+ return false;
253
+ }
254
+ }
255
+
256
+ function uniqueResolvedCandidates(candidates) {
257
+ return Array.from(new Set(candidates.filter(Boolean).map((p) => resolve(p))));
258
+ }
259
+
260
+ function labelPath(targetPath, env = process.env) {
261
+ const home = env.IMA2_TEST_HOME || homedir();
262
+ const resolved = resolve(targetPath);
263
+ const resolvedHome = resolve(home);
264
+ if (resolved === resolvedHome) return "~";
265
+ if (resolved.startsWith(resolvedHome + sep)) return `~${sep}${resolved.slice(resolvedHome.length + 1)}`;
266
+ return resolved;
102
267
  }
103
268
 
104
269
  function getGlobalPrefixCandidates({ env, execPath, argv1 }) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ima2-gen",
3
- "version": "1.0.10",
4
- "description": "GPT Image 2 generator with OAuth & API key support",
3
+ "version": "1.1.0",
4
+ "description": "Local OAuth image generation studio with classic and node workflows",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "ima2": "./bin/ima2.js"
@@ -39,6 +39,7 @@
39
39
  "lib/",
40
40
  "routes/",
41
41
  "ui/dist/",
42
+ "docs/",
42
43
  "assets/",
43
44
  "server.js",
44
45
  "config.js",