autodev-cli 1.4.149 → 1.4.151
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/media/gimp/LICENSE +674 -0
- package/media/gimp/gimp_mcp_server.py +267 -0
- package/out/commands/mcp.js +39 -0
- package/out/commands/mcp.js.map +1 -1
- package/out/configManager.js +23 -0
- package/out/configManager.js.map +1 -1
- package/out/core/gimpPluginInstall.d.ts +57 -0
- package/out/core/gimpPluginInstall.js +273 -0
- package/out/core/gimpPluginInstall.js.map +1 -0
- package/out/core/gimpPluginSource.d.ts +4 -0
- package/out/core/gimpPluginSource.js +284 -0
- package/out/core/gimpPluginSource.js.map +1 -0
- package/out/core/mcpCatalog.js +4 -4
- package/out/core/mcpCatalog.js.map +1 -1
- package/package.json +4 -2
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type DetectAppResult } from './binDetect';
|
|
2
|
+
export interface GimpInstallDeps {
|
|
3
|
+
platform: NodeJS.Platform;
|
|
4
|
+
env: NodeJS.ProcessEnv;
|
|
5
|
+
homedir: () => string;
|
|
6
|
+
fileExists: (p: string) => boolean;
|
|
7
|
+
listDir: (p: string) => string[];
|
|
8
|
+
mkdirp: (p: string) => void;
|
|
9
|
+
writeFile: (p: string, data: string) => void;
|
|
10
|
+
chmod: (p: string, mode: number) => void;
|
|
11
|
+
copyFile: (src: string, dst: string) => void;
|
|
12
|
+
/** Read an existing file (for idempotent-skip content comparison). '' if unreadable. */
|
|
13
|
+
readFile: (p: string) => string;
|
|
14
|
+
/**
|
|
15
|
+
* The auto-start plugin source. Defaults to the EMBEDDED string
|
|
16
|
+
* (GIMP_PLUGIN_SOURCE) so it works when esbuild-bundled into the VS Code
|
|
17
|
+
* extension where media/gimp/gimp_mcp_server.py is NOT on disk at runtime.
|
|
18
|
+
*/
|
|
19
|
+
readPluginSource: () => string;
|
|
20
|
+
/** GIMP detection (to gate on major < 3). Defaults to detectApp('gimp'). */
|
|
21
|
+
detectGimp: () => DetectAppResult;
|
|
22
|
+
}
|
|
23
|
+
export interface GimpInstallResult {
|
|
24
|
+
installed: boolean;
|
|
25
|
+
/** Absolute path(s) of the written gimp_mcp_server.py file(s). */
|
|
26
|
+
paths: string[];
|
|
27
|
+
/** The GIMP minor-version dir chosen (e.g. "3.2"), or null when not installed. */
|
|
28
|
+
version: string | null;
|
|
29
|
+
warnings: string[];
|
|
30
|
+
}
|
|
31
|
+
export interface GimpInstallOpts {
|
|
32
|
+
/**
|
|
33
|
+
* Explicit versioned GIMP config dir to target (e.g. `~/.config/GIMP/3.2`),
|
|
34
|
+
* bypassing the auto-resolver. Same semantics as GIMP3_DIRECTORY.
|
|
35
|
+
*/
|
|
36
|
+
targetDir?: string;
|
|
37
|
+
}
|
|
38
|
+
/** Absolute path to the vendored auto-start plugin shipped in the tarball. */
|
|
39
|
+
export declare function vendoredPluginSourcePath(): string;
|
|
40
|
+
/** Extract the GIMP MAJOR version from a launcher path (e.g. gimp-2.10 → 2). */
|
|
41
|
+
export declare function gimpMajorFromPath(p: string | null): number | null;
|
|
42
|
+
/**
|
|
43
|
+
* Install the auto-start GIMP MCP plugin. Resolves the target plug-ins dir(s)
|
|
44
|
+
* per-OS and writes the vendored plugin so GIMP opens the localhost:9876 socket
|
|
45
|
+
* on launch. Does NOT install when a clearly < 3 GIMP is the only one detected.
|
|
46
|
+
*/
|
|
47
|
+
export declare function installGimpPlugin(opts?: GimpInstallOpts, deps?: Partial<GimpInstallDeps>): GimpInstallResult;
|
|
48
|
+
/**
|
|
49
|
+
* Sync-path variant: install the auto-start plugin ONLY when GIMP 3.x is actually
|
|
50
|
+
* DETECTED. Unlike installGimpPlugin (which will create a 3.0 config dir even with
|
|
51
|
+
* no GIMP present — appropriate for the explicit `mcp detect --enable gimp` /
|
|
52
|
+
* `gimp-install-plugin` flows), this refuses to touch the filesystem when GIMP is
|
|
53
|
+
* absent or is clearly < 3, so a routine ConfigManager.syncProjectMcpServers on a
|
|
54
|
+
* box without GIMP never spuriously creates a config dir. Returns a skip result
|
|
55
|
+
* (installed:false + warning) instead of throwing so callers can swallow it.
|
|
56
|
+
*/
|
|
57
|
+
export declare function installGimpPluginIfDetected(opts?: GimpInstallOpts, deps?: Partial<GimpInstallDeps>): GimpInstallResult;
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// gimpPluginInstall — drop the vendored AUTO-START GIMP MCP plugin into the
|
|
4
|
+
// correct GIMP 3.x user plug-ins folder so the socket bridge opens on GIMP
|
|
5
|
+
// launch (no manual copy, no menu click).
|
|
6
|
+
//
|
|
7
|
+
// GROUNDED RULES (see AutoDev research):
|
|
8
|
+
// - GIMP 3.x user config is MINOR-versioned: GIMP 3.2 → .../GIMP/3.2/ (NOT 3.0).
|
|
9
|
+
// Bases: linux $XDG_CONFIG_HOME/GIMP or ~/.config/GIMP; win32 %APPDATA%\GIMP;
|
|
10
|
+
// darwin ~/Library/Application Support/GIMP. Honor GIMP3_DIRECTORY (overrides
|
|
11
|
+
// the WHOLE versioned dir). Glob GIMP/3.* and pick the HIGHEST minor; if none
|
|
12
|
+
// exists, create 3.0 and warn (user should launch GIMP once).
|
|
13
|
+
// - GIMP 3.x layout: plug-ins/gimp_mcp_server/gimp_mcp_server.py — the .py MUST
|
|
14
|
+
// sit in a sub-folder whose name == the .py basename (a loose .py is ignored).
|
|
15
|
+
// On non-Windows the .py must be executable (0o755).
|
|
16
|
+
// - Windows Store (MSIX) GIMP reads the REAL %APPDATA%\GIMP\<ver>. As a safety
|
|
17
|
+
// net we ALSO write the virtualized path when a package dir exists:
|
|
18
|
+
// %LOCALAPPDATA%\Packages\GIMP.<...>_*\LocalCache\Roaming\GIMP\<ver>\plug-ins.
|
|
19
|
+
//
|
|
20
|
+
// Everything is dependency-injected (platform/env/homedir/fileExists/listDir/
|
|
21
|
+
// mkdirp/writeFile/chmod/copyFile/readPluginSource/detectGimp) so the resolver is
|
|
22
|
+
// unit-testable without a real GIMP or filesystem.
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
27
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
28
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
29
|
+
}
|
|
30
|
+
Object.defineProperty(o, k2, desc);
|
|
31
|
+
}) : (function(o, m, k, k2) {
|
|
32
|
+
if (k2 === undefined) k2 = k;
|
|
33
|
+
o[k2] = m[k];
|
|
34
|
+
}));
|
|
35
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
36
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
37
|
+
}) : function(o, v) {
|
|
38
|
+
o["default"] = v;
|
|
39
|
+
});
|
|
40
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
41
|
+
var ownKeys = function(o) {
|
|
42
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
43
|
+
var ar = [];
|
|
44
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
45
|
+
return ar;
|
|
46
|
+
};
|
|
47
|
+
return ownKeys(o);
|
|
48
|
+
};
|
|
49
|
+
return function (mod) {
|
|
50
|
+
if (mod && mod.__esModule) return mod;
|
|
51
|
+
var result = {};
|
|
52
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
53
|
+
__setModuleDefault(result, mod);
|
|
54
|
+
return result;
|
|
55
|
+
};
|
|
56
|
+
})();
|
|
57
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
58
|
+
exports.vendoredPluginSourcePath = vendoredPluginSourcePath;
|
|
59
|
+
exports.gimpMajorFromPath = gimpMajorFromPath;
|
|
60
|
+
exports.installGimpPlugin = installGimpPlugin;
|
|
61
|
+
exports.installGimpPluginIfDetected = installGimpPluginIfDetected;
|
|
62
|
+
const fs = __importStar(require("fs"));
|
|
63
|
+
const path = __importStar(require("path"));
|
|
64
|
+
const os = __importStar(require("os"));
|
|
65
|
+
const binDetect_1 = require("./binDetect");
|
|
66
|
+
const gimpPluginSource_1 = require("./gimpPluginSource");
|
|
67
|
+
/** Absolute path to the vendored auto-start plugin shipped in the tarball. */
|
|
68
|
+
function vendoredPluginSourcePath() {
|
|
69
|
+
// Compiled to out/core/gimpPluginInstall.js → package root is ../../ .
|
|
70
|
+
return path.join(__dirname, '..', '..', 'media', 'gimp', 'gimp_mcp_server.py');
|
|
71
|
+
}
|
|
72
|
+
function realDeps() {
|
|
73
|
+
return {
|
|
74
|
+
platform: process.platform,
|
|
75
|
+
env: process.env,
|
|
76
|
+
homedir: os.homedir,
|
|
77
|
+
fileExists: (p) => { try {
|
|
78
|
+
return fs.existsSync(p);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
} },
|
|
83
|
+
listDir: (p) => { try {
|
|
84
|
+
return fs.readdirSync(p);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return [];
|
|
88
|
+
} },
|
|
89
|
+
mkdirp: (p) => { fs.mkdirSync(p, { recursive: true }); },
|
|
90
|
+
writeFile: (p, data) => { fs.writeFileSync(p, data); },
|
|
91
|
+
chmod: (p, mode) => { try {
|
|
92
|
+
fs.chmodSync(p, mode);
|
|
93
|
+
}
|
|
94
|
+
catch { /* non-POSIX fs */ } },
|
|
95
|
+
copyFile: (src, dst) => { fs.copyFileSync(src, dst); },
|
|
96
|
+
readFile: (p) => { try {
|
|
97
|
+
return fs.readFileSync(p, 'utf8');
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return '';
|
|
101
|
+
} },
|
|
102
|
+
// Runtime uses the EMBEDDED source (bundle-safe). media/gimp/gimp_mcp_server.py
|
|
103
|
+
// stays the canonical source + GPL attribution, but is no longer read at runtime.
|
|
104
|
+
readPluginSource: () => gimpPluginSource_1.GIMP_PLUGIN_SOURCE,
|
|
105
|
+
detectGimp: () => (0, binDetect_1.detectApp)('gimp'),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/** GIMP user-config BASE dir (the folder that CONTAINS the 3.x version dirs). */
|
|
109
|
+
function gimpConfigBase(d) {
|
|
110
|
+
const home = d.homedir();
|
|
111
|
+
if (d.platform === 'win32') {
|
|
112
|
+
const appData = d.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
113
|
+
return path.join(appData, 'GIMP');
|
|
114
|
+
}
|
|
115
|
+
if (d.platform === 'darwin') {
|
|
116
|
+
return path.join(home, 'Library', 'Application Support', 'GIMP');
|
|
117
|
+
}
|
|
118
|
+
const xdg = d.env.XDG_CONFIG_HOME && d.env.XDG_CONFIG_HOME.trim();
|
|
119
|
+
const cfg = xdg ? xdg : path.join(home, '.config');
|
|
120
|
+
return path.join(cfg, 'GIMP');
|
|
121
|
+
}
|
|
122
|
+
/** Parse the numeric minor from a `3.<minor>` dir name; null if not `3.*`. */
|
|
123
|
+
function minorOf(name) {
|
|
124
|
+
const m = /^3\.(\d+)$/.exec(name);
|
|
125
|
+
return m ? parseInt(m[1], 10) : null;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Pick the HIGHEST-minor `3.*` version dir under base. Returns the chosen
|
|
129
|
+
* version string and whether it already existed. When none exist, defaults to
|
|
130
|
+
* "3.0" (created + warned by the caller).
|
|
131
|
+
*/
|
|
132
|
+
function chooseVersion(base, d) {
|
|
133
|
+
const kids = d.listDir(base)
|
|
134
|
+
.map((n) => ({ name: n, minor: minorOf(n) }))
|
|
135
|
+
.filter((x) => x.minor !== null);
|
|
136
|
+
if (kids.length === 0) {
|
|
137
|
+
return { version: '3.0', existed: false };
|
|
138
|
+
}
|
|
139
|
+
kids.sort((a, b) => b.minor - a.minor);
|
|
140
|
+
return { version: kids[0].name, existed: true };
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Windows Store virtualized fallback plug-ins dir(s) for a given version. Only
|
|
144
|
+
* returned when a `GIMP.*` package dir actually exists under %LOCALAPPDATA%\Packages.
|
|
145
|
+
*/
|
|
146
|
+
function storeVirtualizedPluginDirs(version, d) {
|
|
147
|
+
if (d.platform !== 'win32') {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
const home = d.homedir();
|
|
151
|
+
const localAppData = d.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
152
|
+
const packagesDir = path.join(localAppData, 'Packages');
|
|
153
|
+
const pkgs = d.listDir(packagesDir).filter((n) => /^GIMP\./i.test(n));
|
|
154
|
+
return pkgs.map((pkg) => path.join(packagesDir, pkg, 'LocalCache', 'Roaming', 'GIMP', version, 'plug-ins'));
|
|
155
|
+
}
|
|
156
|
+
/** Extract the GIMP MAJOR version from a launcher path (e.g. gimp-2.10 → 2). */
|
|
157
|
+
function gimpMajorFromPath(p) {
|
|
158
|
+
if (!p) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const base = p.replace(/\\/g, '/').split('/').pop() || p;
|
|
162
|
+
const m = /gimp[-_]?(\d+)/i.exec(base);
|
|
163
|
+
return m ? parseInt(m[1], 10) : null;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Write the plugin into one plug-ins dir. IDEMPOTENT: if the target already
|
|
167
|
+
* holds byte-identical content, skip the write (and the .bak) — but still ensure
|
|
168
|
+
* the executable bit on non-Windows. A .bak backup is made ONLY when overwriting
|
|
169
|
+
* content that DIFFERS from the source.
|
|
170
|
+
*/
|
|
171
|
+
function installInto(pluginsDir, source, d) {
|
|
172
|
+
const sub = path.join(pluginsDir, 'gimp_mcp_server');
|
|
173
|
+
const py = path.join(sub, 'gimp_mcp_server.py');
|
|
174
|
+
d.mkdirp(sub);
|
|
175
|
+
if (d.fileExists(py)) {
|
|
176
|
+
const existing = d.readFile(py);
|
|
177
|
+
if (existing === source) {
|
|
178
|
+
// Already up to date — no write, no backup. Still ensure it's executable.
|
|
179
|
+
if (d.platform !== 'win32') {
|
|
180
|
+
d.chmod(py, 0o755);
|
|
181
|
+
}
|
|
182
|
+
return py;
|
|
183
|
+
}
|
|
184
|
+
// Different content → back it up before overwriting.
|
|
185
|
+
d.copyFile(py, py + '.bak');
|
|
186
|
+
}
|
|
187
|
+
d.writeFile(py, source);
|
|
188
|
+
if (d.platform !== 'win32') {
|
|
189
|
+
d.chmod(py, 0o755);
|
|
190
|
+
}
|
|
191
|
+
return py;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Install the auto-start GIMP MCP plugin. Resolves the target plug-ins dir(s)
|
|
195
|
+
* per-OS and writes the vendored plugin so GIMP opens the localhost:9876 socket
|
|
196
|
+
* on launch. Does NOT install when a clearly < 3 GIMP is the only one detected.
|
|
197
|
+
*/
|
|
198
|
+
function installGimpPlugin(opts, deps) {
|
|
199
|
+
const d = { ...realDeps(), ...deps };
|
|
200
|
+
const warnings = [];
|
|
201
|
+
// Gate: if a GIMP is detected and is clearly < 3, do not install.
|
|
202
|
+
const det = d.detectGimp();
|
|
203
|
+
const major = gimpMajorFromPath(det.path);
|
|
204
|
+
if (det.installed && major !== null && major < 3) {
|
|
205
|
+
warnings.push(`Detected GIMP ${major}.x at ${det.path}. GIMP MCP needs GIMP 3.x — ` +
|
|
206
|
+
'the auto-start plugin uses the GIMP 3.0 Python API and was NOT installed. ' +
|
|
207
|
+
'Install GIMP 3 and re-run.');
|
|
208
|
+
return { installed: false, paths: [], version: null, warnings };
|
|
209
|
+
}
|
|
210
|
+
const source = d.readPluginSource();
|
|
211
|
+
// Resolve the versioned config dir(s). GIMP3_DIRECTORY / opts.targetDir
|
|
212
|
+
// override the WHOLE versioned dir.
|
|
213
|
+
const override = (opts?.targetDir && opts.targetDir.trim()) || (d.env.GIMP3_DIRECTORY && d.env.GIMP3_DIRECTORY.trim());
|
|
214
|
+
const targets = [];
|
|
215
|
+
let version;
|
|
216
|
+
if (override) {
|
|
217
|
+
version = path.basename(override) || 'custom';
|
|
218
|
+
if (!d.fileExists(override)) {
|
|
219
|
+
warnings.push(`GIMP dir ${override} does not exist yet — creating it. Launch GIMP once so it initialises its config.`);
|
|
220
|
+
}
|
|
221
|
+
targets.push(path.join(override, 'plug-ins'));
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
const base = gimpConfigBase(d);
|
|
225
|
+
const chosen = chooseVersion(base, d);
|
|
226
|
+
version = chosen.version;
|
|
227
|
+
if (!chosen.existed) {
|
|
228
|
+
warnings.push(`No GIMP 3.x config dir found under ${base} — creating ${version}. ` +
|
|
229
|
+
'Launch GIMP 3 at least once (so it creates its real config dir), then re-run to be sure it lands in the right place.');
|
|
230
|
+
}
|
|
231
|
+
targets.push(path.join(base, version, 'plug-ins'));
|
|
232
|
+
// Windows Store virtualized safety net (only when a package dir exists).
|
|
233
|
+
for (const vdir of storeVirtualizedPluginDirs(version, d)) {
|
|
234
|
+
targets.push(vdir);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const paths = [];
|
|
238
|
+
for (const pluginsDir of targets) {
|
|
239
|
+
try {
|
|
240
|
+
paths.push(installInto(pluginsDir, source, d));
|
|
241
|
+
}
|
|
242
|
+
catch (e) {
|
|
243
|
+
warnings.push(`Failed to write plugin into ${pluginsDir}: ${e.message}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return { installed: paths.length > 0, paths, version, warnings };
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Sync-path variant: install the auto-start plugin ONLY when GIMP 3.x is actually
|
|
250
|
+
* DETECTED. Unlike installGimpPlugin (which will create a 3.0 config dir even with
|
|
251
|
+
* no GIMP present — appropriate for the explicit `mcp detect --enable gimp` /
|
|
252
|
+
* `gimp-install-plugin` flows), this refuses to touch the filesystem when GIMP is
|
|
253
|
+
* absent or is clearly < 3, so a routine ConfigManager.syncProjectMcpServers on a
|
|
254
|
+
* box without GIMP never spuriously creates a config dir. Returns a skip result
|
|
255
|
+
* (installed:false + warning) instead of throwing so callers can swallow it.
|
|
256
|
+
*/
|
|
257
|
+
function installGimpPluginIfDetected(opts, deps) {
|
|
258
|
+
const d = { ...realDeps(), ...deps };
|
|
259
|
+
const det = d.detectGimp();
|
|
260
|
+
const major = gimpMajorFromPath(det.path);
|
|
261
|
+
// Require a detected GIMP that is not clearly < 3 (major null = version not in
|
|
262
|
+
// the launcher path, e.g. a macOS app bundle — allowed).
|
|
263
|
+
if (!det.installed || (major !== null && major < 3)) {
|
|
264
|
+
return {
|
|
265
|
+
installed: false,
|
|
266
|
+
paths: [],
|
|
267
|
+
version: null,
|
|
268
|
+
warnings: ['GIMP 3.x not detected — auto-start plugin install skipped (sync path).'],
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return installGimpPlugin(opts, deps);
|
|
272
|
+
}
|
|
273
|
+
//# sourceMappingURL=gimpPluginInstall.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gimpPluginInstall.js","sourceRoot":"","sources":["../../src/core/gimpPluginInstall.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** sha256 (first 12 hex) of the canonical media/gimp/gimp_mcp_server.py. */
|
|
2
|
+
export declare const GIMP_PLUGIN_SOURCE_HASH = "93b608412db2";
|
|
3
|
+
/** Full body of the auto-start GIMP 3.x MCP plugin (media/gimp/gimp_mcp_server.py). */
|
|
4
|
+
export declare const GIMP_PLUGIN_SOURCE = "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n#\n# gimp_mcp_server.py \u2014 AutoDev auto-start GIMP 3.x MCP bridge\n#\n# Derived from libreearth/gimp-mcp @49c46dd (GPL-3.0); modified by AutoDev to\n# auto-start as a GIMP 3.x PERSISTENT extension. The upstream plugin registered a\n# manual \"<Image>/Filters/Development/ \u2192 Start MCP Server\" ImageProcedure that the\n# user had to click; this variant registers a PERSISTENT extension so GIMP starts\n# the socket server automatically on launch \u2014 no menu click, no open image needed.\n#\n# The socket / command-marshalling logic (start_server, handle_client,\n# execute_command, GLib.idle_add onto GIMP's main thread) is kept from upstream.\n# ONLY the registration + run() model changed to PERSISTENT + defensive ordering.\n#\n# ---------------------------------------------------------------------------\n# GIMP 3.x plugin layout (REQUIRED): this file must live at\n# <gimp-config>/<ver>/plug-ins/gimp_mcp_server/gimp_mcp_server.py\n# i.e. inside a sub-folder whose name == this file's basename. A loose .py is\n# IGNORED by GIMP 3.x. On Linux/macOS it must ALSO be executable (chmod 0o755).\n# ---------------------------------------------------------------------------\n#\n# Copyright (C) 2023 libreearth (upstream, GPL-3.0)\n# Modifications Copyright (C) 2025 AutoDev\n#\n# This program is free software: you can redistribute it and/or modify it under\n# the terms of the GNU General Public License as published by the Free Software\n# Foundation, either version 3 of the License, or (at your option) any later\n# version.\n#\n# This program is distributed in the hope that it will be useful, but WITHOUT ANY\n# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A\n# PARTICULAR PURPOSE. See the GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License along with\n# this program. If not, see <https://www.gnu.org/licenses/>. A copy is vendored\n# next to this file as LICENSE.\n\nimport os\nimport sys\nimport json\nimport socket\nimport threading\nimport traceback\nimport datetime\n\nimport gi\ngi.require_version('Gimp', '3.0')\nfrom gi.repository import Gimp\nfrom gi.repository import GLib\n\n\n# ---------------------------------------------------------------------------\n# Logging \u2014 an auto-start extension has NO console, so every diagnostic goes to\n# a log file the user can read. We resolve it next to this plug-in first (always\n# writable if the plug-in loaded at all), falling back to the GIMP config dir.\n# Nothing here may raise: a logging failure must never stop GIMP from launching.\n# ---------------------------------------------------------------------------\ndef _log_path():\n try:\n here = os.path.dirname(os.path.abspath(__file__))\n return os.path.join(here, 'gimp-mcp-server.log')\n except Exception:\n try:\n return os.path.join(GLib.get_user_config_dir(), 'gimp-mcp-server.log')\n except Exception:\n return os.path.join(os.path.expanduser('~'), 'gimp-mcp-server.log')\n\n\n_LOG_FILE = _log_path()\n\n\ndef log(msg):\n \"\"\"Best-effort append to the log file AND stderr. Never raises.\"\"\"\n line = '[%s] %s' % (datetime.datetime.now().isoformat(timespec='seconds'), msg)\n try:\n with open(_LOG_FILE, 'a', encoding='utf-8') as fh:\n fh.write(line + '\\n')\n except Exception:\n pass\n try:\n sys.stderr.write(line + '\\n')\n except Exception:\n pass\n\n\nclass MCPPlugin(Gimp.PlugIn):\n # \u2500\u2500 Registration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n def do_query_procedures(self):\n return [\"plug-in-mcp-server-autostart\"]\n\n def do_create_procedure(self, name):\n # PERSISTENT + no args + no menu = GIMP auto-starts this extension at\n # launch. Adding ANY argument, a menu label, or a menu path would turn\n # it back into a user-invoked plug-in and disable auto-start.\n procedure = Gimp.Procedure.new(\n self, name, Gimp.PDBProcType.PERSISTENT, self.run, None,\n )\n try:\n procedure.set_documentation(\n \"AutoDev MCP server (auto-start)\",\n \"Auto-starts a localhost:9876 socket bridge so external MCP \"\n \"clients can drive GIMP. Derived from libreearth/gimp-mcp \"\n \"(GPL-3.0), modified by AutoDev to run as a PERSISTENT extension.\",\n name,\n )\n procedure.set_attribution(\"AutoDev\", \"AutoDev (GPL-3.0)\", \"2025\")\n except Exception as e:\n # Attribution/doc setters are cosmetic \u2014 never let a binding\n # mismatch here prevent the procedure from registering.\n log(\"do_create_procedure: non-fatal setter error: %s\" % e)\n return procedure\n\n # \u2500\u2500 Extension entry point \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n def run(self, procedure, config, run_data):\n \"\"\"PERSISTENT extension entry point.\n\n DEFENSIVE ORDER (the brick-risk part): call persistent_ready() FIRST.\n Its ABSENCE is what hangs GIMP at startup, so it must run before\n anything that could throw. Everything after it is wrapped so a bind\n failure logs + degrades instead of taking GIMP core down with it.\n \"\"\"\n # 1) Hand control back to GIMP core IMMEDIATELY so the UI keeps loading.\n try:\n procedure.persistent_ready()\n log(\"persistent_ready() OK \u2014 extension is live\")\n except Exception as e:\n # A binding mismatch on this exact call is the known lock-up cause;\n # log it (do NOT re-raise) so GIMP still finishes launching.\n log(\"persistent_ready() FAILED (non-fatal): %s\\n%s\"\n % (e, traceback.format_exc()))\n\n # 2) Start the socket server on a daemon thread. A bind failure (port in\n # use, sandbox denial) MUST NOT crash the process or block launch.\n try:\n t = threading.Thread(target=self.start_server, daemon=True)\n t.start()\n log(\"MCP server thread started (listening on localhost:9876)\")\n except Exception as e:\n log(\"failed to start MCP server thread (non-fatal): %s\\n%s\"\n % (e, traceback.format_exc()))\n\n # 3) Keep this extension process alive for the whole GIMP session so the\n # socket stays open and idle_add callbacks can marshal onto core.\n try:\n GLib.MainLoop().run()\n except Exception as e:\n log(\"main loop exited (non-fatal): %s\" % e)\n\n # 4) Standard PERSISTENT success return.\n return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())\n\n # \u2500\u2500 Socket server (kept from upstream) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n def start_server(self):\n host = 'localhost'\n port = 9876\n sock = None\n try:\n sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n sock.bind((host, port))\n sock.listen(1)\n log(\"server listening on %s:%d\" % (host, port))\n except Exception as e:\n # Bind/listen failed (e.g. port already taken by Blender's MCP or a\n # second GIMP). Log + bail out of the thread; GIMP keeps running.\n log(\"bind/listen failed (non-fatal): %s\\n%s\" % (e, traceback.format_exc()))\n try:\n if sock is not None:\n sock.close()\n except Exception:\n pass\n return\n\n while True:\n try:\n client, addr = sock.accept()\n log(\"client connected: %s\" % (addr,))\n threading.Thread(\n target=self.handle_client, args=(client,), daemon=True,\n ).start()\n except Exception as e:\n log(\"accept error: %s\" % e)\n break\n try:\n sock.close()\n except Exception:\n pass\n\n def handle_client(self, client):\n buffer = b''\n while True:\n try:\n data = client.recv(1024)\n if not data:\n break\n buffer += data\n try:\n command = json.loads(buffer.decode('utf-8'))\n buffer = b''\n # Marshal onto GIMP's main thread \u2014 the PDB is not thread-safe.\n GLib.idle_add(self.execute_command, command, client)\n except json.JSONDecodeError:\n # Partial frame \u2014 keep buffering until it parses.\n continue\n except Exception as e:\n log(\"client error: %s\" % e)\n break\n try:\n client.close()\n except Exception:\n pass\n\n def execute_command(self, command, client):\n \"\"\"Runs on GIMP's main thread via idle_add. Guarded so a bad command or\n a wrong binding logs + returns an error frame rather than hanging.\"\"\"\n response = {}\n try:\n cmd_type = command.get(\"type\")\n params = command.get(\"params\", {})\n\n if cmd_type == \"call_api\":\n api_path = params[\"api_path\"]\n args = params.get(\"args\", [])\n kwargs = params.get(\"kwargs\", {})\n\n module = Gimp\n for part in api_path.split(\".\"):\n module = getattr(module, part)\n\n result = module(*args, **kwargs)\n\n if hasattr(result, \"ID\"):\n result = {\"id\": result.ID}\n elif isinstance(result, list):\n result = [self.serialize_gimp_object(item) for item in result]\n else:\n result = str(result)\n\n response = {\"status\": \"success\", \"result\": result}\n else:\n response = {\"status\": \"error\", \"message\": \"Unknown command: %s\" % cmd_type}\n except Exception as e:\n response = {\"status\": \"error\", \"message\": str(e),\n \"traceback\": traceback.format_exc()}\n\n try:\n client.sendall(json.dumps(response).encode('utf-8'))\n except Exception as e:\n log(\"failed to send response: %s\" % e)\n return False # one-shot idle callback\n\n def serialize_gimp_object(self, obj):\n try:\n if hasattr(obj, \"ID\"):\n return {\"id\": obj.ID, \"type\": obj.__class__.__name__}\n except Exception:\n pass\n return str(obj)\n\n\ntry:\n Gimp.main(MCPPlugin.__gtype__, sys.argv)\nexcept Exception as _e:\n # If GIMP's main dispatch itself fails, log it \u2014 never let the plug-in\n # process take the launch down with an unhandled traceback.\n log(\"Gimp.main failed: %s\\n%s\" % (_e, traceback.format_exc()))\n";
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// gimpPluginSource.ts — GENERATED. Do not edit by hand.
|
|
4
|
+
//
|
|
5
|
+
// Source of truth: media/gimp/gimp_mcp_server.py (GPL-3.0; see media/gimp/LICENSE).
|
|
6
|
+
// Regenerate with: node scripts/gen-gimp-plugin-source.mjs (runs in `npm run build`).
|
|
7
|
+
//
|
|
8
|
+
// This embeds the auto-start GIMP MCP plugin body as a string so the installer
|
|
9
|
+
// works even when bundled (VS Code extension esbuild) with no media file on disk.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.GIMP_PLUGIN_SOURCE = exports.GIMP_PLUGIN_SOURCE_HASH = void 0;
|
|
13
|
+
/** sha256 (first 12 hex) of the canonical media/gimp/gimp_mcp_server.py. */
|
|
14
|
+
exports.GIMP_PLUGIN_SOURCE_HASH = "93b608412db2";
|
|
15
|
+
/** Full body of the auto-start GIMP 3.x MCP plugin (media/gimp/gimp_mcp_server.py). */
|
|
16
|
+
exports.GIMP_PLUGIN_SOURCE = `#!/usr/bin/env python3
|
|
17
|
+
# -*- coding: utf-8 -*-
|
|
18
|
+
#
|
|
19
|
+
# gimp_mcp_server.py — AutoDev auto-start GIMP 3.x MCP bridge
|
|
20
|
+
#
|
|
21
|
+
# Derived from libreearth/gimp-mcp @49c46dd (GPL-3.0); modified by AutoDev to
|
|
22
|
+
# auto-start as a GIMP 3.x PERSISTENT extension. The upstream plugin registered a
|
|
23
|
+
# manual "<Image>/Filters/Development/ → Start MCP Server" ImageProcedure that the
|
|
24
|
+
# user had to click; this variant registers a PERSISTENT extension so GIMP starts
|
|
25
|
+
# the socket server automatically on launch — no menu click, no open image needed.
|
|
26
|
+
#
|
|
27
|
+
# The socket / command-marshalling logic (start_server, handle_client,
|
|
28
|
+
# execute_command, GLib.idle_add onto GIMP's main thread) is kept from upstream.
|
|
29
|
+
# ONLY the registration + run() model changed to PERSISTENT + defensive ordering.
|
|
30
|
+
#
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# GIMP 3.x plugin layout (REQUIRED): this file must live at
|
|
33
|
+
# <gimp-config>/<ver>/plug-ins/gimp_mcp_server/gimp_mcp_server.py
|
|
34
|
+
# i.e. inside a sub-folder whose name == this file's basename. A loose .py is
|
|
35
|
+
# IGNORED by GIMP 3.x. On Linux/macOS it must ALSO be executable (chmod 0o755).
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
#
|
|
38
|
+
# Copyright (C) 2023 libreearth (upstream, GPL-3.0)
|
|
39
|
+
# Modifications Copyright (C) 2025 AutoDev
|
|
40
|
+
#
|
|
41
|
+
# This program is free software: you can redistribute it and/or modify it under
|
|
42
|
+
# the terms of the GNU General Public License as published by the Free Software
|
|
43
|
+
# Foundation, either version 3 of the License, or (at your option) any later
|
|
44
|
+
# version.
|
|
45
|
+
#
|
|
46
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
47
|
+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
|
48
|
+
# PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
49
|
+
#
|
|
50
|
+
# You should have received a copy of the GNU General Public License along with
|
|
51
|
+
# this program. If not, see <https://www.gnu.org/licenses/>. A copy is vendored
|
|
52
|
+
# next to this file as LICENSE.
|
|
53
|
+
|
|
54
|
+
import os
|
|
55
|
+
import sys
|
|
56
|
+
import json
|
|
57
|
+
import socket
|
|
58
|
+
import threading
|
|
59
|
+
import traceback
|
|
60
|
+
import datetime
|
|
61
|
+
|
|
62
|
+
import gi
|
|
63
|
+
gi.require_version('Gimp', '3.0')
|
|
64
|
+
from gi.repository import Gimp
|
|
65
|
+
from gi.repository import GLib
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# Logging — an auto-start extension has NO console, so every diagnostic goes to
|
|
70
|
+
# a log file the user can read. We resolve it next to this plug-in first (always
|
|
71
|
+
# writable if the plug-in loaded at all), falling back to the GIMP config dir.
|
|
72
|
+
# Nothing here may raise: a logging failure must never stop GIMP from launching.
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
def _log_path():
|
|
75
|
+
try:
|
|
76
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
77
|
+
return os.path.join(here, 'gimp-mcp-server.log')
|
|
78
|
+
except Exception:
|
|
79
|
+
try:
|
|
80
|
+
return os.path.join(GLib.get_user_config_dir(), 'gimp-mcp-server.log')
|
|
81
|
+
except Exception:
|
|
82
|
+
return os.path.join(os.path.expanduser('~'), 'gimp-mcp-server.log')
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
_LOG_FILE = _log_path()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def log(msg):
|
|
89
|
+
"""Best-effort append to the log file AND stderr. Never raises."""
|
|
90
|
+
line = '[%s] %s' % (datetime.datetime.now().isoformat(timespec='seconds'), msg)
|
|
91
|
+
try:
|
|
92
|
+
with open(_LOG_FILE, 'a', encoding='utf-8') as fh:
|
|
93
|
+
fh.write(line + '\\n')
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
try:
|
|
97
|
+
sys.stderr.write(line + '\\n')
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class MCPPlugin(Gimp.PlugIn):
|
|
103
|
+
# ── Registration ──────────────────────────────────────────────────────
|
|
104
|
+
def do_query_procedures(self):
|
|
105
|
+
return ["plug-in-mcp-server-autostart"]
|
|
106
|
+
|
|
107
|
+
def do_create_procedure(self, name):
|
|
108
|
+
# PERSISTENT + no args + no menu = GIMP auto-starts this extension at
|
|
109
|
+
# launch. Adding ANY argument, a menu label, or a menu path would turn
|
|
110
|
+
# it back into a user-invoked plug-in and disable auto-start.
|
|
111
|
+
procedure = Gimp.Procedure.new(
|
|
112
|
+
self, name, Gimp.PDBProcType.PERSISTENT, self.run, None,
|
|
113
|
+
)
|
|
114
|
+
try:
|
|
115
|
+
procedure.set_documentation(
|
|
116
|
+
"AutoDev MCP server (auto-start)",
|
|
117
|
+
"Auto-starts a localhost:9876 socket bridge so external MCP "
|
|
118
|
+
"clients can drive GIMP. Derived from libreearth/gimp-mcp "
|
|
119
|
+
"(GPL-3.0), modified by AutoDev to run as a PERSISTENT extension.",
|
|
120
|
+
name,
|
|
121
|
+
)
|
|
122
|
+
procedure.set_attribution("AutoDev", "AutoDev (GPL-3.0)", "2025")
|
|
123
|
+
except Exception as e:
|
|
124
|
+
# Attribution/doc setters are cosmetic — never let a binding
|
|
125
|
+
# mismatch here prevent the procedure from registering.
|
|
126
|
+
log("do_create_procedure: non-fatal setter error: %s" % e)
|
|
127
|
+
return procedure
|
|
128
|
+
|
|
129
|
+
# ── Extension entry point ─────────────────────────────────────────────
|
|
130
|
+
def run(self, procedure, config, run_data):
|
|
131
|
+
"""PERSISTENT extension entry point.
|
|
132
|
+
|
|
133
|
+
DEFENSIVE ORDER (the brick-risk part): call persistent_ready() FIRST.
|
|
134
|
+
Its ABSENCE is what hangs GIMP at startup, so it must run before
|
|
135
|
+
anything that could throw. Everything after it is wrapped so a bind
|
|
136
|
+
failure logs + degrades instead of taking GIMP core down with it.
|
|
137
|
+
"""
|
|
138
|
+
# 1) Hand control back to GIMP core IMMEDIATELY so the UI keeps loading.
|
|
139
|
+
try:
|
|
140
|
+
procedure.persistent_ready()
|
|
141
|
+
log("persistent_ready() OK — extension is live")
|
|
142
|
+
except Exception as e:
|
|
143
|
+
# A binding mismatch on this exact call is the known lock-up cause;
|
|
144
|
+
# log it (do NOT re-raise) so GIMP still finishes launching.
|
|
145
|
+
log("persistent_ready() FAILED (non-fatal): %s\\n%s"
|
|
146
|
+
% (e, traceback.format_exc()))
|
|
147
|
+
|
|
148
|
+
# 2) Start the socket server on a daemon thread. A bind failure (port in
|
|
149
|
+
# use, sandbox denial) MUST NOT crash the process or block launch.
|
|
150
|
+
try:
|
|
151
|
+
t = threading.Thread(target=self.start_server, daemon=True)
|
|
152
|
+
t.start()
|
|
153
|
+
log("MCP server thread started (listening on localhost:9876)")
|
|
154
|
+
except Exception as e:
|
|
155
|
+
log("failed to start MCP server thread (non-fatal): %s\\n%s"
|
|
156
|
+
% (e, traceback.format_exc()))
|
|
157
|
+
|
|
158
|
+
# 3) Keep this extension process alive for the whole GIMP session so the
|
|
159
|
+
# socket stays open and idle_add callbacks can marshal onto core.
|
|
160
|
+
try:
|
|
161
|
+
GLib.MainLoop().run()
|
|
162
|
+
except Exception as e:
|
|
163
|
+
log("main loop exited (non-fatal): %s" % e)
|
|
164
|
+
|
|
165
|
+
# 4) Standard PERSISTENT success return.
|
|
166
|
+
return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())
|
|
167
|
+
|
|
168
|
+
# ── Socket server (kept from upstream) ────────────────────────────────
|
|
169
|
+
def start_server(self):
|
|
170
|
+
host = 'localhost'
|
|
171
|
+
port = 9876
|
|
172
|
+
sock = None
|
|
173
|
+
try:
|
|
174
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
175
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
176
|
+
sock.bind((host, port))
|
|
177
|
+
sock.listen(1)
|
|
178
|
+
log("server listening on %s:%d" % (host, port))
|
|
179
|
+
except Exception as e:
|
|
180
|
+
# Bind/listen failed (e.g. port already taken by Blender's MCP or a
|
|
181
|
+
# second GIMP). Log + bail out of the thread; GIMP keeps running.
|
|
182
|
+
log("bind/listen failed (non-fatal): %s\\n%s" % (e, traceback.format_exc()))
|
|
183
|
+
try:
|
|
184
|
+
if sock is not None:
|
|
185
|
+
sock.close()
|
|
186
|
+
except Exception:
|
|
187
|
+
pass
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
while True:
|
|
191
|
+
try:
|
|
192
|
+
client, addr = sock.accept()
|
|
193
|
+
log("client connected: %s" % (addr,))
|
|
194
|
+
threading.Thread(
|
|
195
|
+
target=self.handle_client, args=(client,), daemon=True,
|
|
196
|
+
).start()
|
|
197
|
+
except Exception as e:
|
|
198
|
+
log("accept error: %s" % e)
|
|
199
|
+
break
|
|
200
|
+
try:
|
|
201
|
+
sock.close()
|
|
202
|
+
except Exception:
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
def handle_client(self, client):
|
|
206
|
+
buffer = b''
|
|
207
|
+
while True:
|
|
208
|
+
try:
|
|
209
|
+
data = client.recv(1024)
|
|
210
|
+
if not data:
|
|
211
|
+
break
|
|
212
|
+
buffer += data
|
|
213
|
+
try:
|
|
214
|
+
command = json.loads(buffer.decode('utf-8'))
|
|
215
|
+
buffer = b''
|
|
216
|
+
# Marshal onto GIMP's main thread — the PDB is not thread-safe.
|
|
217
|
+
GLib.idle_add(self.execute_command, command, client)
|
|
218
|
+
except json.JSONDecodeError:
|
|
219
|
+
# Partial frame — keep buffering until it parses.
|
|
220
|
+
continue
|
|
221
|
+
except Exception as e:
|
|
222
|
+
log("client error: %s" % e)
|
|
223
|
+
break
|
|
224
|
+
try:
|
|
225
|
+
client.close()
|
|
226
|
+
except Exception:
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
def execute_command(self, command, client):
|
|
230
|
+
"""Runs on GIMP's main thread via idle_add. Guarded so a bad command or
|
|
231
|
+
a wrong binding logs + returns an error frame rather than hanging."""
|
|
232
|
+
response = {}
|
|
233
|
+
try:
|
|
234
|
+
cmd_type = command.get("type")
|
|
235
|
+
params = command.get("params", {})
|
|
236
|
+
|
|
237
|
+
if cmd_type == "call_api":
|
|
238
|
+
api_path = params["api_path"]
|
|
239
|
+
args = params.get("args", [])
|
|
240
|
+
kwargs = params.get("kwargs", {})
|
|
241
|
+
|
|
242
|
+
module = Gimp
|
|
243
|
+
for part in api_path.split("."):
|
|
244
|
+
module = getattr(module, part)
|
|
245
|
+
|
|
246
|
+
result = module(*args, **kwargs)
|
|
247
|
+
|
|
248
|
+
if hasattr(result, "ID"):
|
|
249
|
+
result = {"id": result.ID}
|
|
250
|
+
elif isinstance(result, list):
|
|
251
|
+
result = [self.serialize_gimp_object(item) for item in result]
|
|
252
|
+
else:
|
|
253
|
+
result = str(result)
|
|
254
|
+
|
|
255
|
+
response = {"status": "success", "result": result}
|
|
256
|
+
else:
|
|
257
|
+
response = {"status": "error", "message": "Unknown command: %s" % cmd_type}
|
|
258
|
+
except Exception as e:
|
|
259
|
+
response = {"status": "error", "message": str(e),
|
|
260
|
+
"traceback": traceback.format_exc()}
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
client.sendall(json.dumps(response).encode('utf-8'))
|
|
264
|
+
except Exception as e:
|
|
265
|
+
log("failed to send response: %s" % e)
|
|
266
|
+
return False # one-shot idle callback
|
|
267
|
+
|
|
268
|
+
def serialize_gimp_object(self, obj):
|
|
269
|
+
try:
|
|
270
|
+
if hasattr(obj, "ID"):
|
|
271
|
+
return {"id": obj.ID, "type": obj.__class__.__name__}
|
|
272
|
+
except Exception:
|
|
273
|
+
pass
|
|
274
|
+
return str(obj)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
Gimp.main(MCPPlugin.__gtype__, sys.argv)
|
|
279
|
+
except Exception as _e:
|
|
280
|
+
# If GIMP's main dispatch itself fails, log it — never let the plug-in
|
|
281
|
+
# process take the launch down with an unhandled traceback.
|
|
282
|
+
log("Gimp.main failed: %s\\n%s" % (_e, traceback.format_exc()))
|
|
283
|
+
`;
|
|
284
|
+
//# sourceMappingURL=gimpPluginSource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gimpPluginSource.js","sourceRoot":"","sources":["../../src/core/gimpPluginSource.ts"],"names":[],"mappings":";AAAA,8EAA8E;AAC9E,wDAAwD;AACxD,EAAE;AACF,oFAAoF;AACpF,wFAAwF;AACxF,EAAE;AACF,+EAA+E;AAC/E,kFAAkF;AAClF,8EAA8E;;;AAE9E,4EAA4E;AAC/D,QAAA,uBAAuB,GAAG,cAAc,CAAC;AAEtD,uFAAuF;AAC1E,QAAA,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2QjC,CAAC"}
|