editmamei 0.23.0 → 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.
Files changed (90) hide show
  1. package/NOTICES.md +6 -30
  2. package/README.md +47 -13
  3. package/dist/api/extendscript/_helpers.js +1 -101
  4. package/dist/api/photoshop-api.js +23 -16
  5. package/dist/api/snippet-client.js +38 -2
  6. package/dist/bin/editmamei-core-darwin-arm64 +0 -0
  7. package/dist/bin/editmamei-core-darwin-x64 +0 -0
  8. package/dist/bin/editmamei-core-win-x64.exe +0 -0
  9. package/dist/core/server.js +60 -184
  10. package/dist/core/session.js +9 -10
  11. package/dist/core/tool-registry.js +4 -4
  12. package/dist/delivery/client.js +50 -1
  13. package/dist/delivery/provision.js +18 -1
  14. package/dist/delivery/signing.js +38 -1
  15. package/dist/delivery/store.js +157 -16
  16. package/dist/detection/detect-active-doc.js +42 -11
  17. package/dist/detection/detection-client.js +3 -3
  18. package/dist/detection/face-detector.js +2 -2
  19. package/dist/detection/geometry.js +14 -0
  20. package/dist/detection/landmark-detection-client.js +3 -3
  21. package/dist/detection/mesh-face.js +2 -3
  22. package/dist/detection/object-detector.js +2 -2
  23. package/dist/detection/runtime.js +14 -3
  24. package/dist/detection/sam-segmenter.js +69 -26
  25. package/dist/kernel/kernel.js +5 -9
  26. package/dist/kernel/module-lifecycle.js +187 -0
  27. package/dist/license/entitlement.js +13 -3
  28. package/dist/license/store.js +8 -1
  29. package/dist/modules/ce/index.js +1 -1
  30. package/dist/perception/facets.js +19 -1
  31. package/dist/perception/grounding-anchors.js +2 -12
  32. package/dist/perception/grounding-locate.js +1 -9
  33. package/dist/perception/grounding-review-crop.js +4 -29
  34. package/dist/perception/overlay.js +62 -0
  35. package/dist/perception/pixel-identity.js +41 -0
  36. package/dist/perception/region-precompute.js +96 -20
  37. package/dist/perception/scene-model.js +93 -50
  38. package/dist/perception/select-recipes.js +109 -68
  39. package/dist/perception/sky-ground-flood.js +0 -5
  40. package/dist/perception/sky-mask-transfer.js +73 -14
  41. package/dist/platform/connection.js +60 -43
  42. package/dist/platform/host-platform.js +16 -0
  43. package/dist/platform/macos-detector.js +41 -70
  44. package/dist/platform/macos-runner.js +94 -0
  45. package/dist/platform/script-queue.js +68 -0
  46. package/dist/platform/script-result.js +39 -0
  47. package/dist/platform/windows-detector.js +49 -66
  48. package/dist/platform/windows-runner.js +84 -0
  49. package/dist/skills/editmamei-skill.zip +0 -0
  50. package/dist/tools/adjustment-tools.js +80 -132
  51. package/dist/tools/brush-tools.js +4 -15
  52. package/dist/tools/channel-compose-tools.js +4 -23
  53. package/dist/tools/detection-tools.js +59 -47
  54. package/dist/tools/document-tools.js +92 -127
  55. package/dist/tools/filter-tools.js +490 -731
  56. package/dist/tools/group-tools.js +75 -156
  57. package/dist/tools/guide-tools.js +52 -83
  58. package/dist/tools/history-tools.js +29 -63
  59. package/dist/tools/image-placement-tools.js +18 -37
  60. package/dist/tools/image-tools.js +23 -51
  61. package/dist/tools/layer-ordering-tools.js +2 -9
  62. package/dist/tools/layer-properties-tools.js +207 -395
  63. package/dist/tools/layer-tools.js +224 -190
  64. package/dist/tools/layer-transform-tools.js +41 -106
  65. package/dist/tools/metadata-tools.js +2 -9
  66. package/dist/tools/path-tools.js +4 -15
  67. package/dist/tools/portrait-tools.js +2 -5
  68. package/dist/tools/preview-tools.js +59 -72
  69. package/dist/tools/retouch-tools.js +33 -71
  70. package/dist/tools/scene-tools.js +123 -75
  71. package/dist/tools/selection-tools.js +386 -566
  72. package/dist/tools/shape-tools.js +2 -9
  73. package/dist/tools/text-tools.js +24 -57
  74. package/dist/tools/transform-canvas-tools.js +21 -54
  75. package/dist/tools/vector-mask-tools.js +2 -9
  76. package/dist/update/check.js +1 -1
  77. package/dist/utils/jpeg-quality.js +1 -0
  78. package/dist/utils/logger.js +55 -45
  79. package/dist/utils/operation-timeouts.js +1 -0
  80. package/dist/utils/run-script.js +7 -2
  81. package/dist/utils/session-log.js +134 -12
  82. package/dist/utils/template-storage.js +5 -6
  83. package/dist/utils/tool-helpers.js +43 -0
  84. package/dist/version.js +1 -1
  85. package/package.json +10 -3
  86. package/LICENSE +0 -81
  87. package/dist/platform/detector.js +0 -32
  88. package/dist/platform/macos-executor.js +0 -151
  89. package/dist/platform/windows-executor.js +0 -160
  90. /package/dist/platform/{script-executor.js → ports.js} +0 -0
@@ -3,135 +3,106 @@ import { promisify } from 'util';
3
3
  import { access, constants, readFile } from 'fs/promises';
4
4
  import { Logger } from '../utils/logger.js';
5
5
  const execFileAsync = promisify(execFile);
6
+ export function bundleDisplayName(bundlePath) {
7
+ const leaf = bundlePath.replace(/\/+$/, '').split('/').pop();
8
+ if (!leaf)
9
+ return undefined;
10
+ return leaf.endsWith('.app') ? leaf.slice(0, -'.app'.length) : leaf;
11
+ }
6
12
  export class MacOSDetector {
7
- logger;
8
- constructor() {
9
- this.logger = new Logger('MacOSDetector');
10
- }
13
+ logger = new Logger('MacOSDetector');
11
14
  async detect() {
12
- this.logger.info('Detecting Photoshop on macOS...');
13
- const envPath = process.env.PHOTOSHOP_PATH;
14
- if (envPath) {
15
- this.logger.debug(`Using environment variable: ${envPath}`);
16
- const info = await this.checkPath(envPath);
15
+ this.logger.info('Looking for a Photoshop install on macOS');
16
+ const override = process.env.PHOTOSHOP_PATH;
17
+ if (override) {
18
+ this.logger.debug('Trying PHOTOSHOP_PATH override', override);
19
+ const info = await this.inspectBundle(override);
17
20
  if (info)
18
21
  return info;
19
22
  }
20
23
  try {
21
- const spotlightInfo = await this.detectUsingSpotlight();
22
- if (spotlightInfo)
23
- return spotlightInfo;
24
+ const fromSpotlight = await this.findViaSpotlight();
25
+ if (fromSpotlight)
26
+ return fromSpotlight;
24
27
  }
25
28
  catch (error) {
26
- this.logger.warn('Spotlight detection failed:', error);
29
+ this.logger.warn('Spotlight lookup did not resolve an install', error);
27
30
  }
28
- const commonPaths = this.getCommonPaths();
29
- for (const path of commonPaths) {
30
- const info = await this.checkPath(path);
31
+ for (const path of this.conventionalPaths()) {
32
+ const info = await this.inspectBundle(path);
31
33
  if (info)
32
34
  return info;
33
35
  }
34
- throw new Error('Photoshop not found on this system');
36
+ throw new Error('No Photoshop install could be found on this machine');
35
37
  }
36
- async detectUsingSpotlight() {
38
+ async findViaSpotlight() {
37
39
  try {
38
40
  const { stdout } = await execFileAsync('mdfind', [
39
41
  'kMDItemCFBundleIdentifier == com.adobe.Photoshop',
40
42
  ]);
41
- const apps = stdout
43
+ const bundles = stdout
42
44
  .split('\n')
43
45
  .filter((line) => line.trim() && line.endsWith('.app'))
44
46
  .sort((a, b) => b.localeCompare(a));
45
- for (const appPath of apps) {
46
- const info = await this.checkPath(appPath);
47
+ for (const bundlePath of bundles) {
48
+ const info = await this.inspectBundle(bundlePath);
47
49
  if (info)
48
50
  return info;
49
51
  }
50
52
  }
51
53
  catch (error) {
52
- this.logger.debug('Spotlight search failed:', error);
54
+ this.logger.debug('Spotlight query failed', error);
53
55
  }
54
56
  return null;
55
57
  }
56
- getCommonPaths() {
58
+ conventionalPaths() {
57
59
  const paths = [];
58
- for (let year = 2025; year >= 2012; year--) {
60
+ const newestYear = Math.max(new Date().getFullYear() + 1, 2026);
61
+ for (let year = newestYear; year >= 2012; year--) {
59
62
  paths.push(`/Applications/Adobe Photoshop ${year}/Adobe Photoshop ${year}.app`, `/Applications/Adobe Photoshop CC ${year}/Adobe Photoshop CC ${year}.app`, `/Applications/Adobe Photoshop ${year}.app`);
60
63
  }
61
64
  paths.push('/Applications/Adobe Photoshop CC/Adobe Photoshop CC.app', '/Applications/Adobe Photoshop/Adobe Photoshop.app', '/Applications/Adobe Photoshop.app');
62
65
  return paths;
63
66
  }
64
- async checkPath(path) {
67
+ async inspectBundle(path) {
65
68
  try {
66
69
  const cleanPath = path.trim();
67
70
  await access(cleanPath, constants.F_OK);
68
- const version = await this.extractVersionFromApp(cleanPath);
69
- const appName = cleanPath.split('/').pop()?.replace('.app', '') || 'Adobe Photoshop 2025';
70
- this.logger.info(`Found Photoshop at: ${cleanPath}`);
71
- return {
72
- version,
73
- path: cleanPath,
74
- isRunning: await this.checkIfRunning(cleanPath),
75
- appName,
76
- };
71
+ const version = await this.readBundleVersion(cleanPath);
72
+ const appName = bundleDisplayName(cleanPath);
73
+ this.logger.info('Using Photoshop at', cleanPath);
74
+ return { version, path: cleanPath, appName };
77
75
  }
78
76
  catch {
79
77
  return null;
80
78
  }
81
79
  }
82
- async extractVersionFromApp(appPath) {
80
+ async readBundleVersion(bundlePath) {
83
81
  try {
84
- const plistPath = `${appPath}/Contents/Info.plist`;
82
+ const plistPath = `${bundlePath}/Contents/Info.plist`;
85
83
  try {
86
84
  await access(plistPath, constants.F_OK);
87
- const { stdout: version } = await execFileAsync('/usr/libexec/PlistBuddy', [
85
+ const { stdout } = await execFileAsync('/usr/libexec/PlistBuddy', [
88
86
  '-c',
89
87
  'Print :CFBundleShortVersionString',
90
88
  plistPath,
91
89
  ]);
92
- if (version.trim()) {
93
- return version.trim();
94
- }
90
+ if (stdout.trim())
91
+ return stdout.trim();
95
92
  }
96
93
  catch {
97
94
  const content = await readFile(plistPath, 'utf8');
98
95
  const versionMatch = content.match(/<key>CFBundleShortVersionString<\/key>\s*<string>([^<]+)<\/string>/);
99
- if (versionMatch) {
96
+ if (versionMatch)
100
97
  return versionMatch[1];
101
- }
102
98
  }
103
- const yearMatch = appPath.match(/(\d{4})/);
104
- if (yearMatch) {
99
+ const yearMatch = bundlePath.match(/(\d{4})/);
100
+ if (yearMatch)
105
101
  return yearMatch[1];
106
- }
107
102
  }
108
103
  catch (err) {
109
- this.logger.debug('Failed to extract version from app:', err);
104
+ this.logger.debug('Could not read a version from the bundle', err);
110
105
  }
111
106
  return 'Unknown';
112
107
  }
113
- async checkIfRunning(appPath) {
114
- try {
115
- const appName = appPath.split('/').pop()?.replace('.app', '') || 'Adobe Photoshop';
116
- const { stdout } = await execFileAsync('pgrep', ['-f', appName]);
117
- return stdout.trim().length > 0;
118
- }
119
- catch {
120
- return false;
121
- }
122
- }
123
- async getAppBundleId(appPath) {
124
- try {
125
- const plistPath = `${appPath}/Contents/Info.plist`;
126
- const { stdout } = await execFileAsync('/usr/libexec/PlistBuddy', [
127
- '-c',
128
- 'Print :CFBundleIdentifier',
129
- plistPath,
130
- ]);
131
- return stdout.trim();
132
- }
133
- catch {
134
- return null;
135
- }
136
- }
137
108
  }
@@ -0,0 +1,94 @@
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 { ScriptQueue } from './script-queue.js';
6
+ import { runChildWithTimeout } from './run-child.js';
7
+ import { decodeScriptResult } from './script-result.js';
8
+ const execAsync = promisify(exec);
9
+ const DEFAULT_SCRIPT_TIMEOUT_MS = 30_000;
10
+ const LAUNCH_GRACE_MS = 5_000;
11
+ const UNSAFE_IN_APPLESCRIPT_LITERAL = /["\\\r\n\t]/;
12
+ export class MacOSScriptRunner {
13
+ logger = new Logger('MacOSScriptRunner');
14
+ queue = new ScriptQueue(this.logger);
15
+ appName = null;
16
+ useInstall(install) {
17
+ const name = install.appName;
18
+ if (!name) {
19
+ this.logger.warn('This Photoshop install carries no application name, so AppleScript has nothing to address', install.path);
20
+ return;
21
+ }
22
+ if (UNSAFE_IN_APPLESCRIPT_LITERAL.test(name)) {
23
+ throw new Error(`Photoshop's application name (${JSON.stringify(name)}) contains a character that ` +
24
+ 'cannot be composed into AppleScript.');
25
+ }
26
+ this.appName = name;
27
+ this.logger.debug('Addressing Photoshop as', name);
28
+ }
29
+ async run(script, timeoutMs = DEFAULT_SCRIPT_TIMEOUT_MS) {
30
+ return this.queue.enqueue(() => this.runOnce(script, timeoutMs), timeoutMs);
31
+ }
32
+ async runOnce(script, timeoutMs) {
33
+ const dir = await TempDir.create('editmamei-mac-');
34
+ try {
35
+ const scriptPath = await dir.write('script.jsx', script);
36
+ const wrapperPath = await dir.write('run.scpt', this.buildAppleScriptWrapper(scriptPath, timeoutMs));
37
+ const { stdout, stderr, exitCode } = await runChildWithTimeout('osascript', [wrapperPath], {
38
+ timeout: timeoutMs,
39
+ diagLabel: 'osascript run.scpt',
40
+ });
41
+ if (stderr) {
42
+ this.logger.warn('Script execution reported a warning', stderr);
43
+ }
44
+ if (exitCode !== 0) {
45
+ throw new Error(`osascript exited with code ${exitCode}: ${stderr.trim() || '(no output on stderr)'}`);
46
+ }
47
+ return decodeScriptResult(stdout);
48
+ }
49
+ finally {
50
+ await dir.cleanup();
51
+ }
52
+ }
53
+ buildAppleScriptWrapper(scriptPath, timeoutMs = DEFAULT_SCRIPT_TIMEOUT_MS) {
54
+ if (!this.appName) {
55
+ throw new Error('No Photoshop application name has been set for this runner — the install was never ' +
56
+ 'handed to useInstall(), so there is no application for AppleScript to address.');
57
+ }
58
+ const posixPath = scriptPath.replace(/\\/g, '/');
59
+ if (posixPath.includes('"') || posixPath.includes('\n')) {
60
+ throw new Error('Refusing to compose the wrapper: script path would break the AppleScript');
61
+ }
62
+ const timeoutSeconds = Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.max(1, Math.ceil(timeoutMs / 1000)) : 1;
63
+ const encodedPath = encodeURI(posixPath).replace(/'/g, '%27');
64
+ return `with timeout of ${timeoutSeconds} seconds
65
+ \ttell application "${this.appName}"
66
+ \t\tdo javascript "$.evalFile(decodeURI('${encodedPath}'))"
67
+ \tend tell
68
+ end timeout`;
69
+ }
70
+ async isRunning() {
71
+ try {
72
+ const { stdout } = await execAsync('pgrep -f "Adobe Photoshop"');
73
+ return stdout.trim().length > 0;
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ }
79
+ async launch(executablePath) {
80
+ return new Promise((resolve, reject) => {
81
+ this.logger.info('Launching Photoshop', executablePath);
82
+ const child = spawn('open', ['-a', executablePath], { detached: true, stdio: 'ignore' });
83
+ child.unref();
84
+ const grace = setTimeout(() => {
85
+ grace.unref();
86
+ resolve();
87
+ }, LAUNCH_GRACE_MS);
88
+ child.on('error', (error) => {
89
+ clearTimeout(grace);
90
+ reject(new Error(`Could not launch Photoshop at ${executablePath}: ${error.message}`));
91
+ });
92
+ });
93
+ }
94
+ }
@@ -0,0 +1,68 @@
1
+ export const QUEUE_SLACK_MS = 3500;
2
+ export class ScriptQueue {
3
+ logger;
4
+ queue = [];
5
+ isProcessing = false;
6
+ constructor(logger) {
7
+ this.logger = logger;
8
+ }
9
+ enqueue(run, timeout) {
10
+ return new Promise((resolve, reject) => {
11
+ const task = {
12
+ run,
13
+ resolve,
14
+ reject,
15
+ cancelled: false,
16
+ settled: false,
17
+ timeoutId: null,
18
+ };
19
+ task.timeoutId = setTimeout(() => {
20
+ if (task.settled)
21
+ return;
22
+ task.cancelled = true;
23
+ task.settled = true;
24
+ task.timeoutId = null;
25
+ reject(new Error('Script execution timeout'));
26
+ }, timeout + QUEUE_SLACK_MS);
27
+ this.queue.push(task);
28
+ void this.processQueue();
29
+ });
30
+ }
31
+ settleTask(task, fn) {
32
+ if (task.settled)
33
+ return;
34
+ task.settled = true;
35
+ if (task.timeoutId !== null) {
36
+ clearTimeout(task.timeoutId);
37
+ task.timeoutId = null;
38
+ }
39
+ fn();
40
+ }
41
+ async processQueue() {
42
+ if (this.isProcessing)
43
+ return;
44
+ this.isProcessing = true;
45
+ try {
46
+ while (this.queue.length > 0) {
47
+ const task = this.queue.shift();
48
+ if (task.cancelled)
49
+ continue;
50
+ try {
51
+ const result = await task.run();
52
+ this.settleTask(task, () => task.resolve(result));
53
+ }
54
+ catch (error) {
55
+ if (task.settled) {
56
+ this.logger.error('Script execution failed after settle:', error);
57
+ }
58
+ else {
59
+ this.settleTask(task, () => task.reject(error));
60
+ }
61
+ }
62
+ }
63
+ }
64
+ finally {
65
+ this.isProcessing = false;
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,39 @@
1
+ const TRANSPORT_FAILURE_MARKER = 'ERROR:';
2
+ const ENVELOPE_TAG = '__em';
3
+ function isEnvelope(payload) {
4
+ return (typeof payload === 'object' &&
5
+ payload !== null &&
6
+ ENVELOPE_TAG in payload);
7
+ }
8
+ export class PhotoshopScriptError extends Error {
9
+ psErrorNumber;
10
+ psLine;
11
+ constructor(message, psErrorNumber = null, psLine = null) {
12
+ super(message);
13
+ this.name = 'PhotoshopScriptError';
14
+ this.psErrorNumber = psErrorNumber;
15
+ this.psLine = psLine;
16
+ }
17
+ }
18
+ function asNumberOrNull(value) {
19
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
20
+ }
21
+ export function decodeScriptResult(raw) {
22
+ const payload = raw.trim();
23
+ if (payload.startsWith(TRANSPORT_FAILURE_MARKER)) {
24
+ throw new PhotoshopScriptError(payload.slice(TRANSPORT_FAILURE_MARKER.length).trim());
25
+ }
26
+ let parsed;
27
+ try {
28
+ parsed = JSON.parse(payload);
29
+ }
30
+ catch {
31
+ return payload;
32
+ }
33
+ if (!isEnvelope(parsed))
34
+ return parsed;
35
+ if (parsed.ok)
36
+ return parsed.value;
37
+ const detail = parsed.error ?? {};
38
+ throw new PhotoshopScriptError(typeof detail.message === 'string' ? detail.message : '', asNumberOrNull(detail.number), asNumberOrNull(detail.line));
39
+ }
@@ -4,48 +4,42 @@ import { access, constants } from 'fs/promises';
4
4
  import { Logger } from '../utils/logger.js';
5
5
  const execAsync = promisify(exec);
6
6
  export class WindowsDetector {
7
- logger;
8
- constructor() {
9
- this.logger = new Logger('WindowsDetector');
10
- }
7
+ logger = new Logger('WindowsDetector');
11
8
  async detect() {
12
- this.logger.info('Detecting Photoshop on Windows...');
13
- const envPath = process.env.PHOTOSHOP_PATH;
14
- if (envPath) {
15
- this.logger.debug(`Using environment variable: ${envPath}`);
16
- const info = await this.checkPath(envPath);
9
+ this.logger.info('Looking for a Photoshop install on Windows');
10
+ const override = process.env.PHOTOSHOP_PATH;
11
+ if (override) {
12
+ this.logger.debug('Trying PHOTOSHOP_PATH override', override);
13
+ const info = await this.inspectPath(override);
17
14
  if (info)
18
15
  return info;
19
16
  }
20
17
  try {
21
- const registryInfo = await this.detectFromRegistry();
22
- if (registryInfo)
23
- return registryInfo;
18
+ const fromRegistry = await this.findViaRegistry();
19
+ if (fromRegistry)
20
+ return fromRegistry;
24
21
  }
25
22
  catch (error) {
26
- this.logger.warn('Registry detection failed:', error);
27
- }
28
- const commonPaths = this.getCommonPaths();
29
- for (const path of commonPaths) {
30
- const info = await this.checkPath(path);
31
- if (info)
32
- return info;
23
+ this.logger.warn('Registry lookup did not resolve an install', error);
33
24
  }
34
- throw new Error('Photoshop not found on this system');
25
+ const fromConventionalPaths = await this.probeCandidates(this.conventionalPaths());
26
+ if (fromConventionalPaths)
27
+ return fromConventionalPaths;
28
+ throw new Error('No Photoshop install could be found on this machine');
35
29
  }
36
- async detectFromRegistry() {
30
+ async findViaRegistry() {
37
31
  try {
38
- const registryPaths = [
32
+ const productKeys = [
39
33
  'HKLM\\SOFTWARE\\Adobe\\Photoshop',
40
34
  'HKLM\\SOFTWARE\\WOW6432Node\\Adobe\\Photoshop',
41
35
  ];
42
- for (const regPath of registryPaths) {
36
+ for (const key of productKeys) {
43
37
  try {
44
- const { stdout } = await execAsync(`reg query "${regPath}" /s`);
45
- const entries = this.parseRegistryOutput(stdout);
38
+ const { stdout } = await execAsync(`reg query "${key}" /s`);
39
+ const entries = this.readRegistryEntries(stdout);
46
40
  if (entries.length > 0) {
47
- const latest = entries.sort((a, b) => b.version.localeCompare(a.version))[0];
48
- const info = await this.checkPath(latest.path);
41
+ const newest = entries.sort((a, b) => b.version.localeCompare(a.version))[0];
42
+ const info = await this.inspectPath(newest.path);
49
43
  if (info)
50
44
  return info;
51
45
  }
@@ -54,16 +48,16 @@ export class WindowsDetector {
54
48
  continue;
55
49
  }
56
50
  }
57
- const clsidPaths = [
51
+ const comRegistrations = [
58
52
  'HKCR\\CLSID\\{06870682-6f3c-4b97-9143-f03e85c0bd3e}\\LocalServer32',
59
53
  'HKCR\\Wow6432Node\\CLSID\\{06870682-6f3c-4b97-9143-f03e85c0bd3e}\\LocalServer32',
60
54
  ];
61
- for (const clsidPath of clsidPaths) {
55
+ for (const key of comRegistrations) {
62
56
  try {
63
- const { stdout } = await execAsync(`reg query "${clsidPath}" /ve`);
64
- const exePath = this.extractPathFromCLSID(stdout);
57
+ const { stdout } = await execAsync(`reg query "${key}" /ve`);
58
+ const exePath = this.readComServerPath(stdout);
65
59
  if (exePath) {
66
- const info = await this.checkPath(exePath);
60
+ const info = await this.inspectPath(exePath);
67
61
  if (info)
68
62
  return info;
69
63
  }
@@ -74,82 +68,71 @@ export class WindowsDetector {
74
68
  }
75
69
  }
76
70
  catch (err) {
77
- this.logger.error('Registry query failed:', err);
71
+ this.logger.error('Registry query failed outright', err);
78
72
  }
79
73
  return null;
80
74
  }
81
- parseRegistryOutput(output) {
75
+ readRegistryEntries(output) {
82
76
  const entries = [];
83
- const lines = output.split('\n');
84
77
  let currentVersion = '';
85
- for (const line of lines) {
78
+ for (const line of output.split('\n')) {
86
79
  const versionMatch = line.match(/Photoshop\\(\d+\.\d+)/);
87
80
  if (versionMatch) {
88
81
  currentVersion = versionMatch[1];
89
82
  }
90
83
  const pathMatch = line.match(/ApplicationPath\s+REG_SZ\s+(.+)/);
91
84
  if (pathMatch && currentVersion) {
92
- entries.push({
93
- version: currentVersion,
94
- path: pathMatch[1].trim(),
95
- });
85
+ entries.push({ version: currentVersion, path: pathMatch[1].trim() });
96
86
  }
97
87
  }
98
88
  return entries;
99
89
  }
100
- extractPathFromCLSID(output) {
90
+ readComServerPath(output) {
101
91
  const match = output.match(/REG_SZ\s+"?(.+?\.exe)"?/i);
102
92
  if (!match)
103
93
  return null;
104
94
  return match[1].trim();
105
95
  }
106
- getCommonPaths() {
96
+ conventionalPaths() {
107
97
  const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
108
98
  const programFilesX86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
109
99
  const paths = [];
110
- for (let year = 2025; year >= 2012; year--) {
100
+ const newestYear = Math.max(new Date().getFullYear() + 1, 2026);
101
+ for (let year = newestYear; year >= 2012; year--) {
111
102
  paths.push(`${programFiles}\\Adobe\\Adobe Photoshop ${year}\\Photoshop.exe`, `${programFilesX86}\\Adobe\\Adobe Photoshop ${year}\\Photoshop.exe`, `${programFiles}\\Adobe\\Adobe Photoshop CC ${year}\\Photoshop.exe`, `${programFilesX86}\\Adobe\\Adobe Photoshop CC ${year}\\Photoshop.exe`);
112
103
  }
113
104
  paths.push(`${programFiles}\\Adobe\\Adobe Photoshop CC\\Photoshop.exe`, `${programFiles}\\Adobe\\Photoshop CC\\Photoshop.exe`);
114
105
  return paths;
115
106
  }
116
- async checkPath(path) {
107
+ async probeCandidates(candidates) {
108
+ const results = await Promise.all(candidates.map((path) => this.inspectPath(path)));
109
+ const selected = results.find((result) => result !== null) ?? null;
110
+ if (selected) {
111
+ this.logger.info('Using Photoshop at', selected.path);
112
+ }
113
+ return selected;
114
+ }
115
+ async inspectPath(path) {
117
116
  try {
118
117
  let cleanPath = path.trim().replace(/^"|"$/g, '');
119
118
  if (!cleanPath.toLowerCase().endsWith('.exe')) {
120
- cleanPath = `${cleanPath}\\Photoshop.exe`;
119
+ cleanPath = `${cleanPath.replace(/[\\/]+$/, '')}\\Photoshop.exe`;
121
120
  }
122
121
  await access(cleanPath, constants.F_OK);
123
- const version = this.extractVersionFromPath(cleanPath);
124
- this.logger.info(`Found Photoshop at: ${cleanPath}`);
125
- return {
126
- version,
127
- path: cleanPath,
128
- isRunning: await this.checkIfRunning(),
129
- };
122
+ this.logger.debug('Candidate install exists', cleanPath);
123
+ return { version: this.readVersionFromPath(cleanPath), path: cleanPath };
130
124
  }
131
125
  catch {
132
126
  return null;
133
127
  }
134
128
  }
135
- extractVersionFromPath(path) {
129
+ readVersionFromPath(path) {
136
130
  const yearMatch = path.match(/(\d{4})/);
137
- if (yearMatch) {
131
+ if (yearMatch)
138
132
  return yearMatch[1];
139
- }
140
133
  const versionMatch = path.match(/(\d+\.\d+)/);
141
- if (versionMatch) {
134
+ if (versionMatch)
142
135
  return versionMatch[1];
143
- }
144
136
  return 'Unknown';
145
137
  }
146
- async checkIfRunning() {
147
- try {
148
- const { stdout } = await execAsync('tasklist /FI "IMAGENAME eq Photoshop.exe"');
149
- return stdout.toLowerCase().includes('photoshop.exe');
150
- }
151
- catch {
152
- return false;
153
- }
154
- }
155
138
  }
@@ -0,0 +1,84 @@
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 { ScriptQueue } from './script-queue.js';
6
+ import { runChildWithTimeout } from './run-child.js';
7
+ import { decodeScriptResult } from './script-result.js';
8
+ const execAsync = promisify(exec);
9
+ const DEFAULT_SCRIPT_TIMEOUT_MS = 30_000;
10
+ const LAUNCH_GRACE_MS = 5_000;
11
+ export class WindowsScriptRunner {
12
+ logger = new Logger('WindowsScriptRunner');
13
+ queue = new ScriptQueue(this.logger);
14
+ async run(script, timeoutMs = DEFAULT_SCRIPT_TIMEOUT_MS) {
15
+ return this.queue.enqueue(() => this.runOnce(script, timeoutMs), timeoutMs);
16
+ }
17
+ async runOnce(script, timeoutMs) {
18
+ const dir = await TempDir.create('editmamei-win-');
19
+ try {
20
+ const scriptPath = await dir.write('script.jsx', script);
21
+ const shimPath = await dir.write('run.vbs', this.buildComShim(scriptPath));
22
+ const { stdout, stderr } = await runChildWithTimeout('cscript', ['//nologo', shimPath], {
23
+ timeout: timeoutMs,
24
+ diagLabel: 'cscript run.vbs',
25
+ });
26
+ if (stderr) {
27
+ this.logger.warn('Script execution reported a warning', stderr);
28
+ }
29
+ return decodeScriptResult(stdout);
30
+ }
31
+ finally {
32
+ await dir.cleanup();
33
+ }
34
+ }
35
+ buildComShim(scriptPath) {
36
+ if (scriptPath.includes('"')) {
37
+ throw new Error('Refusing to compose the COM shim: script path contains a double quote');
38
+ }
39
+ return `
40
+ On Error Resume Next
41
+ Dim ps
42
+ Set ps = CreateObject("Photoshop.Application")
43
+
44
+ If Err.Number <> 0 Then
45
+ WScript.Echo "ERROR: could not attach to Photoshop over COM - " & Err.Description
46
+ WScript.Quit 1
47
+ End If
48
+
49
+ Dim scriptResult
50
+ scriptResult = ps.DoJavaScript("$.evalFile(""" & Replace("${scriptPath}", "\\", "\\\\") & """);")
51
+
52
+ If Err.Number <> 0 Then
53
+ WScript.Echo "ERROR: " & Err.Description
54
+ WScript.Quit 1
55
+ Else
56
+ WScript.Echo scriptResult
57
+ End If
58
+ `.trim();
59
+ }
60
+ async isRunning() {
61
+ try {
62
+ const { stdout } = await execAsync('tasklist /FI "IMAGENAME eq Photoshop.exe"');
63
+ return stdout.toLowerCase().includes('photoshop.exe');
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }
69
+ async launch(executablePath) {
70
+ return new Promise((resolve, reject) => {
71
+ this.logger.info('Launching Photoshop', executablePath);
72
+ const child = spawn(executablePath, [], { detached: true, stdio: 'ignore' });
73
+ child.unref();
74
+ const grace = setTimeout(() => {
75
+ grace.unref();
76
+ resolve();
77
+ }, LAUNCH_GRACE_MS);
78
+ child.on('error', (error) => {
79
+ clearTimeout(grace);
80
+ reject(new Error(`Could not launch Photoshop at ${executablePath}: ${error.message}`));
81
+ });
82
+ });
83
+ }
84
+ }
Binary file