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.
- package/README.md +56 -27
- package/bin/commands/annotate.js +137 -0
- package/bin/commands/annotate.ts +118 -0
- package/bin/commands/cancel.js +37 -33
- package/bin/commands/cancel.ts +45 -0
- package/bin/commands/canvas-versions.js +91 -0
- package/bin/commands/canvas-versions.ts +80 -0
- package/bin/commands/cardnews.js +293 -0
- package/bin/commands/cardnews.ts +248 -0
- package/bin/commands/comfy.js +63 -0
- package/bin/commands/comfy.ts +54 -0
- package/bin/commands/config.js +270 -0
- package/bin/commands/config.ts +265 -0
- package/bin/commands/edit.js +97 -72
- package/bin/commands/edit.ts +116 -0
- package/bin/commands/gen.js +140 -118
- package/bin/commands/gen.ts +176 -0
- package/bin/commands/history.js +164 -0
- package/bin/commands/history.ts +145 -0
- package/bin/commands/ls.js +60 -42
- package/bin/commands/ls.ts +60 -0
- package/bin/commands/metadata.js +45 -0
- package/bin/commands/metadata.ts +36 -0
- package/bin/commands/multimode.js +159 -0
- package/bin/commands/multimode.ts +146 -0
- package/bin/commands/node.js +176 -0
- package/bin/commands/node.ts +157 -0
- package/bin/commands/observability.js +201 -0
- package/bin/commands/observability.ts +176 -0
- package/bin/commands/ping.js +26 -20
- package/bin/commands/ping.ts +29 -0
- package/bin/commands/prompt.js +506 -0
- package/bin/commands/prompt.ts +421 -0
- package/bin/commands/ps.js +78 -71
- package/bin/commands/ps.ts +78 -0
- package/bin/commands/session.js +308 -0
- package/bin/commands/session.ts +265 -0
- package/bin/commands/show.js +75 -40
- package/bin/commands/show.ts +69 -0
- package/bin/ima2.js +324 -310
- package/bin/ima2.ts +444 -0
- package/bin/lib/args.js +75 -66
- package/bin/lib/args.ts +73 -0
- package/bin/lib/browser-id.js +15 -0
- package/bin/lib/browser-id.ts +16 -0
- package/bin/lib/client.js +91 -83
- package/bin/lib/client.ts +109 -0
- package/bin/lib/error-hints.js +14 -17
- package/bin/lib/error-hints.ts +23 -0
- package/bin/lib/files.js +26 -28
- package/bin/lib/files.ts +39 -0
- package/bin/lib/output.js +44 -42
- package/bin/lib/output.ts +58 -0
- package/bin/lib/platform.js +60 -56
- package/bin/lib/platform.ts +97 -0
- package/bin/lib/sse.js +73 -0
- package/bin/lib/sse.ts +73 -0
- package/bin/lib/star-prompt.js +69 -76
- package/bin/lib/star-prompt.ts +97 -0
- package/bin/lib/storage-doctor.js +34 -35
- package/bin/lib/storage-doctor.ts +38 -0
- package/config.js +147 -243
- package/config.ts +331 -0
- package/docs/API.md +48 -8
- package/docs/CLI.md +190 -0
- package/docs/FAQ.ko.md +5 -5
- package/docs/FAQ.md +5 -5
- package/docs/README.ja.md +71 -25
- package/docs/README.ko.md +61 -24
- package/docs/README.zh-CN.md +73 -27
- package/lib/assetLifecycle.js +130 -130
- package/lib/assetLifecycle.ts +142 -0
- package/lib/canvasVersionStore.js +135 -153
- package/lib/canvasVersionStore.ts +181 -0
- package/lib/cardNewsGenerator.js +127 -142
- package/lib/cardNewsGenerator.ts +162 -0
- package/lib/cardNewsJobStore.js +78 -84
- package/lib/cardNewsJobStore.ts +107 -0
- package/lib/cardNewsManifestStore.js +88 -93
- package/lib/cardNewsManifestStore.ts +112 -0
- package/lib/cardNewsPlanner.js +157 -152
- package/lib/cardNewsPlanner.ts +180 -0
- package/lib/cardNewsPlannerClient.js +101 -98
- package/lib/cardNewsPlannerClient.ts +114 -0
- package/lib/cardNewsPlannerPrompt.js +56 -56
- package/lib/cardNewsPlannerPrompt.ts +60 -0
- package/lib/cardNewsPlannerSchema.js +231 -223
- package/lib/cardNewsPlannerSchema.ts +259 -0
- package/lib/cardNewsRoleTemplateStore.js +39 -41
- package/lib/cardNewsRoleTemplateStore.ts +47 -0
- package/lib/cardNewsTemplateStore.js +171 -175
- package/lib/cardNewsTemplateStore.ts +210 -0
- package/lib/codexDetect.js +44 -47
- package/lib/codexDetect.ts +69 -0
- package/lib/comfyBridge.js +164 -184
- package/lib/comfyBridge.ts +214 -0
- package/lib/db.js +41 -51
- package/lib/db.ts +166 -0
- package/lib/errorClassify.js +62 -78
- package/lib/errorClassify.ts +100 -0
- package/lib/generationErrors.js +140 -103
- package/lib/generationErrors.ts +125 -0
- package/lib/historyList.js +149 -147
- package/lib/historyList.ts +164 -0
- package/lib/imageMetadata.js +86 -89
- package/lib/imageMetadata.ts +111 -0
- package/lib/imageMetadataStore.js +46 -51
- package/lib/imageMetadataStore.ts +67 -0
- package/lib/imageModels.js +38 -45
- package/lib/imageModels.ts +52 -0
- package/lib/inflight.js +131 -150
- package/lib/inflight.ts +204 -0
- package/lib/localImportStore.js +87 -93
- package/lib/localImportStore.ts +111 -0
- package/lib/logger.js +105 -112
- package/lib/logger.ts +150 -0
- package/lib/nodeStore.js +65 -64
- package/lib/nodeStore.ts +81 -0
- package/lib/oauthLauncher.js +61 -59
- package/lib/oauthLauncher.ts +64 -0
- package/lib/oauthNormalize.js +15 -19
- package/lib/oauthNormalize.ts +30 -0
- package/lib/oauthProxy.js +834 -832
- package/lib/oauthProxy.ts +995 -0
- package/lib/openDirectory.js +41 -40
- package/lib/openDirectory.ts +45 -0
- package/lib/pngInfo.js +18 -20
- package/lib/pngInfo.ts +26 -0
- package/lib/promptImport/curatedSources.js +125 -129
- package/lib/promptImport/curatedSources.ts +139 -0
- package/lib/promptImport/discoveryRegistry.js +185 -203
- package/lib/promptImport/discoveryRegistry.ts +236 -0
- package/lib/promptImport/errors.js +10 -10
- package/lib/promptImport/errors.ts +18 -0
- package/lib/promptImport/githubDiscovery.js +209 -219
- package/lib/promptImport/githubDiscovery.ts +248 -0
- package/lib/promptImport/githubFolder.js +253 -259
- package/lib/promptImport/githubFolder.ts +308 -0
- package/lib/promptImport/githubSource.js +189 -200
- package/lib/promptImport/githubSource.ts +239 -0
- package/lib/promptImport/gptImageHints.js +49 -56
- package/lib/promptImport/gptImageHints.ts +68 -0
- package/lib/promptImport/parsePromptCandidates.js +108 -123
- package/lib/promptImport/parsePromptCandidates.ts +153 -0
- package/lib/promptImport/promptIndex.js +190 -208
- package/lib/promptImport/promptIndex.ts +248 -0
- package/lib/promptImport/rankPromptCandidates.js +46 -43
- package/lib/promptImport/rankPromptCandidates.ts +49 -0
- package/lib/providerOptions.js +31 -0
- package/lib/providerOptions.ts +41 -0
- package/lib/referenceImageCompress.js +51 -62
- package/lib/referenceImageCompress.ts +75 -0
- package/lib/refs.js +93 -81
- package/lib/refs.ts +117 -0
- package/lib/requestLogger.js +32 -38
- package/lib/requestLogger.ts +48 -0
- package/lib/responsesImageAdapter.js +351 -0
- package/lib/responsesImageAdapter.ts +352 -0
- package/lib/runtimePorts.js +71 -73
- package/lib/runtimePorts.ts +93 -0
- package/lib/sessionStore.js +179 -230
- package/lib/sessionStore.ts +272 -0
- package/lib/storageMigration.js +247 -245
- package/lib/storageMigration.ts +284 -0
- package/lib/styleSheet.js +86 -90
- package/lib/styleSheet.ts +128 -0
- package/lib/systemTrash.js +18 -0
- package/lib/systemTrash.ts +20 -0
- package/package.json +26 -10
- package/routes/annotations.js +76 -79
- package/routes/annotations.ts +95 -0
- package/routes/canvasVersions.js +50 -54
- package/routes/canvasVersions.ts +64 -0
- package/routes/cardNews.js +158 -171
- package/routes/cardNews.ts +183 -0
- package/routes/comfy.js +23 -31
- package/routes/comfy.ts +39 -0
- package/routes/edit.js +183 -214
- package/routes/edit.ts +230 -0
- package/routes/generate.js +269 -291
- package/routes/generate.ts +309 -0
- package/routes/health.js +102 -107
- package/routes/health.ts +114 -0
- package/routes/history.js +136 -144
- package/routes/history.ts +153 -0
- package/routes/imageImport.js +27 -27
- package/routes/imageImport.ts +33 -0
- package/routes/index.js +17 -17
- package/routes/index.ts +35 -0
- package/routes/metadata.js +60 -64
- package/routes/metadata.ts +71 -0
- package/routes/multimode.js +228 -263
- package/routes/multimode.ts +280 -0
- package/routes/nodes.js +378 -424
- package/routes/nodes.ts +455 -0
- package/routes/promptImport.js +284 -324
- package/routes/promptImport.ts +354 -0
- package/routes/prompts.js +333 -360
- package/routes/prompts.ts +379 -0
- package/routes/sessions.js +277 -285
- package/routes/sessions.ts +292 -0
- package/routes/storage.js +29 -31
- package/routes/storage.ts +39 -0
- package/server.js +189 -196
- package/server.ts +235 -0
- package/ui/dist/.vite/manifest.json +101 -0
- package/ui/dist/assets/CardNewsWorkspace-BJOCey7Z.js +2 -0
- package/ui/dist/assets/NodeCanvas-BZV40eAE.css +1 -0
- package/ui/dist/assets/NodeCanvas-C3dzYNsk.js +7 -0
- package/ui/dist/assets/PromptImportDialog-Dqu1VpUh.js +2 -0
- package/ui/dist/assets/PromptImportDiscoverySection-Dg8T9X0L.js +1 -0
- package/ui/dist/assets/PromptImportFolderSection-DBaqsFO4.js +1 -0
- package/ui/dist/assets/PromptLibraryPanel-p5QqR97M.js +2 -0
- package/ui/dist/assets/SettingsWorkspace-B5bSAZ6u.js +1 -0
- package/ui/dist/assets/index-C9cXwiWE.js +25 -0
- package/ui/dist/assets/index-CGMIkZXn.css +1 -0
- package/ui/dist/assets/index-Cvld7dUZ.js +1 -0
- package/ui/dist/index.html +2 -2
- package/assets/phase-a-bg-cleanup-test.png +0 -0
- package/assets/screenshot.png +0 -0
- package/assets/screenshots/classic-generate-light.png +0 -0
- package/assets/screenshots/node-graph-branching.png +0 -0
- package/assets/screenshots/settings-oauth-generation.png +0 -0
- package/assets/screenshots/settings-workspace.png +0 -0
- package/assets/screenshots/style-sheet-editor.png +0 -0
- package/integrations/comfyui/ima2_gen_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
- package/integrations/comfyui/ima2_gen_bridge/__pycache__/nodes.cpython-313.pyc +0 -0
- package/ui/dist/assets/index-BDffwmLs.css +0 -1
- package/ui/dist/assets/index-D0fdHLkJ.js +0 -31
- 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
|
-
|
|
35
|
-
|
|
36
|
-
Array.isArray(v)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
sheet
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
]
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
+
}
|