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
@@ -0,0 +1,54 @@
1
+ import { writeFile, access } from "fs/promises";
2
+ import { parseArgs } from "../lib/args.js";
3
+ import { resolveServer, request } from "../lib/client.js";
4
+ import { out, die, color, json, exitCodeForError } from "../lib/output.js";
5
+
6
+ const HELP = `
7
+ ima2 comfy <subcommand> [options]
8
+
9
+ Subcommands:
10
+ export <filename> [-o <out>] [--force]
11
+ `;
12
+
13
+ const FLAGS = {
14
+ json: { type: "boolean" },
15
+ server: { type: "string" },
16
+ out: { short: "o", type: "string" },
17
+ force: { type: "boolean" },
18
+ help: { short: "h", type: "boolean" },
19
+ };
20
+
21
+ async function exportSub(argv) {
22
+ const args = parseArgs(argv, { flags: FLAGS });
23
+ const filename = args.positional[0];
24
+ if (!filename) die(2, "filename required");
25
+ let server;
26
+ try { server = await resolveServer({ serverFlag: args.server }); }
27
+ catch (e: any) { die(exitCodeForError(e), e.message); throw e; }
28
+ const resp: any = await request(server.base, "/api/comfy/export-image", {
29
+ method: "POST",
30
+ body: { filename },
31
+ }).catch((e) => die(exitCodeForError(e), `${e.message}${e.code ? ` (${e.code})` : ""}`));
32
+ const target = args.out || `${filename}.workflow.json`;
33
+ if (!args.force) {
34
+ try {
35
+ await access(target);
36
+ die(2, `${target} already exists. Pass --force to overwrite.`);
37
+ } catch { /* file does not exist — proceed */ }
38
+ }
39
+ await writeFile(target, JSON.stringify(resp, null, 2));
40
+ if (args.json) { json({ path: target }); return; }
41
+ out(color.green("✓ ") + target);
42
+ }
43
+
44
+ const SUB: Record<string, (argv: any[]) => Promise<void>> = {
45
+ export: exportSub,
46
+ };
47
+
48
+ export default async function comfyCmd(argv) {
49
+ const sub = argv[0];
50
+ if (!sub || sub === "--help" || sub === "-h") { out(HELP); return; }
51
+ const handler = SUB[sub];
52
+ if (!handler) die(2, `unknown subcommand: ${sub}\n${HELP}`);
53
+ return handler(argv.slice(1));
54
+ }
@@ -0,0 +1,270 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
2
+ import { createInterface } from "readline/promises";
3
+ import { parseArgs } from "../lib/args.js";
4
+ import { out, die, color, json } from "../lib/output.js";
5
+ import { config as runtimeConfig } from "../../config.js";
6
+ const CONFIG_FILE = runtimeConfig.storage.configFile;
7
+ const CONFIG_DIR = runtimeConfig.storage.configDir;
8
+ const HELP = `
9
+ ima2 config <subcommand> [options]
10
+
11
+ Subcommands:
12
+ path Print config file path
13
+ ls [--effective] [--json] List file layer (or merged effective config with --effective)
14
+ get <key> [--json] Get a dotted key from effective config (redacts secrets)
15
+ set <key> <value> [-y] Write a key to the file layer
16
+ rm <key> Remove a key from the file layer
17
+
18
+ Keys use dot notation, e.g.: imageModels.default, log.level, features.cardNews
19
+
20
+ Options:
21
+ --effective Use effective (merged env+file+defaults) config for ls/get
22
+ --json Output raw JSON
23
+ -y, --yes Skip confirmation prompts
24
+ `;
25
+ const FLAGS = {
26
+ effective: { type: "boolean" },
27
+ json: { type: "boolean" },
28
+ yes: { short: "y", type: "boolean" },
29
+ help: { short: "h", type: "boolean" },
30
+ };
31
+ // Keys config set is allowed to write
32
+ const KNOWN_KEYS = new Set([
33
+ "imageModels.default",
34
+ "imageModels.reasoningEffort",
35
+ "log.level",
36
+ "features.cardNews",
37
+ "cardNewsPlanner.enabled",
38
+ "cardNewsPlanner.model",
39
+ "cardNewsPlanner.timeoutMs",
40
+ "cardNewsPlanner.deterministicFallback",
41
+ "comfy.defaultUrl",
42
+ "comfy.uploadTimeoutMs",
43
+ "comfy.maxUploadBytes",
44
+ "storage.generatedDir",
45
+ "storage.generatedDirName",
46
+ "server.port",
47
+ "server.host",
48
+ "server.bodyLimit",
49
+ "oauth.proxyPort",
50
+ "oauth.statusTimeoutMs",
51
+ "oauth.restartDelayMs",
52
+ "limits.maxRefCount",
53
+ "limits.maxParallel",
54
+ "history.defaultPageSize",
55
+ "history.maxPageCap",
56
+ ]);
57
+ // Auth keys live in the same file but must go through setup/login
58
+ const AUTH_KEYS = new Set(["provider", "apiKey"]);
59
+ const REDACT_PATTERN = /token|secret|apikey|password/i;
60
+ const ALWAYS_REDACT = new Set(["provider", "apiKey", "oauth.token", "oauth.refreshToken"]);
61
+ // Env var that overrides each writable key
62
+ const KEY_TO_ENV = {
63
+ "imageModels.default": "IMA2_IMAGE_MODEL_DEFAULT",
64
+ "imageModels.reasoningEffort": "IMA2_REASONING_EFFORT",
65
+ "log.level": "IMA2_LOG_LEVEL",
66
+ "features.cardNews": "IMA2_CARD_NEWS",
67
+ "server.port": "IMA2_PORT",
68
+ "server.host": "IMA2_HOST",
69
+ "server.bodyLimit": "IMA2_BODY_LIMIT",
70
+ "oauth.proxyPort": "IMA2_OAUTH_PROXY_PORT",
71
+ "storage.generatedDir": "IMA2_GENERATED_DIR",
72
+ "cardNewsPlanner.enabled": "IMA2_CARD_NEWS_PLANNER",
73
+ "cardNewsPlanner.model": "IMA2_CARD_NEWS_PLANNER_MODEL",
74
+ "cardNewsPlanner.timeoutMs": "IMA2_CARD_NEWS_PLANNER_TIMEOUT_MS",
75
+ "limits.maxParallel": "IMA2_MAX_PARALLEL",
76
+ "limits.maxRefCount": "IMA2_MAX_REF_COUNT",
77
+ "history.defaultPageSize": "IMA2_HISTORY_PAGE_SIZE",
78
+ };
79
+ function redactValue(key, value) {
80
+ if (ALWAYS_REDACT.has(key) || REDACT_PATTERN.test(key)) {
81
+ return value ? "<redacted>" : value;
82
+ }
83
+ return value;
84
+ }
85
+ function loadFileCfg() {
86
+ if (!existsSync(CONFIG_FILE))
87
+ return {};
88
+ try {
89
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
90
+ }
91
+ catch {
92
+ return {};
93
+ }
94
+ }
95
+ function saveFileCfg(cfg) {
96
+ mkdirSync(CONFIG_DIR, { recursive: true });
97
+ writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
98
+ }
99
+ function getNestedKey(obj, dotKey) {
100
+ const parts = dotKey.split(".");
101
+ let cur = obj;
102
+ for (const p of parts) {
103
+ if (cur == null || typeof cur !== "object")
104
+ return undefined;
105
+ cur = cur[p];
106
+ }
107
+ return cur;
108
+ }
109
+ function setNestedKey(obj, dotKey, value) {
110
+ const parts = dotKey.split(".");
111
+ let cur = obj;
112
+ for (let i = 0; i < parts.length - 1; i++) {
113
+ if (cur[parts[i]] == null || typeof cur[parts[i]] !== "object")
114
+ cur[parts[i]] = {};
115
+ cur = cur[parts[i]];
116
+ }
117
+ cur[parts[parts.length - 1]] = value;
118
+ }
119
+ function deleteNestedKey(obj, dotKey) {
120
+ const parts = dotKey.split(".");
121
+ let cur = obj;
122
+ for (let i = 0; i < parts.length - 1; i++) {
123
+ if (cur == null || typeof cur !== "object")
124
+ return false;
125
+ cur = cur[parts[i]];
126
+ }
127
+ if (cur == null || typeof cur !== "object")
128
+ return false;
129
+ const last = parts[parts.length - 1];
130
+ if (!(last in cur))
131
+ return false;
132
+ delete cur[last];
133
+ return true;
134
+ }
135
+ function stripSets(v) {
136
+ if (v instanceof Set)
137
+ return [...v];
138
+ if (Array.isArray(v))
139
+ return v.map(stripSets);
140
+ if (v && typeof v === "object") {
141
+ const r = {};
142
+ for (const [k, val] of Object.entries(v))
143
+ r[k] = stripSets(val);
144
+ return r;
145
+ }
146
+ return v;
147
+ }
148
+ function buildEffectiveConfig() {
149
+ return stripSets(runtimeConfig);
150
+ }
151
+ function displayPath(p) {
152
+ const home = process.env.HOME || "";
153
+ return home && p.startsWith(home) ? p.replace(home, "~") : p;
154
+ }
155
+ async function pathSub(_argv) {
156
+ out(CONFIG_FILE);
157
+ }
158
+ async function lsSub(argv) {
159
+ const args = parseArgs(argv, { flags: FLAGS });
160
+ if (args.effective) {
161
+ const eff = buildEffectiveConfig();
162
+ if (args.json) {
163
+ json(eff);
164
+ return;
165
+ }
166
+ out(JSON.stringify(eff, null, 2));
167
+ }
168
+ else {
169
+ const fileCfg = loadFileCfg();
170
+ if (args.json) {
171
+ json(fileCfg);
172
+ return;
173
+ }
174
+ out(JSON.stringify(fileCfg, null, 2));
175
+ }
176
+ }
177
+ async function getSub(argv) {
178
+ const args = parseArgs(argv, { flags: FLAGS });
179
+ const key = args.positional[0];
180
+ if (!key)
181
+ die(2, "key required. Usage: config get <dotted.key>");
182
+ const eff = buildEffectiveConfig();
183
+ const raw = getNestedKey(eff, key);
184
+ const value = redactValue(key, raw);
185
+ if (args.json) {
186
+ json({ key, value });
187
+ return;
188
+ }
189
+ if (value === undefined) {
190
+ out(color.dim(`(key not found: ${key})`));
191
+ }
192
+ else {
193
+ out(typeof value === "object" ? JSON.stringify(value, null, 2) : String(value));
194
+ }
195
+ }
196
+ async function setSub(argv) {
197
+ const args = parseArgs(argv, { flags: FLAGS });
198
+ const [key, rawValue] = args.positional;
199
+ if (!key || rawValue === undefined)
200
+ die(2, "usage: config set <key> <value>");
201
+ if (AUTH_KEYS.has(key)) {
202
+ die(2, `"${key}" is an auth key. Use 'ima2 setup' or 'ima2 login' to change authentication.`);
203
+ }
204
+ if (!KNOWN_KEYS.has(key)) {
205
+ die(2, `unknown config key: "${key}". Run 'ima2 config ls --effective' to see the config structure.`);
206
+ }
207
+ // Parse value: try JSON, fall back to raw string
208
+ let value = rawValue;
209
+ try {
210
+ value = JSON.parse(rawValue);
211
+ }
212
+ catch { }
213
+ // Warn if env var is overriding this key
214
+ const envVar = KEY_TO_ENV[key];
215
+ if (envVar && process.env[envVar] !== undefined) {
216
+ out(color.yellow(`warning: env ${envVar}=${process.env[envVar]} is currently overriding this value.`));
217
+ out(`The file change will only apply after unsetting the env var and restarting the server.`);
218
+ }
219
+ // Confirm if writing a sensitive key
220
+ if ((ALWAYS_REDACT.has(key) || REDACT_PATTERN.test(key)) && !args.yes) {
221
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
222
+ const ans = await rl.question(`warning: "${key}" is a sensitive credential. Write to config file? [y/N] `);
223
+ rl.close();
224
+ if (!ans.trim().toLowerCase().startsWith("y")) {
225
+ out("Aborted.");
226
+ process.exit(0);
227
+ }
228
+ }
229
+ const fileCfg = loadFileCfg();
230
+ setNestedKey(fileCfg, key, value);
231
+ saveFileCfg(fileCfg);
232
+ out(color.green("✓ ") + `wrote ${key}=${JSON.stringify(value)} to ${displayPath(CONFIG_FILE)}`);
233
+ out(color.dim("note: server must be restarted to pick up config changes (run `ima2 serve`)"));
234
+ }
235
+ async function rmSub(argv) {
236
+ const args = parseArgs(argv, { flags: FLAGS });
237
+ const key = args.positional[0];
238
+ if (!key)
239
+ die(2, "key required. Usage: config rm <key>");
240
+ if (AUTH_KEYS.has(key)) {
241
+ die(2, `"${key}" is an auth key. Use 'ima2 setup' or 'ima2 login' to change authentication.`);
242
+ }
243
+ const fileCfg = loadFileCfg();
244
+ const removed = deleteNestedKey(fileCfg, key);
245
+ if (!removed) {
246
+ out(color.dim(`(key not found in file layer: ${key})`));
247
+ return;
248
+ }
249
+ saveFileCfg(fileCfg);
250
+ out(color.green("✓ ") + `removed ${key} from ${displayPath(CONFIG_FILE)}`);
251
+ out(color.dim("note: server must be restarted to pick up config changes (run `ima2 serve`)"));
252
+ }
253
+ const SUB = {
254
+ path: pathSub,
255
+ ls: lsSub,
256
+ get: getSub,
257
+ set: setSub,
258
+ rm: rmSub,
259
+ };
260
+ export default async function configCmd(argv) {
261
+ const sub = argv[0];
262
+ if (!sub || sub === "--help" || sub === "-h") {
263
+ out(HELP);
264
+ return;
265
+ }
266
+ const handler = SUB[sub];
267
+ if (!handler)
268
+ die(2, `unknown subcommand: ${sub}\n${HELP}`);
269
+ return handler(argv.slice(1));
270
+ }
@@ -0,0 +1,265 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
2
+ import { createInterface } from "readline/promises";
3
+ import { parseArgs } from "../lib/args.js";
4
+ import { out, die, color, json } from "../lib/output.js";
5
+ import { config as runtimeConfig } from "../../config.js";
6
+
7
+ const CONFIG_FILE = runtimeConfig.storage.configFile;
8
+ const CONFIG_DIR = runtimeConfig.storage.configDir;
9
+
10
+ const HELP = `
11
+ ima2 config <subcommand> [options]
12
+
13
+ Subcommands:
14
+ path Print config file path
15
+ ls [--effective] [--json] List file layer (or merged effective config with --effective)
16
+ get <key> [--json] Get a dotted key from effective config (redacts secrets)
17
+ set <key> <value> [-y] Write a key to the file layer
18
+ rm <key> Remove a key from the file layer
19
+
20
+ Keys use dot notation, e.g.: imageModels.default, log.level, features.cardNews
21
+
22
+ Options:
23
+ --effective Use effective (merged env+file+defaults) config for ls/get
24
+ --json Output raw JSON
25
+ -y, --yes Skip confirmation prompts
26
+ `;
27
+
28
+ const FLAGS = {
29
+ effective: { type: "boolean" },
30
+ json: { type: "boolean" },
31
+ yes: { short: "y", type: "boolean" },
32
+ help: { short: "h", type: "boolean" },
33
+ };
34
+
35
+ // Keys config set is allowed to write
36
+ const KNOWN_KEYS = new Set([
37
+ "imageModels.default",
38
+ "imageModels.reasoningEffort",
39
+ "log.level",
40
+ "features.cardNews",
41
+ "cardNewsPlanner.enabled",
42
+ "cardNewsPlanner.model",
43
+ "cardNewsPlanner.timeoutMs",
44
+ "cardNewsPlanner.deterministicFallback",
45
+ "comfy.defaultUrl",
46
+ "comfy.uploadTimeoutMs",
47
+ "comfy.maxUploadBytes",
48
+ "storage.generatedDir",
49
+ "storage.generatedDirName",
50
+ "server.port",
51
+ "server.host",
52
+ "server.bodyLimit",
53
+ "oauth.proxyPort",
54
+ "oauth.statusTimeoutMs",
55
+ "oauth.restartDelayMs",
56
+ "limits.maxRefCount",
57
+ "limits.maxParallel",
58
+ "history.defaultPageSize",
59
+ "history.maxPageCap",
60
+ ]);
61
+
62
+ // Auth keys live in the same file but must go through setup/login
63
+ const AUTH_KEYS = new Set(["provider", "apiKey"]);
64
+
65
+ const REDACT_PATTERN = /token|secret|apikey|password/i;
66
+ const ALWAYS_REDACT = new Set(["provider", "apiKey", "oauth.token", "oauth.refreshToken"]);
67
+
68
+ // Env var that overrides each writable key
69
+ const KEY_TO_ENV: Record<string, string> = {
70
+ "imageModels.default": "IMA2_IMAGE_MODEL_DEFAULT",
71
+ "imageModels.reasoningEffort": "IMA2_REASONING_EFFORT",
72
+ "log.level": "IMA2_LOG_LEVEL",
73
+ "features.cardNews": "IMA2_CARD_NEWS",
74
+ "server.port": "IMA2_PORT",
75
+ "server.host": "IMA2_HOST",
76
+ "server.bodyLimit": "IMA2_BODY_LIMIT",
77
+ "oauth.proxyPort": "IMA2_OAUTH_PROXY_PORT",
78
+ "storage.generatedDir": "IMA2_GENERATED_DIR",
79
+ "cardNewsPlanner.enabled": "IMA2_CARD_NEWS_PLANNER",
80
+ "cardNewsPlanner.model": "IMA2_CARD_NEWS_PLANNER_MODEL",
81
+ "cardNewsPlanner.timeoutMs": "IMA2_CARD_NEWS_PLANNER_TIMEOUT_MS",
82
+ "limits.maxParallel": "IMA2_MAX_PARALLEL",
83
+ "limits.maxRefCount": "IMA2_MAX_REF_COUNT",
84
+ "history.defaultPageSize": "IMA2_HISTORY_PAGE_SIZE",
85
+ };
86
+
87
+ function redactValue(key: string, value: any): any {
88
+ if (ALWAYS_REDACT.has(key) || REDACT_PATTERN.test(key)) {
89
+ return value ? "<redacted>" : value;
90
+ }
91
+ return value;
92
+ }
93
+
94
+ function loadFileCfg(): Record<string, any> {
95
+ if (!existsSync(CONFIG_FILE)) return {};
96
+ try { return JSON.parse(readFileSync(CONFIG_FILE, "utf-8")); }
97
+ catch { return {}; }
98
+ }
99
+
100
+ function saveFileCfg(cfg: Record<string, any>) {
101
+ mkdirSync(CONFIG_DIR, { recursive: true });
102
+ writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
103
+ }
104
+
105
+ function getNestedKey(obj: any, dotKey: string): any {
106
+ const parts = dotKey.split(".");
107
+ let cur = obj;
108
+ for (const p of parts) {
109
+ if (cur == null || typeof cur !== "object") return undefined;
110
+ cur = cur[p];
111
+ }
112
+ return cur;
113
+ }
114
+
115
+ function setNestedKey(obj: any, dotKey: string, value: any): void {
116
+ const parts = dotKey.split(".");
117
+ let cur = obj;
118
+ for (let i = 0; i < parts.length - 1; i++) {
119
+ if (cur[parts[i]] == null || typeof cur[parts[i]] !== "object") cur[parts[i]] = {};
120
+ cur = cur[parts[i]];
121
+ }
122
+ cur[parts[parts.length - 1]] = value;
123
+ }
124
+
125
+ function deleteNestedKey(obj: any, dotKey: string): boolean {
126
+ const parts = dotKey.split(".");
127
+ let cur = obj;
128
+ for (let i = 0; i < parts.length - 1; i++) {
129
+ if (cur == null || typeof cur !== "object") return false;
130
+ cur = cur[parts[i]];
131
+ }
132
+ if (cur == null || typeof cur !== "object") return false;
133
+ const last = parts[parts.length - 1];
134
+ if (!(last in cur)) return false;
135
+ delete cur[last];
136
+ return true;
137
+ }
138
+
139
+ function stripSets(v: any): any {
140
+ if (v instanceof Set) return [...v];
141
+ if (Array.isArray(v)) return v.map(stripSets);
142
+ if (v && typeof v === "object") {
143
+ const r: any = {};
144
+ for (const [k, val] of Object.entries(v)) r[k] = stripSets(val);
145
+ return r;
146
+ }
147
+ return v;
148
+ }
149
+
150
+ function buildEffectiveConfig(): Record<string, any> {
151
+ return stripSets(runtimeConfig);
152
+ }
153
+
154
+ function displayPath(p: string): string {
155
+ const home = process.env.HOME || "";
156
+ return home && p.startsWith(home) ? p.replace(home, "~") : p;
157
+ }
158
+
159
+ async function pathSub(_argv) {
160
+ out(CONFIG_FILE);
161
+ }
162
+
163
+ async function lsSub(argv) {
164
+ const args = parseArgs(argv, { flags: FLAGS });
165
+ if (args.effective) {
166
+ const eff = buildEffectiveConfig();
167
+ if (args.json) { json(eff); return; }
168
+ out(JSON.stringify(eff, null, 2));
169
+ } else {
170
+ const fileCfg = loadFileCfg();
171
+ if (args.json) { json(fileCfg); return; }
172
+ out(JSON.stringify(fileCfg, null, 2));
173
+ }
174
+ }
175
+
176
+ async function getSub(argv) {
177
+ const args = parseArgs(argv, { flags: FLAGS });
178
+ const key = args.positional[0];
179
+ if (!key) die(2, "key required. Usage: config get <dotted.key>");
180
+ const eff = buildEffectiveConfig();
181
+ const raw = getNestedKey(eff, key);
182
+ const value = redactValue(key, raw);
183
+ if (args.json) { json({ key, value }); return; }
184
+ if (value === undefined) {
185
+ out(color.dim(`(key not found: ${key})`));
186
+ } else {
187
+ out(typeof value === "object" ? JSON.stringify(value, null, 2) : String(value));
188
+ }
189
+ }
190
+
191
+ async function setSub(argv) {
192
+ const args = parseArgs(argv, { flags: FLAGS });
193
+ const [key, rawValue] = args.positional;
194
+ if (!key || rawValue === undefined) die(2, "usage: config set <key> <value>");
195
+
196
+ if (AUTH_KEYS.has(key)) {
197
+ die(2, `"${key}" is an auth key. Use 'ima2 setup' or 'ima2 login' to change authentication.`);
198
+ }
199
+ if (!KNOWN_KEYS.has(key)) {
200
+ die(2, `unknown config key: "${key}". Run 'ima2 config ls --effective' to see the config structure.`);
201
+ }
202
+
203
+ // Parse value: try JSON, fall back to raw string
204
+ let value: any = rawValue;
205
+ try { value = JSON.parse(rawValue); } catch {}
206
+
207
+ // Warn if env var is overriding this key
208
+ const envVar = KEY_TO_ENV[key];
209
+ if (envVar && process.env[envVar] !== undefined) {
210
+ out(color.yellow(`warning: env ${envVar}=${process.env[envVar]} is currently overriding this value.`));
211
+ out(`The file change will only apply after unsetting the env var and restarting the server.`);
212
+ }
213
+
214
+ // Confirm if writing a sensitive key
215
+ if ((ALWAYS_REDACT.has(key) || REDACT_PATTERN.test(key)) && !args.yes) {
216
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
217
+ const ans = await rl.question(`warning: "${key}" is a sensitive credential. Write to config file? [y/N] `);
218
+ rl.close();
219
+ if (!ans.trim().toLowerCase().startsWith("y")) { out("Aborted."); process.exit(0); }
220
+ }
221
+
222
+ const fileCfg = loadFileCfg();
223
+ setNestedKey(fileCfg, key, value);
224
+ saveFileCfg(fileCfg);
225
+
226
+ out(color.green("✓ ") + `wrote ${key}=${JSON.stringify(value)} to ${displayPath(CONFIG_FILE)}`);
227
+ out(color.dim("note: server must be restarted to pick up config changes (run `ima2 serve`)"));
228
+ }
229
+
230
+ async function rmSub(argv) {
231
+ const args = parseArgs(argv, { flags: FLAGS });
232
+ const key = args.positional[0];
233
+ if (!key) die(2, "key required. Usage: config rm <key>");
234
+
235
+ if (AUTH_KEYS.has(key)) {
236
+ die(2, `"${key}" is an auth key. Use 'ima2 setup' or 'ima2 login' to change authentication.`);
237
+ }
238
+
239
+ const fileCfg = loadFileCfg();
240
+ const removed = deleteNestedKey(fileCfg, key);
241
+ if (!removed) {
242
+ out(color.dim(`(key not found in file layer: ${key})`));
243
+ return;
244
+ }
245
+ saveFileCfg(fileCfg);
246
+ out(color.green("✓ ") + `removed ${key} from ${displayPath(CONFIG_FILE)}`);
247
+ out(color.dim("note: server must be restarted to pick up config changes (run `ima2 serve`)"));
248
+ }
249
+
250
+ type Sub = (argv: any[]) => Promise<void>;
251
+ const SUB: Record<string, Sub> = {
252
+ path: pathSub,
253
+ ls: lsSub,
254
+ get: getSub,
255
+ set: setSub,
256
+ rm: rmSub,
257
+ };
258
+
259
+ export default async function configCmd(argv) {
260
+ const sub = argv[0];
261
+ if (!sub || sub === "--help" || sub === "-h") { out(HELP); return; }
262
+ const handler = SUB[sub];
263
+ if (!handler) die(2, `unknown subcommand: ${sub}\n${HELP}`);
264
+ return handler(argv.slice(1));
265
+ }