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
@@ -0,0 +1,284 @@
1
+ import { mkdir, readdir, copyFile, stat, constants } from "node:fs/promises";
2
+ import { dirname, isAbsolute, join, resolve, sep } from "node:path";
3
+ import { homedir } from "node:os";
4
+
5
+ const PACKAGE_NAME = "ima2-gen";
6
+ const RECOVERY_DOCS_PATH = "docs/RECOVER_OLD_IMAGES.md";
7
+
8
+ function addStats(a, b) {
9
+ return {
10
+ copied: a.copied + b.copied,
11
+ skippedExisting: a.skippedExisting + b.skippedExisting,
12
+ };
13
+ }
14
+
15
+ async function copyMissingTree(srcDir, dstDir) {
16
+ await mkdir(dstDir, { recursive: true });
17
+ const entries = await readdir(srcDir, { withFileTypes: true });
18
+ let stats = { copied: 0, skippedExisting: 0 };
19
+ for (const entry of entries) {
20
+ const src = join(srcDir, entry.name);
21
+ const dst = join(dstDir, entry.name);
22
+ if (entry.isDirectory()) {
23
+ stats = addStats(stats, await copyMissingTree(src, dst));
24
+ continue;
25
+ }
26
+ if (!entry.isFile()) continue;
27
+ try {
28
+ await copyFile(src, dst, constants.COPYFILE_EXCL);
29
+ stats.copied += 1;
30
+ } catch (err: any) {
31
+ if (err?.code !== "EEXIST") throw err;
32
+ stats.skippedExisting += 1;
33
+ }
34
+ }
35
+ return stats;
36
+ }
37
+
38
+ function isSameOrInside(child, parent) {
39
+ const a = resolve(child);
40
+ const b = resolve(parent);
41
+ return a === b || a.startsWith(b + sep);
42
+ }
43
+
44
+ export async function migrateGeneratedStorage(ctx, options: any = {}) {
45
+ const targetDir = ctx.config.storage.generatedDir;
46
+ const candidates = options.legacyDirs || await getLegacyGeneratedCandidates(ctx, options.env);
47
+ const result = {
48
+ copied: 0,
49
+ skippedExisting: 0,
50
+ sourcesScanned: 0,
51
+ sourcesSkipped: 0,
52
+ };
53
+ try {
54
+ for (const legacyDir of candidates) {
55
+ if (isSameOrInside(legacyDir, targetDir) || isSameOrInside(targetDir, legacyDir)) {
56
+ result.sourcesSkipped += 1;
57
+ continue;
58
+ }
59
+ try {
60
+ const legacyStat = await stat(legacyDir);
61
+ if (!legacyStat.isDirectory()) continue;
62
+ result.sourcesScanned += 1;
63
+ const copyStats = await copyMissingTree(legacyDir, targetDir);
64
+ result.copied += copyStats.copied;
65
+ result.skippedExisting += copyStats.skippedExisting;
66
+ } catch (err: any) {
67
+ if (err?.code !== "ENOENT") {
68
+ console.warn("[storage] generated asset migration source skipped:", legacyDir, err.message);
69
+ }
70
+ }
71
+ }
72
+ if (result.copied > 0) console.log(`[storage] migrated ${result.copied} generated assets to ${targetDir}`);
73
+ } catch (err: any) {
74
+ console.warn("[storage] generated asset migration skipped:", err.message);
75
+ }
76
+ return result;
77
+ }
78
+
79
+ export async function getLegacyGeneratedCandidates(ctx, env = process.env) {
80
+ const home = env.IMA2_TEST_HOME || homedir();
81
+ const execPath = env.IMA2_TEST_EXEC_PATH || process.execPath;
82
+ const argv1 = env.IMA2_TEST_ARGV1 || process.argv[1] || "";
83
+ const nodePrefix = dirname(dirname(execPath));
84
+ const prefixes = getGlobalPrefixCandidates({ env, execPath, argv1 });
85
+ const appData = env.APPDATA || join(home, "AppData", "Roaming");
86
+ const localAppData = env.LOCALAPPDATA || join(home, "AppData", "Local");
87
+ const npmCache = env.npm_config_cache || join(home, ".npm");
88
+ const xdgDataHome = env.XDG_DATA_HOME || join(home, ".local", "share");
89
+ const pnpmHome = env.PNPM_HOME || "";
90
+ const nvmHome = env.NVM_HOME || join(appData, "nvm");
91
+
92
+ const candidates = [
93
+ join(ctx.rootDir, "generated"),
94
+ join(appData, "npm", "node_modules", PACKAGE_NAME, "generated"),
95
+ join(home, ".npm-global", "lib", "node_modules", PACKAGE_NAME, "generated"),
96
+ join(home, ".nvm", "versions", "node", process.version, "lib", "node_modules", PACKAGE_NAME, "generated"),
97
+ join(home, ".volta", "tools", "image", "packages", PACKAGE_NAME, "lib", "node_modules", PACKAGE_NAME, "generated"),
98
+ join(home, ".fnm", "node-versions", process.version, "installation", "lib", "node_modules", PACKAGE_NAME, "generated"),
99
+ join(home, ".bun", "install", "global", "node_modules", PACKAGE_NAME, "generated"),
100
+ join(home, ".config", "yarn", "global", "node_modules", PACKAGE_NAME, "generated"),
101
+ join(localAppData, "Yarn", "Data", "global", "node_modules", PACKAGE_NAME, "generated"),
102
+ join(localAppData, "Volta", "tools", "image", "packages", PACKAGE_NAME, "lib", "node_modules", PACKAGE_NAME, "generated"),
103
+ join(nvmHome, process.version, "node_modules", PACKAGE_NAME, "generated"),
104
+ join(dirname(execPath), "node_modules", PACKAGE_NAME, "generated"),
105
+ ];
106
+
107
+ for (const prefix of prefixes) {
108
+ candidates.push(join(prefix, "lib", "node_modules", PACKAGE_NAME, "generated"));
109
+ candidates.push(join(prefix, "node_modules", PACKAGE_NAME, "generated"));
110
+ }
111
+
112
+ candidates.push(join(nodePrefix, "lib", "node_modules", PACKAGE_NAME, "generated"));
113
+ candidates.push(
114
+ ...await expandOneLevelCandidates([
115
+ [join(home, ".nvm", "versions", "node"), ["*", "lib", "node_modules", PACKAGE_NAME, "generated"]],
116
+ [join(home, ".fnm", "node-versions"), ["*", "installation", "lib", "node_modules", PACKAGE_NAME, "generated"]],
117
+ [join(home, ".asdf", "installs", "nodejs"), ["*", "lib", "node_modules", PACKAGE_NAME, "generated"]],
118
+ [join(home, ".local", "share", "mise", "installs", "node"), ["*", "lib", "node_modules", PACKAGE_NAME, "generated"]],
119
+ [join(home, "Library", "pnpm", "global"), ["*", "node_modules", PACKAGE_NAME, "generated"]],
120
+ [join(xdgDataHome, "pnpm", "global"), ["*", "node_modules", PACKAGE_NAME, "generated"]],
121
+ [join(localAppData, "pnpm", "global"), ["*", "node_modules", PACKAGE_NAME, "generated"]],
122
+ [pnpmHome ? join(pnpmHome, "global") : "", ["*", "node_modules", PACKAGE_NAME, "generated"]],
123
+ [join(npmCache, "_npx"), ["*", "node_modules", PACKAGE_NAME, "generated"]],
124
+ [join(localAppData, "npm-cache", "_npx"), ["*", "node_modules", PACKAGE_NAME, "generated"]],
125
+ [join(appData, "npm-cache", "_npx"), ["*", "node_modules", PACKAGE_NAME, "generated"]],
126
+ [nvmHome, ["*", "node_modules", PACKAGE_NAME, "generated"]],
127
+ ]),
128
+ );
129
+ return uniqueResolvedCandidates(candidates);
130
+ }
131
+
132
+ export async function inspectGeneratedStorage(ctx, options: any = {}) {
133
+ const env = options.env || process.env;
134
+ const targetDir = ctx.config.storage.generatedDir;
135
+ try {
136
+ const candidates = options.legacyDirs || await getLegacyGeneratedCandidates(ctx, env);
137
+ const targetFileCount = await countFiles(targetDir);
138
+ const legacySources = [];
139
+
140
+ for (const candidate of candidates) {
141
+ if (isSameOrInside(candidate, targetDir) || isSameOrInside(targetDir, candidate)) continue;
142
+ try {
143
+ const candidateStat = await stat(candidate);
144
+ if (!candidateStat.isDirectory()) continue;
145
+ const fileCount = await countFiles(candidate);
146
+ if (fileCount > 0) legacySources.push({ path: candidate, fileCount });
147
+ } catch (err: any) {
148
+ if (err?.code !== "ENOENT") {
149
+ console.warn("[storage] legacy candidate inspect skipped:", candidate, err.message);
150
+ }
151
+ }
152
+ }
153
+
154
+ const legacyFilesFound = legacySources.reduce((sum, source) => sum + source.fileCount, 0);
155
+ const state =
156
+ targetFileCount > 0 ? "ok"
157
+ : legacyFilesFound > 0 ? "recoverable"
158
+ : "not_found";
159
+
160
+ return {
161
+ ok: true,
162
+ targetDir,
163
+ generatedDirLabel: labelPath(targetDir, env),
164
+ targetExists: await isDirectory(targetDir),
165
+ targetFileCount,
166
+ legacyCandidatesScanned: candidates.length,
167
+ legacySourcesFound: legacySources.length,
168
+ legacyFilesFound,
169
+ legacySources,
170
+ overrides: {
171
+ generatedDir: Boolean(env.IMA2_GENERATED_DIR),
172
+ configDir: Boolean(env.IMA2_CONFIG_DIR),
173
+ },
174
+ state,
175
+ messageKind: state === "not_found" ? "apology" : state,
176
+ recoveryDocsPath: RECOVERY_DOCS_PATH,
177
+ doctorCommand: "ima2 doctor",
178
+ };
179
+ } catch (err: any) {
180
+ return {
181
+ ok: false,
182
+ targetDir,
183
+ generatedDirLabel: labelPath(targetDir, env),
184
+ targetExists: false,
185
+ targetFileCount: 0,
186
+ legacyCandidatesScanned: 0,
187
+ legacySourcesFound: 0,
188
+ legacyFilesFound: 0,
189
+ legacySources: [],
190
+ overrides: {
191
+ generatedDir: Boolean(env.IMA2_GENERATED_DIR),
192
+ configDir: Boolean(env.IMA2_CONFIG_DIR),
193
+ },
194
+ state: "unknown",
195
+ messageKind: "unknown",
196
+ recoveryDocsPath: RECOVERY_DOCS_PATH,
197
+ doctorCommand: "ima2 doctor",
198
+ error: err?.message || String(err),
199
+ };
200
+ }
201
+ }
202
+
203
+ async function expandOneLevelCandidates(patterns) {
204
+ const candidates = [];
205
+ for (const [baseDir, segments] of patterns) {
206
+ if (!baseDir) continue;
207
+ candidates.push(...await expandOneLevelPattern(baseDir, segments));
208
+ }
209
+ return candidates;
210
+ }
211
+
212
+ async function expandOneLevelPattern(baseDir, segments) {
213
+ const wildcardIndex = segments.indexOf("*");
214
+ if (wildcardIndex < 0) return [join(baseDir, ...segments)];
215
+
216
+ const before = segments.slice(0, wildcardIndex);
217
+ const after = segments.slice(wildcardIndex + 1);
218
+ const wildcardBase = join(baseDir, ...before);
219
+ try {
220
+ const entries = await readdir(wildcardBase, { withFileTypes: true });
221
+ return entries
222
+ .filter((entry) => entry.isDirectory())
223
+ .map((entry) => join(wildcardBase, entry.name, ...after));
224
+ } catch (err: any) {
225
+ if (err?.code === "ENOENT") return [];
226
+ console.warn("[storage] legacy candidate scan skipped:", wildcardBase, err.message);
227
+ return [];
228
+ }
229
+ }
230
+
231
+ async function countFiles(dir) {
232
+ try {
233
+ const entries = await readdir(dir, { withFileTypes: true });
234
+ let count = 0;
235
+ for (const entry of entries) {
236
+ if (entry.name === ".trash") continue;
237
+ const fullPath = join(dir, entry.name);
238
+ if (entry.isDirectory()) count += await countFiles(fullPath);
239
+ else if (entry.isFile()) count += 1;
240
+ }
241
+ return count;
242
+ } catch (err: any) {
243
+ if (err?.code === "ENOENT") return 0;
244
+ throw err;
245
+ }
246
+ }
247
+
248
+ async function isDirectory(dir) {
249
+ try {
250
+ return (await stat(dir)).isDirectory();
251
+ } catch {
252
+ return false;
253
+ }
254
+ }
255
+
256
+ function uniqueResolvedCandidates(candidates) {
257
+ return Array.from(new Set(candidates.filter(Boolean).map((p) => resolve(p))));
258
+ }
259
+
260
+ function labelPath(targetPath, env = process.env) {
261
+ const home = env.IMA2_TEST_HOME || homedir();
262
+ const resolved = resolve(targetPath);
263
+ const resolvedHome = resolve(home);
264
+ if (resolved === resolvedHome) return "~";
265
+ if (resolved.startsWith(resolvedHome + sep)) return `~${sep}${resolved.slice(resolvedHome.length + 1)}`;
266
+ return resolved;
267
+ }
268
+
269
+ function getGlobalPrefixCandidates({ env, execPath, argv1 }) {
270
+ const prefixes: Set<string> = new Set();
271
+ if (env.npm_config_prefix) prefixes.add(env.npm_config_prefix);
272
+ if (isAbsolute(argv1)) prefixes.add(dirname(dirname(argv1)));
273
+ prefixes.add(dirname(dirname(execPath)));
274
+ addHomebrewPrefix(prefixes, execPath);
275
+ prefixes.add("/opt/homebrew");
276
+ prefixes.add("/usr/local");
277
+ return Array.from(prefixes);
278
+ }
279
+
280
+ function addHomebrewPrefix(prefixes, execPath) {
281
+ const marker = `${sep}Cellar${sep}node`;
282
+ const idx = execPath.indexOf(marker);
283
+ if (idx > 0) prefixes.add(execPath.slice(0, idx));
284
+ }
package/lib/styleSheet.js CHANGED
@@ -17,10 +17,8 @@
17
17
  //
18
18
  // The module is pure JS + openai SDK. When no API key is configured it throws
19
19
  // STYLE_SHEET_NO_KEY so callers can surface a friendly "connect key" UI.
20
-
21
20
  import { config } from "../config.js";
22
21
  const STYLE_SHEET_MODEL = config.styleSheet.model;
23
-
24
22
  const SYSTEM_PROMPT = `You extract a reusable visual style guide from a user
25
23
  image prompt (and an optional reference image). Return ONLY a JSON object with
26
24
  these keys: palette (array of 3-6 concrete color names), composition (one
@@ -29,100 +27,98 @@ naming technique/material), subject_details (one sentence capturing identity
29
27
  cues: face, outfit, pose, distinctive features), negative (array of 0-4 short
30
28
  phrases of things to avoid). Keep entries tight — each under 120 characters.
31
29
  Do not wrap in markdown. Do not add commentary.`;
32
-
33
30
  function coerceStyleSheet(raw) {
34
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
35
- const arr = (v, max = 6) =>
36
- Array.isArray(v)
37
- ? v
38
- .filter((x) => typeof x === "string" && x.trim())
39
- .slice(0, max)
40
- .map((s) => s.trim())
41
- : [];
42
- const str = (v) => (typeof v === "string" ? v.trim().slice(0, 400) : "");
43
- const sheet = {
44
- palette: arr(raw.palette, 6),
45
- composition: str(raw.composition),
46
- mood: str(raw.mood),
47
- medium: str(raw.medium),
48
- subject_details: str(raw.subject_details),
49
- negative: arr(raw.negative, 4),
50
- };
51
- const hasContent =
52
- sheet.palette.length > 0 ||
53
- sheet.negative.length > 0 ||
54
- sheet.composition ||
55
- sheet.mood ||
56
- sheet.medium ||
57
- sheet.subject_details;
58
- return hasContent ? sheet : null;
31
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
32
+ return null;
33
+ const arr = (v, max = 6) => Array.isArray(v)
34
+ ? v
35
+ .filter((x) => typeof x === "string" && x.trim())
36
+ .slice(0, max)
37
+ .map((s) => s.trim())
38
+ : [];
39
+ const str = (v) => (typeof v === "string" ? v.trim().slice(0, 400) : "");
40
+ const sheet = {
41
+ palette: arr(raw.palette, 6),
42
+ composition: str(raw.composition),
43
+ mood: str(raw.mood),
44
+ medium: str(raw.medium),
45
+ subject_details: str(raw.subject_details),
46
+ negative: arr(raw.negative, 4),
47
+ };
48
+ const hasContent = sheet.palette.length > 0 ||
49
+ sheet.negative.length > 0 ||
50
+ sheet.composition ||
51
+ sheet.mood ||
52
+ sheet.medium ||
53
+ sheet.subject_details;
54
+ return hasContent ? sheet : null;
59
55
  }
60
-
61
56
  export async function extractStyleSheet(openai, { prompt, referenceDataUrl }) {
62
- if (!openai) {
63
- const err = new Error("No OpenAI client configured for style-sheet extraction");
64
- err.code = "STYLE_SHEET_NO_KEY";
65
- throw err;
66
- }
67
- if (!prompt || typeof prompt !== "string" || !prompt.trim()) {
68
- const err = new Error("prompt is required");
69
- err.code = "STYLE_SHEET_BAD_INPUT";
70
- throw err;
71
- }
72
-
73
- const userContent = referenceDataUrl
74
- ? [
75
- { type: "text", text: prompt },
76
- { type: "image_url", image_url: { url: referenceDataUrl } },
77
- ]
78
- : prompt;
79
-
80
- const resp = await openai.chat.completions.create({
81
- model: STYLE_SHEET_MODEL,
82
- response_format: { type: "json_object" },
83
- messages: [
84
- { role: "system", content: SYSTEM_PROMPT },
85
- { role: "user", content: userContent },
86
- ],
87
- });
88
-
89
- const raw = resp.choices?.[0]?.message?.content;
90
- if (!raw) {
91
- const err = new Error("Empty response from style-sheet model");
92
- err.code = "STYLE_SHEET_EMPTY";
93
- throw err;
94
- }
95
-
96
- let parsed;
97
- try {
98
- parsed = JSON.parse(raw);
99
- } catch {
100
- const err = new Error("Style-sheet model returned non-JSON");
101
- err.code = "STYLE_SHEET_PARSE";
102
- throw err;
103
- }
104
-
105
- const sheet = coerceStyleSheet(parsed);
106
- if (!sheet) {
107
- const err = new Error("Style-sheet shape invalid");
108
- err.code = "STYLE_SHEET_SHAPE";
109
- throw err;
110
- }
111
- return sheet;
57
+ if (!openai) {
58
+ const err = new Error("No OpenAI client configured for style-sheet extraction");
59
+ err.code = "STYLE_SHEET_NO_KEY";
60
+ throw err;
61
+ }
62
+ if (!prompt || typeof prompt !== "string" || !prompt.trim()) {
63
+ const err = new Error("prompt is required");
64
+ err.code = "STYLE_SHEET_BAD_INPUT";
65
+ throw err;
66
+ }
67
+ const userContent = referenceDataUrl
68
+ ? [
69
+ { type: "text", text: prompt },
70
+ { type: "image_url", image_url: { url: referenceDataUrl } },
71
+ ]
72
+ : prompt;
73
+ const resp = await openai.chat.completions.create({
74
+ model: STYLE_SHEET_MODEL,
75
+ response_format: { type: "json_object" },
76
+ messages: [
77
+ { role: "system", content: SYSTEM_PROMPT },
78
+ { role: "user", content: userContent },
79
+ ],
80
+ });
81
+ const raw = resp.choices?.[0]?.message?.content;
82
+ if (!raw) {
83
+ const err = new Error("Empty response from style-sheet model");
84
+ err.code = "STYLE_SHEET_EMPTY";
85
+ throw err;
86
+ }
87
+ let parsed;
88
+ try {
89
+ parsed = JSON.parse(raw);
90
+ }
91
+ catch {
92
+ const err = new Error("Style-sheet model returned non-JSON");
93
+ err.code = "STYLE_SHEET_PARSE";
94
+ throw err;
95
+ }
96
+ const sheet = coerceStyleSheet(parsed);
97
+ if (!sheet) {
98
+ const err = new Error("Style-sheet shape invalid");
99
+ err.code = "STYLE_SHEET_SHAPE";
100
+ throw err;
101
+ }
102
+ return sheet;
112
103
  }
113
-
114
104
  // Render a style sheet into a prompt preamble that gpt-image-1/2 can consume.
115
105
  // Kept short so it doesn't blow the 4K prompt window on long user prompts.
116
106
  export function renderStyleSheetPrefix(sheet) {
117
- if (!sheet) return "";
118
- const parts = [];
119
- if (sheet.medium) parts.push(`Medium: ${sheet.medium}.`);
120
- if (sheet.palette?.length) parts.push(`Palette: ${sheet.palette.join(", ")}.`);
121
- if (sheet.composition) parts.push(`Composition: ${sheet.composition}.`);
122
- if (sheet.mood) parts.push(`Mood: ${sheet.mood}.`);
123
- if (sheet.subject_details) parts.push(`Subject: ${sheet.subject_details}.`);
124
- if (sheet.negative?.length) parts.push(`Avoid: ${sheet.negative.join(", ")}.`);
125
- return parts.join(" ");
107
+ if (!sheet)
108
+ return "";
109
+ const parts = [];
110
+ if (sheet.medium)
111
+ parts.push(`Medium: ${sheet.medium}.`);
112
+ if (sheet.palette?.length)
113
+ parts.push(`Palette: ${sheet.palette.join(", ")}.`);
114
+ if (sheet.composition)
115
+ parts.push(`Composition: ${sheet.composition}.`);
116
+ if (sheet.mood)
117
+ parts.push(`Mood: ${sheet.mood}.`);
118
+ if (sheet.subject_details)
119
+ parts.push(`Subject: ${sheet.subject_details}.`);
120
+ if (sheet.negative?.length)
121
+ parts.push(`Avoid: ${sheet.negative.join(", ")}.`);
122
+ return parts.join(" ");
126
123
  }
127
-
128
124
  export { STYLE_SHEET_MODEL, SYSTEM_PROMPT, coerceStyleSheet };
@@ -0,0 +1,128 @@
1
+ // Style-sheet extractor (0.10)
2
+ //
3
+ // Uses GPT-5.4 (chat completions) to derive a structured "style guide" from a
4
+ // user prompt and optional reference image. The style guide is stored per
5
+ // session and automatically prepended to subsequent image generations so
6
+ // continuations feel cohesive — closer to ChatGPT 4o's image carry-over.
7
+ //
8
+ // Shape:
9
+ // {
10
+ // palette: string[], // e.g. ["deep navy", "gold leaf"]
11
+ // composition: string, // e.g. "centered 3/4 portrait, shallow depth"
12
+ // mood: string, // e.g. "melancholic, reverent"
13
+ // medium: string, // e.g. "oil painting, glazed layers"
14
+ // subject_details: string, // identity/pose/outfit cues for character continuity
15
+ // negative: string[] // things to avoid
16
+ // }
17
+ //
18
+ // The module is pure JS + openai SDK. When no API key is configured it throws
19
+ // STYLE_SHEET_NO_KEY so callers can surface a friendly "connect key" UI.
20
+
21
+ import { config } from "../config.js";
22
+ const STYLE_SHEET_MODEL = config.styleSheet.model;
23
+
24
+ const SYSTEM_PROMPT = `You extract a reusable visual style guide from a user
25
+ image prompt (and an optional reference image). Return ONLY a JSON object with
26
+ these keys: palette (array of 3-6 concrete color names), composition (one
27
+ sentence), mood (2-4 comma-separated adjectives), medium (one short phrase
28
+ naming technique/material), subject_details (one sentence capturing identity
29
+ cues: face, outfit, pose, distinctive features), negative (array of 0-4 short
30
+ phrases of things to avoid). Keep entries tight — each under 120 characters.
31
+ Do not wrap in markdown. Do not add commentary.`;
32
+
33
+ function coerceStyleSheet(raw) {
34
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
35
+ const arr = (v, max = 6) =>
36
+ Array.isArray(v)
37
+ ? v
38
+ .filter((x) => typeof x === "string" && x.trim())
39
+ .slice(0, max)
40
+ .map((s) => s.trim())
41
+ : [];
42
+ const str = (v) => (typeof v === "string" ? v.trim().slice(0, 400) : "");
43
+ const sheet = {
44
+ palette: arr(raw.palette, 6),
45
+ composition: str(raw.composition),
46
+ mood: str(raw.mood),
47
+ medium: str(raw.medium),
48
+ subject_details: str(raw.subject_details),
49
+ negative: arr(raw.negative, 4),
50
+ };
51
+ const hasContent =
52
+ sheet.palette.length > 0 ||
53
+ sheet.negative.length > 0 ||
54
+ sheet.composition ||
55
+ sheet.mood ||
56
+ sheet.medium ||
57
+ sheet.subject_details;
58
+ return hasContent ? sheet : null;
59
+ }
60
+
61
+ export async function extractStyleSheet(openai, { prompt, referenceDataUrl }) {
62
+ if (!openai) {
63
+ const err: any = new Error("No OpenAI client configured for style-sheet extraction");
64
+ err.code = "STYLE_SHEET_NO_KEY";
65
+ throw err;
66
+ }
67
+ if (!prompt || typeof prompt !== "string" || !prompt.trim()) {
68
+ const err: any = new Error("prompt is required");
69
+ err.code = "STYLE_SHEET_BAD_INPUT";
70
+ throw err;
71
+ }
72
+
73
+ const userContent = referenceDataUrl
74
+ ? [
75
+ { type: "text", text: prompt },
76
+ { type: "image_url", image_url: { url: referenceDataUrl } },
77
+ ]
78
+ : prompt;
79
+
80
+ const resp = await openai.chat.completions.create({
81
+ model: STYLE_SHEET_MODEL,
82
+ response_format: { type: "json_object" },
83
+ messages: [
84
+ { role: "system", content: SYSTEM_PROMPT },
85
+ { role: "user", content: userContent },
86
+ ],
87
+ });
88
+
89
+ const raw = resp.choices?.[0]?.message?.content;
90
+ if (!raw) {
91
+ const err: any = new Error("Empty response from style-sheet model");
92
+ err.code = "STYLE_SHEET_EMPTY";
93
+ throw err;
94
+ }
95
+
96
+ let parsed;
97
+ try {
98
+ parsed = JSON.parse(raw);
99
+ } catch {
100
+ const err: any = new Error("Style-sheet model returned non-JSON");
101
+ err.code = "STYLE_SHEET_PARSE";
102
+ throw err;
103
+ }
104
+
105
+ const sheet = coerceStyleSheet(parsed);
106
+ if (!sheet) {
107
+ const err: any = new Error("Style-sheet shape invalid");
108
+ err.code = "STYLE_SHEET_SHAPE";
109
+ throw err;
110
+ }
111
+ return sheet;
112
+ }
113
+
114
+ // Render a style sheet into a prompt preamble that gpt-image-1/2 can consume.
115
+ // Kept short so it doesn't blow the 4K prompt window on long user prompts.
116
+ export function renderStyleSheetPrefix(sheet) {
117
+ if (!sheet) return "";
118
+ const parts = [];
119
+ if (sheet.medium) parts.push(`Medium: ${sheet.medium}.`);
120
+ if (sheet.palette?.length) parts.push(`Palette: ${sheet.palette.join(", ")}.`);
121
+ if (sheet.composition) parts.push(`Composition: ${sheet.composition}.`);
122
+ if (sheet.mood) parts.push(`Mood: ${sheet.mood}.`);
123
+ if (sheet.subject_details) parts.push(`Subject: ${sheet.subject_details}.`);
124
+ if (sheet.negative?.length) parts.push(`Avoid: ${sheet.negative.join(", ")}.`);
125
+ return parts.join(" ");
126
+ }
127
+
128
+ export { STYLE_SHEET_MODEL, SYSTEM_PROMPT, coerceStyleSheet };
@@ -0,0 +1,18 @@
1
+ import { mkdir, rename } from "fs/promises";
2
+ import { basename, join } from "path";
3
+ import trash from "trash";
4
+ export async function moveToSystemTrash(paths) {
5
+ if (process.env.NODE_ENV === "test" && process.env.IMA2_TEST_SYSTEM_TRASH_DIR) {
6
+ if (process.env.IMA2_TEST_SYSTEM_TRASH_FAIL === "1") {
7
+ throw new Error("Simulated system trash failure");
8
+ }
9
+ const targetDir = process.env.IMA2_TEST_SYSTEM_TRASH_DIR;
10
+ await mkdir(targetDir, { recursive: true });
11
+ const prefix = `${Date.now()}_`;
12
+ for (const path of paths) {
13
+ await rename(path, join(targetDir, `${prefix}${basename(path)}`));
14
+ }
15
+ return;
16
+ }
17
+ await trash(paths, { glob: false });
18
+ }
@@ -0,0 +1,20 @@
1
+ import { mkdir, rename } from "fs/promises";
2
+ import { basename, join } from "path";
3
+ import trash from "trash";
4
+
5
+ export async function moveToSystemTrash(paths: string[]): Promise<void> {
6
+ if (process.env.NODE_ENV === "test" && process.env.IMA2_TEST_SYSTEM_TRASH_DIR) {
7
+ if (process.env.IMA2_TEST_SYSTEM_TRASH_FAIL === "1") {
8
+ throw new Error("Simulated system trash failure");
9
+ }
10
+ const targetDir = process.env.IMA2_TEST_SYSTEM_TRASH_DIR;
11
+ await mkdir(targetDir, { recursive: true });
12
+ const prefix = `${Date.now()}_`;
13
+ for (const path of paths) {
14
+ await rename(path, join(targetDir, `${prefix}${basename(path)}`));
15
+ }
16
+ return;
17
+ }
18
+
19
+ await trash(paths, { glob: false });
20
+ }