ima2-gen 1.1.7 → 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 (229) 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 -190
  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 +105 -0
  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 +135 -0
  130. package/lib/promptImport/curatedSources.ts +139 -0
  131. package/lib/promptImport/discoveryRegistry.js +218 -0
  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 +238 -0
  136. package/lib/promptImport/githubDiscovery.ts +248 -0
  137. package/lib/promptImport/githubFolder.js +302 -0
  138. package/lib/promptImport/githubFolder.ts +308 -0
  139. package/lib/promptImport/githubSource.js +194 -171
  140. package/lib/promptImport/githubSource.ts +239 -0
  141. package/lib/promptImport/gptImageHints.js +61 -0
  142. package/lib/promptImport/gptImageHints.ts +68 -0
  143. package/lib/promptImport/parsePromptCandidates.js +110 -112
  144. package/lib/promptImport/parsePromptCandidates.ts +153 -0
  145. package/lib/promptImport/promptIndex.js +230 -0
  146. package/lib/promptImport/promptIndex.ts +248 -0
  147. package/lib/promptImport/rankPromptCandidates.js +52 -0
  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 +33 -0
  187. package/routes/imageImport.ts +33 -0
  188. package/routes/index.js +18 -16
  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 +291 -152
  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 +6 -3
  219. package/assets/screenshot.png +0 -0
  220. package/assets/screenshots/classic-generate-light.png +0 -0
  221. package/assets/screenshots/node-graph-branching.png +0 -0
  222. package/assets/screenshots/settings-oauth-generation.png +0 -0
  223. package/assets/screenshots/settings-workspace.png +0 -0
  224. package/assets/screenshots/style-sheet-editor.png +0 -0
  225. package/integrations/comfyui/ima2_gen_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
  226. package/integrations/comfyui/ima2_gen_bridge/__pycache__/nodes.cpython-313.pyc +0 -0
  227. package/ui/dist/assets/index-DARPdT4Q.css +0 -1
  228. package/ui/dist/assets/index-ht80GMq4.js +0 -31
  229. package/ui/dist/assets/index-ht80GMq4.js.map +0 -1
@@ -1,144 +1,144 @@
1
1
  import { getDb } from "./db.js";
2
- import { rename, unlink, mkdir, access } from "fs/promises";
3
- import { join, resolve, sep } from "path";
2
+ import { rename, unlink, access } from "fs/promises";
3
+ import { resolve, sep } from "path";
4
+ import { moveToSystemTrash } from "./systemTrash.js";
4
5
  import { config } from "../config.js";
5
-
6
- const DIR = config.storage.generatedDirName;
7
- const TRASH = config.storage.trashDirName;
8
- const TRASH_TTL_MS = config.trash.ttlMs;
9
-
10
6
  function resolveInGenerated(rootDir, relPath) {
11
- void rootDir;
12
- if (typeof relPath !== "string" || relPath.length === 0) {
13
- const err = new Error("filename required");
14
- err.status = 400;
15
- err.code = "INVALID_FILENAME";
16
- throw err;
17
- }
18
- if (relPath.includes("\0")) {
19
- const err = new Error("invalid filename");
20
- err.status = 400;
21
- err.code = "INVALID_FILENAME";
22
- throw err;
23
- }
24
- const baseDir = resolve(config.storage.generatedDir);
25
- const target = resolve(baseDir, relPath);
26
- if (target !== baseDir && !target.startsWith(baseDir + sep)) {
27
- const err = new Error("filename escapes generated/");
28
- err.status = 400;
29
- err.code = "INVALID_FILENAME";
30
- throw err;
31
- }
32
- return target;
7
+ void rootDir;
8
+ if (typeof relPath !== "string" || relPath.length === 0) {
9
+ const err = new Error("filename required");
10
+ err.status = 400;
11
+ err.code = "INVALID_FILENAME";
12
+ throw err;
13
+ }
14
+ if (relPath.includes("\0")) {
15
+ const err = new Error("invalid filename");
16
+ err.status = 400;
17
+ err.code = "INVALID_FILENAME";
18
+ throw err;
19
+ }
20
+ const baseDir = resolve(config.storage.generatedDir);
21
+ const target = resolve(baseDir, relPath);
22
+ if (target !== baseDir && !target.startsWith(baseDir + sep)) {
23
+ const err = new Error("filename escapes generated/");
24
+ err.status = 400;
25
+ err.code = "INVALID_FILENAME";
26
+ throw err;
27
+ }
28
+ return target;
33
29
  }
34
-
35
30
  function nodesReferencingFilename(filename) {
36
- // The client stores imageUrl as `/generated/<encoded filename>` in node data JSON.
37
- // We scan all sessions' nodes for substring match on the decoded and encoded forms.
38
- const db = getDb();
39
- const encoded = encodeURIComponent(filename);
40
- const rows = db
41
- .prepare("SELECT session_id AS sessionId, id, data FROM nodes WHERE data LIKE ? OR data LIKE ?")
42
- .all(`%${filename}%`, `%${encoded}%`);
43
- return rows;
31
+ // The client stores imageUrl as `/generated/<encoded filename>` in node data JSON.
32
+ // We scan all sessions' nodes for substring match on the decoded and encoded forms.
33
+ const db = getDb();
34
+ const encoded = encodeURIComponent(filename);
35
+ const rows = db
36
+ .prepare("SELECT session_id AS sessionId, id, data FROM nodes WHERE data LIKE ? OR data LIKE ?")
37
+ .all(`%${filename}%`, `%${encoded}%`);
38
+ return rows;
44
39
  }
45
-
46
40
  function markNodesAssetMissing(filename) {
47
- const db = getDb();
48
- const rows = nodesReferencingFilename(filename);
49
- if (rows.length === 0) return { sessionsTouched: 0, nodesTouched: 0 };
50
- const touchedSessions = new Set();
51
- const update = db.prepare("UPDATE nodes SET data = ? WHERE session_id = ? AND id = ?");
52
- const bumpSession = db.prepare("UPDATE sessions SET graph_version = graph_version + 1, updated_at = ? WHERE id = ?");
53
- const tx = db.transaction(() => {
54
- for (const r of rows) {
55
- let data;
56
- try { data = JSON.parse(r.data); } catch { data = {}; }
57
- const imgRef = data?.imageUrl || "";
58
- if (imgRef.includes(filename) || imgRef.includes(encodeURIComponent(filename))) {
59
- data.imageUrl = null;
60
- data.status = "asset-missing";
61
- update.run(JSON.stringify(data), r.sessionId, r.id);
62
- touchedSessions.add(r.sessionId);
63
- }
64
- }
65
- const t = Date.now();
66
- for (const sid of touchedSessions) bumpSession.run(t, sid);
67
- });
68
- tx();
69
- return { sessionsTouched: touchedSessions.size, nodesTouched: rows.length };
41
+ const db = getDb();
42
+ const rows = nodesReferencingFilename(filename);
43
+ if (rows.length === 0)
44
+ return { sessionsTouched: 0, nodesTouched: 0 };
45
+ const touchedSessions = new Set();
46
+ const update = db.prepare("UPDATE nodes SET data = ? WHERE session_id = ? AND id = ?");
47
+ const bumpSession = db.prepare("UPDATE sessions SET graph_version = graph_version + 1, updated_at = ? WHERE id = ?");
48
+ const tx = db.transaction(() => {
49
+ for (const r of rows) {
50
+ let data;
51
+ try {
52
+ data = JSON.parse(r.data);
53
+ }
54
+ catch {
55
+ data = {};
56
+ }
57
+ const imgRef = data?.imageUrl || "";
58
+ if (imgRef.includes(filename) || imgRef.includes(encodeURIComponent(filename))) {
59
+ data.imageUrl = null;
60
+ data.status = "asset-missing";
61
+ update.run(JSON.stringify(data), r.sessionId, r.id);
62
+ touchedSessions.add(r.sessionId);
63
+ }
64
+ }
65
+ const t = Date.now();
66
+ for (const sid of touchedSessions)
67
+ bumpSession.run(t, sid);
68
+ });
69
+ tx();
70
+ return { sessionsTouched: touchedSessions.size, nodesTouched: rows.length };
70
71
  }
71
-
72
72
  export async function trashAsset(rootDir, filename) {
73
- const src = resolveInGenerated(rootDir, filename);
74
- try {
75
- await access(src);
76
- } catch {
77
- const err = new Error("Asset not found");
78
- err.status = 404;
79
- err.code = "ASSET_NOT_FOUND";
80
- throw err;
81
- }
82
- const trashDir = resolve(config.storage.trashDir);
83
- await mkdir(trashDir, { recursive: true });
84
- // Flatten filename (subdir separators -> __) so trash is flat & easy to restore
85
- const flat = filename.replace(/[\\/]+/g, "__");
86
- const trashPath = join(trashDir, `${Date.now()}_${flat}`);
87
- await rename(src, trashPath);
88
- // Move sidecar too (best-effort)
89
- await rename(src + ".json", trashPath + ".json").catch(() => {});
90
-
91
- const summary = markNodesAssetMissing(filename);
92
-
93
- // Schedule hard delete after TTL
94
- const unlinkAt = Date.now() + TRASH_TTL_MS;
95
- setTimeout(async () => {
96
- await unlink(trashPath).catch(() => {});
97
- await unlink(trashPath + ".json").catch(() => {});
98
- }, TRASH_TTL_MS).unref?.();
99
-
100
- return {
101
- ok: true,
102
- trashId: trashPath.slice(trashDir.length + 1),
103
- filename,
104
- unlinkAt,
105
- sessionsTouched: summary.sessionsTouched,
106
- nodesTouched: summary.nodesTouched,
107
- };
73
+ const src = resolveInGenerated(rootDir, filename);
74
+ try {
75
+ await access(src);
76
+ }
77
+ catch {
78
+ const err = new Error("Asset not found");
79
+ err.status = 404;
80
+ err.code = "ASSET_NOT_FOUND";
81
+ throw err;
82
+ }
83
+ const sidecar = `${src}.json`;
84
+ const paths = [src];
85
+ try {
86
+ await access(sidecar);
87
+ paths.push(sidecar);
88
+ }
89
+ catch { }
90
+ try {
91
+ await moveToSystemTrash(paths);
92
+ }
93
+ catch (cause) {
94
+ const err = new Error("Could not move asset to system trash");
95
+ err.status = 500;
96
+ err.code = "SYSTEM_TRASH_FAILED";
97
+ err.cause = cause;
98
+ throw err;
99
+ }
100
+ const summary = markNodesAssetMissing(filename);
101
+ return {
102
+ ok: true,
103
+ filename,
104
+ trash: "system",
105
+ undoableInApp: false,
106
+ sessionsTouched: summary.sessionsTouched,
107
+ nodesTouched: summary.nodesTouched,
108
+ };
108
109
  }
109
-
110
110
  export async function deleteAssetPermanent(rootDir, filename) {
111
- const src = resolveInGenerated(rootDir, filename);
112
- try {
113
- await access(src);
114
- } catch {
115
- const err = new Error("Asset not found");
116
- err.status = 404;
117
- err.code = "ASSET_NOT_FOUND";
118
- throw err;
119
- }
120
- await unlink(src);
121
- await unlink(src + ".json").catch(() => {});
122
- const summary = markNodesAssetMissing(filename);
123
- return {
124
- ok: true,
125
- filename,
126
- sessionsTouched: summary.sessionsTouched,
127
- nodesTouched: summary.nodesTouched,
128
- };
111
+ const src = resolveInGenerated(rootDir, filename);
112
+ try {
113
+ await access(src);
114
+ }
115
+ catch {
116
+ const err = new Error("Asset not found");
117
+ err.status = 404;
118
+ err.code = "ASSET_NOT_FOUND";
119
+ throw err;
120
+ }
121
+ await unlink(src);
122
+ await unlink(src + ".json").catch(() => { });
123
+ const summary = markNodesAssetMissing(filename);
124
+ return {
125
+ ok: true,
126
+ filename,
127
+ sessionsTouched: summary.sessionsTouched,
128
+ nodesTouched: summary.nodesTouched,
129
+ };
129
130
  }
130
-
131
131
  export async function restoreAsset(rootDir, trashId, originalFilename) {
132
- void rootDir;
133
- const trashDir = resolve(config.storage.trashDir);
134
- const src = resolve(trashDir, trashId);
135
- if (!src.startsWith(trashDir + sep) && src !== trashDir) {
136
- const err = new Error("invalid trashId");
137
- err.status = 400;
138
- throw err;
139
- }
140
- const dst = resolveInGenerated(rootDir, originalFilename);
141
- await rename(src, dst);
142
- await rename(src + ".json", dst + ".json").catch(() => {});
143
- return { ok: true };
132
+ void rootDir;
133
+ const trashDir = resolve(config.storage.trashDir);
134
+ const src = resolve(trashDir, trashId);
135
+ if (!src.startsWith(trashDir + sep) && src !== trashDir) {
136
+ const err = new Error("invalid trashId");
137
+ err.status = 400;
138
+ throw err;
139
+ }
140
+ const dst = resolveInGenerated(rootDir, originalFilename);
141
+ await rename(src, dst);
142
+ await rename(src + ".json", dst + ".json").catch(() => { });
143
+ return { ok: true };
144
144
  }
@@ -0,0 +1,142 @@
1
+ import { getDb } from "./db.js";
2
+ import { rename, unlink, access } from "fs/promises";
3
+ import { resolve, sep } from "path";
4
+ import { moveToSystemTrash } from "./systemTrash.js";
5
+ import { config } from "../config.js";
6
+
7
+ function resolveInGenerated(rootDir, relPath) {
8
+ void rootDir;
9
+ if (typeof relPath !== "string" || relPath.length === 0) {
10
+ const err: any = new Error("filename required");
11
+ err.status = 400;
12
+ err.code = "INVALID_FILENAME";
13
+ throw err;
14
+ }
15
+ if (relPath.includes("\0")) {
16
+ const err: any = new Error("invalid filename");
17
+ err.status = 400;
18
+ err.code = "INVALID_FILENAME";
19
+ throw err;
20
+ }
21
+ const baseDir = resolve(config.storage.generatedDir);
22
+ const target = resolve(baseDir, relPath);
23
+ if (target !== baseDir && !target.startsWith(baseDir + sep)) {
24
+ const err: any = new Error("filename escapes generated/");
25
+ err.status = 400;
26
+ err.code = "INVALID_FILENAME";
27
+ throw err;
28
+ }
29
+ return target;
30
+ }
31
+
32
+ function nodesReferencingFilename(filename) {
33
+ // The client stores imageUrl as `/generated/<encoded filename>` in node data JSON.
34
+ // We scan all sessions' nodes for substring match on the decoded and encoded forms.
35
+ const db = getDb();
36
+ const encoded = encodeURIComponent(filename);
37
+ const rows = db
38
+ .prepare("SELECT session_id AS sessionId, id, data FROM nodes WHERE data LIKE ? OR data LIKE ?")
39
+ .all(`%${filename}%`, `%${encoded}%`);
40
+ return rows;
41
+ }
42
+
43
+ function markNodesAssetMissing(filename) {
44
+ const db = getDb();
45
+ const rows = nodesReferencingFilename(filename);
46
+ if (rows.length === 0) return { sessionsTouched: 0, nodesTouched: 0 };
47
+ const touchedSessions = new Set();
48
+ const update = db.prepare("UPDATE nodes SET data = ? WHERE session_id = ? AND id = ?");
49
+ const bumpSession = db.prepare("UPDATE sessions SET graph_version = graph_version + 1, updated_at = ? WHERE id = ?");
50
+ const tx = db.transaction(() => {
51
+ for (const r of rows) {
52
+ let data;
53
+ try { data = JSON.parse(r.data); } catch { data = {}; }
54
+ const imgRef = data?.imageUrl || "";
55
+ if (imgRef.includes(filename) || imgRef.includes(encodeURIComponent(filename))) {
56
+ data.imageUrl = null;
57
+ data.status = "asset-missing";
58
+ update.run(JSON.stringify(data), r.sessionId, r.id);
59
+ touchedSessions.add(r.sessionId);
60
+ }
61
+ }
62
+ const t = Date.now();
63
+ for (const sid of touchedSessions) bumpSession.run(t, sid);
64
+ });
65
+ tx();
66
+ return { sessionsTouched: touchedSessions.size, nodesTouched: rows.length };
67
+ }
68
+
69
+ export async function trashAsset(rootDir, filename) {
70
+ const src = resolveInGenerated(rootDir, filename);
71
+ try {
72
+ await access(src);
73
+ } catch {
74
+ const err: any = new Error("Asset not found");
75
+ err.status = 404;
76
+ err.code = "ASSET_NOT_FOUND";
77
+ throw err;
78
+ }
79
+
80
+ const sidecar = `${src}.json`;
81
+ const paths = [src];
82
+ try {
83
+ await access(sidecar);
84
+ paths.push(sidecar);
85
+ } catch {}
86
+
87
+ try {
88
+ await moveToSystemTrash(paths);
89
+ } catch (cause) {
90
+ const err: any = new Error("Could not move asset to system trash");
91
+ err.status = 500;
92
+ err.code = "SYSTEM_TRASH_FAILED";
93
+ err.cause = cause;
94
+ throw err;
95
+ }
96
+
97
+ const summary = markNodesAssetMissing(filename);
98
+ return {
99
+ ok: true,
100
+ filename,
101
+ trash: "system",
102
+ undoableInApp: false,
103
+ sessionsTouched: summary.sessionsTouched,
104
+ nodesTouched: summary.nodesTouched,
105
+ };
106
+ }
107
+
108
+ export async function deleteAssetPermanent(rootDir, filename) {
109
+ const src = resolveInGenerated(rootDir, filename);
110
+ try {
111
+ await access(src);
112
+ } catch {
113
+ const err: any = new Error("Asset not found");
114
+ err.status = 404;
115
+ err.code = "ASSET_NOT_FOUND";
116
+ throw err;
117
+ }
118
+ await unlink(src);
119
+ await unlink(src + ".json").catch(() => {});
120
+ const summary = markNodesAssetMissing(filename);
121
+ return {
122
+ ok: true,
123
+ filename,
124
+ sessionsTouched: summary.sessionsTouched,
125
+ nodesTouched: summary.nodesTouched,
126
+ };
127
+ }
128
+
129
+ export async function restoreAsset(rootDir, trashId, originalFilename) {
130
+ void rootDir;
131
+ const trashDir = resolve(config.storage.trashDir);
132
+ const src = resolve(trashDir, trashId);
133
+ if (!src.startsWith(trashDir + sep) && src !== trashDir) {
134
+ const err: any = new Error("invalid trashId");
135
+ err.status = 400;
136
+ throw err;
137
+ }
138
+ const dst = resolveInGenerated(rootDir, originalFilename);
139
+ await rename(src, dst);
140
+ await rename(src + ".json", dst + ".json").catch(() => {});
141
+ return { ok: true };
142
+ }