@kitschpatrol/aphex 0.1.3 → 0.1.5

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/dist/aphex-swift CHANGED
Binary file
package/dist/index.d.ts CHANGED
@@ -8,11 +8,11 @@ import { OmitDeep, PartialDeep, Simplify } from "type-fest";
8
8
  *
9
9
  * @throws {Error} If the session is already active or fails to start
10
10
  */
11
- declare function interactiveSessionStart(): Promise<void>;
11
+ export declare function interactiveSessionStart(): Promise<void>;
12
12
  /**
13
13
  * Stop the interactive session if one is active.
14
14
  */
15
- declare function interactiveSessionStop(): Promise<void>;
15
+ export declare function interactiveSessionStop(): Promise<void>;
16
16
  /**
17
17
  * TypeScript type definition for ResourceInfo from the Swift implementation
18
18
  */
@@ -63,7 +63,7 @@ declare function aphexPhotoInfo(identifiers: string | string[], caseSensitive?:
63
63
  declare function aphexAlbumInfo(identifiers: string | string[], caseSensitive?: boolean): Promise<AlbumInfo[]>;
64
64
  //#endregion
65
65
  //#region src/utilities/image/mime.d.ts
66
- declare const IMAGE_MIME_TYPES: readonly ["arw", "avif", "bmp", "cr2", "cr3", "crw", "tga", "dng", "gif", "heic", "heif", "jpeg", "nef", "pef", "png", "psd", "svg+xml", "tiff", "webp"];
66
+ export declare const IMAGE_MIME_TYPES: readonly ["arw", "avif", "bmp", "cr2", "cr3", "crw", "tga", "dng", "gif", "heic", "heif", "jpeg", "nef", "pef", "png", "psd", "svg+xml", "tiff", "webp"];
67
67
  type ImageMimeType = (typeof IMAGE_MIME_TYPES)[number];
68
68
  //#endregion
69
69
  //#region src/pipeline/engines/applescript-gui.d.ts
@@ -327,7 +327,7 @@ type ProcessImageResult = {
327
327
  *
328
328
  * Run in parallel through a worker for album processing
329
329
  */
330
- declare function processImage(sourceImagePath: string, destinationDirectory: string, options: ProcessImageOptions): Promise<ProcessImageResult>;
330
+ export declare function processImage(sourceImagePath: string, destinationDirectory: string, options: ProcessImageOptions): Promise<ProcessImageResult>;
331
331
  //#endregion
332
332
  //#region src/pipeline/image-sync.d.ts
333
333
  type DiffStrategy = 'exif-tags' | 'export-options' | 'file-name' | 'force-update' | 'metadata-options' | 'photo-info' | 'process-options';
@@ -354,7 +354,7 @@ type SyncResult = {
354
354
  * This should be called when you're done using exiftool to prevent hanging
355
355
  * processes. It's safe to call multiple times.
356
356
  */
357
- declare function endExiftool(): Promise<void>;
357
+ export declare function endExiftool(): Promise<void>;
358
358
  //#endregion
359
359
  //#region src/utilities/log.d.ts
360
360
  /**
@@ -364,22 +364,22 @@ declare function endExiftool(): Promise<void>;
364
364
  * @param logger - Accepts either a LogLayer instance or a Console- or
365
365
  * Stream-like log target
366
366
  */
367
- declare function setLogger(logger?: ILogBasic | ILogLayer<unknown>): void;
367
+ export declare function setLogger(logger?: ILogBasic | ILogLayer<unknown>): void;
368
368
  //#endregion
369
369
  //#region src/index.d.ts
370
370
  /**
371
371
  * Helper for deep merging ExportOptions object against library defaults.
372
372
  * Exported for unplugin-aphex.
373
373
  */
374
- declare function mergeDefaultExportOptions(options: PartialDeep<ExportOptions> | undefined): ExportOptions;
375
- type ExportOptions = {
374
+ export declare function mergeDefaultExportOptions(options: PartialDeep<ExportOptions> | undefined): ExportOptions;
375
+ export type ExportOptions = {
376
376
  exportOptions: ExportApplePhotoOptions;
377
377
  metadataOptions: 'disabled' | ManageMetadataOptions;
378
378
  processOptions: 'disabled' | ProcessImageOptions;
379
379
  syncOptions: 'disabled' | SyncOptions;
380
380
  };
381
- declare const defaultSyncOptions: SyncOptions;
382
- declare const defaultExportOptions: ExportOptions;
381
+ export declare const defaultSyncOptions: SyncOptions;
382
+ export declare const defaultExportOptions: ExportOptions;
383
383
  /** Some fields omitted for relevance... */
384
384
  type ExportResults = {
385
385
  exportResult: Simplify<Omit<ExportApplePhotoResult, 'exportOptions' | 'path' | 'photoInfo'>> | undefined;
@@ -387,7 +387,7 @@ type ExportResults = {
387
387
  processResult: Simplify<OmitDeep<ProcessImageResult, 'input.path' | 'output.path' | 'path'>> | undefined;
388
388
  syncResult: Simplify<Omit<SyncResult['plan'][number], 'photoInfo'>> | undefined;
389
389
  };
390
- type ExportResult = {
390
+ export type ExportResult = {
391
391
  options: ExportOptions;
392
392
  path: string;
393
393
  photoInfo: PhotoInfo;
@@ -396,10 +396,10 @@ type ExportResult = {
396
396
  /**
397
397
  * Export a single photo
398
398
  */
399
- declare function exportPhoto(identifier: PhotoInfo | string, destinationDirectory: string, options?: PartialDeep<ExportOptions>): Promise<ExportResult>;
399
+ export declare function exportPhoto(identifier: PhotoInfo | string, destinationDirectory: string, options?: PartialDeep<ExportOptions>): Promise<ExportResult>;
400
400
  /**
401
401
  * Export photos
402
402
  */
403
- declare function exportPhotos(identifiers: Array<AlbumInfo | PhotoInfo | string>, destinationDirectory: string, options?: PartialDeep<ExportOptions>): Promise<ExportResult[]>;
403
+ export declare function exportPhotos(identifiers: Array<AlbumInfo | PhotoInfo | string>, destinationDirectory: string, options?: PartialDeep<ExportOptions>): Promise<ExportResult[]>;
404
404
  //#endregion
405
- export { ExportOptions, ExportResult, IMAGE_MIME_TYPES, type ImageMimeType, defaultExportOptions, defaultSyncOptions, endExiftool, exportPhoto, exportPhotos, aphexAlbumInfo as getAlbumInfo, aphexPhotoInfo as getPhotoInfo, interactiveSessionStart, interactiveSessionStop, mergeDefaultExportOptions, processImage, setLogger };
405
+ export { type ImageMimeType, aphexAlbumInfo as getAlbumInfo, aphexPhotoInfo as getPhotoInfo };
package/dist/index.js CHANGED
@@ -75,7 +75,9 @@ function scheduleProcessCleanup() {
75
75
  process.on("beforeExit", () => {
76
76
  if (cleanupScheduled) return;
77
77
  cleanupScheduled = true;
78
- endExiftool();
78
+ endExiftool().catch((error) => {
79
+ log.warn(`Failed to end exiftool processes: ${String(error)}`);
80
+ });
79
81
  });
80
82
  process.on("exit", () => {
81
83
  if (exiftoolInstance.pids.length > 0) log.debug(`Exiftool processes still running at exit: ${exiftoolInstance.pids.join(", ")}`);
@@ -83,14 +85,22 @@ function scheduleProcessCleanup() {
83
85
  process.on("SIGINT", () => {
84
86
  (async () => {
85
87
  log.debug("Received SIGINT, cleaning up exiftool...");
86
- await endExiftool();
88
+ try {
89
+ await endExiftool();
90
+ } catch (error) {
91
+ log.warn(`Failed to end exiftool processes: ${String(error)}`);
92
+ }
87
93
  process.exit(130);
88
94
  })();
89
95
  });
90
96
  process.on("SIGTERM", () => {
91
97
  (async () => {
92
98
  log.debug("Received SIGTERM, cleaning up exiftool...");
93
- await endExiftool();
99
+ try {
100
+ await endExiftool();
101
+ } catch (error) {
102
+ log.warn(`Failed to end exiftool processes: ${String(error)}`);
103
+ }
94
104
  process.exit(143);
95
105
  })();
96
106
  });
@@ -172,10 +182,9 @@ async function sipsTempCleanup() {
172
182
  "--mime",
173
183
  filePath
174
184
  ]);
175
- if (stdout.startsWith("image/")) {
176
- await fse.rm(filePath, { force: true });
177
- cleanCount += 1;
178
- }
185
+ if (!stdout.startsWith("image/")) continue;
186
+ await fse.rm(filePath, { force: true });
187
+ cleanCount += 1;
179
188
  }
180
189
  return cleanCount;
181
190
  }
@@ -340,10 +349,7 @@ async function executeInteractiveCommand(command) {
340
349
  * Arguments with spaces or special characters are quoted.
341
350
  */
342
351
  function escapeCommand(args) {
343
- return args.map((arg) => {
344
- if (SHELL_SPECIAL_CHARS_REGEX.test(arg)) return `'${arg.replaceAll("'", String.raw`'\''`)}'`;
345
- return arg;
346
- }).join(" ");
352
+ return args.map((arg) => SHELL_SPECIAL_CHARS_REGEX.test(arg) ? `'${arg.replaceAll("'", String.raw`'\''`)}'` : arg).join(" ");
347
353
  }
348
354
  /**
349
355
  * Runtime type guard for ResourceInfo
@@ -816,14 +822,14 @@ function parseUserComment(userComment) {
816
822
  * Set the tags on an image
817
823
  */
818
824
  async function setTags(imagePath, imageTags) {
819
- const { aphexMetadata, creator, credit, description, label, preservedFileName } = imageTags;
825
+ const { aphexMetadata, creator = "", credit = "", description = "", label = "", preservedFileName = "" } = imageTags;
820
826
  const userComment = aphexMetadata ? JSON.stringify(aphexMetadata) : void 0;
821
827
  const tags = {
822
- ..."creator" in imageTags && { "XMP:Creator": creator ?? "" },
823
- ..."description" in imageTags && { "XMP:Description": description ?? "" },
824
- ..."credit" in imageTags && { "XMP:Credit": credit ?? "" },
825
- ..."label" in imageTags && { "XMP:Label": label ?? "" },
826
- ..."preservedFileName" in imageTags && { "XMP:PreservedFileName": preservedFileName ?? "" },
828
+ ..."creator" in imageTags && { "XMP:Creator": creator },
829
+ ..."description" in imageTags && { "XMP:Description": description },
830
+ ..."credit" in imageTags && { "XMP:Credit": credit },
831
+ ..."label" in imageTags && { "XMP:Label": label },
832
+ ..."preservedFileName" in imageTags && { "XMP:PreservedFileName": preservedFileName },
827
833
  ..."aphexMetadata" in imageTags && { "XMP:UserComment": userComment ?? "" }
828
834
  };
829
835
  await getExiftool().write(imagePath, tags, { writeArgs: ["-overwrite_original_in_place"] });
package/package.json CHANGED
@@ -1,101 +1,102 @@
1
1
  {
2
- "name": "@kitschpatrol/aphex",
3
- "version": "0.1.3",
4
- "description": "Apple Photos Export. TypeScript library and CLI tool to export and process images and albums from your macOS Photos.app library.",
5
- "keywords": [
6
- "apple",
7
- "apple-photos",
8
- "photokit",
9
- "photos",
10
- "npm-package"
11
- ],
12
- "homepage": "https://github.com/kitschpatrol/aphex",
13
- "bugs": "https://github.com/kitschpatrol/aphex/issues",
14
- "repository": {
15
- "type": "git",
16
- "url": "git+https://github.com/kitschpatrol/aphex.git"
17
- },
18
- "license": "MIT",
19
- "author": {
20
- "name": "Eric Mika",
21
- "email": "eric@ericmika.com",
22
- "url": "https://ericmika.com"
23
- },
24
- "sideEffects": false,
25
- "type": "module",
26
- "exports": {
27
- ".": {
28
- "types": "./dist/index.d.ts",
29
- "import": "./dist/index.js"
30
- }
31
- },
32
- "main": "./dist/index.js",
33
- "module": "./dist/index.js",
34
- "types": "./dist/index.d.ts",
35
- "bin": {
36
- "aphex": "./dist/aphex-swift"
37
- },
38
- "files": [
39
- "dist/*"
40
- ],
41
- "scripts": {
42
- "build": "pnpm --sequential /^build:/",
43
- "build:1-lib": "tsdown",
44
- "build:2-native": "cd native/aphex-swift && ./build.sh",
45
- "build:3-publint": "publint",
46
- "clean": "git rm -f pnpm-lock.yaml && git clean -fdX -e !.claude/",
47
- "fix": "ksc fix && mdat ./native/aphex-swift/readme.md",
48
- "lint": "ksc lint",
49
- "release": "bumpp --commit 'Release: %s' && pnpm build && pnpm publish --otp $(op item get npm --otp)",
50
- "test": "vitest run",
51
- "test:coverage": "vitest run --coverage"
52
- },
53
- "dependencies": {
54
- "@sindresorhus/is": "^8.1.0",
55
- "defu": "^6.1.7",
56
- "execa": "^10.0.1",
57
- "exiftool-vendored": "^37.2.0",
58
- "fast-equals": "^6.0.2",
59
- "fs-extra": "^11.4.0",
60
- "github-slugger": "^2.0.0",
61
- "image-size": "^2.0.2",
62
- "importx": "^0.5.2",
63
- "lognow": "^0.8.0",
64
- "mrmime": "^2.0.1",
65
- "piscina": "^5.3.1",
66
- "type-fest": "^5.8.0"
67
- },
68
- "devDependencies": {
69
- "@arethetypeswrong/core": "^0.18.5",
70
- "@clack/prompts": "^1.7.0",
71
- "@kitschpatrol/shared-config": "^8.6.0",
72
- "@types/fs-extra": "^11.0.4",
73
- "@types/node": "~24.13.3",
74
- "@vitest/coverage-v8": "^4.1.11",
75
- "bumpp": "^12.2.1",
76
- "globby": "^16.2.4",
77
- "markdown-table": "^3.0.4",
78
- "mdat-plugin-cli-help": "^3.1.0",
79
- "open": "^11.0.1",
80
- "pretty-bytes": "^7.1.1",
81
- "pretty-ms": "^9.3.0",
82
- "publint": "^0.3.24",
83
- "tsdown": "^0.22.14",
84
- "tsx": "^4.23.12",
85
- "typescript": "~6.0.3",
86
- "vitest": "^4.1.11"
87
- },
88
- "packageManager": "pnpm@11.22.0",
89
- "engines": {
90
- "node": ">=24.16.0"
91
- },
92
- "os": [
93
- "darwin"
94
- ],
95
- "cpu": [
96
- "arm64"
97
- ],
98
- "publishConfig": {
99
- "access": "public"
100
- }
101
- }
2
+ "name": "@kitschpatrol/aphex",
3
+ "version": "0.1.5",
4
+ "description": "Apple Photos Export. TypeScript library and CLI tool to export and process images and albums from your macOS Photos.app library.",
5
+ "keywords": [
6
+ "apple",
7
+ "apple-photos",
8
+ "photokit",
9
+ "photos",
10
+ "npm-package"
11
+ ],
12
+ "homepage": "https://github.com/kitschpatrol/aphex",
13
+ "bugs": "https://github.com/kitschpatrol/aphex/issues",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/kitschpatrol/aphex.git"
17
+ },
18
+ "license": "MIT",
19
+ "author": {
20
+ "name": "Eric Mika",
21
+ "email": "eric@ericmika.com",
22
+ "url": "https://ericmika.com"
23
+ },
24
+ "sideEffects": false,
25
+ "type": "module",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js"
30
+ }
31
+ },
32
+ "main": "./dist/index.js",
33
+ "module": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "bin": {
36
+ "aphex": "./dist/aphex-swift"
37
+ },
38
+ "files": [
39
+ "dist/*"
40
+ ],
41
+ "dependencies": {
42
+ "@sindresorhus/is": "^8.1.0",
43
+ "defu": "^6.1.7",
44
+ "execa": "^10.1.0",
45
+ "exiftool-vendored": "^39.0.0",
46
+ "fast-equals": "^6.1.0",
47
+ "fs-extra": "^11.4.1",
48
+ "github-slugger": "^2.0.0",
49
+ "image-size": "^2.0.4",
50
+ "importx": "^0.5.2",
51
+ "lognow": "^0.8.5",
52
+ "mrmime": "^2.0.1",
53
+ "piscina": "^5.3.2",
54
+ "type-fest": "^5.10.0"
55
+ },
56
+ "devDependencies": {
57
+ "@arethetypeswrong/core": "^0.18.5",
58
+ "@clack/prompts": "^1.8.1",
59
+ "@kitschpatrol/shared-config": "^8.12.1",
60
+ "@types/fs-extra": "^11.0.4",
61
+ "@types/node": "~24.13.6",
62
+ "@vitest/coverage-v8": "^5.0.3",
63
+ "brewpub": "^0.5.0",
64
+ "bumpp": "^12.3.0",
65
+ "globby": "^16.2.4",
66
+ "markdown-table": "^3.0.4",
67
+ "mdat-plugin-cli-help": "^3.4.1",
68
+ "open": "^11.0.4",
69
+ "pretty-bytes": "^7.2.0",
70
+ "pretty-ms": "^9.3.1",
71
+ "publint": "^0.3.25",
72
+ "skills": "^1.7.0",
73
+ "tsdown": "^0.23.0",
74
+ "tsx": "^4.23.15",
75
+ "typescript": "~6.0.3",
76
+ "vitest": "^5.0.3"
77
+ },
78
+ "engines": {
79
+ "node": "^24.16.0 || >=26.3.0"
80
+ },
81
+ "os": [
82
+ "darwin"
83
+ ],
84
+ "cpu": [
85
+ "arm64"
86
+ ],
87
+ "publishConfig": {
88
+ "access": "public"
89
+ },
90
+ "scripts": {
91
+ "build": "pnpm run --sequential /^build:/",
92
+ "build:1-lib": "tsdown",
93
+ "build:2-native": "cd native/aphex-swift && ./build.sh",
94
+ "build:3-publint": "publint",
95
+ "clean-deep": "git rm -f pnpm-lock.yaml && git clean -fdX -e !.claude/",
96
+ "fix": "ksc fix && mdat ./native/aphex-swift/readme.md",
97
+ "lint": "ksc lint",
98
+ "release": "bumpp --commit 'Release: %s' && pnpm build && pnpm publish --otp $(op item get npm --otp) && brewpub --tap kitschpatrol/tap",
99
+ "test": "vitest run",
100
+ "test:coverage": "vitest run --coverage"
101
+ }
102
+ }
package/readme.md CHANGED
@@ -10,6 +10,7 @@
10
10
 
11
11
  [![NPM Package @kitschpatrol/aphex](https://img.shields.io/npm/v/@kitschpatrol/aphex.svg)](https://www.npmjs.com/package/@kitschpatrol/aphex)
12
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/license/mit)
13
+ [![Homebrew](https://img.shields.io/badge/Homebrew-kitschpatrol%2Ftap%2Faphex-FBB040?logo=homebrew&logoColor=white)](https://github.com/kitschpatrol/homebrew-tap/blob/HEAD/Formula/aphex.rb)
13
14
 
14
15
  <!-- /badges -->
15
16
 
@@ -37,7 +38,7 @@ It makes it simple to export high-quality versions of specific photos or albums
37
38
 
38
39
  I created this library for integration in static website content management asset pipelines, and to attempt to work around some issues related to [exporting high-quality versions of edited images](https://github.com/RhetTbull/osxphotos/discussions/1522) from the Photos.app library. (See the [unplugin-aphex](https://github.com/kitschpatrol/unplugin-aphex) project for an additional layer of integration with various build tools, and the [vscode-aphex-preview](https://github.com/kitschpatrol/vscode-aphex-preview) plugin for hover previews of Aphex links in VS Code.)
39
40
 
40
- This repository also embeds the `aphex-swift` CLI project, which provides a minimal and performant wrapper around parts of Apple's PhotoKit framework. It's not intended for direct use, instead it provides just enough functionality to support the parts of the methods provided by the `aphex` TypeScript library that can only be implemented natively.
41
+ This repository also embeds the `aphex-swift` CLI project, which provides a minimal and performant wrapper around parts of Apple's PhotoKit framework. It's not really intended for direct use beyond querying the Photos library and simple exports. It provides just enough functionality to support the parts of the methods provided by the `aphex` TypeScript library that can only be implemented natively.
41
42
 
42
43
  <!-- /* spell-checker:disable */ -->
43
44
 
@@ -47,12 +48,35 @@ The name "Aphex" is a concatenation of **A**pple **PH**otos **EX**port.
47
48
 
48
49
  ## Getting started
49
50
 
51
+ <!-- dependencies -->
52
+
50
53
  ### Dependencies
51
54
 
52
- Requires an Apple Silicon (`arm64`) Mac with Photos.app installed and [Node 22.18.0](https://nodejs.org/en/download/) or newer. No Intel (`x86_64`) build of the bundled native binary is provided.
55
+ - [Node.js](https://nodejs.org/) 24.16.0 or newer (specifically `^24.16.0 || >=26.3.0`)
56
+ - Supported operating systems: macOS
57
+
58
+ <!-- /dependencies -->
59
+
60
+ Requires an Apple Silicon (`arm64`) Mac with Photos.app installed. No Intel (`x86_64`) build of the bundled native binary is provided.
61
+
62
+ ### Installation
63
+
64
+ Pick the option that matches how you plan to use it.
65
+
66
+ #### Library installation
67
+
68
+ Add it to your project to import the TypeScript API. This also puts the `aphex` CLI on your project's path:
69
+
70
+ ```sh
71
+ npm install @kitschpatrol/aphex
72
+ ```
53
73
 
54
74
  Full image processing functionality also requires a number of command-line tools available via [Homebrew](https://brew.sh):
55
75
 
76
+ ```sh
77
+ brew install libavif mozjpeg imagemagick webp dssim ffmpeg guetzli oxipng
78
+ ```
79
+
56
80
  | Tool | Used for |
57
81
  | ------------- | ------------------------------------------------------------------------- |
58
82
  | `libavif` | AVIF encoding |
@@ -64,13 +88,28 @@ Full image processing functionality also requires a number of command-line tools
64
88
  | `ffmpeg` | Media probing used by some conversion paths |
65
89
  | `dssim` | Perceptual similarity metrics (only needed if `logSimilarity` is enabled) |
66
90
 
67
- If you skip image processing (`processOptions: 'disabled'`) you can omit the Homebrew dependencies.
91
+ If you skip image processing (`processOptions: 'disabled'`) you can omit these.
68
92
 
69
- ### Installation
93
+ #### CLI installation
94
+
95
+ Note that the Aphex CLI does _not_ expose all the functionality from the TypeScript library. It's a native bridge focused on querying your library for images and metadata, and also certain fast-path export strategies.
96
+
97
+ Run it once without installing:
70
98
 
71
99
  ```sh
72
- brew install libavif mozjpeg imagemagick webp dssim ffmpeg guetzli oxipng
73
- npm install @kitschpatrol/aphex
100
+ npx @kitschpatrol/aphex
101
+ ```
102
+
103
+ Or install it globally with Homebrew:
104
+
105
+ ```sh
106
+ brew install kitschpatrol/tap/aphex
107
+ ```
108
+
109
+ Or install it globally with npm:
110
+
111
+ ```sh
112
+ npm install --global @kitschpatrol/aphex
74
113
  ```
75
114
 
76
115
  ### Permissions
@@ -192,7 +231,7 @@ setLogger(console)
192
231
 
193
232
  ##### Exporting a photo by filename
194
233
 
195
- Let's assume you have an album named "Trip" in your Photos.app library containing a photo with the filename "IMG\_1922.jpeg":
234
+ Let's assume you have an album named "Trip" in your Photos.app library containing a photo with the filename "IMG_1922.jpeg":
196
235
 
197
236
  ```ts
198
237
  const result = await exportPhoto('Trip/IMG_1922.jpeg', '~/Desktop')