ima2-gen 1.1.21 → 1.1.22

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 (208) hide show
  1. package/README.md +30 -4
  2. package/bin/ima2.js +14 -4
  3. package/bin/lib/platform.js +34 -5
  4. package/docs/README.ko.md +31 -0
  5. package/lib/agentQueueWorker.js +6 -0
  6. package/lib/agentRuntime.js +3 -2
  7. package/lib/atomicWrite.js +14 -0
  8. package/lib/grokProxyLauncher.js +5 -3
  9. package/lib/inflight.js +1 -1
  10. package/lib/oauthLauncher.js +5 -0
  11. package/lib/videoFrameExtract.js +3 -3
  12. package/package.json +5 -7
  13. package/routes/edit.js +2 -1
  14. package/routes/generate.js +4 -3
  15. package/routes/health.js +4 -3
  16. package/routes/multimode.js +2 -1
  17. package/routes/video.js +4 -2
  18. package/server.js +29 -2
  19. package/ui/dist/.vite/manifest.json +12 -12
  20. package/ui/dist/assets/{AgentWorkspace-B_hq9CLg.js → AgentWorkspace-COxQ5TjU.js} +1 -1
  21. package/ui/dist/assets/{CardNewsWorkspace-wD12J7qk.js → CardNewsWorkspace-B0OkcuVz.js} +1 -1
  22. package/ui/dist/assets/{NodeCanvas-CI_wuPMf.js → NodeCanvas-BSsclEBh.js} +1 -1
  23. package/ui/dist/assets/{PromptBuilderPanel-CUTujJUV.js → PromptBuilderPanel-DpC9A5Rz.js} +1 -1
  24. package/ui/dist/assets/{PromptImportDialog-CUi66jPK.js → PromptImportDialog-CVwT0rLd.js} +2 -2
  25. package/ui/dist/assets/{PromptImportDiscoverySection-Cm3vrjY4.js → PromptImportDiscoverySection-BDCkRCRs.js} +1 -1
  26. package/ui/dist/assets/{PromptImportFolderSection-DOtWTD9n.js → PromptImportFolderSection-QoKbZD83.js} +1 -1
  27. package/ui/dist/assets/{PromptLibraryPanel-BMjQegRa.js → PromptLibraryPanel-BhFgeKnY.js} +2 -2
  28. package/ui/dist/assets/SettingsWorkspace-CfjrlH5R.js +1 -0
  29. package/ui/dist/assets/index-C-mur7pa.css +1 -0
  30. package/ui/dist/assets/index-CCP5nUOj.js +42 -0
  31. package/ui/dist/assets/{index-31uVIdt4.js → index-Cxhzi3bs.js} +1 -1
  32. package/ui/dist/index.html +2 -2
  33. package/bin/commands/annotate.ts +0 -119
  34. package/bin/commands/cancel.ts +0 -48
  35. package/bin/commands/canvas-versions.ts +0 -80
  36. package/bin/commands/capabilities.ts +0 -110
  37. package/bin/commands/cardnews.ts +0 -249
  38. package/bin/commands/comfy.ts +0 -54
  39. package/bin/commands/config.ts +0 -186
  40. package/bin/commands/defaults.ts +0 -192
  41. package/bin/commands/doctor.ts +0 -202
  42. package/bin/commands/edit.ts +0 -150
  43. package/bin/commands/gen.ts +0 -214
  44. package/bin/commands/grok.ts +0 -90
  45. package/bin/commands/history.ts +0 -146
  46. package/bin/commands/ls.ts +0 -64
  47. package/bin/commands/metadata.ts +0 -39
  48. package/bin/commands/multimode.ts +0 -196
  49. package/bin/commands/node.ts +0 -166
  50. package/bin/commands/observability.ts +0 -176
  51. package/bin/commands/ping.ts +0 -31
  52. package/bin/commands/prompt-sub/build.ts +0 -101
  53. package/bin/commands/prompt.ts +0 -492
  54. package/bin/commands/ps.ts +0 -81
  55. package/bin/commands/session.ts +0 -266
  56. package/bin/commands/show.ts +0 -72
  57. package/bin/commands/skill.ts +0 -70
  58. package/bin/commands/video.ts +0 -442
  59. package/bin/ima2.ts +0 -430
  60. package/bin/lib/args.ts +0 -92
  61. package/bin/lib/browser-id.ts +0 -16
  62. package/bin/lib/client.ts +0 -122
  63. package/bin/lib/config-store.ts +0 -120
  64. package/bin/lib/destructive-confirm.ts +0 -19
  65. package/bin/lib/doctor-checks.ts +0 -91
  66. package/bin/lib/error-hints.ts +0 -23
  67. package/bin/lib/files.ts +0 -39
  68. package/bin/lib/output.ts +0 -73
  69. package/bin/lib/platform.ts +0 -99
  70. package/bin/lib/recover-output.ts +0 -139
  71. package/bin/lib/sse.ts +0 -73
  72. package/bin/lib/star-prompt.ts +0 -97
  73. package/bin/lib/storage-doctor.ts +0 -39
  74. package/bin/lib/ui-build.ts +0 -85
  75. package/config.ts +0 -354
  76. package/lib/agentCommandParser.ts +0 -69
  77. package/lib/agentGenerationPlanner.ts +0 -273
  78. package/lib/agentQuestionResponder.ts +0 -266
  79. package/lib/agentQueueStore.ts +0 -270
  80. package/lib/agentQueueWorker.ts +0 -89
  81. package/lib/agentRuntime.ts +0 -604
  82. package/lib/agentSettings.ts +0 -72
  83. package/lib/agentStore.ts +0 -422
  84. package/lib/agentStoreRows.ts +0 -136
  85. package/lib/agentTypes.ts +0 -154
  86. package/lib/apiCachePolicy.ts +0 -11
  87. package/lib/assetLifecycle.ts +0 -146
  88. package/lib/canvasVersionStore.ts +0 -223
  89. package/lib/capabilities.ts +0 -126
  90. package/lib/cardNewsGenerator.ts +0 -271
  91. package/lib/cardNewsJobStore.ts +0 -142
  92. package/lib/cardNewsManifestStore.ts +0 -154
  93. package/lib/cardNewsPlanner.ts +0 -236
  94. package/lib/cardNewsPlannerClient.ts +0 -155
  95. package/lib/cardNewsPlannerPrompt.ts +0 -62
  96. package/lib/cardNewsPlannerSchema.ts +0 -321
  97. package/lib/cardNewsRoleTemplateStore.ts +0 -47
  98. package/lib/cardNewsTemplateStore.ts +0 -252
  99. package/lib/codexDetect.ts +0 -71
  100. package/lib/comfyBridge.ts +0 -235
  101. package/lib/composerSnapshot.ts +0 -33
  102. package/lib/configKeys.ts +0 -62
  103. package/lib/db.ts +0 -295
  104. package/lib/errInfo.ts +0 -43
  105. package/lib/errorClassify.ts +0 -100
  106. package/lib/generationCancel.ts +0 -28
  107. package/lib/generationErrors.ts +0 -238
  108. package/lib/grokImageAdapter.ts +0 -513
  109. package/lib/grokMultimodeAdapter.ts +0 -84
  110. package/lib/grokProxyLauncher.ts +0 -153
  111. package/lib/grokRuntime.ts +0 -23
  112. package/lib/grokSizeMapper.ts +0 -71
  113. package/lib/grokVideoAdapter.ts +0 -458
  114. package/lib/grokVideoCanvas.ts +0 -26
  115. package/lib/grokVideoDownload.ts +0 -59
  116. package/lib/grokVideoPlannerPrompt.ts +0 -67
  117. package/lib/historyIndex.ts +0 -51
  118. package/lib/historyList.ts +0 -181
  119. package/lib/imageMetadata.ts +0 -113
  120. package/lib/imageMetadataStore.ts +0 -67
  121. package/lib/imageModels.ts +0 -165
  122. package/lib/inflight.ts +0 -281
  123. package/lib/localImportStore.ts +0 -114
  124. package/lib/logger.ts +0 -161
  125. package/lib/nodeStore.ts +0 -91
  126. package/lib/oauthLauncher.ts +0 -94
  127. package/lib/oauthNormalize.ts +0 -30
  128. package/lib/oauthProxy/errors.ts +0 -128
  129. package/lib/oauthProxy/generators.ts +0 -494
  130. package/lib/oauthProxy/index.ts +0 -28
  131. package/lib/oauthProxy/prompts.ts +0 -123
  132. package/lib/oauthProxy/references.ts +0 -45
  133. package/lib/oauthProxy/runtime.ts +0 -115
  134. package/lib/oauthProxy/streams.ts +0 -232
  135. package/lib/oauthProxy/types.ts +0 -9
  136. package/lib/oauthProxy.ts +0 -3
  137. package/lib/openDirectory.ts +0 -47
  138. package/lib/pngInfo.ts +0 -26
  139. package/lib/promptBuilder/attachments.ts +0 -74
  140. package/lib/promptBuilder/client.ts +0 -130
  141. package/lib/promptBuilder/constants.ts +0 -9
  142. package/lib/promptBuilder/context.ts +0 -36
  143. package/lib/promptBuilder/errors.ts +0 -12
  144. package/lib/promptBuilder/requestSchema.ts +0 -56
  145. package/lib/promptBuilder/responseParser.ts +0 -219
  146. package/lib/promptBuilder/systemPrompt.ts +0 -135
  147. package/lib/promptBuilder/transport.ts +0 -94
  148. package/lib/promptBuilder/types.ts +0 -109
  149. package/lib/promptImport/curatedSources.ts +0 -141
  150. package/lib/promptImport/discoveryRegistry.ts +0 -329
  151. package/lib/promptImport/errors.ts +0 -18
  152. package/lib/promptImport/githubDiscovery.ts +0 -309
  153. package/lib/promptImport/githubFolder.ts +0 -397
  154. package/lib/promptImport/githubSource.ts +0 -257
  155. package/lib/promptImport/gptImageHints.ts +0 -70
  156. package/lib/promptImport/parsePromptCandidates.ts +0 -179
  157. package/lib/promptImport/promptIndex.ts +0 -326
  158. package/lib/promptImport/rankPromptCandidates.ts +0 -65
  159. package/lib/promptImport/types.ts +0 -103
  160. package/lib/promptSafetyPolicy.ts +0 -5
  161. package/lib/providerOptions.ts +0 -56
  162. package/lib/referenceImageCompress.ts +0 -84
  163. package/lib/refs.ts +0 -133
  164. package/lib/requestLogger.ts +0 -49
  165. package/lib/responsesDoctor.ts +0 -456
  166. package/lib/responsesErrors.ts +0 -83
  167. package/lib/responsesFallback.ts +0 -114
  168. package/lib/responsesImageAdapter.ts +0 -466
  169. package/lib/responsesParse.ts +0 -452
  170. package/lib/responsesTools.ts +0 -28
  171. package/lib/runtimeContext.ts +0 -146
  172. package/lib/runtimePorts.ts +0 -105
  173. package/lib/sessionStore.ts +0 -308
  174. package/lib/storageMigration.ts +0 -310
  175. package/lib/styleSheet.ts +0 -139
  176. package/lib/systemTrash.ts +0 -20
  177. package/lib/videoContinuity.ts +0 -180
  178. package/lib/videoFrameExtract.ts +0 -78
  179. package/lib/videoSeriesChain.ts +0 -29
  180. package/lib/visibleTextLanguagePolicy.ts +0 -7
  181. package/routes/agent.ts +0 -308
  182. package/routes/annotations.ts +0 -118
  183. package/routes/canvasVersions.ts +0 -69
  184. package/routes/capabilities.ts +0 -18
  185. package/routes/cardNews.ts +0 -211
  186. package/routes/comfy.ts +0 -43
  187. package/routes/edit.ts +0 -352
  188. package/routes/generate.ts +0 -492
  189. package/routes/grok.ts +0 -24
  190. package/routes/health.ts +0 -123
  191. package/routes/history.ts +0 -221
  192. package/routes/imageImport.ts +0 -37
  193. package/routes/index.ts +0 -52
  194. package/routes/metadata.ts +0 -77
  195. package/routes/multimode.ts +0 -499
  196. package/routes/nodes.ts +0 -578
  197. package/routes/promptBuilder.ts +0 -37
  198. package/routes/promptImport.ts +0 -379
  199. package/routes/prompts.ts +0 -428
  200. package/routes/quota.ts +0 -89
  201. package/routes/sessions.ts +0 -317
  202. package/routes/storage.ts +0 -47
  203. package/routes/video.ts +0 -300
  204. package/routes/videoExtended.ts +0 -284
  205. package/server.ts +0 -293
  206. package/ui/dist/assets/SettingsWorkspace-PiaVnsdA.js +0 -1
  207. package/ui/dist/assets/index-CjgnNtgt.css +0 -1
  208. package/ui/dist/assets/index-Da2s4_-5.js +0 -36
package/README.md CHANGED
@@ -41,6 +41,27 @@ If `3333` is already occupied, `ima2-gen` binds the next available port and writ
41
41
 
42
42
  > **Using npx?** See [docs/NPX_QUICKSTART.md](docs/NPX_QUICKSTART.md) for the `npx ima2-gen serve` workflow.
43
43
 
44
+ ### One-Click Install (no npm required)
45
+
46
+ Don't have Node.js or npm? Use the platform install script — it detects your environment, installs Node LTS if needed, then installs ima2-gen.
47
+
48
+ **macOS:**
49
+ ```bash
50
+ curl -fsSL https://lidge-jun.github.io/ima2-gen/install-mac.sh | bash
51
+ ```
52
+
53
+ **Windows (PowerShell):**
54
+ ```powershell
55
+ irm https://lidge-jun.github.io/ima2-gen/install-windows.ps1 | iex
56
+ ```
57
+
58
+ **Linux / WSL:**
59
+ ```bash
60
+ curl -fsSL https://lidge-jun.github.io/ima2-gen/install-linux.sh | bash
61
+ ```
62
+
63
+ Each script checks for nvm/fnm/brew/winget, installs Node LTS through the best available method, and handles stale process cleanup automatically.
64
+
44
65
  ### Setup
45
66
 
46
67
  `ima2 setup` offers four authentication choices:
@@ -52,10 +73,15 @@ If `3333` is already occupied, `ima2-gen` binds the next available port and writ
52
73
 
53
74
  Video generation requires Grok OAuth (option 2 or 3). Run `ima2 grok login` separately if you already have GPT OAuth configured and want to add video support; it defaults to the manual-paste flow.
54
75
 
55
- Before updating a global install on Windows, stop any running `ima2 serve`
56
- process. If npm reports `EBUSY` or `resource busy or locked`, close ima2
57
- terminals, end stale `node.exe` processes if needed, and retry. If the lock
58
- persists, reboot and run the update before starting ima2 again.
76
+ ### Updating
77
+
78
+ Stop the running server with Ctrl+C, then:
79
+
80
+ ```bash
81
+ npm install -g ima2-gen@latest
82
+ ```
83
+
84
+ Ctrl+C now performs a clean shutdown — closing the database, stopping child processes, and releasing file locks. On older versions (< 1.1.22) or if you see `EBUSY` on Windows, use the install script which handles stale process cleanup automatically.
59
85
 
60
86
  ## What It Does
61
87
 
package/bin/ima2.js CHANGED
@@ -6,7 +6,7 @@ import { fileURLToPath } from "url";
6
6
  import { spawn, execSync } from "child_process";
7
7
  import { confirmDestructiveAction } from "./lib/destructive-confirm.js";
8
8
  import { doctor } from "./commands/doctor.js";
9
- import { openUrl, resolveBin } from "./lib/platform.js";
9
+ import { openUrl, resolveBin, killProcessTree } from "./lib/platform.js";
10
10
  import { maybePromptGithubStar } from "./lib/star-prompt.js";
11
11
  import { ensureFreshUiDist } from "./lib/ui-build.js";
12
12
  import { detectCodexAuth } from "../lib/codexDetect.js";
@@ -197,9 +197,16 @@ async function serve(serveArgs = []) {
197
197
  env,
198
198
  cwd: ROOT,
199
199
  });
200
+ child.on("error", (err) => {
201
+ console.error(`[ima2] Failed to start server: ${err.message}`);
202
+ process.exit(1);
203
+ });
200
204
  child.on("exit", (code) => process.exit(code));
201
- process.on("SIGINT", () => child.kill("SIGINT"));
202
- process.on("SIGTERM", () => child.kill("SIGTERM"));
205
+ process.on("SIGINT", () => killProcessTree(child.pid));
206
+ process.on("SIGTERM", () => killProcessTree(child.pid));
207
+ if (process.platform === "win32") {
208
+ process.on("SIGBREAK", () => killProcessTree(child.pid));
209
+ }
203
210
  }
204
211
  async function showStatus() {
205
212
  const config = loadConfig();
@@ -335,7 +342,10 @@ switch (command) {
335
342
  break;
336
343
  case "setup":
337
344
  case "login":
338
- setup().then(() => console.log(" Done. Run 'ima2 serve' to start."));
345
+ setup().then(() => console.log(" Done. Run 'ima2 serve' to start.")).catch((e) => {
346
+ console.error(`Setup failed: ${e?.message || e}`);
347
+ process.exit(1);
348
+ });
339
349
  break;
340
350
  case "status":
341
351
  showStatus();
@@ -80,20 +80,29 @@ export function openUrl(url) {
80
80
  * Windows does NOT raise SIGTERM from the OS — SIGINT (Ctrl+C) and SIGBREAK
81
81
  * (Ctrl+Break) are the observable signals. We still register SIGTERM so that
82
82
  * Node-internal `child.kill("SIGTERM")` calls work in tests.
83
+ *
84
+ * Handlers may return a Promise — they run with a grace period (default 3s)
85
+ * before forceful exit, giving file handles and sockets time to close cleanly.
83
86
  */
87
+ const SHUTDOWN_GRACE_MS = 3_000;
88
+ let shutdownStarted = false;
84
89
  export function onShutdown(handler) {
85
90
  const signals = isWin
86
91
  ? ["SIGINT", "SIGTERM", "SIGBREAK"]
87
92
  : ["SIGINT", "SIGTERM", "SIGHUP"];
88
93
  for (const sig of signals) {
89
94
  try {
90
- process.on(sig, () => {
95
+ process.on(sig, async () => {
96
+ if (shutdownStarted)
97
+ return;
98
+ shutdownStarted = true;
99
+ const forceExit = setTimeout(() => process.exit(0), SHUTDOWN_GRACE_MS);
100
+ forceExit.unref?.();
91
101
  try {
92
- handler(sig);
93
- }
94
- finally {
95
- process.exit(0);
102
+ await handler(sig);
96
103
  }
104
+ catch { }
105
+ process.exit(0);
97
106
  });
98
107
  }
99
108
  catch {
@@ -101,3 +110,23 @@ export function onShutdown(handler) {
101
110
  }
102
111
  }
103
112
  }
113
+ /**
114
+ * Kill an entire process tree. On Windows, child.kill() only kills the
115
+ * immediate process, leaving grandchildren alive and holding file locks.
116
+ * taskkill /T /F kills the whole tree.
117
+ */
118
+ export function killProcessTree(pid) {
119
+ if (!pid)
120
+ return;
121
+ try {
122
+ if (isWin) {
123
+ execSync(`taskkill /T /F /PID ${pid}`, { stdio: "ignore" });
124
+ }
125
+ else {
126
+ process.kill(pid, "SIGTERM");
127
+ }
128
+ }
129
+ catch {
130
+ // Process already exited
131
+ }
132
+ }
package/docs/README.ko.md CHANGED
@@ -30,6 +30,37 @@ ima2 serve
30
30
 
31
31
  > **npx로 실행하고 싶다면?** [NPX_QUICKSTART.md](NPX_QUICKSTART.md)를 참고하세요.
32
32
 
33
+ ### 원클릭 설치 (npm 없어도 됩니다)
34
+
35
+ Node.js나 npm이 없어도 플랫폼별 설치 스크립트로 한 번에 설치할 수 있습니다.
36
+
37
+ **macOS:**
38
+ ```bash
39
+ curl -fsSL https://lidge-jun.github.io/ima2-gen/install-mac.sh | bash
40
+ ```
41
+
42
+ **Windows (PowerShell):**
43
+ ```powershell
44
+ irm https://lidge-jun.github.io/ima2-gen/install-windows.ps1 | iex
45
+ ```
46
+
47
+ **Linux / WSL:**
48
+ ```bash
49
+ curl -fsSL https://lidge-jun.github.io/ima2-gen/install-linux.sh | bash
50
+ ```
51
+
52
+ 각 스크립트가 nvm/fnm/brew/winget을 감지하고, 없으면 Node LTS를 자동 설치한 뒤, ima2-gen을 설치합니다.
53
+
54
+ ### 업데이트
55
+
56
+ Ctrl+C로 서버를 종료한 뒤:
57
+
58
+ ```bash
59
+ npm install -g ima2-gen@latest
60
+ ```
61
+
62
+ v1.1.22부터 Ctrl+C가 DB, 소켓, 자식 프로세스를 깨끗하게 정리합니다. 이전 버전이거나 Windows에서 `EBUSY` 에러가 나면 위의 설치 스크립트를 다시 실행하세요 — 잔여 프로세스를 자동으로 정리합니다.
63
+
33
64
  ### 설정
34
65
 
35
66
  `ima2 setup`으로 인증 방식을 선택합니다:
@@ -18,6 +18,12 @@ export function ensureAgentQueueWorker(ctx) {
18
18
  workerTimer.unref?.();
19
19
  void tickAgentQueueWorker(ctx);
20
20
  }
21
+ export function stopAgentQueueWorker() {
22
+ if (workerTimer) {
23
+ clearInterval(workerTimer);
24
+ workerTimer = null;
25
+ }
26
+ }
21
27
  export async function tickAgentQueueWorker(ctx) {
22
28
  if (ticking)
23
29
  return;
@@ -1,5 +1,6 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
3
+ import { atomicWriteJson } from "./atomicWrite.js";
3
4
  import { join } from "node:path";
4
5
  import { ulid } from "ulid";
5
6
  import { embedImageMetadataBestEffort } from "./imageMetadataStore.js";
@@ -305,7 +306,7 @@ async function persistAgentImage(ctx, sessionId, prompt, format, requestId, resp
305
306
  const filePath = join(ctx.config.storage.generatedDir, filename);
306
307
  await writeFile(filePath, embedded.buffer);
307
308
  try {
308
- await writeFile(`${filePath}.json`, JSON.stringify(meta));
309
+ await atomicWriteJson(`${filePath}.json`, meta);
309
310
  }
310
311
  catch (err) {
311
312
  await unlink(filePath).catch(() => { });
@@ -409,7 +410,7 @@ async function persistAgentVideo(ctx, sessionId, prompt, requestId, result) {
409
410
  const filePath = join(ctx.config.storage.generatedDir, filename);
410
411
  await writeFile(filePath, result.videoBuffer);
411
412
  try {
412
- await writeFile(`${filePath}.json`, JSON.stringify(meta));
413
+ await atomicWriteJson(`${filePath}.json`, meta);
413
414
  }
414
415
  catch (err) {
415
416
  await unlink(filePath).catch(() => { });
@@ -0,0 +1,14 @@
1
+ import { writeFile, rename, unlink } from "node:fs/promises";
2
+ export async function atomicWriteJson(path, data) {
3
+ const tmp = `${path}.${process.pid}.tmp`;
4
+ await writeFile(tmp, JSON.stringify(data));
5
+ await rename(tmp, path);
6
+ }
7
+ export async function safeWriteSidecar(path, data) {
8
+ try {
9
+ await atomicWriteJson(path, data);
10
+ }
11
+ catch {
12
+ await unlink(`${path}.${process.pid}.tmp`).catch(() => { });
13
+ }
14
+ }
@@ -66,6 +66,11 @@ export async function startGrokProxy(options = {}) {
66
66
  });
67
67
  currentChild = child;
68
68
  authRequired = false;
69
+ child.on("error", (err) => {
70
+ console.error(`[grok] failed to start progrok proxy: ${err.message}`);
71
+ if (currentChild === child)
72
+ currentChild = null;
73
+ });
69
74
  child.stdout?.on("data", (d) => {
70
75
  const msg = normalizeGrokProxyMessage(d.toString().trim());
71
76
  if (!msg)
@@ -90,9 +95,6 @@ export async function startGrokProxy(options = {}) {
90
95
  authRequired = true;
91
96
  }
92
97
  });
93
- child.on("error", (err) => {
94
- console.error(`[grok] failed to start progrok proxy: ${err.message}`);
95
- });
96
98
  child.on("exit", (code) => {
97
99
  if (currentChild === child)
98
100
  currentChild = null;
package/lib/inflight.js CHANGED
@@ -113,7 +113,7 @@ export function finishJob(requestId, options = {}) {
113
113
  abortControllers.delete(requestId);
114
114
  reapTerminalJobs();
115
115
  }
116
- function reapTerminalJobs() {
116
+ export function reapTerminalJobs() {
117
117
  const now = Date.now();
118
118
  for (const [id, j] of terminalJobs) {
119
119
  if (now - j.finishedAt > config.inflight.terminalTtlMs)
@@ -29,6 +29,11 @@ export function startOAuthProxy(options = {}) {
29
29
  env: { ...process.env },
30
30
  });
31
31
  currentChild = child;
32
+ child.on("error", (err) => {
33
+ console.error(`[gpt-oauth] failed to start proxy: ${err.message}`);
34
+ if (currentChild === child)
35
+ currentChild = null;
36
+ });
32
37
  child.stdout?.on("data", (d) => {
33
38
  const msg = d.toString().trim();
34
39
  if (!msg)
@@ -1,7 +1,7 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { randomBytes } from "node:crypto";
3
3
  import { open, readFile, realpath, stat, unlink } from "node:fs/promises";
4
- import { extname, join, resolve, sep } from "node:path";
4
+ import { extname, isAbsolute, join, resolve, sep } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  const execFileAsync = promisify(execFile);
7
7
  const MAX_LOCAL_VIDEO_BYTES = 100 * 1024 * 1024;
@@ -12,7 +12,7 @@ function routeError(message, status = 400) {
12
12
  }
13
13
  export async function safeGeneratedFilePath(generatedDir, file, options = {}) {
14
14
  const base = resolve(generatedDir);
15
- const target = file.startsWith("/") ? resolve(file) : resolve(base, file);
15
+ const target = isAbsolute(file) ? resolve(file) : resolve(base, file);
16
16
  if (target !== base && !target.startsWith(`${base}${sep}`)) {
17
17
  throw routeError("invalid file path", 400);
18
18
  }
@@ -54,7 +54,7 @@ export async function assertLocalMp4(path) {
54
54
  }
55
55
  }
56
56
  export async function extractVideoFrame(input, output, position) {
57
- const options = { timeout: FFMPEG_TIMEOUT_MS, killSignal: "SIGKILL", maxBuffer: 1024 * 1024 };
57
+ const options = { timeout: FFMPEG_TIMEOUT_MS, killSignal: (process.platform === "win32" ? "SIGTERM" : "SIGKILL"), maxBuffer: 1024 * 1024 };
58
58
  if (position === "last") {
59
59
  await execFileAsync("ffmpeg", ["-y", "-sseof", "-3", "-i", input, "-update", "1", "-q:v", "1", output], options);
60
60
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ima2-gen",
3
- "version": "1.1.21",
3
+ "version": "1.1.22",
4
4
  "description": "Local OAuth image generation studio with classic and node workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  "setup": "node bin/ima2.js setup",
21
21
  "prepack": "npm run ui:build && npm run build:server && npm run build:cli",
22
22
  "prepublishOnly": "npm run typecheck && npm run typecheck:tests && npm run test:inventory && npm run ui:build && npm run build:server && npm run build:cli && npm run lint:pkg && npm run test:package-install",
23
- "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/','routes/','skills/','integrations/comfyui/ima2_gen_bridge/__init__.py','integrations/comfyui/ima2_gen_bridge/nodes.py','integrations/comfyui/ima2_gen_bridge/README.md','assets/card-news/templates/','vendor/','server.js','LICENSE']; for(const f of mustInclude){if(!p.files.includes(f))throw new Error('files[] must include '+f)}\"",
23
+ "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/**/*.js','lib/**/*.js','routes/**/*.js','skills/','integrations/comfyui/ima2_gen_bridge/__init__.py','integrations/comfyui/ima2_gen_bridge/nodes.py','integrations/comfyui/ima2_gen_bridge/README.md','assets/card-news/templates/','vendor/','server.js','LICENSE']; for(const f of mustInclude){if(!p.files.includes(f))throw new Error('files[] must include '+f)}\"",
24
24
  "publish:dry-run": "node scripts/publish-dry-run.mjs",
25
25
  "release:patch": "npm version patch && npm publish && git push origin main --tags",
26
26
  "release:minor": "npm version minor && npm publish && git push origin main --tags",
@@ -43,9 +43,9 @@
43
43
  "url": "git+https://github.com/lidge-jun/ima2-gen.git"
44
44
  },
45
45
  "files": [
46
- "bin/",
47
- "lib/",
48
- "routes/",
46
+ "bin/**/*.js",
47
+ "lib/**/*.js",
48
+ "routes/**/*.js",
49
49
  "skills/",
50
50
  "integrations/comfyui/ima2_gen_bridge/__init__.py",
51
51
  "integrations/comfyui/ima2_gen_bridge/nodes.py",
@@ -54,8 +54,6 @@
54
54
  "docs/",
55
55
  "vendor/",
56
56
  "assets/card-news/templates/",
57
- "server.ts",
58
- "config.ts",
59
57
  ".env.example",
60
58
  "README.md",
61
59
  "LICENSE",
package/routes/edit.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { mkdir, writeFile } from "fs/promises";
2
+ import { safeWriteSidecar } from "../lib/atomicWrite.js";
2
3
  import { join } from "path";
3
4
  import { randomBytes } from "crypto";
4
5
  import { detectImageMimeFromB64 } from "../lib/refs.js";
@@ -222,7 +223,7 @@ export function registerEditRoutes(app, ctxRaw) {
222
223
  webSearchCalls,
223
224
  webSearchEnabled,
224
225
  };
225
- await writeFile(join(ctx.config.storage.generatedDir, filename + ".json"), JSON.stringify(meta)).catch(() => { });
226
+ await safeWriteSidecar(join(ctx.config.storage.generatedDir, filename + ".json"), meta);
226
227
  invalidateHistoryIndex();
227
228
  finishHttpStatus = 200;
228
229
  finishMeta = { filename, imageChars: resultB64.length };
@@ -1,4 +1,5 @@
1
1
  import { mkdir, readFile, writeFile } from "fs/promises";
2
+ import { safeWriteSidecar, atomicWriteJson } from "../lib/atomicWrite.js";
2
3
  import { join } from "path";
3
4
  import { randomBytes } from "crypto";
4
5
  import { detectImageMimeFromB64, summarizeReferencePayload, validateAndNormalizeRefs } from "../lib/refs.js";
@@ -260,7 +261,7 @@ export function registerGenerateRoutes(app, ctxRaw) {
260
261
  });
261
262
  }
262
263
  await writeFile(join(ctx.config.storage.generatedDir, filename), embedded.buffer);
263
- await writeFile(join(ctx.config.storage.generatedDir, filename + ".json"), JSON.stringify(meta)).catch(() => { });
264
+ await safeWriteSidecar(join(ctx.config.storage.generatedDir, filename + ".json"), meta);
264
265
  invalidateHistoryIndex();
265
266
  images.push({
266
267
  image: `data:${resultMime};base64,${r.value.b64}`,
@@ -346,10 +347,10 @@ export function registerGenerateRoutes(app, ctxRaw) {
346
347
  const sidecarPath = join(ctx.config.storage.generatedDir, filename + ".json");
347
348
  const sidecarMeta = JSON.parse(await readFile(sidecarPath, "utf-8"));
348
349
  sidecarMeta.elapsed = elapsed;
349
- await writeFile(sidecarPath, JSON.stringify(sidecarMeta));
350
+ await atomicWriteJson(sidecarPath, sidecarMeta);
350
351
  }
351
352
  catch {
352
- /* best-effort, matches the sidecar write .catch(() => {}) above */
353
+ /* best-effort elapsed patch */
353
354
  }
354
355
  }));
355
356
  const firstRevised = images[0]?.revisedPrompt || null;
package/routes/health.js CHANGED
@@ -95,10 +95,11 @@ export function registerHealthRoutes(app, ctxRaw) {
95
95
  const headers = { Authorization: `Bearer ${ctx.apiKey}`, "Content-Type": "application/json" };
96
96
  const start = Math.floor(new Date(new Date().getFullYear(), new Date().getMonth(), 1).getTime() / 1000);
97
97
  const end = Math.floor(Date.now() / 1000);
98
+ const billingSignal = AbortSignal.timeout(10_000);
98
99
  const [subRes, usageRes, modelsRes] = await Promise.allSettled([
99
- fetch(`https://api.openai.com/v1/organization/costs?start_time=${start}&end_time=${end}&bucket_width=1d&limit=31`, { headers }),
100
- fetch("https://api.openai.com/dashboard/billing/credit_grants", { headers }),
101
- fetch("https://api.openai.com/v1/models", { headers }),
100
+ fetch(`https://api.openai.com/v1/organization/costs?start_time=${start}&end_time=${end}&bucket_width=1d&limit=31`, { headers, signal: billingSignal }),
101
+ fetch("https://api.openai.com/dashboard/billing/credit_grants", { headers, signal: billingSignal }),
102
+ fetch("https://api.openai.com/v1/models", { headers, signal: billingSignal }),
102
103
  ]);
103
104
  const billing = { apiKeySource: ctx.apiKeySource ?? "env" };
104
105
  if (subRes.status === "fulfilled" && subRes.value.ok)
@@ -1,4 +1,5 @@
1
1
  import { mkdir, writeFile } from "fs/promises";
2
+ import { safeWriteSidecar } from "../lib/atomicWrite.js";
2
3
  import { join } from "path";
3
4
  import { randomBytes } from "crypto";
4
5
  import { detectImageMimeFromB64, summarizeReferencePayload, validateAndNormalizeRefs } from "../lib/refs.js";
@@ -217,7 +218,7 @@ export function registerMultimodeRoutes(app, ctxRaw) {
217
218
  version: ctx.packageVersion,
218
219
  });
219
220
  await writeFile(join(ctx.config.storage.generatedDir, filename), embedded.buffer);
220
- await writeFile(join(ctx.config.storage.generatedDir, filename + ".json"), JSON.stringify(meta)).catch(() => { });
221
+ await safeWriteSidecar(join(ctx.config.storage.generatedDir, filename + ".json"), meta);
221
222
  invalidateHistoryIndex();
222
223
  const item = {
223
224
  image: `data:${resultMime};base64,${image.b64}`,
package/routes/video.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { mkdir, readFile, unlink, writeFile } from "fs/promises";
2
+ import { atomicWriteJson } from "../lib/atomicWrite.js";
2
3
  import { join } from "path";
3
4
  import { randomBytes } from "crypto";
4
5
  import { startJob, finishJob, registerJobAbortController, isJobCanceled, setJobPhase } from "../lib/inflight.js";
@@ -26,7 +27,7 @@ export async function saveGeneratedVideoArtifact(ctx, filename, buffer, metadata
26
27
  const filePath = join(ctx.config.storage.generatedDir, filename);
27
28
  await writeFile(filePath, buffer);
28
29
  try {
29
- await writeFile(`${filePath}.json`, JSON.stringify(metadata));
30
+ await atomicWriteJson(`${filePath}.json`, metadata);
30
31
  }
31
32
  catch (err) {
32
33
  await unlink(filePath).catch(() => { });
@@ -36,8 +37,9 @@ export async function saveGeneratedVideoArtifact(ctx, filename, buffer, metadata
36
37
  async function resolveSourceImage(ctx, sourceImage, sourceFilename) {
37
38
  if (typeof sourceFilename === "string" && sourceFilename) {
38
39
  const safe = sourceFilename.replace(/^\/+/, "");
39
- if (safe.includes(".."))
40
+ if (safe.includes("..") || safe.includes("/") || safe.includes("\\")) {
40
41
  throw { status: 400, code: "GROK_VIDEO_INVALID_MODE", message: "invalid source filename" };
42
+ }
41
43
  if (/\.mp4$/i.test(safe))
42
44
  throw { status: 400, code: "GROK_VIDEO_INVALID_MODE", message: "use continueFromVideo for generated video continuation" };
43
45
  const buf = await readFile(join(ctx.config.storage.generatedDir, safe));
package/server.js CHANGED
@@ -16,6 +16,10 @@ import { configureApiCachePolicy } from "./lib/apiCachePolicy.js";
16
16
  import { configureRoutes } from "./routes/index.js";
17
17
  import { config } from "./config.js";
18
18
  import { getServerPort, listenWithPortFallback } from "./lib/runtimePorts.js";
19
+ import { closeDb } from "./lib/db.js";
20
+ import { stopAgentQueueWorker } from "./lib/agentQueueWorker.js";
21
+ import { reapCardNewsJobs } from "./lib/cardNewsJobStore.js";
22
+ import { reapTerminalJobs } from "./lib/inflight.js";
19
23
  import { errInfo } from "./lib/errInfo.js";
20
24
  const rootDir = dirname(fileURLToPath(import.meta.url));
21
25
  async function loadApiKey() {
@@ -228,7 +232,9 @@ export async function startServer(overrides = {}) {
228
232
  },
229
233
  })
230
234
  : null;
231
- onShutdown(() => {
235
+ let server;
236
+ let reapTimer;
237
+ onShutdown(async () => {
232
238
  unadvertise(ctx);
233
239
  try {
234
240
  oauthChild?.stop?.();
@@ -246,9 +252,18 @@ export async function startServer(overrides = {}) {
246
252
  grokChild?.kill?.();
247
253
  }
248
254
  catch { }
255
+ stopAgentQueueWorker();
256
+ clearInterval(reapTimer);
257
+ await new Promise((resolve) => {
258
+ if (server)
259
+ server.close(() => resolve());
260
+ else
261
+ resolve();
262
+ });
263
+ closeDb();
249
264
  });
250
265
  process.on("exit", () => unadvertise(ctx));
251
- const server = await listenWithPortFallback(app, ctx.config.server.port, {
266
+ server = await listenWithPortFallback(app, ctx.config.server.port, {
252
267
  host: ctx.config.server.host,
253
268
  label: "server",
254
269
  onFallback: ({ requestedPort, actualPort }) => {
@@ -272,6 +287,18 @@ export async function startServer(overrides = {}) {
272
287
  console.error("[server] Failed to start:", err?.message || err);
273
288
  process.exit(1);
274
289
  });
290
+ reapTimer = setInterval(() => {
291
+ reapTerminalJobs();
292
+ reapCardNewsJobs();
293
+ }, 60_000);
294
+ reapTimer.unref?.();
295
+ process.on("uncaughtException", (err) => {
296
+ console.error("[fatal] uncaughtException:", err);
297
+ process.exit(1);
298
+ });
299
+ process.on("unhandledRejection", (reason) => {
300
+ console.error("[fatal] unhandledRejection:", reason);
301
+ });
275
302
  return { app, server, oauthChild, ctx };
276
303
  }
277
304
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "index.html": {
3
- "file": "assets/index-Da2s4_-5.js",
3
+ "file": "assets/index-CCP5nUOj.js",
4
4
  "name": "index",
5
5
  "src": "index.html",
6
6
  "isEntry": true,
@@ -16,11 +16,11 @@
16
16
  "src/components/PromptLibraryPanel.tsx"
17
17
  ],
18
18
  "css": [
19
- "assets/index-CjgnNtgt.css"
19
+ "assets/index-C-mur7pa.css"
20
20
  ]
21
21
  },
22
22
  "src/components/NodeCanvas.tsx": {
23
- "file": "assets/NodeCanvas-CI_wuPMf.js",
23
+ "file": "assets/NodeCanvas-BSsclEBh.js",
24
24
  "name": "NodeCanvas",
25
25
  "src": "src/components/NodeCanvas.tsx",
26
26
  "isDynamicEntry": true,
@@ -32,7 +32,7 @@
32
32
  ]
33
33
  },
34
34
  "src/components/PromptImportDialog.tsx": {
35
- "file": "assets/PromptImportDialog-CUi66jPK.js",
35
+ "file": "assets/PromptImportDialog-CVwT0rLd.js",
36
36
  "name": "PromptImportDialog",
37
37
  "src": "src/components/PromptImportDialog.tsx",
38
38
  "isDynamicEntry": true,
@@ -45,7 +45,7 @@
45
45
  ]
46
46
  },
47
47
  "src/components/PromptImportDiscoverySection.tsx": {
48
- "file": "assets/PromptImportDiscoverySection-Cm3vrjY4.js",
48
+ "file": "assets/PromptImportDiscoverySection-BDCkRCRs.js",
49
49
  "name": "PromptImportDiscoverySection",
50
50
  "src": "src/components/PromptImportDiscoverySection.tsx",
51
51
  "isDynamicEntry": true,
@@ -54,7 +54,7 @@
54
54
  ]
55
55
  },
56
56
  "src/components/PromptImportFolderSection.tsx": {
57
- "file": "assets/PromptImportFolderSection-DOtWTD9n.js",
57
+ "file": "assets/PromptImportFolderSection-QoKbZD83.js",
58
58
  "name": "PromptImportFolderSection",
59
59
  "src": "src/components/PromptImportFolderSection.tsx",
60
60
  "isDynamicEntry": true,
@@ -63,7 +63,7 @@
63
63
  ]
64
64
  },
65
65
  "src/components/PromptLibraryPanel.tsx": {
66
- "file": "assets/PromptLibraryPanel-BMjQegRa.js",
66
+ "file": "assets/PromptLibraryPanel-BhFgeKnY.js",
67
67
  "name": "PromptLibraryPanel",
68
68
  "src": "src/components/PromptLibraryPanel.tsx",
69
69
  "isDynamicEntry": true,
@@ -75,7 +75,7 @@
75
75
  ]
76
76
  },
77
77
  "src/components/SettingsWorkspace.tsx": {
78
- "file": "assets/SettingsWorkspace-PiaVnsdA.js",
78
+ "file": "assets/SettingsWorkspace-CfjrlH5R.js",
79
79
  "name": "SettingsWorkspace",
80
80
  "src": "src/components/SettingsWorkspace.tsx",
81
81
  "isDynamicEntry": true,
@@ -84,7 +84,7 @@
84
84
  ]
85
85
  },
86
86
  "src/components/agent/AgentWorkspace.tsx": {
87
- "file": "assets/AgentWorkspace-B_hq9CLg.js",
87
+ "file": "assets/AgentWorkspace-COxQ5TjU.js",
88
88
  "name": "AgentWorkspace",
89
89
  "src": "src/components/agent/AgentWorkspace.tsx",
90
90
  "isDynamicEntry": true,
@@ -93,7 +93,7 @@
93
93
  ]
94
94
  },
95
95
  "src/components/canvas-mode/index.ts": {
96
- "file": "assets/index-31uVIdt4.js",
96
+ "file": "assets/index-Cxhzi3bs.js",
97
97
  "name": "index",
98
98
  "src": "src/components/canvas-mode/index.ts",
99
99
  "isDynamicEntry": true,
@@ -102,7 +102,7 @@
102
102
  ]
103
103
  },
104
104
  "src/components/card-news/CardNewsWorkspace.tsx": {
105
- "file": "assets/CardNewsWorkspace-wD12J7qk.js",
105
+ "file": "assets/CardNewsWorkspace-B0OkcuVz.js",
106
106
  "name": "CardNewsWorkspace",
107
107
  "src": "src/components/card-news/CardNewsWorkspace.tsx",
108
108
  "isDynamicEntry": true,
@@ -111,7 +111,7 @@
111
111
  ]
112
112
  },
113
113
  "src/components/prompt-builder/PromptBuilderPanel.tsx": {
114
- "file": "assets/PromptBuilderPanel-CUTujJUV.js",
114
+ "file": "assets/PromptBuilderPanel-DpC9A5Rz.js",
115
115
  "name": "PromptBuilderPanel",
116
116
  "src": "src/components/prompt-builder/PromptBuilderPanel.tsx",
117
117
  "isDynamicEntry": true,