ima2-gen 1.1.8 → 1.1.9

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 (230) hide show
  1. package/README.md +56 -27
  2. package/bin/commands/annotate.js +137 -0
  3. package/bin/commands/annotate.ts +118 -0
  4. package/bin/commands/cancel.js +37 -33
  5. package/bin/commands/cancel.ts +45 -0
  6. package/bin/commands/canvas-versions.js +91 -0
  7. package/bin/commands/canvas-versions.ts +80 -0
  8. package/bin/commands/cardnews.js +293 -0
  9. package/bin/commands/cardnews.ts +248 -0
  10. package/bin/commands/comfy.js +63 -0
  11. package/bin/commands/comfy.ts +54 -0
  12. package/bin/commands/config.js +270 -0
  13. package/bin/commands/config.ts +265 -0
  14. package/bin/commands/edit.js +97 -72
  15. package/bin/commands/edit.ts +116 -0
  16. package/bin/commands/gen.js +140 -118
  17. package/bin/commands/gen.ts +176 -0
  18. package/bin/commands/history.js +164 -0
  19. package/bin/commands/history.ts +145 -0
  20. package/bin/commands/ls.js +60 -42
  21. package/bin/commands/ls.ts +60 -0
  22. package/bin/commands/metadata.js +45 -0
  23. package/bin/commands/metadata.ts +36 -0
  24. package/bin/commands/multimode.js +159 -0
  25. package/bin/commands/multimode.ts +146 -0
  26. package/bin/commands/node.js +176 -0
  27. package/bin/commands/node.ts +157 -0
  28. package/bin/commands/observability.js +201 -0
  29. package/bin/commands/observability.ts +176 -0
  30. package/bin/commands/ping.js +26 -20
  31. package/bin/commands/ping.ts +29 -0
  32. package/bin/commands/prompt.js +506 -0
  33. package/bin/commands/prompt.ts +421 -0
  34. package/bin/commands/ps.js +78 -71
  35. package/bin/commands/ps.ts +78 -0
  36. package/bin/commands/session.js +308 -0
  37. package/bin/commands/session.ts +265 -0
  38. package/bin/commands/show.js +75 -40
  39. package/bin/commands/show.ts +69 -0
  40. package/bin/ima2.js +324 -310
  41. package/bin/ima2.ts +444 -0
  42. package/bin/lib/args.js +75 -66
  43. package/bin/lib/args.ts +73 -0
  44. package/bin/lib/browser-id.js +15 -0
  45. package/bin/lib/browser-id.ts +16 -0
  46. package/bin/lib/client.js +91 -83
  47. package/bin/lib/client.ts +109 -0
  48. package/bin/lib/error-hints.js +14 -17
  49. package/bin/lib/error-hints.ts +23 -0
  50. package/bin/lib/files.js +26 -28
  51. package/bin/lib/files.ts +39 -0
  52. package/bin/lib/output.js +44 -42
  53. package/bin/lib/output.ts +58 -0
  54. package/bin/lib/platform.js +60 -56
  55. package/bin/lib/platform.ts +97 -0
  56. package/bin/lib/sse.js +73 -0
  57. package/bin/lib/sse.ts +73 -0
  58. package/bin/lib/star-prompt.js +69 -76
  59. package/bin/lib/star-prompt.ts +97 -0
  60. package/bin/lib/storage-doctor.js +34 -35
  61. package/bin/lib/storage-doctor.ts +38 -0
  62. package/config.js +147 -243
  63. package/config.ts +331 -0
  64. package/docs/API.md +48 -8
  65. package/docs/CLI.md +190 -0
  66. package/docs/FAQ.ko.md +5 -5
  67. package/docs/FAQ.md +5 -5
  68. package/docs/README.ja.md +71 -25
  69. package/docs/README.ko.md +61 -24
  70. package/docs/README.zh-CN.md +73 -27
  71. package/lib/assetLifecycle.js +130 -130
  72. package/lib/assetLifecycle.ts +142 -0
  73. package/lib/canvasVersionStore.js +135 -153
  74. package/lib/canvasVersionStore.ts +181 -0
  75. package/lib/cardNewsGenerator.js +127 -142
  76. package/lib/cardNewsGenerator.ts +162 -0
  77. package/lib/cardNewsJobStore.js +78 -84
  78. package/lib/cardNewsJobStore.ts +107 -0
  79. package/lib/cardNewsManifestStore.js +88 -93
  80. package/lib/cardNewsManifestStore.ts +112 -0
  81. package/lib/cardNewsPlanner.js +157 -152
  82. package/lib/cardNewsPlanner.ts +180 -0
  83. package/lib/cardNewsPlannerClient.js +101 -98
  84. package/lib/cardNewsPlannerClient.ts +114 -0
  85. package/lib/cardNewsPlannerPrompt.js +56 -56
  86. package/lib/cardNewsPlannerPrompt.ts +60 -0
  87. package/lib/cardNewsPlannerSchema.js +231 -223
  88. package/lib/cardNewsPlannerSchema.ts +259 -0
  89. package/lib/cardNewsRoleTemplateStore.js +39 -41
  90. package/lib/cardNewsRoleTemplateStore.ts +47 -0
  91. package/lib/cardNewsTemplateStore.js +171 -175
  92. package/lib/cardNewsTemplateStore.ts +210 -0
  93. package/lib/codexDetect.js +44 -47
  94. package/lib/codexDetect.ts +69 -0
  95. package/lib/comfyBridge.js +164 -184
  96. package/lib/comfyBridge.ts +214 -0
  97. package/lib/db.js +41 -51
  98. package/lib/db.ts +166 -0
  99. package/lib/errorClassify.js +62 -78
  100. package/lib/errorClassify.ts +100 -0
  101. package/lib/generationErrors.js +140 -103
  102. package/lib/generationErrors.ts +125 -0
  103. package/lib/historyList.js +149 -147
  104. package/lib/historyList.ts +164 -0
  105. package/lib/imageMetadata.js +86 -89
  106. package/lib/imageMetadata.ts +111 -0
  107. package/lib/imageMetadataStore.js +46 -51
  108. package/lib/imageMetadataStore.ts +67 -0
  109. package/lib/imageModels.js +38 -45
  110. package/lib/imageModels.ts +52 -0
  111. package/lib/inflight.js +131 -150
  112. package/lib/inflight.ts +204 -0
  113. package/lib/localImportStore.js +87 -93
  114. package/lib/localImportStore.ts +111 -0
  115. package/lib/logger.js +105 -112
  116. package/lib/logger.ts +150 -0
  117. package/lib/nodeStore.js +65 -64
  118. package/lib/nodeStore.ts +81 -0
  119. package/lib/oauthLauncher.js +61 -59
  120. package/lib/oauthLauncher.ts +64 -0
  121. package/lib/oauthNormalize.js +15 -19
  122. package/lib/oauthNormalize.ts +30 -0
  123. package/lib/oauthProxy.js +834 -832
  124. package/lib/oauthProxy.ts +995 -0
  125. package/lib/openDirectory.js +41 -40
  126. package/lib/openDirectory.ts +45 -0
  127. package/lib/pngInfo.js +18 -20
  128. package/lib/pngInfo.ts +26 -0
  129. package/lib/promptImport/curatedSources.js +125 -129
  130. package/lib/promptImport/curatedSources.ts +139 -0
  131. package/lib/promptImport/discoveryRegistry.js +185 -203
  132. package/lib/promptImport/discoveryRegistry.ts +236 -0
  133. package/lib/promptImport/errors.js +10 -10
  134. package/lib/promptImport/errors.ts +18 -0
  135. package/lib/promptImport/githubDiscovery.js +209 -219
  136. package/lib/promptImport/githubDiscovery.ts +248 -0
  137. package/lib/promptImport/githubFolder.js +253 -259
  138. package/lib/promptImport/githubFolder.ts +308 -0
  139. package/lib/promptImport/githubSource.js +189 -200
  140. package/lib/promptImport/githubSource.ts +239 -0
  141. package/lib/promptImport/gptImageHints.js +49 -56
  142. package/lib/promptImport/gptImageHints.ts +68 -0
  143. package/lib/promptImport/parsePromptCandidates.js +108 -123
  144. package/lib/promptImport/parsePromptCandidates.ts +153 -0
  145. package/lib/promptImport/promptIndex.js +190 -208
  146. package/lib/promptImport/promptIndex.ts +248 -0
  147. package/lib/promptImport/rankPromptCandidates.js +46 -43
  148. package/lib/promptImport/rankPromptCandidates.ts +49 -0
  149. package/lib/providerOptions.js +31 -0
  150. package/lib/providerOptions.ts +41 -0
  151. package/lib/referenceImageCompress.js +51 -62
  152. package/lib/referenceImageCompress.ts +75 -0
  153. package/lib/refs.js +93 -81
  154. package/lib/refs.ts +117 -0
  155. package/lib/requestLogger.js +32 -38
  156. package/lib/requestLogger.ts +48 -0
  157. package/lib/responsesImageAdapter.js +351 -0
  158. package/lib/responsesImageAdapter.ts +352 -0
  159. package/lib/runtimePorts.js +71 -73
  160. package/lib/runtimePorts.ts +93 -0
  161. package/lib/sessionStore.js +179 -230
  162. package/lib/sessionStore.ts +272 -0
  163. package/lib/storageMigration.js +247 -245
  164. package/lib/storageMigration.ts +284 -0
  165. package/lib/styleSheet.js +86 -90
  166. package/lib/styleSheet.ts +128 -0
  167. package/lib/systemTrash.js +18 -0
  168. package/lib/systemTrash.ts +20 -0
  169. package/package.json +26 -10
  170. package/routes/annotations.js +76 -79
  171. package/routes/annotations.ts +95 -0
  172. package/routes/canvasVersions.js +50 -54
  173. package/routes/canvasVersions.ts +64 -0
  174. package/routes/cardNews.js +158 -171
  175. package/routes/cardNews.ts +183 -0
  176. package/routes/comfy.js +23 -31
  177. package/routes/comfy.ts +39 -0
  178. package/routes/edit.js +183 -214
  179. package/routes/edit.ts +230 -0
  180. package/routes/generate.js +269 -291
  181. package/routes/generate.ts +309 -0
  182. package/routes/health.js +102 -107
  183. package/routes/health.ts +114 -0
  184. package/routes/history.js +136 -144
  185. package/routes/history.ts +153 -0
  186. package/routes/imageImport.js +27 -27
  187. package/routes/imageImport.ts +33 -0
  188. package/routes/index.js +17 -17
  189. package/routes/index.ts +35 -0
  190. package/routes/metadata.js +60 -64
  191. package/routes/metadata.ts +71 -0
  192. package/routes/multimode.js +228 -263
  193. package/routes/multimode.ts +280 -0
  194. package/routes/nodes.js +378 -424
  195. package/routes/nodes.ts +455 -0
  196. package/routes/promptImport.js +284 -324
  197. package/routes/promptImport.ts +354 -0
  198. package/routes/prompts.js +333 -360
  199. package/routes/prompts.ts +379 -0
  200. package/routes/sessions.js +277 -285
  201. package/routes/sessions.ts +292 -0
  202. package/routes/storage.js +29 -31
  203. package/routes/storage.ts +39 -0
  204. package/server.js +189 -196
  205. package/server.ts +235 -0
  206. package/ui/dist/.vite/manifest.json +101 -0
  207. package/ui/dist/assets/CardNewsWorkspace-BJOCey7Z.js +2 -0
  208. package/ui/dist/assets/NodeCanvas-BZV40eAE.css +1 -0
  209. package/ui/dist/assets/NodeCanvas-C3dzYNsk.js +7 -0
  210. package/ui/dist/assets/PromptImportDialog-Dqu1VpUh.js +2 -0
  211. package/ui/dist/assets/PromptImportDiscoverySection-Dg8T9X0L.js +1 -0
  212. package/ui/dist/assets/PromptImportFolderSection-DBaqsFO4.js +1 -0
  213. package/ui/dist/assets/PromptLibraryPanel-p5QqR97M.js +2 -0
  214. package/ui/dist/assets/SettingsWorkspace-B5bSAZ6u.js +1 -0
  215. package/ui/dist/assets/index-C9cXwiWE.js +25 -0
  216. package/ui/dist/assets/index-CGMIkZXn.css +1 -0
  217. package/ui/dist/assets/index-Cvld7dUZ.js +1 -0
  218. package/ui/dist/index.html +2 -2
  219. package/assets/phase-a-bg-cleanup-test.png +0 -0
  220. package/assets/screenshot.png +0 -0
  221. package/assets/screenshots/classic-generate-light.png +0 -0
  222. package/assets/screenshots/node-graph-branching.png +0 -0
  223. package/assets/screenshots/settings-oauth-generation.png +0 -0
  224. package/assets/screenshots/settings-workspace.png +0 -0
  225. package/assets/screenshots/style-sheet-editor.png +0 -0
  226. package/integrations/comfyui/ima2_gen_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
  227. package/integrations/comfyui/ima2_gen_bridge/__pycache__/nodes.cpython-313.pyc +0 -0
  228. package/ui/dist/assets/index-BDffwmLs.css +0 -1
  229. package/ui/dist/assets/index-D0fdHLkJ.js +0 -31
  230. package/ui/dist/assets/index-D0fdHLkJ.js.map +0 -1
package/server.js CHANGED
@@ -1,13 +1,7 @@
1
1
  import "dotenv/config";
2
2
  import express from "express";
3
3
  import { readFile } from "fs/promises";
4
- import {
5
- existsSync,
6
- writeFileSync,
7
- unlinkSync,
8
- mkdirSync,
9
- readFileSync as fsReadFileSync,
10
- } from "fs";
4
+ import { existsSync, writeFileSync, unlinkSync, mkdirSync, readFileSync as fsReadFileSync, } from "fs";
11
5
  import { dirname, join } from "path";
12
6
  import { fileURLToPath, pathToFileURL } from "url";
13
7
  import { onShutdown } from "./bin/lib/platform.js";
@@ -20,216 +14,215 @@ import { createRequestLogger } from "./lib/requestLogger.js";
20
14
  import { configureRoutes } from "./routes/index.js";
21
15
  import { config } from "./config.js";
22
16
  import { getServerPort, listenWithPortFallback } from "./lib/runtimePorts.js";
23
-
24
17
  const rootDir = dirname(fileURLToPath(import.meta.url));
25
-
26
18
  async function loadApiKey() {
27
- if (process.env.OPENAI_API_KEY) {
28
- return { apiKey: process.env.OPENAI_API_KEY, apiKeySource: "env" };
29
- }
30
- const candidates = [
31
- config.storage.configFile,
32
- join(rootDir, ".ima2", "config.json"),
33
- ];
34
- for (const cfgPath of candidates) {
35
- if (!existsSync(cfgPath)) continue;
36
- try {
37
- const cfg = JSON.parse(await readFile(cfgPath, "utf-8"));
38
- if (cfg.apiKey) return { apiKey: cfg.apiKey, apiKeySource: "config" };
39
- } catch {}
40
- }
41
- return { apiKey: null, apiKeySource: "none" };
19
+ if (process.env.OPENAI_API_KEY) {
20
+ return { apiKey: process.env.OPENAI_API_KEY, apiKeySource: "env" };
21
+ }
22
+ const candidates = [
23
+ config.storage.configFile,
24
+ join(rootDir, ".ima2", "config.json"),
25
+ ];
26
+ for (const cfgPath of candidates) {
27
+ if (!existsSync(cfgPath))
28
+ continue;
29
+ try {
30
+ const cfg = JSON.parse(await readFile(cfgPath, "utf-8"));
31
+ if (cfg.apiKey)
32
+ return { apiKey: cfg.apiKey, apiKeySource: "config" };
33
+ }
34
+ catch { }
35
+ }
36
+ return { apiKey: null, apiKeySource: "none" };
42
37
  }
43
-
44
38
  async function createOpenAI(apiKey) {
45
- if (!apiKey) return null;
46
- const OpenAI = (await import("openai")).default;
47
- return new OpenAI({ apiKey });
39
+ if (!apiKey)
40
+ return null;
41
+ const OpenAI = (await import("openai")).default;
42
+ return new OpenAI({ apiKey });
48
43
  }
49
-
50
44
  function readPackageVersion() {
51
- try {
52
- return JSON.parse(fsReadFileSync(join(rootDir, "package.json"), "utf-8")).version;
53
- } catch {
54
- return "0.0.0";
55
- }
45
+ try {
46
+ return JSON.parse(fsReadFileSync(join(rootDir, "package.json"), "utf-8")).version;
47
+ }
48
+ catch {
49
+ return "0.0.0";
50
+ }
56
51
  }
57
-
58
52
  function setUiStaticHeaders(res, filePath) {
59
- const normalized = filePath.replace(/\\/g, "/");
60
- if (normalized.endsWith("/index.html")) {
61
- res.setHeader("Cache-Control", "no-store, max-age=0");
62
- return;
63
- }
64
- if (normalized.includes("/assets/")) {
65
- res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
66
- }
53
+ const normalized = filePath.replace(/\\/g, "/");
54
+ if (normalized.endsWith("/index.html")) {
55
+ res.setHeader("Cache-Control", "no-store, max-age=0");
56
+ return;
57
+ }
58
+ if (normalized.includes("/assets/")) {
59
+ res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
60
+ }
67
61
  }
68
-
69
62
  export function buildApp(ctx) {
70
- const app = express();
71
- configureLogger({ level: ctx.config.log.level });
72
- app.use(createRequestLogger());
73
- app.use(express.json({ limit: ctx.config.server.bodyLimit }));
74
- app.use(express.static(join(ctx.rootDir, "ui", "dist"), {
75
- setHeaders: setUiStaticHeaders,
76
- }));
77
- app.use("/assets", (_req, res) => {
78
- res.status(404).type("text/plain").send("Asset not found");
79
- });
80
- app.use("/generated", express.static(ctx.config.storage.generatedDir, {
81
- maxAge: ctx.config.storage.staticMaxAge,
82
- immutable: true,
83
- }));
84
- configureRoutes(app, ctx);
85
- return app;
63
+ const app = express();
64
+ configureLogger({ level: ctx.config.log.level });
65
+ app.use(createRequestLogger());
66
+ app.use(express.json({ limit: ctx.config.server.bodyLimit }));
67
+ app.use(express.static(join(ctx.rootDir, "ui", "dist"), {
68
+ setHeaders: setUiStaticHeaders,
69
+ }));
70
+ app.use("/assets", (_req, res) => {
71
+ res.status(404).type("text/plain").send("Asset not found");
72
+ });
73
+ app.use("/generated", express.static(ctx.config.storage.generatedDir, {
74
+ maxAge: ctx.config.storage.staticMaxAge,
75
+ immutable: true,
76
+ }));
77
+ configureRoutes(app, ctx);
78
+ return app;
86
79
  }
87
-
88
80
  function runtimeHostUrl(host) {
89
- if (!host || host === "0.0.0.0" || host === "::") return "localhost";
90
- return host;
81
+ if (!host || host === "0.0.0.0" || host === "::")
82
+ return "localhost";
83
+ return host;
91
84
  }
92
-
93
85
  function advertise(ctx) {
94
- try {
95
- mkdirSync(dirname(ctx.config.storage.advertiseFile), { recursive: true });
96
- writeFileSync(
97
- ctx.config.storage.advertiseFile,
98
- JSON.stringify({
99
- port: Number(ctx.serverActualPort || ctx.config.server.port),
100
- url: ctx.serverUrl,
101
- pid: process.pid,
102
- startedAt: ctx.startedAt,
103
- version: ctx.packageVersion,
104
- backend: {
105
- configuredPort: Number(ctx.serverConfiguredPort || ctx.config.server.port),
106
- actualPort: Number(ctx.serverActualPort || ctx.config.server.port),
107
- url: ctx.serverUrl,
108
- },
109
- oauth: {
110
- configuredPort: Number(ctx.oauthPort),
111
- actualPort: Number(ctx.oauthActualPort || ctx.oauthPort),
112
- url: ctx.oauthUrl,
113
- status: ctx.oauthReadyState,
114
- },
115
- }),
116
- );
117
- } catch (e) {
118
- console.warn("[advertise] skipped:", e.message);
119
- }
86
+ try {
87
+ mkdirSync(dirname(ctx.config.storage.advertiseFile), { recursive: true });
88
+ writeFileSync(ctx.config.storage.advertiseFile, JSON.stringify({
89
+ port: Number(ctx.serverActualPort || ctx.config.server.port),
90
+ url: ctx.serverUrl,
91
+ pid: process.pid,
92
+ startedAt: ctx.startedAt,
93
+ version: ctx.packageVersion,
94
+ backend: {
95
+ configuredPort: Number(ctx.serverConfiguredPort || ctx.config.server.port),
96
+ actualPort: Number(ctx.serverActualPort || ctx.config.server.port),
97
+ url: ctx.serverUrl,
98
+ },
99
+ oauth: {
100
+ configuredPort: Number(ctx.oauthPort),
101
+ actualPort: Number(ctx.oauthActualPort || ctx.oauthPort),
102
+ url: ctx.oauthUrl,
103
+ status: ctx.oauthReadyState,
104
+ },
105
+ }));
106
+ }
107
+ catch (e) {
108
+ console.warn("[advertise] skipped:", e.message);
109
+ }
120
110
  }
121
-
122
111
  function unadvertise(ctx) {
123
- try {
124
- if (!existsSync(ctx.config.storage.advertiseFile)) return;
125
- const cur = JSON.parse(fsReadFileSync(ctx.config.storage.advertiseFile, "utf-8"));
126
- if (cur.pid === process.pid) unlinkSync(ctx.config.storage.advertiseFile);
127
- } catch {}
112
+ try {
113
+ if (!existsSync(ctx.config.storage.advertiseFile))
114
+ return;
115
+ const cur = JSON.parse(fsReadFileSync(ctx.config.storage.advertiseFile, "utf-8"));
116
+ if (cur.pid === process.pid)
117
+ unlinkSync(ctx.config.storage.advertiseFile);
118
+ }
119
+ catch { }
128
120
  }
129
-
130
121
  export async function createRuntimeContext(overrides = {}) {
131
- const loadedKey =
132
- overrides.apiKey !== undefined
133
- ? {
134
- apiKey: overrides.apiKey,
135
- apiKeySource: overrides.apiKeySource ?? (overrides.apiKey ? "env" : "none"),
122
+ const loadedKey = overrides.apiKey !== undefined
123
+ ? {
124
+ apiKey: overrides.apiKey,
125
+ apiKeySource: overrides.apiKeySource ?? (overrides.apiKey ? "env" : "none"),
136
126
  }
137
- : await loadApiKey();
138
- const apiKey = loadedKey.apiKey;
139
- const openai = overrides.openai ?? await createOpenAI(apiKey);
140
- const oauthPort = config.oauth.proxyPort;
141
- const ctx = {
142
- rootDir,
143
- config,
144
- serverConfiguredPort: config.server.port,
145
- serverActualPort: null,
146
- serverUrl: `http://${runtimeHostUrl(config.server.host)}:${config.server.port}`,
147
- oauthPort,
148
- oauthActualPort: oauthPort,
149
- oauthUrl: `http://127.0.0.1:${oauthPort}`,
150
- oauthReadyState: config.oauth.autoStart ? "starting" : "disabled",
151
- hasApiKey: !!apiKey,
152
- apiKey,
153
- apiKeySource: loadedKey.apiKeySource,
154
- openai,
155
- startedAt: overrides.startedAt ?? Date.now(),
156
- packageVersion: overrides.packageVersion ?? readPackageVersion(),
157
- };
158
- let resolveOAuthReady;
159
- ctx.oauthReadyPromise = new Promise((resolve) => {
160
- resolveOAuthReady = resolve;
161
- });
162
- ctx.markOAuthReady = ({ url, port } = {}) => {
163
- if (url) ctx.oauthUrl = url;
164
- if (port) ctx.oauthActualPort = port;
165
- ctx.oauthReadyState = "ready";
166
- resolveOAuthReady(ctx.oauthUrl);
167
- };
168
- ctx.markOAuthFailed = () => {
169
- ctx.oauthReadyState = "failed";
170
- resolveOAuthReady(null);
171
- };
172
- if (!config.oauth.autoStart) ctx.markOAuthReady({ url: ctx.oauthUrl, port: ctx.oauthPort });
173
- return ctx;
127
+ : await loadApiKey();
128
+ const apiKey = loadedKey.apiKey;
129
+ const openai = overrides.openai ?? await createOpenAI(apiKey);
130
+ const oauthPort = config.oauth.proxyPort;
131
+ const ctx = {
132
+ rootDir,
133
+ config,
134
+ serverConfiguredPort: config.server.port,
135
+ serverActualPort: null,
136
+ serverUrl: `http://${runtimeHostUrl(config.server.host)}:${config.server.port}`,
137
+ oauthPort,
138
+ oauthActualPort: oauthPort,
139
+ oauthUrl: `http://127.0.0.1:${oauthPort}`,
140
+ oauthReadyState: config.oauth.autoStart ? "starting" : "disabled",
141
+ hasApiKey: !!apiKey,
142
+ apiKey,
143
+ apiKeySource: loadedKey.apiKeySource,
144
+ openai,
145
+ startedAt: overrides.startedAt ?? Date.now(),
146
+ packageVersion: overrides.packageVersion ?? readPackageVersion(),
147
+ };
148
+ let resolveOAuthReady;
149
+ ctx.oauthReadyPromise = new Promise((resolve) => {
150
+ resolveOAuthReady = resolve;
151
+ });
152
+ ctx.markOAuthReady = ({ url, port } = {}) => {
153
+ if (url)
154
+ ctx.oauthUrl = url;
155
+ if (port)
156
+ ctx.oauthActualPort = port;
157
+ ctx.oauthReadyState = "ready";
158
+ resolveOAuthReady(ctx.oauthUrl);
159
+ };
160
+ ctx.markOAuthFailed = () => {
161
+ ctx.oauthReadyState = "failed";
162
+ resolveOAuthReady(null);
163
+ };
164
+ if (!config.oauth.autoStart)
165
+ ctx.markOAuthReady({ url: ctx.oauthUrl, port: ctx.oauthPort });
166
+ return ctx;
174
167
  }
175
-
176
168
  export async function startServer(overrides = {}) {
177
- const ctx = await createRuntimeContext(overrides);
178
- await migrateGeneratedStorage(ctx);
179
- purgeStaleJobs();
180
- const app = buildApp(ctx);
181
- const oauthChild =
182
- overrides.oauthChild !== undefined
183
- ? overrides.oauthChild
184
- : !ctx.config.oauth.autoStart
185
- ? null
186
- : startOAuthProxy({
187
- oauthPort: ctx.oauthPort,
188
- restartDelayMs: ctx.config.oauth.restartDelayMs,
189
- onReady: ({ url, port }) => {
190
- ctx.markOAuthReady({ url, port });
191
- advertise(ctx);
192
- },
193
- onExit: () => ctx.markOAuthFailed(),
194
- });
195
- if (overrides.oauthChild !== undefined || !ctx.config.oauth.autoStart) {
196
- ctx.markOAuthReady({ url: ctx.oauthUrl, port: ctx.oauthPort });
197
- }
198
-
199
- onShutdown(() => {
200
- unadvertise(ctx);
201
- try { oauthChild?.stop?.(); } catch {}
202
- try { oauthChild?.kill?.(); } catch {}
203
- });
204
- process.on("exit", () => unadvertise(ctx));
205
-
206
- const server = await listenWithPortFallback(app, ctx.config.server.port, {
207
- host: ctx.config.server.host,
208
- label: "server",
209
- onFallback: ({ requestedPort, actualPort }) => {
210
- console.log(`[server.port] requested=${requestedPort} actual=${actualPort} reason=EADDRINUSE`);
211
- },
212
- });
213
- ctx.serverActualPort = getServerPort(server) || ctx.config.server.port;
214
- ctx.serverUrl = `http://${runtimeHostUrl(ctx.config.server.host)}:${ctx.serverActualPort}`;
215
- console.log(`Image Gen running at ${ctx.serverUrl}`);
216
- console.log(`Provider policy: OAuth only (API key hard-disabled). OAuth proxy port ${ctx.oauthPort}.`);
217
- advertise(ctx);
218
- try {
219
- const s = ensureDefaultSession();
220
- console.log(`[db] default session: ${s.id} (${s.title})`);
221
- } catch (err) {
222
- console.error("[db] bootstrap failed:", err.message);
223
- }
224
-
225
- server.on("error", (err) => {
226
- console.error("[server] Failed to start:", err?.message || err);
227
- process.exit(1);
228
- });
229
-
230
- return { app, server, oauthChild, ctx };
169
+ const ctx = await createRuntimeContext(overrides);
170
+ await migrateGeneratedStorage(ctx);
171
+ purgeStaleJobs();
172
+ const app = buildApp(ctx);
173
+ const oauthChild = overrides.oauthChild !== undefined
174
+ ? overrides.oauthChild
175
+ : !ctx.config.oauth.autoStart
176
+ ? null
177
+ : startOAuthProxy({
178
+ oauthPort: ctx.oauthPort,
179
+ restartDelayMs: ctx.config.oauth.restartDelayMs,
180
+ onReady: ({ url, port }) => {
181
+ ctx.markOAuthReady({ url, port });
182
+ advertise(ctx);
183
+ },
184
+ onExit: () => ctx.markOAuthFailed(),
185
+ });
186
+ if (overrides.oauthChild !== undefined || !ctx.config.oauth.autoStart) {
187
+ ctx.markOAuthReady({ url: ctx.oauthUrl, port: ctx.oauthPort });
188
+ }
189
+ onShutdown(() => {
190
+ unadvertise(ctx);
191
+ try {
192
+ oauthChild?.stop?.();
193
+ }
194
+ catch { }
195
+ try {
196
+ oauthChild?.kill?.();
197
+ }
198
+ catch { }
199
+ });
200
+ process.on("exit", () => unadvertise(ctx));
201
+ const server = await listenWithPortFallback(app, ctx.config.server.port, {
202
+ host: ctx.config.server.host,
203
+ label: "server",
204
+ onFallback: ({ requestedPort, actualPort }) => {
205
+ console.log(`[server.port] requested=${requestedPort} actual=${actualPort} reason=EADDRINUSE`);
206
+ },
207
+ });
208
+ ctx.serverActualPort = getServerPort(server) || ctx.config.server.port;
209
+ ctx.serverUrl = `http://${runtimeHostUrl(ctx.config.server.host)}:${ctx.serverActualPort}`;
210
+ console.log(`Image Gen running at ${ctx.serverUrl}`);
211
+ console.log(`Provider policy: OAuth and API-key Responses providers. OAuth proxy port ${ctx.oauthPort}.`);
212
+ advertise(ctx);
213
+ try {
214
+ const s = ensureDefaultSession();
215
+ console.log(`[db] default session: ${s.id} (${s.title})`);
216
+ }
217
+ catch (err) {
218
+ console.error("[db] bootstrap failed:", err.message);
219
+ }
220
+ server.on("error", (err) => {
221
+ console.error("[server] Failed to start:", err?.message || err);
222
+ process.exit(1);
223
+ });
224
+ return { app, server, oauthChild, ctx };
231
225
  }
232
-
233
226
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
234
- await startServer();
227
+ await startServer();
235
228
  }
package/server.ts ADDED
@@ -0,0 +1,235 @@
1
+ import "dotenv/config";
2
+ import express from "express";
3
+ import { readFile } from "fs/promises";
4
+ import {
5
+ existsSync,
6
+ writeFileSync,
7
+ unlinkSync,
8
+ mkdirSync,
9
+ readFileSync as fsReadFileSync,
10
+ } from "fs";
11
+ import { dirname, join } from "path";
12
+ import { fileURLToPath, pathToFileURL } from "url";
13
+ import { onShutdown } from "./bin/lib/platform.js";
14
+ import { ensureDefaultSession } from "./lib/sessionStore.js";
15
+ import { startOAuthProxy } from "./lib/oauthLauncher.js";
16
+ import { migrateGeneratedStorage } from "./lib/storageMigration.js";
17
+ import { purgeStaleJobs } from "./lib/inflight.js";
18
+ import { configureLogger } from "./lib/logger.js";
19
+ import { createRequestLogger } from "./lib/requestLogger.js";
20
+ import { configureRoutes } from "./routes/index.js";
21
+ import { config } from "./config.js";
22
+ import { getServerPort, listenWithPortFallback } from "./lib/runtimePorts.js";
23
+
24
+ const rootDir = dirname(fileURLToPath(import.meta.url));
25
+
26
+ async function loadApiKey() {
27
+ if (process.env.OPENAI_API_KEY) {
28
+ return { apiKey: process.env.OPENAI_API_KEY, apiKeySource: "env" };
29
+ }
30
+ const candidates = [
31
+ config.storage.configFile,
32
+ join(rootDir, ".ima2", "config.json"),
33
+ ];
34
+ for (const cfgPath of candidates) {
35
+ if (!existsSync(cfgPath)) continue;
36
+ try {
37
+ const cfg = JSON.parse(await readFile(cfgPath, "utf-8"));
38
+ if (cfg.apiKey) return { apiKey: cfg.apiKey, apiKeySource: "config" };
39
+ } catch {}
40
+ }
41
+ return { apiKey: null, apiKeySource: "none" };
42
+ }
43
+
44
+ async function createOpenAI(apiKey) {
45
+ if (!apiKey) return null;
46
+ const OpenAI = (await import("openai")).default;
47
+ return new OpenAI({ apiKey });
48
+ }
49
+
50
+ function readPackageVersion() {
51
+ try {
52
+ return JSON.parse(fsReadFileSync(join(rootDir, "package.json"), "utf-8")).version;
53
+ } catch {
54
+ return "0.0.0";
55
+ }
56
+ }
57
+
58
+ function setUiStaticHeaders(res, filePath) {
59
+ const normalized = filePath.replace(/\\/g, "/");
60
+ if (normalized.endsWith("/index.html")) {
61
+ res.setHeader("Cache-Control", "no-store, max-age=0");
62
+ return;
63
+ }
64
+ if (normalized.includes("/assets/")) {
65
+ res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
66
+ }
67
+ }
68
+
69
+ export function buildApp(ctx) {
70
+ const app = express();
71
+ configureLogger({ level: ctx.config.log.level });
72
+ app.use(createRequestLogger());
73
+ app.use(express.json({ limit: ctx.config.server.bodyLimit }));
74
+ app.use(express.static(join(ctx.rootDir, "ui", "dist"), {
75
+ setHeaders: setUiStaticHeaders,
76
+ }));
77
+ app.use("/assets", (_req, res) => {
78
+ res.status(404).type("text/plain").send("Asset not found");
79
+ });
80
+ app.use("/generated", express.static(ctx.config.storage.generatedDir, {
81
+ maxAge: ctx.config.storage.staticMaxAge,
82
+ immutable: true,
83
+ }));
84
+ configureRoutes(app, ctx);
85
+ return app;
86
+ }
87
+
88
+ function runtimeHostUrl(host) {
89
+ if (!host || host === "0.0.0.0" || host === "::") return "localhost";
90
+ return host;
91
+ }
92
+
93
+ function advertise(ctx) {
94
+ try {
95
+ mkdirSync(dirname(ctx.config.storage.advertiseFile), { recursive: true });
96
+ writeFileSync(
97
+ ctx.config.storage.advertiseFile,
98
+ JSON.stringify({
99
+ port: Number(ctx.serverActualPort || ctx.config.server.port),
100
+ url: ctx.serverUrl,
101
+ pid: process.pid,
102
+ startedAt: ctx.startedAt,
103
+ version: ctx.packageVersion,
104
+ backend: {
105
+ configuredPort: Number(ctx.serverConfiguredPort || ctx.config.server.port),
106
+ actualPort: Number(ctx.serverActualPort || ctx.config.server.port),
107
+ url: ctx.serverUrl,
108
+ },
109
+ oauth: {
110
+ configuredPort: Number(ctx.oauthPort),
111
+ actualPort: Number(ctx.oauthActualPort || ctx.oauthPort),
112
+ url: ctx.oauthUrl,
113
+ status: ctx.oauthReadyState,
114
+ },
115
+ }),
116
+ );
117
+ } catch (e) {
118
+ console.warn("[advertise] skipped:", e.message);
119
+ }
120
+ }
121
+
122
+ function unadvertise(ctx) {
123
+ try {
124
+ if (!existsSync(ctx.config.storage.advertiseFile)) return;
125
+ const cur = JSON.parse(fsReadFileSync(ctx.config.storage.advertiseFile, "utf-8"));
126
+ if (cur.pid === process.pid) unlinkSync(ctx.config.storage.advertiseFile);
127
+ } catch {}
128
+ }
129
+
130
+ export async function createRuntimeContext(overrides: any = {}) {
131
+ const loadedKey =
132
+ overrides.apiKey !== undefined
133
+ ? {
134
+ apiKey: overrides.apiKey,
135
+ apiKeySource: overrides.apiKeySource ?? (overrides.apiKey ? "env" : "none"),
136
+ }
137
+ : await loadApiKey();
138
+ const apiKey = loadedKey.apiKey;
139
+ const openai = overrides.openai ?? await createOpenAI(apiKey);
140
+ const oauthPort = config.oauth.proxyPort;
141
+ const ctx: any = {
142
+ rootDir,
143
+ config,
144
+ serverConfiguredPort: config.server.port,
145
+ serverActualPort: null,
146
+ serverUrl: `http://${runtimeHostUrl(config.server.host)}:${config.server.port}`,
147
+ oauthPort,
148
+ oauthActualPort: oauthPort,
149
+ oauthUrl: `http://127.0.0.1:${oauthPort}`,
150
+ oauthReadyState: config.oauth.autoStart ? "starting" : "disabled",
151
+ hasApiKey: !!apiKey,
152
+ apiKey,
153
+ apiKeySource: loadedKey.apiKeySource,
154
+ openai,
155
+ startedAt: overrides.startedAt ?? Date.now(),
156
+ packageVersion: overrides.packageVersion ?? readPackageVersion(),
157
+ };
158
+ let resolveOAuthReady;
159
+ ctx.oauthReadyPromise = new Promise((resolve) => {
160
+ resolveOAuthReady = resolve;
161
+ });
162
+ ctx.markOAuthReady = ({ url, port }: any = {}) => {
163
+ if (url) ctx.oauthUrl = url;
164
+ if (port) ctx.oauthActualPort = port;
165
+ ctx.oauthReadyState = "ready";
166
+ resolveOAuthReady(ctx.oauthUrl);
167
+ };
168
+ ctx.markOAuthFailed = () => {
169
+ ctx.oauthReadyState = "failed";
170
+ resolveOAuthReady(null);
171
+ };
172
+ if (!config.oauth.autoStart) ctx.markOAuthReady({ url: ctx.oauthUrl, port: ctx.oauthPort });
173
+ return ctx;
174
+ }
175
+
176
+ export async function startServer(overrides: any = {}) {
177
+ const ctx = await createRuntimeContext(overrides);
178
+ await migrateGeneratedStorage(ctx);
179
+ purgeStaleJobs();
180
+ const app = buildApp(ctx);
181
+ const oauthChild =
182
+ overrides.oauthChild !== undefined
183
+ ? overrides.oauthChild
184
+ : !ctx.config.oauth.autoStart
185
+ ? null
186
+ : startOAuthProxy({
187
+ oauthPort: ctx.oauthPort,
188
+ restartDelayMs: ctx.config.oauth.restartDelayMs,
189
+ onReady: ({ url, port }) => {
190
+ ctx.markOAuthReady({ url, port });
191
+ advertise(ctx);
192
+ },
193
+ onExit: () => ctx.markOAuthFailed(),
194
+ });
195
+ if (overrides.oauthChild !== undefined || !ctx.config.oauth.autoStart) {
196
+ ctx.markOAuthReady({ url: ctx.oauthUrl, port: ctx.oauthPort });
197
+ }
198
+
199
+ onShutdown(() => {
200
+ unadvertise(ctx);
201
+ try { oauthChild?.stop?.(); } catch {}
202
+ try { oauthChild?.kill?.(); } catch {}
203
+ });
204
+ process.on("exit", () => unadvertise(ctx));
205
+
206
+ const server: any = await listenWithPortFallback(app, ctx.config.server.port, {
207
+ host: ctx.config.server.host,
208
+ label: "server",
209
+ onFallback: ({ requestedPort, actualPort }) => {
210
+ console.log(`[server.port] requested=${requestedPort} actual=${actualPort} reason=EADDRINUSE`);
211
+ },
212
+ });
213
+ ctx.serverActualPort = getServerPort(server) || ctx.config.server.port;
214
+ ctx.serverUrl = `http://${runtimeHostUrl(ctx.config.server.host)}:${ctx.serverActualPort}`;
215
+ console.log(`Image Gen running at ${ctx.serverUrl}`);
216
+ console.log(`Provider policy: OAuth and API-key Responses providers. OAuth proxy port ${ctx.oauthPort}.`);
217
+ advertise(ctx);
218
+ try {
219
+ const s = ensureDefaultSession();
220
+ console.log(`[db] default session: ${s.id} (${s.title})`);
221
+ } catch (err) {
222
+ console.error("[db] bootstrap failed:", err.message);
223
+ }
224
+
225
+ server.on("error", (err) => {
226
+ console.error("[server] Failed to start:", err?.message || err);
227
+ process.exit(1);
228
+ });
229
+
230
+ return { app, server, oauthChild, ctx };
231
+ }
232
+
233
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
234
+ await startServer();
235
+ }