ima2-gen 1.0.3 → 1.0.4

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.
@@ -1,8 +1,7 @@
1
1
  import { parseArgs } from "../lib/args.js";
2
2
  import { resolveServer, request } from "../lib/client.js";
3
- import { execSync } from "node:child_process";
3
+ import { openUrl } from "../lib/platform.js";
4
4
  import { out, die, color, json, exitCodeForError } from "../lib/output.js";
5
- import { join } from "node:path";
6
5
 
7
6
  const SPEC = {
8
7
  flags: {
@@ -42,16 +41,8 @@ export default async function showCmd(argv) {
42
41
 
43
42
  if (args.reveal) {
44
43
  const url = item.url ? `${server.base}${item.url}` : null;
45
- try {
46
- if (process.platform === "darwin") {
47
- execSync(`open "${server.base}${item.url || ""}"`);
48
- } else if (process.platform === "win32") {
49
- execSync(`start "" "${url}"`);
50
- } else {
51
- execSync(`xdg-open "${url}"`);
52
- }
53
- } catch {
54
- out(color.yellow("(could not reveal)"));
55
- }
44
+ if (!url) { out(color.yellow("(no url)")); return; }
45
+ const res = openUrl(url);
46
+ if (!res.ok) out(color.yellow("(could not reveal)"));
56
47
  }
57
48
  }
package/bin/ima2.js CHANGED
@@ -5,12 +5,17 @@ import { join, dirname } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import { spawn, execSync } from "child_process";
7
7
  import { networkInterfaces, homedir } from "os";
8
+ import { openUrl, resolveBin } from "./lib/platform.js";
8
9
 
9
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
11
  const ROOT = join(__dirname, "..");
11
12
  const HOME = homedir();
12
- const CONFIG_DIR = join(ROOT, ".ima2");
13
+ // Config lives in $IMA2_CONFIG_DIR (tests) or ~/.ima2 to match server.js and
14
+ // ~/.ima2/server.json advertise path. Legacy installs that stored config at
15
+ // <packageRoot>/.ima2/config.json will be migrated on first write.
16
+ const CONFIG_DIR = process.env.IMA2_CONFIG_DIR || join(HOME, ".ima2");
13
17
  const CONFIG_FILE = join(CONFIG_DIR, "config.json");
18
+ const LEGACY_CONFIG_FILE = join(ROOT, ".ima2", "config.json");
14
19
 
15
20
  // Load package.json for version
16
21
  let pkg = { version: "?", name: "ima2-gen" };
@@ -22,6 +27,10 @@ function loadConfig() {
22
27
  if (existsSync(CONFIG_FILE)) {
23
28
  return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
24
29
  }
30
+ // One-time read from legacy location so users who set up on <1.0.4 don't lose auth.
31
+ if (existsSync(LEGACY_CONFIG_FILE)) {
32
+ try { return JSON.parse(readFileSync(LEGACY_CONFIG_FILE, "utf-8")); } catch {}
33
+ }
25
34
  return {};
26
35
  }
27
36
 
@@ -66,7 +75,7 @@ async function setup() {
66
75
  if (!hasAuth) {
67
76
  console.log(" Running 'codex login' — follow the browser prompt.\n");
68
77
  try {
69
- execSync("npx @openai/codex login", { stdio: "inherit" });
78
+ execSync(`${resolveBin("npx")} @openai/codex login`, { stdio: "inherit" });
70
79
  } catch {
71
80
  console.log("\n Login failed or cancelled. You can retry with 'ima2 serve'.\n");
72
81
  rl.close();
@@ -98,7 +107,7 @@ async function serve() {
98
107
  if (hasUiSrc) {
99
108
  console.log("\n ui/dist missing — running 'npm run build' first...\n");
100
109
  try {
101
- execSync("npm run build", { stdio: "inherit", cwd: ROOT });
110
+ execSync(`${resolveBin("npm")} run build`, { stdio: "inherit", cwd: ROOT });
102
111
  } catch {
103
112
  console.log("\n Build failed. Try: cd ui && npm install && npm run build\n");
104
113
  process.exit(1);
@@ -219,17 +228,10 @@ async function doctor() {
219
228
  function openBrowser() {
220
229
  const port = process.env.PORT || 3333;
221
230
  const url = `http://localhost:${port}`;
222
-
223
- const platform = process.platform;
224
- let cmd;
225
- if (platform === "darwin") cmd = "open";
226
- else if (platform === "win32") cmd = "start";
227
- else cmd = "xdg-open";
228
-
229
- try {
230
- execSync(`${cmd} ${url}`, { stdio: "ignore" });
231
+ const res = openUrl(url);
232
+ if (res.ok) {
231
233
  console.log(`\n Opening ${url} ...\n`);
232
- } catch {
234
+ } else {
233
235
  console.log(`\n Could not open browser. Visit: ${url}\n`);
234
236
  }
235
237
  }
@@ -0,0 +1,89 @@
1
+ // Cross-platform helpers (Windows / macOS / Linux / WSL).
2
+ // Keep this file tiny & dependency-free. Node 18+ only.
3
+
4
+ import { spawn, execSync } from "node:child_process";
5
+ import { readFileSync } from "node:fs";
6
+
7
+ export const isWin = process.platform === "win32";
8
+ export const isMac = process.platform === "darwin";
9
+ export const isLinux = !isWin && !isMac;
10
+
11
+ let _wslCached = null;
12
+ export function isWsl() {
13
+ if (_wslCached !== null) return _wslCached;
14
+ if (!isLinux) return (_wslCached = false);
15
+ try {
16
+ _wslCached = readFileSync("/proc/version", "utf-8").toLowerCase().includes("microsoft");
17
+ } catch {
18
+ _wslCached = false;
19
+ }
20
+ return _wslCached;
21
+ }
22
+
23
+ export function hasDesktopSession() {
24
+ return Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
25
+ }
26
+
27
+ /**
28
+ * Resolve an executable name that differs between Windows and Unix.
29
+ * On Windows, npm global shims are .cmd files; spawn() without shell:true
30
+ * cannot resolve them and fails with ENOENT.
31
+ */
32
+ export function resolveBin(name) {
33
+ return isWin ? `${name}.cmd` : name;
34
+ }
35
+
36
+ /**
37
+ * spawn() wrapper that works for npm/npx/any PATH-resolved exe on Windows.
38
+ */
39
+ export function spawnBin(name, args, opts = {}) {
40
+ return spawn(resolveBin(name), args, { windowsHide: true, ...opts });
41
+ }
42
+
43
+ /**
44
+ * Open a URL in the user's default browser.
45
+ * Returns { ok: boolean, error?: string }.
46
+ * Handles WSL (via powershell.exe) and refuses on headless Linux without DISPLAY.
47
+ */
48
+ export function openUrl(url) {
49
+ try {
50
+ if (isMac) {
51
+ execSync(`open ${JSON.stringify(url)}`, { stdio: "ignore" });
52
+ } else if (isWin) {
53
+ execSync(`cmd /c start "" ${JSON.stringify(url)}`, { stdio: "ignore" });
54
+ } else if (isWsl()) {
55
+ // WSL: hand off to Windows via powershell
56
+ execSync(`powershell.exe -NoProfile -Command Start-Process ${JSON.stringify(url)}`, { stdio: "ignore" });
57
+ } else {
58
+ if (!hasDesktopSession()) {
59
+ return { ok: false, error: "no desktop session (DISPLAY/WAYLAND_DISPLAY unset)" };
60
+ }
61
+ execSync(`xdg-open ${JSON.stringify(url)}`, { stdio: "ignore" });
62
+ }
63
+ return { ok: true };
64
+ } catch (e) {
65
+ return { ok: false, error: e.message || String(e) };
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Register graceful shutdown handlers.
71
+ * Windows does NOT raise SIGTERM from the OS — SIGINT (Ctrl+C) and SIGBREAK
72
+ * (Ctrl+Break) are the observable signals. We still register SIGTERM so that
73
+ * Node-internal `child.kill("SIGTERM")` calls work in tests.
74
+ */
75
+ export function onShutdown(handler) {
76
+ const signals = isWin
77
+ ? ["SIGINT", "SIGTERM", "SIGBREAK"]
78
+ : ["SIGINT", "SIGTERM", "SIGHUP"];
79
+ for (const sig of signals) {
80
+ try {
81
+ process.on(sig, () => {
82
+ try { handler(sig); } finally { process.exit(0); }
83
+ });
84
+ } catch {
85
+ // Some signals aren't installable on certain platforms; ignore.
86
+ }
87
+ }
88
+ }
89
+
package/lib/db.js ADDED
@@ -0,0 +1,92 @@
1
+ import Database from "better-sqlite3";
2
+ import { mkdirSync, existsSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ import { homedir } from "os";
5
+
6
+ const DEFAULT_DB_DIR = join(homedir(), ".ima2");
7
+ const DEFAULT_DB_PATH = join(DEFAULT_DB_DIR, "sessions.db");
8
+
9
+ let db = null;
10
+
11
+ export function getDbPath() {
12
+ return process.env.IMA2_DB_PATH || DEFAULT_DB_PATH;
13
+ }
14
+
15
+ export function getDb() {
16
+ if (db) return db;
17
+ const dbPath = getDbPath();
18
+ const dir = dirname(dbPath);
19
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
20
+
21
+ db = new Database(dbPath);
22
+ db.pragma("journal_mode = WAL");
23
+ db.pragma("foreign_keys = ON");
24
+ migrate(db);
25
+ return db;
26
+ }
27
+
28
+ function migrate(database) {
29
+ database.exec(`
30
+ CREATE TABLE IF NOT EXISTS _meta (
31
+ key TEXT PRIMARY KEY,
32
+ value TEXT NOT NULL
33
+ );
34
+
35
+ CREATE TABLE IF NOT EXISTS sessions (
36
+ id TEXT PRIMARY KEY,
37
+ title TEXT NOT NULL DEFAULT 'Untitled',
38
+ created_at INTEGER NOT NULL,
39
+ updated_at INTEGER NOT NULL,
40
+ graph_version INTEGER NOT NULL DEFAULT 0
41
+ );
42
+
43
+ CREATE TABLE IF NOT EXISTS nodes (
44
+ session_id TEXT NOT NULL,
45
+ id TEXT NOT NULL,
46
+ x REAL NOT NULL DEFAULT 0,
47
+ y REAL NOT NULL DEFAULT 0,
48
+ data TEXT NOT NULL DEFAULT '{}',
49
+ PRIMARY KEY (session_id, id),
50
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
51
+ );
52
+
53
+ CREATE TABLE IF NOT EXISTS edges (
54
+ session_id TEXT NOT NULL,
55
+ id TEXT NOT NULL,
56
+ source TEXT NOT NULL,
57
+ target TEXT NOT NULL,
58
+ data TEXT NOT NULL DEFAULT '{}',
59
+ PRIMARY KEY (session_id, id),
60
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
61
+ );
62
+
63
+ CREATE INDEX IF NOT EXISTS idx_nodes_session ON nodes(session_id);
64
+ CREATE INDEX IF NOT EXISTS idx_edges_session ON edges(session_id);
65
+ `);
66
+
67
+ const sessionColumns = database
68
+ .prepare("PRAGMA table_info(sessions)")
69
+ .all()
70
+ .map((row) => row.name);
71
+ if (!sessionColumns.includes("graph_version")) {
72
+ database.exec(
73
+ "ALTER TABLE sessions ADD COLUMN graph_version INTEGER NOT NULL DEFAULT 0",
74
+ );
75
+ }
76
+
77
+ const row = database.prepare("SELECT value FROM _meta WHERE key = 'schema_version'").get();
78
+ if (!row) {
79
+ database.prepare("INSERT INTO _meta (key, value) VALUES ('schema_version', '2')").run();
80
+ } else if (row.value !== "2") {
81
+ database
82
+ .prepare("UPDATE _meta SET value = '2' WHERE key = 'schema_version'")
83
+ .run();
84
+ }
85
+ }
86
+
87
+ export function closeDb() {
88
+ if (db) {
89
+ db.close();
90
+ db = null;
91
+ }
92
+ }
@@ -0,0 +1,57 @@
1
+ // In-memory inflight job registry.
2
+ // Tracks generation requests that are currently running on the server so clients
3
+ // can reconcile optimistic UI state after a reload or across tabs.
4
+ //
5
+ // This is intentionally process-local: if the server restarts, inflight jobs
6
+ // are lost (which is correct — the fetch they came from is already gone).
7
+
8
+ const jobs = new Map(); // requestId -> { requestId, kind, prompt, meta, startedAt, phase, phaseAt }
9
+
10
+ // Phases: "queued" → "streaming" (upstream connection open, waiting for image)
11
+ // → "decoding" (b64 received, writing to disk)
12
+ // finishJob removes the entry entirely.
13
+ export function startJob({ requestId, kind, prompt, meta = {} }) {
14
+ if (!requestId) return;
15
+ jobs.set(requestId, {
16
+ requestId,
17
+ kind,
18
+ prompt: typeof prompt === "string" ? prompt.slice(0, 500) : "",
19
+ meta,
20
+ startedAt: Date.now(),
21
+ phase: "queued",
22
+ phaseAt: Date.now(),
23
+ });
24
+ }
25
+
26
+ export function setJobPhase(requestId, phase) {
27
+ if (!requestId) return;
28
+ const j = jobs.get(requestId);
29
+ if (!j) return;
30
+ j.phase = phase;
31
+ j.phaseAt = Date.now();
32
+ }
33
+
34
+ export function finishJob(requestId, _options = {}) {
35
+ if (!requestId) return;
36
+ jobs.delete(requestId);
37
+ }
38
+
39
+ export function listJobs(filters = {}) {
40
+ // Stale reaping: > 10 min is almost certainly a crashed fetch.
41
+ const now = Date.now();
42
+ for (const [id, j] of jobs) {
43
+ if (now - j.startedAt > 10 * 60 * 1000) jobs.delete(id);
44
+ }
45
+ const { kind, sessionId } = filters;
46
+ return Array.from(jobs.values())
47
+ .filter((j) => {
48
+ if (kind && j.kind !== kind) return false;
49
+ if (sessionId && j.meta?.sessionId !== sessionId) return false;
50
+ return true;
51
+ })
52
+ .sort((a, b) => a.startedAt - b.startedAt);
53
+ }
54
+
55
+ export function _resetForTests() {
56
+ jobs.clear();
57
+ }
@@ -0,0 +1,66 @@
1
+ import { writeFile, readFile, access } from "fs/promises";
2
+ import { join, resolve, sep } from "path";
3
+ import { randomBytes } from "crypto";
4
+
5
+ const DIR = "generated";
6
+
7
+ export function newNodeId() {
8
+ return "n_" + randomBytes(5).toString("hex");
9
+ }
10
+
11
+ export async function saveNode(rootDir, { nodeId, b64, meta, ext = "png" }) {
12
+ const filename = `${nodeId}.${ext}`;
13
+ await writeFile(join(rootDir, DIR, filename), Buffer.from(b64, "base64"));
14
+ await writeFile(join(rootDir, DIR, filename + ".json"), JSON.stringify(meta, null, 2));
15
+ return { filename };
16
+ }
17
+
18
+ export async function loadNodeB64(rootDir, filename) {
19
+ const p = resolveGeneratedPath(rootDir, filename);
20
+ try { await access(p); } catch {
21
+ const err = new Error(`Node file not found: ${filename}`);
22
+ err.code = "NODE_NOT_FOUND";
23
+ err.status = 404;
24
+ throw err;
25
+ }
26
+ const buf = await readFile(p);
27
+ return buf.toString("base64");
28
+ }
29
+
30
+ export async function loadNodeMeta(rootDir, nodeId, ext = "png") {
31
+ try {
32
+ return JSON.parse(await readFile(join(rootDir, DIR, `${nodeId}.${ext}.json`), "utf-8"));
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ export async function loadAssetB64(rootDir, externalSrc) {
39
+ const p = resolveGeneratedPath(rootDir, externalSrc);
40
+ try { await access(p); } catch {
41
+ const err = new Error(`Asset file not found: ${externalSrc}`);
42
+ err.code = "NODE_NOT_FOUND";
43
+ err.status = 404;
44
+ throw err;
45
+ }
46
+ const buf = await readFile(p);
47
+ return buf.toString("base64");
48
+ }
49
+
50
+ function resolveGeneratedPath(rootDir, relPath) {
51
+ if (typeof relPath !== "string" || relPath.length === 0) {
52
+ const err = new Error("Asset path is required");
53
+ err.code = "NODE_SOURCE_INVALID";
54
+ err.status = 400;
55
+ throw err;
56
+ }
57
+ const baseDir = resolve(rootDir, DIR);
58
+ const target = resolve(baseDir, relPath);
59
+ if (target !== baseDir && !target.startsWith(baseDir + sep)) {
60
+ const err = new Error(`Asset path escapes generated/: ${relPath}`);
61
+ err.code = "NODE_SOURCE_INVALID";
62
+ err.status = 400;
63
+ throw err;
64
+ }
65
+ return target;
66
+ }
@@ -0,0 +1,182 @@
1
+ import { ulid } from "ulid";
2
+ import { getDb } from "./db.js";
3
+
4
+ function now() {
5
+ return Date.now();
6
+ }
7
+
8
+ export function createSession({ title = "Untitled" } = {}) {
9
+ const db = getDb();
10
+ const id = "s_" + ulid();
11
+ const t = now();
12
+ db.prepare(
13
+ "INSERT INTO sessions (id, title, created_at, updated_at, graph_version) VALUES (?, ?, ?, ?, 0)",
14
+ ).run(id, title, t, t);
15
+ return { id, title, createdAt: t, updatedAt: t, graphVersion: 0 };
16
+ }
17
+
18
+ export function listSessions() {
19
+ const db = getDb();
20
+ const rows = db
21
+ .prepare(
22
+ "SELECT id, title, created_at AS createdAt, updated_at AS updatedAt, graph_version AS graphVersion FROM sessions ORDER BY updated_at DESC",
23
+ )
24
+ .all();
25
+ return rows.map((r) => ({
26
+ ...r,
27
+ nodeCount: db
28
+ .prepare("SELECT COUNT(*) AS c FROM nodes WHERE session_id = ?")
29
+ .get(r.id).c,
30
+ }));
31
+ }
32
+
33
+ export function getSession(id) {
34
+ const db = getDb();
35
+ const session = db
36
+ .prepare(
37
+ "SELECT id, title, created_at AS createdAt, updated_at AS updatedAt, graph_version AS graphVersion FROM sessions WHERE id = ?",
38
+ )
39
+ .get(id);
40
+ if (!session) return null;
41
+ const nodes = db
42
+ .prepare("SELECT id, x, y, data FROM nodes WHERE session_id = ?")
43
+ .all(id)
44
+ .map((n) => ({ id: n.id, x: n.x, y: n.y, data: safeParse(n.data) }));
45
+ const edges = db
46
+ .prepare("SELECT id, source, target, data FROM edges WHERE session_id = ?")
47
+ .all(id)
48
+ .map((e) => ({
49
+ id: e.id,
50
+ source: e.source,
51
+ target: e.target,
52
+ data: safeParse(e.data),
53
+ }));
54
+ return { ...session, nodes, edges };
55
+ }
56
+
57
+ export function renameSession(id, title) {
58
+ const db = getDb();
59
+ const res = db
60
+ .prepare("UPDATE sessions SET title = ?, updated_at = ? WHERE id = ?")
61
+ .run(title, now(), id);
62
+ return res.changes > 0;
63
+ }
64
+
65
+ export function deleteSession(id) {
66
+ const db = getDb();
67
+ const res = db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
68
+ return res.changes > 0;
69
+ }
70
+
71
+ const MAX_STR = 10_000;
72
+
73
+ function cleanStr(v) {
74
+ if (typeof v !== "string") return "";
75
+ return v.length > MAX_STR ? v.slice(0, MAX_STR) : v;
76
+ }
77
+
78
+ function cleanData(v) {
79
+ try {
80
+ const json = JSON.stringify(v ?? {});
81
+ return json.length > MAX_STR * 10 ? "{}" : json;
82
+ } catch {
83
+ return "{}";
84
+ }
85
+ }
86
+
87
+ export function saveGraph(sessionId, { nodes = [], edges = [], expectedVersion = null }) {
88
+ const db = getDb();
89
+ const sessionExists = db
90
+ .prepare("SELECT 1 FROM sessions WHERE id = ?")
91
+ .get(sessionId);
92
+ if (!sessionExists) {
93
+ const err = new Error(`Session not found: ${sessionId}`);
94
+ err.code = "SESSION_NOT_FOUND";
95
+ err.status = 404;
96
+ throw err;
97
+ }
98
+
99
+ const versionRow = db
100
+ .prepare("SELECT graph_version AS graphVersion FROM sessions WHERE id = ?")
101
+ .get(sessionId);
102
+ const currentVersion = versionRow?.graphVersion ?? 0;
103
+ if (
104
+ typeof expectedVersion === "number" &&
105
+ Number.isFinite(expectedVersion) &&
106
+ expectedVersion !== currentVersion
107
+ ) {
108
+ const err = new Error(
109
+ `Graph version conflict for session ${sessionId}: expected ${expectedVersion}, got ${currentVersion}`,
110
+ );
111
+ err.code = "GRAPH_VERSION_CONFLICT";
112
+ err.status = 409;
113
+ err.currentVersion = currentVersion;
114
+ throw err;
115
+ }
116
+
117
+ // Validate edges reference existing nodes (drop dangling).
118
+ const nodeIds = new Set(nodes.map((n) => n?.id).filter(Boolean).map(String));
119
+ const cleanEdges = edges.filter(
120
+ (e) => e?.id && e?.source && e?.target && nodeIds.has(String(e.source)) && nodeIds.has(String(e.target)),
121
+ );
122
+
123
+ const tx = db.transaction(() => {
124
+ db.prepare("DELETE FROM nodes WHERE session_id = ?").run(sessionId);
125
+ db.prepare("DELETE FROM edges WHERE session_id = ?").run(sessionId);
126
+
127
+ const insNode = db.prepare(
128
+ "INSERT INTO nodes (session_id, id, x, y, data) VALUES (?, ?, ?, ?, ?)",
129
+ );
130
+ for (const n of nodes) {
131
+ if (!n?.id) continue;
132
+ const x = Number(n.x ?? n.position?.x ?? 0);
133
+ const y = Number(n.y ?? n.position?.y ?? 0);
134
+ insNode.run(
135
+ sessionId,
136
+ cleanStr(String(n.id)),
137
+ Number.isFinite(x) ? x : 0,
138
+ Number.isFinite(y) ? y : 0,
139
+ cleanData(n.data),
140
+ );
141
+ }
142
+
143
+ const insEdge = db.prepare(
144
+ "INSERT INTO edges (session_id, id, source, target, data) VALUES (?, ?, ?, ?, ?)",
145
+ );
146
+ for (const e of cleanEdges) {
147
+ insEdge.run(
148
+ sessionId,
149
+ cleanStr(String(e.id)),
150
+ cleanStr(String(e.source)),
151
+ cleanStr(String(e.target)),
152
+ cleanData(e.data),
153
+ );
154
+ }
155
+
156
+ db.prepare("UPDATE sessions SET updated_at = ?, graph_version = graph_version + 1 WHERE id = ?").run(
157
+ now(),
158
+ sessionId,
159
+ );
160
+
161
+ return db
162
+ .prepare("SELECT graph_version AS graphVersion FROM sessions WHERE id = ?")
163
+ .get(sessionId).graphVersion;
164
+ });
165
+
166
+ const nextVersion = tx();
167
+ return { ok: true, graphVersion: nextVersion };
168
+ }
169
+
170
+ function safeParse(json) {
171
+ try {
172
+ return JSON.parse(json);
173
+ } catch {
174
+ return {};
175
+ }
176
+ }
177
+
178
+ export function ensureDefaultSession() {
179
+ const sessions = listSessions();
180
+ if (sessions.length > 0) return sessions[0];
181
+ return createSession({ title: "My first graph" });
182
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ima2-gen",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "GPT Image 2 generator with OAuth & API key support",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,10 +13,10 @@
13
13
  "ui:dev": "cd ui && npm run dev",
14
14
  "ui:build": "cd ui && npm run build",
15
15
  "build": "npm run ui:build",
16
- "test": "node --test tests/**/*.test.js",
16
+ "test": "node scripts/run-tests.mjs",
17
17
  "setup": "node bin/ima2.js setup",
18
18
  "prepublishOnly": "npm run build && npm run lint:pkg",
19
- "lint:pkg": "node -e \"const p=require('./package.json'); if(!p.name||!p.version||!p.bin) throw new Error('missing fields')\"",
19
+ "lint:pkg": "node -e \"const p=require('./package.json'); const req=['name','version','bin']; for(const k of req){if(!p[k])throw new Error('missing '+k)} const mustInclude=['bin/','lib/','server.js']; for(const f of mustInclude){if(!p.files.includes(f))throw new Error('files[] must include '+f)}\"",
20
20
  "release:patch": "npm version patch && npm publish && git push origin main --tags",
21
21
  "release:minor": "npm version minor && npm publish && git push origin main --tags",
22
22
  "release:major": "npm version major && npm publish && git push origin main --tags"
@@ -35,6 +35,7 @@
35
35
  },
36
36
  "files": [
37
37
  "bin/",
38
+ "lib/",
38
39
  "ui/dist/",
39
40
  "assets/",
40
41
  "server.js",
@@ -42,7 +43,7 @@
42
43
  "README.md"
43
44
  ],
44
45
  "engines": {
45
- "node": ">=18"
46
+ "node": ">=20"
46
47
  },
47
48
  "dependencies": {
48
49
  "better-sqlite3": "^12.9.0",