ima2-gen 1.1.10 → 1.1.11
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.
- package/README.md +6 -1
- package/bin/commands/capabilities.js +90 -0
- package/bin/commands/capabilities.ts +93 -0
- package/bin/commands/config.js +11 -143
- package/bin/commands/config.ts +27 -141
- package/bin/commands/defaults.js +180 -0
- package/bin/commands/defaults.ts +192 -0
- package/bin/commands/edit.js +35 -2
- package/bin/commands/edit.ts +33 -2
- package/bin/commands/gen.js +42 -4
- package/bin/commands/gen.ts +39 -4
- package/bin/commands/ls.js +3 -1
- package/bin/commands/ls.ts +2 -1
- package/bin/commands/multimode.js +62 -5
- package/bin/commands/multimode.ts +56 -5
- package/bin/commands/node.js +11 -1
- package/bin/commands/node.ts +10 -1
- package/bin/commands/observability.js +1 -1
- package/bin/commands/observability.ts +1 -1
- package/bin/commands/ps.js +1 -1
- package/bin/commands/ps.ts +1 -1
- package/bin/commands/skill.js +62 -0
- package/bin/commands/skill.ts +70 -0
- package/bin/ima2.js +10 -1
- package/bin/ima2.ts +10 -1
- package/bin/lib/config-store.js +156 -0
- package/bin/lib/config-store.ts +163 -0
- package/bin/lib/recover-output.js +104 -0
- package/bin/lib/recover-output.ts +139 -0
- package/docs/CLI.md +64 -8
- package/docs/migration/runtime-test-inventory.md +12 -1
- package/lib/canvasVersionStore.js +2 -0
- package/lib/canvasVersionStore.ts +2 -0
- package/lib/capabilities.js +70 -0
- package/lib/capabilities.ts +84 -0
- package/lib/cardNewsGenerator.js +6 -0
- package/lib/cardNewsGenerator.ts +6 -0
- package/lib/cardNewsPlannerPrompt.js +2 -0
- package/lib/cardNewsPlannerPrompt.ts +2 -0
- package/lib/generationCancel.js +18 -0
- package/lib/generationCancel.ts +28 -0
- package/lib/historyIndex.js +34 -0
- package/lib/historyIndex.ts +51 -0
- package/lib/inflight.js +31 -0
- package/lib/inflight.ts +34 -0
- package/lib/localImportStore.js +2 -0
- package/lib/localImportStore.ts +2 -0
- package/lib/nodeStore.js +2 -0
- package/lib/nodeStore.ts +2 -0
- package/lib/oauthProxy/prompts.js +17 -2
- package/lib/oauthProxy/prompts.ts +17 -2
- package/lib/openDirectory.js +1 -1
- package/lib/openDirectory.ts +1 -1
- package/lib/responsesImageAdapter.js +39 -8
- package/lib/responsesImageAdapter.ts +62 -7
- package/lib/visibleTextLanguagePolicy.js +7 -0
- package/lib/visibleTextLanguagePolicy.ts +7 -0
- package/package.json +3 -2
- package/routes/capabilities.js +13 -0
- package/routes/capabilities.ts +18 -0
- package/routes/edit.js +29 -2
- package/routes/edit.ts +33 -2
- package/routes/generate.js +38 -2
- package/routes/generate.ts +42 -2
- package/routes/health.js +2 -3
- package/routes/health.ts +2 -3
- package/routes/history.js +56 -22
- package/routes/history.ts +71 -21
- package/routes/index.js +2 -0
- package/routes/index.ts +2 -0
- package/routes/multimode.js +144 -27
- package/routes/multimode.ts +187 -50
- package/routes/nodes.js +25 -6
- package/routes/nodes.ts +35 -6
- package/skills/ima2/SKILL.md +206 -0
- package/ui/dist/.vite/manifest.json +10 -10
- package/ui/dist/assets/{CardNewsWorkspace-C9Cpxuxc.js → CardNewsWorkspace-j4ULtNdk.js} +1 -1
- package/ui/dist/assets/NodeCanvas-Bc7BUViM.js +7 -0
- package/ui/dist/assets/{PromptImportDialog-D8EMO--u.js → PromptImportDialog-DBKprBEo.js} +2 -2
- package/ui/dist/assets/{PromptImportDiscoverySection-BB2FrKuq.js → PromptImportDiscoverySection-m5v55Zsy.js} +1 -1
- package/ui/dist/assets/PromptImportFolderSection-DnPvJkfJ.js +1 -0
- package/ui/dist/assets/{PromptLibraryPanel-Z-4B8RSs.js → PromptLibraryPanel-BMSqfK9C.js} +2 -2
- package/ui/dist/assets/{SettingsWorkspace-DBYdgpPI.js → SettingsWorkspace-Cj3LD0uu.js} +1 -1
- package/ui/dist/assets/{index-Deo5wBiA.js → index-9aOJKFI-.js} +1 -1
- package/ui/dist/assets/index-De-AWE6B.css +1 -0
- package/ui/dist/assets/index-tQhOLR-C.js +28 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/NodeCanvas-BllpfcQW.js +0 -7
- package/ui/dist/assets/PromptImportFolderSection-aVteBUcb.js +0 -1
- package/ui/dist/assets/index-B2TDuGqy.css +0 -1
- package/ui/dist/assets/index-DFlbOIxI.js +0 -25
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ persists, reboot and run the update before starting ima2 again.
|
|
|
53
53
|
- **Node mode**: branch a good image into multiple directions without losing the original.
|
|
54
54
|
- **Multimode batches**: launch several Classic outputs from one prompt, watch slot-by-slot progress, and continue from the best result.
|
|
55
55
|
- **Canvas Mode**: zoom, pan, annotate, erase, clean backgrounds, keep transparent previews, and export either alpha or matte-backed versions.
|
|
56
|
-
- **Local gallery**: keep generated assets on your machine with session-aware history.
|
|
56
|
+
- **Local gallery**: keep generated assets on your machine with session-aware history. By default the gallery shows the current session and an All Images toggle reveals the full history; the default scope is sticky across sessions.
|
|
57
57
|
- **Reference images**: drag, drop, paste, and attach up to 5 references; large images are compressed before upload.
|
|
58
58
|
- **Prompt library imports**: import local prompt packs, GitHub folders, and curated GPT-image prompt hints into the built-in prompt library.
|
|
59
59
|
- **Mobile shell**: use the app bar, compose sheet, and compact settings toggle on smaller screens.
|
|
@@ -196,6 +196,11 @@ environment variables > ~/.ima2/config.json > built-in defaults
|
|
|
196
196
|
| `IMA2_LOG_LEVEL` | `warn` | Normal serve defaults to `warn`; dev mode defaults to `debug`; supports `debug`, `info`, `warn`, `error`, or `silent` |
|
|
197
197
|
| `IMA2_INFLIGHT_TERMINAL_TTL_MS` | `30000` | Recent terminal job retention for debug views |
|
|
198
198
|
| `OPENAI_API_KEY` | — | API key for the `provider: "api"` Responses API image path and auxiliary API-key features |
|
|
199
|
+
| `IMA2_API_IMAGE_MODEL_DEFAULT` | `gpt-5.4-mini` | Default image model for `provider: "api"` |
|
|
200
|
+
| `IMA2_API_REASONING_EFFORT` | `low` | Default reasoning effort for `provider: "api"` |
|
|
201
|
+
| `IMA2_API_IMAGE_SIZE` | `1024x1024` | Default size for `provider: "api"` |
|
|
202
|
+
| `IMA2_API_ALLOW_WEB_SEARCH` | `true` | Toggle web search for `provider: "api"` |
|
|
203
|
+
| `IMA2_OAUTH_MASKED_EDIT_ENABLED` | `false` | Opt-in feature flag for masked-edit requests on the OAuth path (#31, groundwork only) |
|
|
199
204
|
|
|
200
205
|
### Logging modes
|
|
201
206
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { config } from "../../config.js";
|
|
5
|
+
import { buildIma2Capabilities } from "../../lib/capabilities.js";
|
|
6
|
+
import { parseArgs } from "../lib/args.js";
|
|
7
|
+
import { resolveServer, request } from "../lib/client.js";
|
|
8
|
+
import { color, dieWithError, json, out } from "../lib/output.js";
|
|
9
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
10
|
+
const PACKAGE_PATH = join(ROOT, "package.json");
|
|
11
|
+
const HELP = `
|
|
12
|
+
ima2 capabilities [--json] [--server <url>] [--require-server]
|
|
13
|
+
|
|
14
|
+
Print agent-friendly capability metadata.
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--json Print JSON
|
|
18
|
+
--server <url> Query a specific running server
|
|
19
|
+
--require-server Fail instead of falling back to local package metadata
|
|
20
|
+
`;
|
|
21
|
+
const FLAGS = {
|
|
22
|
+
json: { type: "boolean" },
|
|
23
|
+
server: { type: "string" },
|
|
24
|
+
"require-server": { type: "boolean" },
|
|
25
|
+
help: { short: "h", type: "boolean" },
|
|
26
|
+
};
|
|
27
|
+
function packageVersion() {
|
|
28
|
+
try {
|
|
29
|
+
const pkg = JSON.parse(readFileSync(PACKAGE_PATH, "utf-8"));
|
|
30
|
+
return pkg.version || "?";
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return "?";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function localCapabilities() {
|
|
37
|
+
return buildIma2Capabilities({
|
|
38
|
+
appConfig: config,
|
|
39
|
+
packageVersion: packageVersion(),
|
|
40
|
+
source: "local",
|
|
41
|
+
server: null,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async function readCapabilities(args) {
|
|
45
|
+
try {
|
|
46
|
+
const server = await resolveServer({ serverFlag: args.server });
|
|
47
|
+
return await request(server.base, "/api/capabilities", { timeoutMs: 5000 });
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (args.server || args["require-server"])
|
|
51
|
+
throw error;
|
|
52
|
+
return localCapabilities();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function printText(capabilities) {
|
|
56
|
+
out(`ima2 capabilities (${capabilities.source})`);
|
|
57
|
+
out(`version: ${capabilities.version}`);
|
|
58
|
+
out(`server: ${capabilities.server || "none"}`);
|
|
59
|
+
out("");
|
|
60
|
+
out("defaults:");
|
|
61
|
+
out(` oauth model: ${capabilities.defaults?.oauth?.model}`);
|
|
62
|
+
out(` oauth reasoning: ${capabilities.defaults?.oauth?.reasoningEffort}`);
|
|
63
|
+
out(` api model: ${capabilities.defaults?.api?.model}`);
|
|
64
|
+
out(` api reasoning: ${capabilities.defaults?.api?.reasoningEffort}`);
|
|
65
|
+
out("");
|
|
66
|
+
out("valid:");
|
|
67
|
+
out(` models: ${capabilities.valid?.imageModels?.supported?.join(", ")}`);
|
|
68
|
+
out(` reasoning: ${capabilities.valid?.reasoningEfforts?.join(", ")}`);
|
|
69
|
+
out(` quality: ${capabilities.valid?.quality?.join(", ")}`);
|
|
70
|
+
out("");
|
|
71
|
+
out(`limits: refs=${capabilities.limits?.maxRefCount}, images=${capabilities.limits?.maxGeneratedImages}`);
|
|
72
|
+
out(color.dim(`maxParallel: ${capabilities.limits?.maxParallel?.value} (${capabilities.limits?.maxParallel?.note})`));
|
|
73
|
+
}
|
|
74
|
+
export default async function capabilitiesCmd(argv) {
|
|
75
|
+
const args = parseArgs(argv, { flags: FLAGS });
|
|
76
|
+
if (args.help) {
|
|
77
|
+
out(HELP);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const capabilities = await readCapabilities(args);
|
|
82
|
+
if (args.json)
|
|
83
|
+
json(capabilities);
|
|
84
|
+
else
|
|
85
|
+
printText(capabilities);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
dieWithError(error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { config } from "../../config.js";
|
|
5
|
+
import { buildIma2Capabilities } from "../../lib/capabilities.js";
|
|
6
|
+
import { parseArgs } from "../lib/args.js";
|
|
7
|
+
import { resolveServer, request } from "../lib/client.js";
|
|
8
|
+
import { color, dieWithError, json, out } from "../lib/output.js";
|
|
9
|
+
|
|
10
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
11
|
+
const PACKAGE_PATH = join(ROOT, "package.json");
|
|
12
|
+
|
|
13
|
+
const HELP = `
|
|
14
|
+
ima2 capabilities [--json] [--server <url>] [--require-server]
|
|
15
|
+
|
|
16
|
+
Print agent-friendly capability metadata.
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--json Print JSON
|
|
20
|
+
--server <url> Query a specific running server
|
|
21
|
+
--require-server Fail instead of falling back to local package metadata
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const FLAGS = {
|
|
25
|
+
json: { type: "boolean" },
|
|
26
|
+
server: { type: "string" },
|
|
27
|
+
"require-server": { type: "boolean" },
|
|
28
|
+
help: { short: "h", type: "boolean" },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function packageVersion(): string {
|
|
32
|
+
try {
|
|
33
|
+
const pkg = JSON.parse(readFileSync(PACKAGE_PATH, "utf-8")) as { version?: string };
|
|
34
|
+
return pkg.version || "?";
|
|
35
|
+
} catch {
|
|
36
|
+
return "?";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function localCapabilities() {
|
|
41
|
+
return buildIma2Capabilities({
|
|
42
|
+
appConfig: config,
|
|
43
|
+
packageVersion: packageVersion(),
|
|
44
|
+
source: "local",
|
|
45
|
+
server: null,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function readCapabilities(args: ReturnType<typeof parseArgs>) {
|
|
50
|
+
try {
|
|
51
|
+
const server = await resolveServer({ serverFlag: args.server });
|
|
52
|
+
return await request(server.base, "/api/capabilities", { timeoutMs: 5000 });
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (args.server || args["require-server"]) throw error;
|
|
55
|
+
return localCapabilities();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function printText(capabilities: any): void {
|
|
60
|
+
out(`ima2 capabilities (${capabilities.source})`);
|
|
61
|
+
out(`version: ${capabilities.version}`);
|
|
62
|
+
out(`server: ${capabilities.server || "none"}`);
|
|
63
|
+
out("");
|
|
64
|
+
out("defaults:");
|
|
65
|
+
out(` oauth model: ${capabilities.defaults?.oauth?.model}`);
|
|
66
|
+
out(` oauth reasoning: ${capabilities.defaults?.oauth?.reasoningEffort}`);
|
|
67
|
+
out(` api model: ${capabilities.defaults?.api?.model}`);
|
|
68
|
+
out(` api reasoning: ${capabilities.defaults?.api?.reasoningEffort}`);
|
|
69
|
+
out("");
|
|
70
|
+
out("valid:");
|
|
71
|
+
out(` models: ${capabilities.valid?.imageModels?.supported?.join(", ")}`);
|
|
72
|
+
out(` reasoning: ${capabilities.valid?.reasoningEfforts?.join(", ")}`);
|
|
73
|
+
out(` quality: ${capabilities.valid?.quality?.join(", ")}`);
|
|
74
|
+
out("");
|
|
75
|
+
out(`limits: refs=${capabilities.limits?.maxRefCount}, images=${capabilities.limits?.maxGeneratedImages}`);
|
|
76
|
+
out(color.dim(`maxParallel: ${capabilities.limits?.maxParallel?.value} (${capabilities.limits?.maxParallel?.note})`));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default async function capabilitiesCmd(argv: string[]) {
|
|
80
|
+
const args = parseArgs(argv, { flags: FLAGS });
|
|
81
|
+
if (args.help) {
|
|
82
|
+
out(HELP);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const capabilities = await readCapabilities(args);
|
|
88
|
+
if (args.json) json(capabilities);
|
|
89
|
+
else printText(capabilities);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
dieWithError(error);
|
|
92
|
+
}
|
|
93
|
+
}
|
package/bin/commands/config.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
2
1
|
import { createInterface } from "readline/promises";
|
|
3
2
|
import { parseArgs } from "../lib/args.js";
|
|
4
3
|
import { out, die, color, json } from "../lib/output.js";
|
|
5
|
-
import {
|
|
6
|
-
const CONFIG_FILE = runtimeConfig.storage.configFile;
|
|
7
|
-
const CONFIG_DIR = runtimeConfig.storage.configDir;
|
|
4
|
+
import { CONFIG_FILE, buildEffectiveConfig, deleteNestedKey, displayPath, envOverrideForKey, getNestedKey, isAuthConfigKey, isSensitiveConfigKey, isWritableConfigKey, loadFileCfg, parseConfigValue, redactValue, restartNotice, saveFileCfg, setNestedKey, } from "../lib/config-store.js";
|
|
8
5
|
const HELP = `
|
|
9
6
|
ima2 config <subcommand> [options]
|
|
10
7
|
|
|
@@ -28,130 +25,6 @@ const FLAGS = {
|
|
|
28
25
|
yes: { short: "y", type: "boolean" },
|
|
29
26
|
help: { short: "h", type: "boolean" },
|
|
30
27
|
};
|
|
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
28
|
async function pathSub(_argv) {
|
|
156
29
|
out(CONFIG_FILE);
|
|
157
30
|
}
|
|
@@ -198,26 +71,21 @@ async function setSub(argv) {
|
|
|
198
71
|
const [key, rawValue] = args.positional;
|
|
199
72
|
if (!key || rawValue === undefined)
|
|
200
73
|
die(2, "usage: config set <key> <value>");
|
|
201
|
-
if (
|
|
74
|
+
if (isAuthConfigKey(key)) {
|
|
202
75
|
die(2, `"${key}" is an auth key. Use 'ima2 setup' or 'ima2 login' to change authentication.`);
|
|
203
76
|
}
|
|
204
|
-
if (!
|
|
77
|
+
if (!isWritableConfigKey(key)) {
|
|
205
78
|
die(2, `unknown config key: "${key}". Run 'ima2 config ls --effective' to see the config structure.`);
|
|
206
79
|
}
|
|
207
|
-
|
|
208
|
-
let value = rawValue;
|
|
209
|
-
try {
|
|
210
|
-
value = JSON.parse(rawValue);
|
|
211
|
-
}
|
|
212
|
-
catch { }
|
|
80
|
+
const value = parseConfigValue(rawValue);
|
|
213
81
|
// Warn if env var is overriding this key
|
|
214
|
-
const
|
|
215
|
-
if (
|
|
216
|
-
out(color.yellow(`warning: env ${envVar}=${
|
|
82
|
+
const override = envOverrideForKey(key);
|
|
83
|
+
if (override) {
|
|
84
|
+
out(color.yellow(`warning: env ${override.envVar}=${override.value} is currently overriding this value.`));
|
|
217
85
|
out(`The file change will only apply after unsetting the env var and restarting the server.`);
|
|
218
86
|
}
|
|
219
87
|
// Confirm if writing a sensitive key
|
|
220
|
-
if ((
|
|
88
|
+
if (isSensitiveConfigKey(key) && !args.yes) {
|
|
221
89
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
222
90
|
const ans = await rl.question(`warning: "${key}" is a sensitive credential. Write to config file? [y/N] `);
|
|
223
91
|
rl.close();
|
|
@@ -230,14 +98,14 @@ async function setSub(argv) {
|
|
|
230
98
|
setNestedKey(fileCfg, key, value);
|
|
231
99
|
saveFileCfg(fileCfg);
|
|
232
100
|
out(color.green("✓ ") + `wrote ${key}=${JSON.stringify(value)} to ${displayPath(CONFIG_FILE)}`);
|
|
233
|
-
out(color.dim(
|
|
101
|
+
out(color.dim(restartNotice()));
|
|
234
102
|
}
|
|
235
103
|
async function rmSub(argv) {
|
|
236
104
|
const args = parseArgs(argv, { flags: FLAGS });
|
|
237
105
|
const key = args.positional[0];
|
|
238
106
|
if (!key)
|
|
239
107
|
die(2, "key required. Usage: config rm <key>");
|
|
240
|
-
if (
|
|
108
|
+
if (isAuthConfigKey(key)) {
|
|
241
109
|
die(2, `"${key}" is an auth key. Use 'ima2 setup' or 'ima2 login' to change authentication.`);
|
|
242
110
|
}
|
|
243
111
|
const fileCfg = loadFileCfg();
|
|
@@ -248,7 +116,7 @@ async function rmSub(argv) {
|
|
|
248
116
|
}
|
|
249
117
|
saveFileCfg(fileCfg);
|
|
250
118
|
out(color.green("✓ ") + `removed ${key} from ${displayPath(CONFIG_FILE)}`);
|
|
251
|
-
out(color.dim(
|
|
119
|
+
out(color.dim(restartNotice()));
|
|
252
120
|
}
|
|
253
121
|
const SUB = {
|
|
254
122
|
path: pathSub,
|
package/bin/commands/config.ts
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
2
1
|
import { createInterface } from "readline/promises";
|
|
3
2
|
import { parseArgs } from "../lib/args.js";
|
|
4
3
|
import { out, die, color, json } from "../lib/output.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
import {
|
|
5
|
+
CONFIG_FILE,
|
|
6
|
+
buildEffectiveConfig,
|
|
7
|
+
deleteNestedKey,
|
|
8
|
+
displayPath,
|
|
9
|
+
envOverrideForKey,
|
|
10
|
+
getNestedKey,
|
|
11
|
+
isAuthConfigKey,
|
|
12
|
+
isSensitiveConfigKey,
|
|
13
|
+
isWritableConfigKey,
|
|
14
|
+
loadFileCfg,
|
|
15
|
+
parseConfigValue,
|
|
16
|
+
redactValue,
|
|
17
|
+
restartNotice,
|
|
18
|
+
saveFileCfg,
|
|
19
|
+
setNestedKey,
|
|
20
|
+
} from "../lib/config-store.js";
|
|
9
21
|
|
|
10
22
|
const HELP = `
|
|
11
23
|
ima2 config <subcommand> [options]
|
|
@@ -32,130 +44,6 @@ const FLAGS = {
|
|
|
32
44
|
help: { short: "h", type: "boolean" },
|
|
33
45
|
};
|
|
34
46
|
|
|
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
47
|
async function pathSub(_argv: string[]) {
|
|
160
48
|
out(CONFIG_FILE);
|
|
161
49
|
}
|
|
@@ -193,26 +81,24 @@ async function setSub(argv: string[]) {
|
|
|
193
81
|
const [key, rawValue] = args.positional;
|
|
194
82
|
if (!key || rawValue === undefined) die(2, "usage: config set <key> <value>");
|
|
195
83
|
|
|
196
|
-
if (
|
|
84
|
+
if (isAuthConfigKey(key)) {
|
|
197
85
|
die(2, `"${key}" is an auth key. Use 'ima2 setup' or 'ima2 login' to change authentication.`);
|
|
198
86
|
}
|
|
199
|
-
if (!
|
|
87
|
+
if (!isWritableConfigKey(key)) {
|
|
200
88
|
die(2, `unknown config key: "${key}". Run 'ima2 config ls --effective' to see the config structure.`);
|
|
201
89
|
}
|
|
202
90
|
|
|
203
|
-
|
|
204
|
-
let value: any = rawValue;
|
|
205
|
-
try { value = JSON.parse(rawValue); } catch {}
|
|
91
|
+
const value = parseConfigValue(rawValue);
|
|
206
92
|
|
|
207
93
|
// Warn if env var is overriding this key
|
|
208
|
-
const
|
|
209
|
-
if (
|
|
210
|
-
out(color.yellow(`warning: env ${envVar}=${
|
|
94
|
+
const override = envOverrideForKey(key);
|
|
95
|
+
if (override) {
|
|
96
|
+
out(color.yellow(`warning: env ${override.envVar}=${override.value} is currently overriding this value.`));
|
|
211
97
|
out(`The file change will only apply after unsetting the env var and restarting the server.`);
|
|
212
98
|
}
|
|
213
99
|
|
|
214
100
|
// Confirm if writing a sensitive key
|
|
215
|
-
if ((
|
|
101
|
+
if (isSensitiveConfigKey(key) && !args.yes) {
|
|
216
102
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
217
103
|
const ans = await rl.question(`warning: "${key}" is a sensitive credential. Write to config file? [y/N] `);
|
|
218
104
|
rl.close();
|
|
@@ -224,7 +110,7 @@ async function setSub(argv: string[]) {
|
|
|
224
110
|
saveFileCfg(fileCfg);
|
|
225
111
|
|
|
226
112
|
out(color.green("✓ ") + `wrote ${key}=${JSON.stringify(value)} to ${displayPath(CONFIG_FILE)}`);
|
|
227
|
-
out(color.dim(
|
|
113
|
+
out(color.dim(restartNotice()));
|
|
228
114
|
}
|
|
229
115
|
|
|
230
116
|
async function rmSub(argv: string[]) {
|
|
@@ -232,7 +118,7 @@ async function rmSub(argv: string[]) {
|
|
|
232
118
|
const key = args.positional[0];
|
|
233
119
|
if (!key) die(2, "key required. Usage: config rm <key>");
|
|
234
120
|
|
|
235
|
-
if (
|
|
121
|
+
if (isAuthConfigKey(key)) {
|
|
236
122
|
die(2, `"${key}" is an auth key. Use 'ima2 setup' or 'ima2 login' to change authentication.`);
|
|
237
123
|
}
|
|
238
124
|
|
|
@@ -244,7 +130,7 @@ async function rmSub(argv: string[]) {
|
|
|
244
130
|
}
|
|
245
131
|
saveFileCfg(fileCfg);
|
|
246
132
|
out(color.green("✓ ") + `removed ${key} from ${displayPath(CONFIG_FILE)}`);
|
|
247
|
-
out(color.dim(
|
|
133
|
+
out(color.dim(restartNotice()));
|
|
248
134
|
}
|
|
249
135
|
|
|
250
136
|
type Sub = (argv: any[]) => Promise<void>;
|