editmamei 0.24.1 → 1.0.0
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/NOTICES.md +6 -30
- package/README.md +39 -7
- package/dist/api/extendscript/_helpers.js +1 -1
- package/dist/api/photoshop-api.js +20 -14
- package/dist/bin/editmamei-core-darwin-arm64 +0 -0
- package/dist/bin/editmamei-core-darwin-x64 +0 -0
- package/dist/bin/editmamei-core-win-x64.exe +0 -0
- package/dist/core/server.js +6 -6
- package/dist/core/session.js +9 -10
- package/dist/core/tool-registry.js +4 -4
- package/dist/delivery/store.js +10 -2
- package/dist/detection/detect-active-doc.js +1 -1
- package/dist/kernel/module-lifecycle.js +7 -1
- package/dist/perception/region-precompute.js +13 -5
- package/dist/perception/select-recipes.js +2 -2
- package/dist/perception/sky-mask-transfer.js +1 -1
- package/dist/platform/connection.js +32 -46
- package/dist/platform/host-platform.js +16 -0
- package/dist/platform/macos-detector.js +41 -71
- package/dist/platform/macos-runner.js +94 -0
- package/dist/platform/script-result.js +39 -0
- package/dist/platform/windows-detector.js +44 -68
- package/dist/platform/windows-runner.js +84 -0
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/tools/adjustment-tools.js +18 -2
- package/dist/tools/brush-tools.js +1 -1
- package/dist/tools/document-tools.js +9 -2
- package/dist/tools/filter-tools.js +1 -1
- package/dist/tools/layer-tools.js +132 -7
- package/dist/tools/path-tools.js +1 -1
- package/dist/tools/preview-tools.js +8 -8
- package/dist/tools/retouch-tools.js +1 -1
- package/dist/tools/scene-tools.js +29 -2
- package/dist/tools/selection-tools.js +81 -3
- package/dist/update/check.js +1 -1
- package/dist/utils/logger.js +53 -46
- package/dist/utils/session-log.js +1 -1
- package/dist/utils/tool-helpers.js +9 -3
- package/dist/version.js +1 -1
- package/package.json +10 -3
- package/LICENSE +0 -81
- package/dist/platform/detector.js +0 -32
- package/dist/platform/macos-executor.js +0 -96
- package/dist/platform/windows-executor.js +0 -105
- /package/dist/platform/{script-executor.js → ports.js} +0 -0
- /package/dist/platform/{executor-queue.js → script-queue.js} +0 -0
package/dist/utils/logger.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { recordLogLine } from './log-buffer.js';
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
function
|
|
10
|
-
if (
|
|
11
|
-
const parts = [`${
|
|
12
|
-
if (
|
|
13
|
-
parts.push(
|
|
2
|
+
export const LOG_LEVELS = {
|
|
3
|
+
debug: 0,
|
|
4
|
+
info: 1,
|
|
5
|
+
warn: 2,
|
|
6
|
+
error: 3,
|
|
7
|
+
};
|
|
8
|
+
const LEVEL_LABELS = ['DEBUG', 'INFO', 'WARN', 'ERROR'];
|
|
9
|
+
function describeLogValue(value) {
|
|
10
|
+
if (value instanceof Error) {
|
|
11
|
+
const parts = [`${value.name}: ${value.message}`];
|
|
12
|
+
if (value.stack)
|
|
13
|
+
parts.push(value.stack);
|
|
14
14
|
const extras = {};
|
|
15
|
-
for (const key of Object.keys(
|
|
15
|
+
for (const key of Object.keys(value)) {
|
|
16
16
|
if (key !== 'name' && key !== 'message' && key !== 'stack') {
|
|
17
|
-
extras[key] =
|
|
17
|
+
extras[key] = value[key];
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
if (Object.keys(extras).length > 0) {
|
|
@@ -26,35 +26,40 @@ function serializeLogArg(arg) {
|
|
|
26
26
|
}
|
|
27
27
|
return parts.join('\n');
|
|
28
28
|
}
|
|
29
|
-
if (
|
|
30
|
-
return String(
|
|
31
|
-
if (typeof
|
|
29
|
+
if (value === null || value === undefined)
|
|
30
|
+
return String(value);
|
|
31
|
+
if (typeof value === 'object') {
|
|
32
32
|
try {
|
|
33
|
-
return JSON.stringify(
|
|
33
|
+
return JSON.stringify(value);
|
|
34
34
|
}
|
|
35
35
|
catch {
|
|
36
|
-
return String(
|
|
36
|
+
return String(value);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
return String(
|
|
39
|
+
return String(value);
|
|
40
|
+
}
|
|
41
|
+
function formatRecord(record) {
|
|
42
|
+
const prefix = `[${record.at.toISOString()}] [${LEVEL_LABELS[record.level]}] [${record.scope}]`;
|
|
43
|
+
const details = record.details.map(describeLogValue).join(' ');
|
|
44
|
+
return `${prefix} ${record.message} ${details}`.trim();
|
|
40
45
|
}
|
|
41
46
|
export function parseLogLevel(raw, fallback) {
|
|
42
47
|
if (raw === undefined || raw === '')
|
|
43
48
|
return fallback;
|
|
44
49
|
const asNum = Number(raw);
|
|
45
|
-
if (Number.isFinite(asNum) && asNum >=
|
|
50
|
+
if (Number.isFinite(asNum) && asNum >= LOG_LEVELS.debug && asNum <= LOG_LEVELS.error) {
|
|
46
51
|
return asNum;
|
|
47
52
|
}
|
|
48
53
|
switch (raw.toUpperCase()) {
|
|
49
54
|
case 'DEBUG':
|
|
50
|
-
return
|
|
55
|
+
return LOG_LEVELS.debug;
|
|
51
56
|
case 'INFO':
|
|
52
|
-
return
|
|
57
|
+
return LOG_LEVELS.info;
|
|
53
58
|
case 'WARN':
|
|
54
59
|
case 'WARNING':
|
|
55
|
-
return
|
|
60
|
+
return LOG_LEVELS.warn;
|
|
56
61
|
case 'ERROR':
|
|
57
|
-
return
|
|
62
|
+
return LOG_LEVELS.error;
|
|
58
63
|
default:
|
|
59
64
|
return fallback;
|
|
60
65
|
}
|
|
@@ -66,39 +71,41 @@ function isVerboseEnabled(v) {
|
|
|
66
71
|
return s === 'true' || s === '1';
|
|
67
72
|
}
|
|
68
73
|
export class Logger {
|
|
69
|
-
|
|
74
|
+
scope;
|
|
70
75
|
logLevel;
|
|
71
|
-
constructor(
|
|
72
|
-
this.
|
|
76
|
+
constructor(scope, logLevel = LOG_LEVELS.info) {
|
|
77
|
+
this.scope = scope;
|
|
73
78
|
const fallback = isVerboseEnabled(process.env.EDITMAMEI_VERBOSE_LOGGING)
|
|
74
|
-
?
|
|
79
|
+
? LOG_LEVELS.debug
|
|
75
80
|
: logLevel;
|
|
76
81
|
this.logLevel = parseLogLevel(process.env.LOG_LEVEL, fallback);
|
|
77
82
|
}
|
|
78
|
-
log(level, message, ...
|
|
83
|
+
log(level, message, ...details) {
|
|
79
84
|
if (level < this.logLevel)
|
|
80
85
|
return;
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
const line = formatRecord({
|
|
87
|
+
at: new Date(),
|
|
88
|
+
level,
|
|
89
|
+
scope: this.scope,
|
|
90
|
+
message,
|
|
91
|
+
details,
|
|
92
|
+
});
|
|
93
|
+
process.stderr.write(line + '\n');
|
|
94
|
+
recordLogLine(line);
|
|
88
95
|
}
|
|
89
96
|
isDebugEnabled() {
|
|
90
|
-
return this.logLevel <=
|
|
97
|
+
return this.logLevel <= LOG_LEVELS.debug;
|
|
91
98
|
}
|
|
92
|
-
debug(message, ...
|
|
93
|
-
this.log(
|
|
99
|
+
debug(message, ...details) {
|
|
100
|
+
this.log(LOG_LEVELS.debug, message, ...details);
|
|
94
101
|
}
|
|
95
|
-
info(message, ...
|
|
96
|
-
this.log(
|
|
102
|
+
info(message, ...details) {
|
|
103
|
+
this.log(LOG_LEVELS.info, message, ...details);
|
|
97
104
|
}
|
|
98
|
-
warn(message, ...
|
|
99
|
-
this.log(
|
|
105
|
+
warn(message, ...details) {
|
|
106
|
+
this.log(LOG_LEVELS.warn, message, ...details);
|
|
100
107
|
}
|
|
101
|
-
error(message, ...
|
|
102
|
-
this.log(
|
|
108
|
+
error(message, ...details) {
|
|
109
|
+
this.log(LOG_LEVELS.error, message, ...details);
|
|
103
110
|
}
|
|
104
111
|
}
|
|
@@ -197,8 +197,8 @@ export class SessionLog {
|
|
|
197
197
|
};
|
|
198
198
|
}
|
|
199
199
|
async emitMeta() {
|
|
200
|
-
await this.writeLine(JSON.stringify(this.buildMetaEntry()));
|
|
201
200
|
this.metaEmitted = true;
|
|
201
|
+
await this.writeLine(JSON.stringify(this.buildMetaEntry()));
|
|
202
202
|
}
|
|
203
203
|
async append(entry, result) {
|
|
204
204
|
if (!this.metaEmitted) {
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { runScript } from './run-script.js';
|
|
2
2
|
import { validateArgs } from './validate.js';
|
|
3
3
|
const PENDING_STATE_HINT = ' This usually means an operation is pending in Photoshop (active transform, open dialog, uncommitted text edit). Commit or cancel it in Photoshop and retry.';
|
|
4
|
-
const PENDING_STATE_RE = /pending|in progress|modal|currently in|is open/i;
|
|
4
|
+
const PENDING_STATE_RE = /pending|in progress|modal|currently in|(?:dialog|window|panel) is open/i;
|
|
5
5
|
const HAS_OWN_ADVICE_RE = /dismiss any open Photoshop dialog/i;
|
|
6
6
|
export function toolErrorResult(prefix, error) {
|
|
7
7
|
const msg = error instanceof Error ? error.message : String(error);
|
|
8
|
-
const
|
|
8
|
+
const wantsHint = PENDING_STATE_RE.test(msg) && !HAS_OWN_ADVICE_RE.test(msg);
|
|
9
|
+
const separator = wantsHint && !/[.!?]$/.test(msg.trimEnd()) ? '.' : '';
|
|
9
10
|
return {
|
|
10
|
-
content: [
|
|
11
|
+
content: [
|
|
12
|
+
{
|
|
13
|
+
type: 'text',
|
|
14
|
+
text: `${prefix}: ${msg}${separator}${wantsHint ? PENDING_STATE_HINT : ''}`,
|
|
15
|
+
},
|
|
16
|
+
],
|
|
11
17
|
isError: true,
|
|
12
18
|
};
|
|
13
19
|
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '1.0.0';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "editmamei",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Photoshop MCP server: natural-language AI photo editing with your own Photoshop (Community Edition)",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -18,16 +18,23 @@
|
|
|
18
18
|
},
|
|
19
19
|
"keywords": [
|
|
20
20
|
"mcp",
|
|
21
|
+
"mcp-server",
|
|
21
22
|
"photoshop",
|
|
23
|
+
"photoshop-mcp",
|
|
22
24
|
"adobe",
|
|
23
25
|
"automation",
|
|
26
|
+
"photoshop-automation",
|
|
24
27
|
"ai",
|
|
28
|
+
"ai-photo-editing",
|
|
29
|
+
"photoshop-assistant",
|
|
25
30
|
"model-context-protocol",
|
|
26
31
|
"image-editing",
|
|
32
|
+
"batch-editing",
|
|
33
|
+
"claude",
|
|
27
34
|
"editmamei"
|
|
28
35
|
],
|
|
29
36
|
"author": "Editmamei (https://editmamei.com)",
|
|
30
|
-
"license": "
|
|
37
|
+
"license": "FSL-1.1-MIT",
|
|
31
38
|
"repository": {
|
|
32
39
|
"type": "git",
|
|
33
40
|
"url": "git+https://github.com/editmamei/editmamei.git"
|
package/LICENSE
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
Editmamei License Agreement
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 EMBD Artifacts LLC. All rights reserved.
|
|
4
|
-
|
|
5
|
-
This License Agreement (the "Agreement") governs your use of the Editmamei
|
|
6
|
-
software distributed in this package — including the command-line interface,
|
|
7
|
-
the Model Context Protocol (MCP) server, the bundled engine binaries, and all
|
|
8
|
-
accompanying files (collectively, the "Software"). By installing, copying, or
|
|
9
|
-
using the Software, you agree to be bound by this Agreement. If you do not
|
|
10
|
-
agree, do not install or use the Software.
|
|
11
|
-
|
|
12
|
-
1. License grant.
|
|
13
|
-
Subject to your compliance with this Agreement, EMBD Artifacts LLC grants
|
|
14
|
-
you a personal, worldwide, royalty-free, non-exclusive, non-transferable,
|
|
15
|
-
and revocable license to install and use the Software, at no charge, on
|
|
16
|
-
devices you own or control, for your own personal or commercial
|
|
17
|
-
photo-editing work.
|
|
18
|
-
|
|
19
|
-
2. Paid features.
|
|
20
|
-
Some features (the "Pro" features) are not included in the no-charge license
|
|
21
|
-
granted above and require a separate paid license. Access to Pro features is
|
|
22
|
-
governed by the additional terms presented at the time of purchase. Nothing
|
|
23
|
-
in this Agreement grants any right to Pro features without a valid paid
|
|
24
|
-
license.
|
|
25
|
-
|
|
26
|
-
3. Restrictions.
|
|
27
|
-
Except as expressly permitted in Section 1, or to the extent a restriction
|
|
28
|
-
below is prohibited by applicable law, you may not:
|
|
29
|
-
(a) copy (other than a reasonable number of backup copies), publish,
|
|
30
|
-
distribute, sublicense, rent, lease, lend, or sell the Software;
|
|
31
|
-
(b) modify, adapt, translate, or create derivative works of the Software;
|
|
32
|
-
(c) reverse engineer, decompile, or disassemble the Software, or otherwise
|
|
33
|
-
attempt to derive its source code or underlying ideas;
|
|
34
|
-
(d) remove, obscure, or alter any copyright, trademark, or other proprietary
|
|
35
|
-
notice in the Software or this Agreement; or
|
|
36
|
-
(e) circumvent, disable, or interfere with any licensing, entitlement, or
|
|
37
|
-
technical protection measure, including to access Pro features without a
|
|
38
|
-
valid paid license.
|
|
39
|
-
|
|
40
|
-
4. Ownership.
|
|
41
|
-
The Software is licensed, not sold. EMBD Artifacts LLC and its licensors
|
|
42
|
-
retain all right, title, and interest in and to the Software, including all
|
|
43
|
-
intellectual property rights. All rights not expressly granted in this
|
|
44
|
-
Agreement are reserved.
|
|
45
|
-
|
|
46
|
-
5. Third-party components.
|
|
47
|
-
The Software incorporates components provided by third parties under their
|
|
48
|
-
own licenses. Those licenses continue to apply to the corresponding
|
|
49
|
-
components and, in the event of a conflict, govern those components only.
|
|
50
|
-
Required attributions and notices are reproduced in the accompanying
|
|
51
|
-
NOTICES.md file; the full license text for each dependency ships with that
|
|
52
|
-
dependency through the npm package manager (see node_modules/<package>/
|
|
53
|
-
after install).
|
|
54
|
-
|
|
55
|
-
6. Term and termination.
|
|
56
|
-
This Agreement is effective until terminated. The license granted in
|
|
57
|
-
Section 1 terminates automatically, without notice, if you breach any term
|
|
58
|
-
of this Agreement. Upon termination you must cease all use of the Software
|
|
59
|
-
and delete all copies in your possession or control. Sections 3 through 9
|
|
60
|
-
survive termination.
|
|
61
|
-
|
|
62
|
-
7. Disclaimer of warranty.
|
|
63
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
64
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
65
|
-
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
|
|
66
|
-
|
|
67
|
-
8. Limitation of liability.
|
|
68
|
-
IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES, OR
|
|
69
|
-
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE,
|
|
70
|
-
ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
71
|
-
DEALINGS IN THE SOFTWARE.
|
|
72
|
-
|
|
73
|
-
9. General.
|
|
74
|
-
This Agreement is the entire agreement between you and EMBD Artifacts LLC
|
|
75
|
-
regarding the Software and supersedes any prior or contemporaneous
|
|
76
|
-
understanding. If any provision is held unenforceable, the remaining
|
|
77
|
-
provisions remain in full force and effect. A failure to enforce any
|
|
78
|
-
provision is not a waiver of it.
|
|
79
|
-
|
|
80
|
-
For commercial licensing, redistribution rights, or other inquiries, contact:
|
|
81
|
-
support@editmamei.com
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { platform } from 'os';
|
|
2
|
-
import { Logger } from '../utils/logger.js';
|
|
3
|
-
import { WindowsDetector } from './windows-detector.js';
|
|
4
|
-
import { MacOSDetector } from './macos-detector.js';
|
|
5
|
-
export class PhotoshopDetector {
|
|
6
|
-
logger;
|
|
7
|
-
platformType;
|
|
8
|
-
windowsDetector;
|
|
9
|
-
macosDetector;
|
|
10
|
-
constructor() {
|
|
11
|
-
this.logger = new Logger('PhotoshopDetector');
|
|
12
|
-
this.platformType = platform();
|
|
13
|
-
if (this.platformType === 'win32') {
|
|
14
|
-
this.windowsDetector = new WindowsDetector();
|
|
15
|
-
}
|
|
16
|
-
else if (this.platformType === 'darwin') {
|
|
17
|
-
this.macosDetector = new MacOSDetector();
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
async detect() {
|
|
21
|
-
this.logger.info(`Detecting Photoshop on ${this.platformType}...`);
|
|
22
|
-
if (this.platformType === 'win32' && this.windowsDetector) {
|
|
23
|
-
return await this.windowsDetector.detect();
|
|
24
|
-
}
|
|
25
|
-
else if (this.platformType === 'darwin' && this.macosDetector) {
|
|
26
|
-
return await this.macosDetector.detect();
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
throw new Error(`Unsupported platform: ${this.platformType}`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { exec, spawn } from 'child_process';
|
|
2
|
-
import { promisify } from 'util';
|
|
3
|
-
import { Logger } from '../utils/logger.js';
|
|
4
|
-
import { TempDir } from '../utils/temp.js';
|
|
5
|
-
import { runChildWithTimeout } from './run-child.js';
|
|
6
|
-
import { ScriptQueue } from './executor-queue.js';
|
|
7
|
-
const execAsync = promisify(exec);
|
|
8
|
-
export class MacOSExecutor {
|
|
9
|
-
logger;
|
|
10
|
-
queue;
|
|
11
|
-
appName = 'Adobe Photoshop 2025';
|
|
12
|
-
constructor() {
|
|
13
|
-
this.logger = new Logger('MacOSExecutor');
|
|
14
|
-
this.queue = new ScriptQueue(this.logger);
|
|
15
|
-
}
|
|
16
|
-
setAppName(appName) {
|
|
17
|
-
if (/["\\\r\n\t]/.test(appName)) {
|
|
18
|
-
throw new Error('appName contains a character that would break AppleScript interpolation');
|
|
19
|
-
}
|
|
20
|
-
this.appName = appName;
|
|
21
|
-
this.logger.debug(`App name set to: ${appName}`);
|
|
22
|
-
}
|
|
23
|
-
async execute(script, timeout = 30000) {
|
|
24
|
-
return this.queue.enqueue(() => this.executeScript(script, timeout), timeout);
|
|
25
|
-
}
|
|
26
|
-
async executeScript(script, timeout) {
|
|
27
|
-
const dir = await TempDir.create('editmamei-mac-');
|
|
28
|
-
try {
|
|
29
|
-
const jsxPath = await dir.write('script.jsx', script);
|
|
30
|
-
const wrapperPath = await dir.write('wrapper.scpt', this.createAppleScriptWrapper(jsxPath, timeout));
|
|
31
|
-
const { stdout, stderr, exitCode } = await runChildWithTimeout('osascript', [wrapperPath], {
|
|
32
|
-
timeout,
|
|
33
|
-
diagLabel: 'osascript wrapper.scpt',
|
|
34
|
-
});
|
|
35
|
-
if (stderr) {
|
|
36
|
-
this.logger.warn('Script execution warning:', stderr);
|
|
37
|
-
}
|
|
38
|
-
if (exitCode !== 0) {
|
|
39
|
-
throw new Error(`osascript exited with code ${exitCode}: ${stderr.trim() || '(no stderr)'}`);
|
|
40
|
-
}
|
|
41
|
-
return this.parseResult(stdout);
|
|
42
|
-
}
|
|
43
|
-
finally {
|
|
44
|
-
await dir.cleanup();
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
createAppleScriptWrapper(jsxPath, timeoutMs = 30000) {
|
|
48
|
-
const posixPath = jsxPath.replace(/\\/g, '/');
|
|
49
|
-
if (posixPath.includes('"') || posixPath.includes('\n')) {
|
|
50
|
-
throw new Error('jsxPath contains a character that would break AppleScript interpolation');
|
|
51
|
-
}
|
|
52
|
-
const timeoutSeconds = Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.max(1, Math.ceil(timeoutMs / 1000)) : 1;
|
|
53
|
-
const encodedPath = encodeURI(posixPath).replace(/'/g, '%27');
|
|
54
|
-
return `with timeout of ${timeoutSeconds} seconds
|
|
55
|
-
\ttell application "${this.appName}"
|
|
56
|
-
\t\tdo javascript "$.evalFile(decodeURI('${encodedPath}'))"
|
|
57
|
-
\tend tell
|
|
58
|
-
end timeout`;
|
|
59
|
-
}
|
|
60
|
-
parseResult(output) {
|
|
61
|
-
const trimmed = output.trim();
|
|
62
|
-
if (trimmed.startsWith('ERROR:')) {
|
|
63
|
-
throw new Error(trimmed.substring(6).trim());
|
|
64
|
-
}
|
|
65
|
-
try {
|
|
66
|
-
return JSON.parse(trimmed);
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
return trimmed;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
async isPhotoshopRunning() {
|
|
73
|
-
try {
|
|
74
|
-
const { stdout } = await execAsync('pgrep -f "Adobe Photoshop"');
|
|
75
|
-
return stdout.trim().length > 0;
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
async launchPhotoshop(photoshopPath) {
|
|
82
|
-
return new Promise((resolve, reject) => {
|
|
83
|
-
this.logger.info(`Launching Photoshop: ${photoshopPath}`);
|
|
84
|
-
const child = spawn('open', ['-a', photoshopPath], { detached: true, stdio: 'ignore' });
|
|
85
|
-
child.unref();
|
|
86
|
-
const ready = setTimeout(() => {
|
|
87
|
-
ready.unref();
|
|
88
|
-
resolve();
|
|
89
|
-
}, 5000);
|
|
90
|
-
child.on('error', (error) => {
|
|
91
|
-
clearTimeout(ready);
|
|
92
|
-
reject(new Error(`Failed to launch Photoshop: ${error.message}`));
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import { promisify } from 'util';
|
|
3
|
-
import { exec } from 'child_process';
|
|
4
|
-
import { Logger } from '../utils/logger.js';
|
|
5
|
-
import { TempDir } from '../utils/temp.js';
|
|
6
|
-
import { runChildWithTimeout } from './run-child.js';
|
|
7
|
-
import { ScriptQueue } from './executor-queue.js';
|
|
8
|
-
const execAsync = promisify(exec);
|
|
9
|
-
export class WindowsExecutor {
|
|
10
|
-
logger;
|
|
11
|
-
queue;
|
|
12
|
-
constructor() {
|
|
13
|
-
this.logger = new Logger('WindowsExecutor');
|
|
14
|
-
this.queue = new ScriptQueue(this.logger);
|
|
15
|
-
}
|
|
16
|
-
async execute(script, timeout = 30000) {
|
|
17
|
-
return this.queue.enqueue(() => this.executeScript(script, timeout), timeout);
|
|
18
|
-
}
|
|
19
|
-
async executeScript(script, timeout) {
|
|
20
|
-
const dir = await TempDir.create('editmamei-win-');
|
|
21
|
-
try {
|
|
22
|
-
const jsxPath = await dir.write('script.jsx', script);
|
|
23
|
-
const vbsPath = await dir.write('wrapper.vbs', this.createVBSWrapper(jsxPath));
|
|
24
|
-
const { stdout, stderr } = await runChildWithTimeout('cscript', ['//nologo', vbsPath], {
|
|
25
|
-
timeout,
|
|
26
|
-
diagLabel: 'cscript wrapper.vbs',
|
|
27
|
-
});
|
|
28
|
-
if (stderr) {
|
|
29
|
-
this.logger.warn('Script execution warning:', stderr);
|
|
30
|
-
}
|
|
31
|
-
return this.parseResult(stdout);
|
|
32
|
-
}
|
|
33
|
-
finally {
|
|
34
|
-
await dir.cleanup();
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
createVBSWrapper(jsxPath) {
|
|
38
|
-
if (jsxPath.includes('"')) {
|
|
39
|
-
throw new Error('jsxPath contains a double-quote — refusing to compose VBS');
|
|
40
|
-
}
|
|
41
|
-
return `
|
|
42
|
-
On Error Resume Next
|
|
43
|
-
Dim photoshopApp
|
|
44
|
-
Set photoshopApp = CreateObject("Photoshop.Application")
|
|
45
|
-
|
|
46
|
-
If Err.Number <> 0 Then
|
|
47
|
-
WScript.Echo "ERROR: Failed to connect to Photoshop - " & Err.Description
|
|
48
|
-
WScript.Quit 1
|
|
49
|
-
End If
|
|
50
|
-
|
|
51
|
-
' Execute the JSX script. The path is the mkdtemp temp file (double-quote-free
|
|
52
|
-
' by the guard above; apostrophes are fine here), but its single backslashes
|
|
53
|
-
' must be DOUBLED here: this string is
|
|
54
|
-
' handed to DoJavaScript as ExtendScript SOURCE, and a backslash inside a quoted
|
|
55
|
-
' path there is a JS string-literal escape (a Users/Temp segment would be parsed
|
|
56
|
-
' as an escape sequence and corrupt the path). The VBS Replace doubles each
|
|
57
|
-
' backslash so ExtendScript resolves it back. Load-bearing — NOT a no-op.
|
|
58
|
-
Dim result
|
|
59
|
-
result = photoshopApp.DoJavaScript("$.evalFile(""" & Replace("${jsxPath}", "\\", "\\\\") & """);")
|
|
60
|
-
|
|
61
|
-
If Err.Number <> 0 Then
|
|
62
|
-
WScript.Echo "ERROR: " & Err.Description
|
|
63
|
-
WScript.Quit 1
|
|
64
|
-
Else
|
|
65
|
-
WScript.Echo result
|
|
66
|
-
End If
|
|
67
|
-
`.trim();
|
|
68
|
-
}
|
|
69
|
-
parseResult(output) {
|
|
70
|
-
const trimmed = output.trim();
|
|
71
|
-
if (trimmed.startsWith('ERROR:')) {
|
|
72
|
-
throw new Error(trimmed.substring(6).trim());
|
|
73
|
-
}
|
|
74
|
-
try {
|
|
75
|
-
return JSON.parse(trimmed);
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
return trimmed;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
async isPhotoshopRunning() {
|
|
82
|
-
try {
|
|
83
|
-
const { stdout } = await execAsync('tasklist /FI "IMAGENAME eq Photoshop.exe"');
|
|
84
|
-
return stdout.toLowerCase().includes('photoshop.exe');
|
|
85
|
-
}
|
|
86
|
-
catch {
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
async launchPhotoshop(photoshopPath) {
|
|
91
|
-
return new Promise((resolve, reject) => {
|
|
92
|
-
this.logger.info(`Launching Photoshop: ${photoshopPath}`);
|
|
93
|
-
const child = spawn(photoshopPath, [], { detached: true, stdio: 'ignore' });
|
|
94
|
-
child.unref();
|
|
95
|
-
const ready = setTimeout(() => {
|
|
96
|
-
ready.unref();
|
|
97
|
-
resolve();
|
|
98
|
-
}, 5000);
|
|
99
|
-
child.on('error', (error) => {
|
|
100
|
-
clearTimeout(ready);
|
|
101
|
-
reject(new Error(`Failed to launch Photoshop: ${error.message}`));
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
}
|
|
File without changes
|
|
File without changes
|