ima2-gen 1.0.8 → 1.0.10

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/lib/nodeStore.js CHANGED
@@ -1,4 +1,4 @@
1
- import { writeFile, readFile, access } from "fs/promises";
1
+ import { writeFile, readFile, access, mkdir } from "fs/promises";
2
2
  import { join, resolve, sep } from "path";
3
3
  import { randomBytes } from "crypto";
4
4
  import { config } from "../config.js";
@@ -7,16 +7,17 @@ export function newNodeId() {
7
7
  return "n_" + randomBytes(config.ids.nodeHexBytes).toString("hex");
8
8
  }
9
9
 
10
- export async function saveNode(rootDir, { nodeId, b64, meta, ext = "png" }) {
10
+ export async function saveNode(rootDir, { nodeId, b64, meta, ext = "png", generatedDir = config.storage.generatedDir }) {
11
11
  void rootDir;
12
12
  const filename = `${nodeId}.${ext}`;
13
- await writeFile(join(config.storage.generatedDir, filename), Buffer.from(b64, "base64"));
14
- await writeFile(join(config.storage.generatedDir, filename + ".json"), JSON.stringify(meta, null, 2));
13
+ await mkdir(generatedDir, { recursive: true });
14
+ await writeFile(join(generatedDir, filename), Buffer.from(b64, "base64"));
15
+ await writeFile(join(generatedDir, filename + ".json"), JSON.stringify(meta, null, 2));
15
16
  return { filename };
16
17
  }
17
18
 
18
- export async function loadNodeB64(rootDir, filename) {
19
- const p = resolveGeneratedPath(rootDir, filename);
19
+ export async function loadNodeB64(rootDir, filename, generatedDir = config.storage.generatedDir) {
20
+ const p = resolveGeneratedPath(rootDir, filename, generatedDir);
20
21
  try { await access(p); } catch {
21
22
  const err = new Error(`Node file not found: ${filename}`);
22
23
  err.code = "NODE_NOT_FOUND";
@@ -27,17 +28,17 @@ export async function loadNodeB64(rootDir, filename) {
27
28
  return buf.toString("base64");
28
29
  }
29
30
 
30
- export async function loadNodeMeta(rootDir, nodeId, ext = "png") {
31
+ export async function loadNodeMeta(rootDir, nodeId, ext = "png", generatedDir = config.storage.generatedDir) {
31
32
  void rootDir;
32
33
  try {
33
- return JSON.parse(await readFile(join(config.storage.generatedDir, `${nodeId}.${ext}.json`), "utf-8"));
34
+ return JSON.parse(await readFile(join(generatedDir, `${nodeId}.${ext}.json`), "utf-8"));
34
35
  } catch {
35
36
  return null;
36
37
  }
37
38
  }
38
39
 
39
- export async function loadAssetB64(rootDir, externalSrc) {
40
- const p = resolveGeneratedPath(rootDir, externalSrc);
40
+ export async function loadAssetB64(rootDir, externalSrc, generatedDir = config.storage.generatedDir) {
41
+ const p = resolveGeneratedPath(rootDir, externalSrc, generatedDir);
41
42
  try { await access(p); } catch {
42
43
  const err = new Error(`Asset file not found: ${externalSrc}`);
43
44
  err.code = "NODE_NOT_FOUND";
@@ -48,7 +49,7 @@ export async function loadAssetB64(rootDir, externalSrc) {
48
49
  return buf.toString("base64");
49
50
  }
50
51
 
51
- function resolveGeneratedPath(rootDir, relPath) {
52
+ function resolveGeneratedPath(rootDir, relPath, generatedDir = config.storage.generatedDir) {
52
53
  void rootDir;
53
54
  if (typeof relPath !== "string" || relPath.length === 0) {
54
55
  const err = new Error("Asset path is required");
@@ -56,7 +57,7 @@ function resolveGeneratedPath(rootDir, relPath) {
56
57
  err.status = 400;
57
58
  throw err;
58
59
  }
59
- const baseDir = resolve(config.storage.generatedDir);
60
+ const baseDir = resolve(generatedDir);
60
61
  const target = resolve(baseDir, relPath);
61
62
  if (target !== baseDir && !target.startsWith(baseDir + sep)) {
62
63
  const err = new Error(`Asset path escapes generated/: ${relPath}`);
@@ -1,21 +1,37 @@
1
1
  import { mkdir, readdir, copyFile, stat } from "node:fs/promises";
2
2
  import { existsSync } from "node:fs";
3
- import { join, resolve, sep } from "node:path";
3
+ import { dirname, isAbsolute, join, resolve, sep } from "node:path";
4
+ import { homedir } from "node:os";
5
+
6
+ const PACKAGE_NAME = "ima2-gen";
7
+
8
+ function addStats(a, b) {
9
+ return {
10
+ copied: a.copied + b.copied,
11
+ skippedExisting: a.skippedExisting + b.skippedExisting,
12
+ };
13
+ }
4
14
 
5
15
  async function copyMissingTree(srcDir, dstDir) {
6
16
  await mkdir(dstDir, { recursive: true });
7
17
  const entries = await readdir(srcDir, { withFileTypes: true });
18
+ let stats = { copied: 0, skippedExisting: 0 };
8
19
  for (const entry of entries) {
9
20
  const src = join(srcDir, entry.name);
10
21
  const dst = join(dstDir, entry.name);
11
22
  if (entry.isDirectory()) {
12
- await copyMissingTree(src, dst);
23
+ stats = addStats(stats, await copyMissingTree(src, dst));
13
24
  continue;
14
25
  }
15
26
  if (!entry.isFile()) continue;
16
- if (existsSync(dst)) continue;
27
+ if (existsSync(dst)) {
28
+ stats.skippedExisting += 1;
29
+ continue;
30
+ }
17
31
  await copyFile(src, dst);
32
+ stats.copied += 1;
18
33
  }
34
+ return stats;
19
35
  }
20
36
 
21
37
  function isSameOrInside(child, parent) {
@@ -24,18 +40,80 @@ function isSameOrInside(child, parent) {
24
40
  return a === b || a.startsWith(b + sep);
25
41
  }
26
42
 
27
- export async function migrateGeneratedStorage(ctx) {
28
- const legacyDir = join(ctx.rootDir, "generated");
43
+ export async function migrateGeneratedStorage(ctx, options = {}) {
29
44
  const targetDir = ctx.config.storage.generatedDir;
30
- if (isSameOrInside(legacyDir, targetDir) || isSameOrInside(targetDir, legacyDir)) return;
45
+ const candidates = options.legacyDirs || getLegacyGeneratedCandidates(ctx, options.env);
46
+ const result = {
47
+ copied: 0,
48
+ skippedExisting: 0,
49
+ sourcesScanned: 0,
50
+ sourcesSkipped: 0,
51
+ };
31
52
  try {
32
- const legacyStat = await stat(legacyDir);
33
- if (!legacyStat.isDirectory()) return;
34
- await copyMissingTree(legacyDir, targetDir);
35
- console.log(`[storage] migrated generated assets to ${targetDir}`);
36
- } catch (err) {
37
- if (err?.code !== "ENOENT") {
38
- console.warn("[storage] generated asset migration skipped:", err.message);
53
+ for (const legacyDir of candidates) {
54
+ if (isSameOrInside(legacyDir, targetDir) || isSameOrInside(targetDir, legacyDir)) {
55
+ result.sourcesSkipped += 1;
56
+ continue;
57
+ }
58
+ try {
59
+ const legacyStat = await stat(legacyDir);
60
+ if (!legacyStat.isDirectory()) continue;
61
+ result.sourcesScanned += 1;
62
+ const copyStats = await copyMissingTree(legacyDir, targetDir);
63
+ result.copied += copyStats.copied;
64
+ result.skippedExisting += copyStats.skippedExisting;
65
+ } catch (err) {
66
+ if (err?.code !== "ENOENT") {
67
+ console.warn("[storage] generated asset migration source skipped:", legacyDir, err.message);
68
+ }
69
+ }
39
70
  }
71
+ if (result.copied > 0) console.log(`[storage] migrated ${result.copied} generated assets to ${targetDir}`);
72
+ } catch (err) {
73
+ console.warn("[storage] generated asset migration skipped:", err.message);
40
74
  }
75
+ return result;
76
+ }
77
+
78
+ export function getLegacyGeneratedCandidates(ctx, env = process.env) {
79
+ const home = env.IMA2_TEST_HOME || homedir();
80
+ const execPath = env.IMA2_TEST_EXEC_PATH || process.execPath;
81
+ const argv1 = env.IMA2_TEST_ARGV1 || process.argv[1] || "";
82
+ const nodePrefix = dirname(dirname(execPath));
83
+ const prefixes = getGlobalPrefixCandidates({ env, execPath, argv1 });
84
+ const appData = env.APPDATA || join(home, "AppData", "Roaming");
85
+
86
+ const candidates = [
87
+ join(ctx.rootDir, "generated"),
88
+ join(appData, "npm", "node_modules", PACKAGE_NAME, "generated"),
89
+ join(home, ".npm-global", "lib", "node_modules", PACKAGE_NAME, "generated"),
90
+ join(home, ".nvm", "versions", "node", process.version, "lib", "node_modules", PACKAGE_NAME, "generated"),
91
+ join(home, ".volta", "tools", "image", "packages", PACKAGE_NAME, "lib", "node_modules", PACKAGE_NAME, "generated"),
92
+ join(home, ".fnm", "node-versions", process.version, "installation", "lib", "node_modules", PACKAGE_NAME, "generated"),
93
+ ];
94
+
95
+ for (const prefix of prefixes) {
96
+ candidates.push(join(prefix, "lib", "node_modules", PACKAGE_NAME, "generated"));
97
+ candidates.push(join(prefix, "node_modules", PACKAGE_NAME, "generated"));
98
+ }
99
+
100
+ candidates.push(join(nodePrefix, "lib", "node_modules", PACKAGE_NAME, "generated"));
101
+ return Array.from(new Set(candidates.map((p) => resolve(p))));
102
+ }
103
+
104
+ function getGlobalPrefixCandidates({ env, execPath, argv1 }) {
105
+ const prefixes = new Set();
106
+ if (env.npm_config_prefix) prefixes.add(env.npm_config_prefix);
107
+ if (isAbsolute(argv1)) prefixes.add(dirname(dirname(argv1)));
108
+ prefixes.add(dirname(dirname(execPath)));
109
+ addHomebrewPrefix(prefixes, execPath);
110
+ prefixes.add("/opt/homebrew");
111
+ prefixes.add("/usr/local");
112
+ return Array.from(prefixes);
113
+ }
114
+
115
+ function addHomebrewPrefix(prefixes, execPath) {
116
+ const marker = `${sep}Cellar${sep}node`;
117
+ const idx = execPath.indexOf(marker);
118
+ if (idx > 0) prefixes.add(execPath.slice(0, idx));
41
119
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ima2-gen",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "GPT Image 2 generator with OAuth & API key support",
5
5
  "type": "module",
6
6
  "bin": {
package/routes/nodes.js CHANGED
@@ -156,9 +156,9 @@ export function registerNodeRoutes(app, ctx) {
156
156
  const startTime = Date.now();
157
157
  let parentB64 = null;
158
158
  if (parentNodeId) {
159
- parentB64 = await loadNodeB64(ctx.rootDir, `${parentNodeId}.png`);
159
+ parentB64 = await loadNodeB64(ctx.rootDir, `${parentNodeId}.png`, ctx.config.storage.generatedDir);
160
160
  } else if (typeof externalSrc === "string" && externalSrc.length > 0) {
161
- parentB64 = await loadAssetB64(ctx.rootDir, externalSrc);
161
+ parentB64 = await loadAssetB64(ctx.rootDir, externalSrc, ctx.config.storage.generatedDir);
162
162
  }
163
163
  logEvent("node", "request", {
164
164
  requestId,
@@ -270,7 +270,13 @@ export function registerNodeRoutes(app, ctx) {
270
270
  moderation,
271
271
  };
272
272
  await mkdir(ctx.config.storage.generatedDir, { recursive: true });
273
- const { filename } = await saveNode(ctx.rootDir, { nodeId, b64, meta, ext: format });
273
+ const { filename } = await saveNode(ctx.rootDir, {
274
+ nodeId,
275
+ b64,
276
+ meta,
277
+ ext: format,
278
+ generatedDir: ctx.config.storage.generatedDir,
279
+ });
274
280
  finishMeta = { nodeId, filename, imageChars: b64.length };
275
281
  finishHttpStatus = 200;
276
282
  logEvent("node", "saved", {
@@ -325,7 +331,7 @@ export function registerNodeRoutes(app, ctx) {
325
331
  app.get("/api/node/:nodeId", async (req, res) => {
326
332
  try {
327
333
  const { nodeId } = req.params;
328
- const meta = await loadNodeMeta(ctx.rootDir, nodeId);
334
+ const meta = await loadNodeMeta(ctx.rootDir, nodeId, "png", ctx.config.storage.generatedDir);
329
335
  if (!meta) {
330
336
  return res.status(404).json({ error: { code: "NODE_NOT_FOUND", message: "Node metadata missing" } });
331
337
  }
@@ -266,13 +266,7 @@ export function registerSessionRoutes(app, ctx) {
266
266
  const code = err.code || "DB_ERROR";
267
267
  const payload = { error: { code, message: err.message } };
268
268
  if (typeof err.currentVersion === "number") payload.currentVersion = err.currentVersion;
269
- if (code === "GRAPH_VERSION_CONFLICT") {
270
- logEvent("session", "graph_conflict", {
271
- sessionId: req.params.id,
272
- code,
273
- currentVersion: err.currentVersion,
274
- });
275
- } else {
269
+ if (code !== "GRAPH_VERSION_CONFLICT") {
276
270
  logError("session", "graph_error", err, { sessionId: req.params.id, code });
277
271
  }
278
272
  res.status(err.status || 500).json(payload);