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/NOTICES.md
CHANGED
|
@@ -72,36 +72,12 @@ above.
|
|
|
72
72
|
terms (Apache-2.0 §4(d)) are preserved by reference to that source.
|
|
73
73
|
- Used to detect the 80 COCO object classes.
|
|
74
74
|
|
|
75
|
-
###
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
- **License:** Apache-2.0 (the model is permissively licensed by Google;
|
|
82
|
-
commercial-safe — see `docs/product/pro-vision-models.md` for the weights-license
|
|
83
|
-
diligence). Any upstream `NOTICE`/attribution terms (Apache-2.0 §4(d)) are
|
|
84
|
-
preserved by reference to those sources.
|
|
85
|
-
- Runs on-device via the same `onnxruntime-web` backend; the image never leaves the
|
|
86
|
-
machine. Used by the Pro `ps_detect_landmarks` tool to locate the 468-point
|
|
87
|
-
face mesh (eyes, brows, lips, nose, jaw). Ships in the **Pro module**
|
|
88
|
-
(`models/pro/`), not the CE tarball.
|
|
89
|
-
|
|
90
|
-
### MobileSAM (`models/pro/mobile_sam_{encoder,decoder}.onnx`) — promptable segmentation (Pro)
|
|
91
|
-
|
|
92
|
-
- **Upstream:** MobileSAM ("Faster Segment Anything: Towards Lightweight SAM for
|
|
93
|
-
Mobile Applications", Zhang et al.; `ChaoningZhang/MobileSAM`) — a distilled
|
|
94
|
-
TinyViT image encoder paired with the Segment Anything prompt-mask decoder,
|
|
95
|
-
exported to ONNX (encoder + decoder). Redistributed here as an **FP16** build
|
|
96
|
-
converted from the FP32 ONNX export (halves the download; INT8 dynamic
|
|
97
|
-
quantization was evaluated and rejected — it collapsed the encoder to
|
|
98
|
-
whole-frame masks).
|
|
99
|
-
- **License:** Apache-2.0. Any upstream `NOTICE`/attribution terms (Apache-2.0
|
|
100
|
-
§4(d)) are preserved by reference to that source.
|
|
101
|
-
- Runs on-device via the same `onnxruntime-web` backend; the image never leaves the
|
|
102
|
-
machine. Used by the Pro `ps_select_object` tool to turn a box/point prompt into a
|
|
103
|
-
pixel-accurate organic selection. Ships in the **Pro module** (`models/pro/`), not
|
|
104
|
-
the CE tarball.
|
|
75
|
+
### Separately licensed modules
|
|
76
|
+
|
|
77
|
+
The Pro module is delivered as its own artifact and bundles additional third-party
|
|
78
|
+
models. Redistribution obligations follow the artifact that carries the files, so
|
|
79
|
+
those notices and their full license texts ship **inside the Pro module**, beside
|
|
80
|
+
its manifest, rather than being enumerated here.
|
|
105
81
|
|
|
106
82
|
---
|
|
107
83
|
|
package/README.md
CHANGED
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
**Unlock Photoshop with natural-language photo editing.** AI orchestration, not generation.
|
|
4
4
|
|
|
5
|
-
(Pronounced like _edamame_. Yes, the snack.)
|
|
6
|
-
|
|
7
5
|
> Independent project, not affiliated with or endorsed by Adobe Inc.
|
|
8
6
|
|
|
9
7
|
[](https://www.npmjs.com/package/editmamei)
|
|
10
|
-
[](LICENSE.md)
|
|
11
9
|
[]()
|
|
12
10
|
|
|
13
11
|
Editmamei is a Model Context Protocol (MCP) server that drives the Adobe Photoshop you already have. You describe the edit in plain words, your AI assistant plans the steps, and your own copy of Photoshop carries them out with its standard adjustment layers, masks, selections, and filters. The AI directs and Photoshop edits. No generative model touches your pixels, and no image is uploaded to perform an edit.
|
|
@@ -108,6 +106,24 @@ These are the choices that shape the surface, and the reason an AI assistant can
|
|
|
108
106
|
|
|
109
107
|
The split is detailed in [pro-features.md](https://github.com/editmamei/editmamei-wiki/blob/main/docs/pro-features.md). Pricing is at [editmamei.com](https://editmamei.com).
|
|
110
108
|
|
|
109
|
+
This repository holds the Community source. Pro is a separately licensed module and its source is not published here; Community never imports it, so this tree builds and runs on its own.
|
|
110
|
+
|
|
111
|
+
## Build from source
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
git clone https://github.com/editmamei/editmamei.git
|
|
115
|
+
cd editmamei
|
|
116
|
+
npm install
|
|
117
|
+
npm run build
|
|
118
|
+
npm test
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
You need Node.js 20+ and a Go toolchain (the build compiles the `editmamei-core` binary that generates Photoshop scripts). The build warns instead of failing when Go is missing, so you can still run the test suite; set `EDITMAMEI_CORE_BIN` to a prebuilt binary if you would rather not install Go.
|
|
122
|
+
|
|
123
|
+
The test suite runs without Photoshop. It verifies the ExtendScript Editmamei generates, never that Photoshop accepted it, so live verification against a real Photoshop is a separate step.
|
|
124
|
+
|
|
125
|
+
Contributions are welcome. Start with [CONTRIBUTING.md](CONTRIBUTING.md), which covers the CLA, the branch and pull-request flow, and what to include in a bug report.
|
|
126
|
+
|
|
111
127
|
## Configuration
|
|
112
128
|
|
|
113
129
|
Settings live in `~/.editmamei/settings.json`, managed by the `editmamei config` CLI:
|
|
@@ -124,17 +140,33 @@ editmamei config set ps_path "/path/to/Photoshop"
|
|
|
124
140
|
|
|
125
141
|
Editmamei runs on your computer and edits in your own Photoshop. No image content, document data, or file paths are sent to Editmamei's servers. It reports anonymous, content-free usage telemetry (on by default, turn it off with `editmamei config set telemetry.usage false`). When your AI assistant needs to see a result, Editmamei sends it a downscaled preview, the same as dropping a photo into a chat with that assistant. Every field is documented in [privacy.md](https://github.com/editmamei/editmamei-wiki/blob/main/docs/privacy.md).
|
|
126
142
|
|
|
143
|
+
The source in this repository is the same code published to npm, so none of the above has to be taken on trust.
|
|
144
|
+
|
|
127
145
|
## Docs and support
|
|
128
146
|
|
|
129
147
|
- **Features and pricing:** [editmamei.com](https://editmamei.com)
|
|
130
148
|
- **Install, getting started, FAQ, Pro features:** [the wiki](https://github.com/editmamei/editmamei-wiki)
|
|
131
149
|
- **Bugs and feature requests:** [the issue tracker](https://github.com/editmamei/editmamei-wiki/issues). If something's broken, ask your assistant to "report a problem" (or run `editmamei report`) to drop an anonymized diagnostic bundle in your Downloads folder, then attach it to the issue.
|
|
132
|
-
- **Security:** see [
|
|
150
|
+
- **Security:** see [SECURITY.md](SECURITY.md) (don't file security issues publicly)
|
|
133
151
|
|
|
134
152
|
## License
|
|
135
153
|
|
|
136
|
-
|
|
154
|
+
Editmamei CE is [Fair Source](https://fair.io) software under the
|
|
155
|
+
[Functional Source License, v1.1, MIT Future License](LICENSE.md) (FSL-1.1-MIT).
|
|
156
|
+
|
|
157
|
+
You can read, run, modify, and redistribute the code for almost anything: commercial photo
|
|
158
|
+
editing, internal tools, education, research, security review. The one thing the license
|
|
159
|
+
holds back, for two years per release, is offering the code to others in a commercial
|
|
160
|
+
product or service that competes with Editmamei CE or Pro. If you redistribute the code,
|
|
161
|
+
keep the license and copyright notices with it. Two years after each version ships, that
|
|
162
|
+
version automatically becomes available under plain [MIT](LICENSE.md).
|
|
163
|
+
|
|
164
|
+
This section is a plain-English summary. The [LICENSE](LICENSE.md) file is the license;
|
|
165
|
+
where they differ, the LICENSE file controls.
|
|
166
|
+
|
|
167
|
+
Editmamei is not open source under the OSI definition, and we don't call it that. It is
|
|
168
|
+
source-available, with the full CE stack developed in the open.
|
|
137
169
|
|
|
138
|
-
|
|
170
|
+
Editmamei Pro is a separate, commercially licensed module; its source is not published.
|
|
139
171
|
|
|
140
|
-
|
|
172
|
+
Third-party dependencies keep their own licenses, listed in [NOTICES.md](NOTICES.md).
|
|
@@ -182,7 +182,7 @@ export function duplicateForOp(opName, applyToActiveLayer) {
|
|
|
182
182
|
`;
|
|
183
183
|
}
|
|
184
184
|
return `
|
|
185
|
-
//
|
|
185
|
+
// Auto-duplicate-first: the destructive op below will act on
|
|
186
186
|
// a fresh copy of the active layer so the original is preserved.
|
|
187
187
|
// Undoing the op is as simple as deleting the copy.
|
|
188
188
|
var __opOriginal = doc.activeLayer;
|
|
@@ -12,7 +12,7 @@ export class PhotoshopAPIFactory {
|
|
|
12
12
|
throw new Error('Photoshop info not available — the local install could not be detected. ' +
|
|
13
13
|
'Check that Photoshop is installed (set PHOTOSHOP_PATH to override detection).');
|
|
14
14
|
}
|
|
15
|
-
this.logger.debug(`
|
|
15
|
+
this.logger.debug(`Photoshop ${info.version} ready for scripting`);
|
|
16
16
|
return new ExtendScriptPhotoshopAPI(this.connection);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -120,17 +120,26 @@ class ExtendScriptPhotoshopAPI {
|
|
|
120
120
|
} catch (e) {}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// sees valid JSON. Handlers that expect a string still receive one.
|
|
129
|
-
return __mcpJsonEncode(result);
|
|
130
|
-
}
|
|
131
|
-
return String(result);
|
|
123
|
+
// Every outcome leaves inside an envelope, so the value a script returned
|
|
124
|
+
// is never confused with how it ended. A script that legitimately returns
|
|
125
|
+
// a string beginning with the failure marker used to come back as a thrown
|
|
126
|
+
// error; inside an envelope it round-trips untouched.
|
|
127
|
+
return __mcpJsonEncode({ __em: 1, ok: true, value: result });
|
|
132
128
|
} catch (error) {
|
|
133
|
-
|
|
129
|
+
// Photoshop's error carries more than a message. Passing the number and
|
|
130
|
+
// line through means a failure can be identified rather than string-matched
|
|
131
|
+
// on prose that changes between releases and locales.
|
|
132
|
+
var __message = '';
|
|
133
|
+
try { __message = (error && error.message) ? String(error.message) : String(error); } catch (e) { __message = 'unreadable error'; }
|
|
134
|
+
var __number = null;
|
|
135
|
+
try { if (error && typeof error.number === 'number') __number = error.number; } catch (e) {}
|
|
136
|
+
var __line = null;
|
|
137
|
+
try { if (error && typeof error.line === 'number') __line = error.line; } catch (e) {}
|
|
138
|
+
return __mcpJsonEncode({
|
|
139
|
+
__em: 1,
|
|
140
|
+
ok: false,
|
|
141
|
+
error: { message: __message, number: __number, line: __line }
|
|
142
|
+
});
|
|
134
143
|
} finally {
|
|
135
144
|
try { if (__originalRulerUnits !== null) app.preferences.rulerUnits = __originalRulerUnits; } catch (e) {}
|
|
136
145
|
try { if (__originalTypeUnits !== null) app.preferences.typeUnits = __originalTypeUnits; } catch (e) {}
|
|
@@ -139,7 +148,4 @@ class ExtendScriptPhotoshopAPI {
|
|
|
139
148
|
})();
|
|
140
149
|
`.trim();
|
|
141
150
|
}
|
|
142
|
-
getAPIType() {
|
|
143
|
-
return 'ExtendScript';
|
|
144
|
-
}
|
|
145
151
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/core/server.js
CHANGED
|
@@ -268,13 +268,13 @@ export class EditmameiServer {
|
|
|
268
268
|
}
|
|
269
269
|
setupHandlers() {
|
|
270
270
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
271
|
-
this.logger.debug('
|
|
271
|
+
this.logger.debug('Reporting the registered tool surface');
|
|
272
272
|
return {
|
|
273
273
|
tools: this.toolRegistry.list(),
|
|
274
274
|
};
|
|
275
275
|
});
|
|
276
276
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
277
|
-
this.logger.debug(`
|
|
277
|
+
this.logger.debug(`Received a call for ${request.params.name}`);
|
|
278
278
|
const args = request.params.arguments || {};
|
|
279
279
|
return this.handleToolCall(request.params.name, args);
|
|
280
280
|
});
|
|
@@ -314,7 +314,7 @@ export class EditmameiServer {
|
|
|
314
314
|
if (!alive) {
|
|
315
315
|
return {
|
|
316
316
|
content: [
|
|
317
|
-
{ type: 'text', text: '
|
|
317
|
+
{ type: 'text', text: 'Photoshop did not respond' + this.updateNote() },
|
|
318
318
|
],
|
|
319
319
|
structuredContent: { connected: false, update_available: this.updateInfo },
|
|
320
320
|
};
|
|
@@ -330,7 +330,7 @@ export class EditmameiServer {
|
|
|
330
330
|
this.logger.warn(`pingState snippet failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
331
331
|
return {
|
|
332
332
|
content: [
|
|
333
|
-
{ type: 'text', text: '
|
|
333
|
+
{ type: 'text', text: 'Photoshop did not respond' + this.updateNote() },
|
|
334
334
|
],
|
|
335
335
|
structuredContent: { connected: false, update_available: this.updateInfo },
|
|
336
336
|
};
|
|
@@ -397,12 +397,12 @@ export class EditmameiServer {
|
|
|
397
397
|
this.server.onclose = () => {
|
|
398
398
|
void this.telemetry.shutdown();
|
|
399
399
|
};
|
|
400
|
-
this.logger.info('
|
|
400
|
+
this.logger.info('Editmamei is listening on stdio');
|
|
401
401
|
}
|
|
402
402
|
async stop() {
|
|
403
403
|
await this.telemetry.shutdown();
|
|
404
404
|
await this.session.disconnect();
|
|
405
405
|
await this.sessionLog.close();
|
|
406
|
-
this.logger.info('
|
|
406
|
+
this.logger.info('Editmamei has shut down');
|
|
407
407
|
}
|
|
408
408
|
}
|
package/dist/core/session.js
CHANGED
|
@@ -3,10 +3,10 @@ import { PhotoshopConnection } from '../platform/connection.js';
|
|
|
3
3
|
import { generateSessionId } from '../utils/session-log.js';
|
|
4
4
|
export class Session {
|
|
5
5
|
logger;
|
|
6
|
-
connection = null;
|
|
7
6
|
config;
|
|
8
|
-
lastActivity;
|
|
9
7
|
sessionId;
|
|
8
|
+
connection = null;
|
|
9
|
+
lastActivity;
|
|
10
10
|
constructor(config = {}) {
|
|
11
11
|
this.logger = new Logger('Session');
|
|
12
12
|
this.config = { autoConnect: true, ...config };
|
|
@@ -23,30 +23,29 @@ export class Session {
|
|
|
23
23
|
return this.sessionId;
|
|
24
24
|
}
|
|
25
25
|
async initialize() {
|
|
26
|
-
this.logger.info('
|
|
26
|
+
this.logger.info('Starting session', this.sessionId);
|
|
27
27
|
if (this.config.autoConnect) {
|
|
28
28
|
await this.connect();
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
async connect() {
|
|
32
32
|
try {
|
|
33
|
-
this.
|
|
34
|
-
|
|
35
|
-
if (connected) {
|
|
33
|
+
const reached = await this.ensureConnection().ping();
|
|
34
|
+
if (reached) {
|
|
36
35
|
this.updateActivity();
|
|
37
|
-
this.logger.info('
|
|
36
|
+
this.logger.info('Photoshop is reachable');
|
|
38
37
|
return true;
|
|
39
38
|
}
|
|
40
|
-
this.logger.warn('
|
|
39
|
+
this.logger.warn('Photoshop did not answer');
|
|
41
40
|
return false;
|
|
42
41
|
}
|
|
43
42
|
catch (error) {
|
|
44
|
-
this.logger.error('
|
|
43
|
+
this.logger.error('Could not reach Photoshop', error);
|
|
45
44
|
return false;
|
|
46
45
|
}
|
|
47
46
|
}
|
|
48
47
|
async disconnect() {
|
|
49
|
-
this.logger.info('
|
|
48
|
+
this.logger.info('Ending session', this.sessionId);
|
|
50
49
|
}
|
|
51
50
|
getConnection() {
|
|
52
51
|
return this.ensureConnection();
|
|
@@ -10,10 +10,10 @@ export class ToolRegistry {
|
|
|
10
10
|
}
|
|
11
11
|
register(name, definition) {
|
|
12
12
|
if (this.tools.has(name)) {
|
|
13
|
-
this.logger.warn(`
|
|
13
|
+
this.logger.warn(`Replacing an already-registered tool: ${name}`);
|
|
14
14
|
}
|
|
15
15
|
this.tools.set(name, definition);
|
|
16
|
-
this.logger.debug(`Registered
|
|
16
|
+
this.logger.debug(`Registered ${name}`);
|
|
17
17
|
}
|
|
18
18
|
registerAll(definitions) {
|
|
19
19
|
for (const def of definitions) {
|
|
@@ -35,9 +35,9 @@ export class ToolRegistry {
|
|
|
35
35
|
async execute(name, args) {
|
|
36
36
|
const definition = this.tools.get(name);
|
|
37
37
|
if (!definition) {
|
|
38
|
-
throw new Error(`
|
|
38
|
+
throw new Error(`No tool is registered under the name '${name}'`);
|
|
39
39
|
}
|
|
40
|
-
this.logger.debug(`
|
|
40
|
+
this.logger.debug(`Dispatching ${name}`);
|
|
41
41
|
const started = Date.now();
|
|
42
42
|
let success = true;
|
|
43
43
|
let error;
|
package/dist/delivery/store.js
CHANGED
|
@@ -77,6 +77,14 @@ export function readInstalledModule(sku, opts = {}) {
|
|
|
77
77
|
return null;
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
+
function removeQuietly(path) {
|
|
81
|
+
try {
|
|
82
|
+
rmSync(path, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
logger.warn(`could not remove superseded directory ${path}: ${err instanceof Error ? err.message : String(err)} (harmless; the current tree is unaffected)`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
80
88
|
function writeFileAtomic(path, data) {
|
|
81
89
|
const dir = dirname(path);
|
|
82
90
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
@@ -277,12 +285,12 @@ export function loadVerifiedModule(sku, opts = {}, pubKeys = MODULE_SIGNING_PUBL
|
|
|
277
285
|
renameSync(dir, oldDir);
|
|
278
286
|
try {
|
|
279
287
|
renameSync(regenDir, dir);
|
|
280
|
-
|
|
288
|
+
removeQuietly(oldDir);
|
|
281
289
|
}
|
|
282
290
|
catch (err) {
|
|
283
291
|
rmSync(regenDir, { recursive: true, force: true });
|
|
284
292
|
if (existsSync(dir)) {
|
|
285
|
-
|
|
293
|
+
removeQuietly(oldDir);
|
|
286
294
|
}
|
|
287
295
|
else {
|
|
288
296
|
renameSync(oldDir, dir);
|
|
@@ -18,7 +18,7 @@ export async function detectActiveDoc(connection, client, opts, deps = {}) {
|
|
|
18
18
|
const tempPath = dir.path('detect.jpg');
|
|
19
19
|
const script = `
|
|
20
20
|
${getContextInfo}
|
|
21
|
-
if (app.documents.length === 0) { throw new Error('No
|
|
21
|
+
if (app.documents.length === 0) { throw new Error('No document is open in Photoshop'); }
|
|
22
22
|
for (var __pi = app.documents.length - 1; __pi >= 0; __pi--) {
|
|
23
23
|
try {
|
|
24
24
|
var __pd = app.documents[__pi];
|
|
@@ -51,8 +51,14 @@ export class ModuleLifecycle {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
if (EDITION === 'dev') {
|
|
54
|
+
const inTreeProSpecifier = '../modules/pro/index.js';
|
|
55
|
+
const inTreePresent = ['../modules/pro/index.js', '../modules/pro/index.ts'].some((rel) => existsSync(new URL(rel, import.meta.url)));
|
|
56
|
+
if (!inTreePresent) {
|
|
57
|
+
this._proModule = null;
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
54
60
|
this._proModule = {
|
|
55
|
-
importer: () => import(
|
|
61
|
+
importer: () => import(inTreeProSpecifier),
|
|
56
62
|
binDir: dirname(resolveProBinaryPath()),
|
|
57
63
|
abi: null,
|
|
58
64
|
};
|
|
@@ -26,7 +26,7 @@ const PRECOMPUTE_TARGETS = [
|
|
|
26
26
|
];
|
|
27
27
|
function deleteSceneChannelsScript() {
|
|
28
28
|
return `
|
|
29
|
-
if (app.documents.length === 0) { throw new Error('No
|
|
29
|
+
if (app.documents.length === 0) { throw new Error('No document is open in Photoshop'); }
|
|
30
30
|
var doc = app.activeDocument;
|
|
31
31
|
var removed = 0;
|
|
32
32
|
for (var i = doc.channels.length - 1; i >= 0; i--) {
|
|
@@ -63,7 +63,7 @@ function channelsExistScript(names) {
|
|
|
63
63
|
const namesLit = `[${names.map((n) => jsLit(n)).join(', ')}]`;
|
|
64
64
|
return `
|
|
65
65
|
// __mcp_scene_chk__ cheap existence check for previously-saved scene:* channels
|
|
66
|
-
if (app.documents.length === 0) { throw new Error('No
|
|
66
|
+
if (app.documents.length === 0) { throw new Error('No document is open in Photoshop'); }
|
|
67
67
|
var doc = app.activeDocument;
|
|
68
68
|
var have = {};
|
|
69
69
|
for (var i = 0; i < doc.channels.length; i++) {
|
|
@@ -92,7 +92,17 @@ async function channelsExist(connection, names) {
|
|
|
92
92
|
}
|
|
93
93
|
function loadNamedChannelScript(name) {
|
|
94
94
|
return `
|
|
95
|
-
|
|
95
|
+
// getSelectionInfo's own source begins with the restoreCompositeChannel
|
|
96
|
+
// definition (see _helpers.ts), so interpolating it here brings BOTH into
|
|
97
|
+
// scope. Interpolating restoreCompositeChannel separately as well emitted a
|
|
98
|
+
// second, byte-identical copy of that function body into every load script —
|
|
99
|
+
// legal (a later function declaration just wins) but pure waste in a string
|
|
100
|
+
// that crosses the COM/AppleScript boundary on every select_by_reference.
|
|
101
|
+
// Hoisted to the TOP so the restoreCompositeChannel(doc) call below sits
|
|
102
|
+
// after its definition textually, rather than relying on hoisting across an
|
|
103
|
+
// interpolation boundary.
|
|
104
|
+
${getSelectionInfo}
|
|
105
|
+
if (app.documents.length === 0) { throw new Error('No document is open in Photoshop'); }
|
|
96
106
|
var doc = app.activeDocument;
|
|
97
107
|
var ch = null;
|
|
98
108
|
for (var i = 0; i < doc.channels.length; i++) {
|
|
@@ -100,12 +110,10 @@ function loadNamedChannelScript(name) {
|
|
|
100
110
|
}
|
|
101
111
|
if (!ch) { return { loaded: false }; }
|
|
102
112
|
doc.selection.load(ch, SelectionType.REPLACE);
|
|
103
|
-
${restoreCompositeChannel}
|
|
104
113
|
restoreCompositeChannel(doc);
|
|
105
114
|
// Measure in the SAME round trip. The fast path used to return
|
|
106
115
|
// selection_info:null, which forced any caller that wanted to verify the mask
|
|
107
116
|
// into a second PS call — the one thing this path exists to avoid.
|
|
108
|
-
${getSelectionInfo}
|
|
109
117
|
return {
|
|
110
118
|
loaded: true,
|
|
111
119
|
width: doc.width.as('px'),
|
|
@@ -134,7 +134,7 @@ function thresholdSkyScript(level, seedX, seedY) {
|
|
|
134
134
|
${getSelectionInfo}
|
|
135
135
|
function cTID(s){ return app.charIDToTypeID(s); }
|
|
136
136
|
function sTID(s){ return app.stringIDToTypeID(s); }
|
|
137
|
-
if (app.documents.length === 0) { throw new Error('No
|
|
137
|
+
if (app.documents.length === 0) { throw new Error('No document is open in Photoshop'); }
|
|
138
138
|
var doc = app.activeDocument;
|
|
139
139
|
var __mk = new ActionDescriptor();
|
|
140
140
|
var __ref = new ActionReference();
|
|
@@ -170,7 +170,7 @@ function posterizeRegionScript(seedX, seedY) {
|
|
|
170
170
|
${getSelectionInfo}
|
|
171
171
|
function cTID(s){ return app.charIDToTypeID(s); }
|
|
172
172
|
function sTID(s){ return app.stringIDToTypeID(s); }
|
|
173
|
-
if (app.documents.length === 0) { throw new Error('No
|
|
173
|
+
if (app.documents.length === 0) { throw new Error('No document is open in Photoshop'); }
|
|
174
174
|
var doc = app.activeDocument;
|
|
175
175
|
var __mk = new ActionDescriptor();
|
|
176
176
|
var __ref = new ActionReference();
|
|
@@ -86,7 +86,7 @@ function buildLoadScript(maskPath, docW, docH, selType) {
|
|
|
86
86
|
return app.executeActionGet(r).hasKey(app.stringIDToTypeID('selection'));
|
|
87
87
|
} catch (eSe) { return false; }
|
|
88
88
|
}
|
|
89
|
-
if (app.documents.length === 0) { throw new Error('No
|
|
89
|
+
if (app.documents.length === 0) { throw new Error('No document is open in Photoshop'); }
|
|
90
90
|
var orig = app.activeDocument;
|
|
91
91
|
var combine = '${selType}';
|
|
92
92
|
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import { platform } from 'os';
|
|
2
1
|
import { Logger } from '../utils/logger.js';
|
|
3
|
-
import {
|
|
4
|
-
import { WindowsExecutor } from './windows-executor.js';
|
|
5
|
-
import { MacOSExecutor } from './macos-executor.js';
|
|
2
|
+
import { resolveHostPlatform } from './host-platform.js';
|
|
6
3
|
export function truncateForLog(s, maxLen) {
|
|
7
4
|
if (s.length <= maxLen)
|
|
8
5
|
return s;
|
|
@@ -11,37 +8,24 @@ export function truncateForLog(s, maxLen) {
|
|
|
11
8
|
export const RUNNING_LATCH_TTL_MS = 30_000;
|
|
12
9
|
export class PhotoshopConnection {
|
|
13
10
|
logger;
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
host;
|
|
12
|
+
now;
|
|
16
13
|
photoshopInfo = null;
|
|
17
14
|
detectInFlight = null;
|
|
18
|
-
macosExecutor;
|
|
19
|
-
now;
|
|
20
15
|
runningLatchUntil = 0;
|
|
21
16
|
constructor(opts = {}) {
|
|
22
17
|
this.logger = new Logger('PhotoshopConnection');
|
|
23
|
-
this.detector = new PhotoshopDetector();
|
|
24
18
|
this.now = opts.now ?? Date.now;
|
|
25
|
-
|
|
26
|
-
if (platformType === 'win32') {
|
|
27
|
-
this.executor = new WindowsExecutor();
|
|
28
|
-
}
|
|
29
|
-
else if (platformType === 'darwin') {
|
|
30
|
-
this.macosExecutor = new MacOSExecutor();
|
|
31
|
-
this.executor = this.macosExecutor;
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
throw new Error(`Unsupported platform: ${platformType}`);
|
|
35
|
-
}
|
|
19
|
+
this.host = opts.host ?? resolveHostPlatform();
|
|
36
20
|
}
|
|
37
21
|
async ping() {
|
|
38
22
|
try {
|
|
39
|
-
this.logger.debug('Pinging Photoshop
|
|
23
|
+
this.logger.debug('Pinging Photoshop');
|
|
40
24
|
const result = await this.executeScript("'pong';");
|
|
41
25
|
return typeof result === 'string' && result.trim() === 'pong';
|
|
42
26
|
}
|
|
43
27
|
catch (error) {
|
|
44
|
-
this.logger.error('Ping failed
|
|
28
|
+
this.logger.error('Ping failed', error);
|
|
45
29
|
return false;
|
|
46
30
|
}
|
|
47
31
|
}
|
|
@@ -49,11 +33,13 @@ export class PhotoshopConnection {
|
|
|
49
33
|
if (this.photoshopInfo)
|
|
50
34
|
return this.photoshopInfo;
|
|
51
35
|
if (!this.detectInFlight) {
|
|
52
|
-
this.detectInFlight = this.detector
|
|
36
|
+
this.detectInFlight = this.host.detector
|
|
53
37
|
.detect()
|
|
54
38
|
.then((info) => {
|
|
55
|
-
if (info)
|
|
39
|
+
if (info) {
|
|
40
|
+
this.host.adapter.useInstall?.(info);
|
|
56
41
|
this.photoshopInfo = info;
|
|
42
|
+
}
|
|
57
43
|
return info;
|
|
58
44
|
})
|
|
59
45
|
.finally(() => {
|
|
@@ -68,7 +54,7 @@ export class PhotoshopConnection {
|
|
|
68
54
|
return this.photoshopInfo?.version || 'Unknown';
|
|
69
55
|
}
|
|
70
56
|
catch (error) {
|
|
71
|
-
this.logger.error('Failed to
|
|
57
|
+
this.logger.error('Failed to read the Photoshop version', error);
|
|
72
58
|
throw error;
|
|
73
59
|
}
|
|
74
60
|
}
|
|
@@ -79,37 +65,37 @@ export class PhotoshopConnection {
|
|
|
79
65
|
throw new Error('Photoshop info not available — the local install could not be detected. ' +
|
|
80
66
|
'Check that Photoshop is installed (set PHOTOSHOP_PATH to override detection).');
|
|
81
67
|
}
|
|
82
|
-
|
|
83
|
-
this.macosExecutor.setAppName(this.photoshopInfo.appName);
|
|
84
|
-
}
|
|
85
|
-
if (this.now() >= this.runningLatchUntil) {
|
|
86
|
-
const isRunning = await this.executor.isPhotoshopRunning();
|
|
87
|
-
if (!isRunning) {
|
|
88
|
-
this.logger.info('Photoshop not running, launching...');
|
|
89
|
-
await this.executor.launchPhotoshop(this.photoshopInfo.path);
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
this.runningLatchUntil = this.now() + RUNNING_LATCH_TTL_MS;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
68
|
+
await this.ensureRunning(this.photoshopInfo);
|
|
95
69
|
if (this.logger.isDebugEnabled()) {
|
|
96
|
-
this.logger.debug(`Executing script (${script.length} chars, PS ${this.photoshopInfo.version}, ${
|
|
70
|
+
this.logger.debug(`Executing script (${script.length} chars, PS ${this.photoshopInfo.version}, ${this.host.os}): ${truncateForLog(script, 200)}`);
|
|
97
71
|
}
|
|
98
|
-
const result = await this.
|
|
72
|
+
const result = await this.host.adapter.run(script, timeout);
|
|
99
73
|
this.runningLatchUntil = this.now() + RUNNING_LATCH_TTL_MS;
|
|
100
74
|
return result;
|
|
101
75
|
}
|
|
102
76
|
catch (error) {
|
|
103
77
|
this.runningLatchUntil = 0;
|
|
104
|
-
|
|
105
|
-
const head = truncateForLog(script, 200);
|
|
106
|
-
this.logger.error(`Script execution failed (PS ${psVersion}, ${platform()}, ${script.length} chars):`, error, `\n--- script head (200 chars) ---\n${head}`);
|
|
107
|
-
if (process.env.LOG_SCRIPT_ON_ERROR === '1') {
|
|
108
|
-
this.logger.error(`--- full failing script ---\n${script}\n--- end script ---`);
|
|
109
|
-
}
|
|
78
|
+
this.logFailure(script, error);
|
|
110
79
|
throw error;
|
|
111
80
|
}
|
|
112
81
|
}
|
|
82
|
+
async ensureRunning(info) {
|
|
83
|
+
if (this.now() < this.runningLatchUntil)
|
|
84
|
+
return;
|
|
85
|
+
if (await this.host.adapter.isRunning()) {
|
|
86
|
+
this.runningLatchUntil = this.now() + RUNNING_LATCH_TTL_MS;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
this.logger.info('Photoshop is not running; launching it');
|
|
90
|
+
await this.host.adapter.launch(info.path);
|
|
91
|
+
}
|
|
92
|
+
logFailure(script, error) {
|
|
93
|
+
const psVersion = this.photoshopInfo?.version ?? 'unknown';
|
|
94
|
+
this.logger.error(`Script execution failed (PS ${psVersion}, ${this.host.os}, ${script.length} chars)`, error, `\n--- script head (200 chars) ---\n${truncateForLog(script, 200)}`);
|
|
95
|
+
if (process.env.LOG_SCRIPT_ON_ERROR === '1') {
|
|
96
|
+
this.logger.error(`--- full failing script ---\n${script}\n--- end script ---`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
113
99
|
getPhotoshopInfo() {
|
|
114
100
|
return this.photoshopInfo;
|
|
115
101
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { platform } from 'os';
|
|
2
|
+
import { MacOSDetector } from './macos-detector.js';
|
|
3
|
+
import { MacOSScriptRunner } from './macos-runner.js';
|
|
4
|
+
import { WindowsDetector } from './windows-detector.js';
|
|
5
|
+
import { WindowsScriptRunner } from './windows-runner.js';
|
|
6
|
+
export function resolveHostPlatform() {
|
|
7
|
+
const os = platform();
|
|
8
|
+
if (os === 'win32') {
|
|
9
|
+
return { os, adapter: new WindowsScriptRunner(), detector: new WindowsDetector() };
|
|
10
|
+
}
|
|
11
|
+
if (os === 'darwin') {
|
|
12
|
+
return { os, adapter: new MacOSScriptRunner(), detector: new MacOSDetector() };
|
|
13
|
+
}
|
|
14
|
+
throw new Error(`Editmamei runs on Windows and macOS; this process is on "${os}". ` +
|
|
15
|
+
'Adobe ships no Linux build of Photoshop, so there is nothing here to drive.');
|
|
16
|
+
}
|