@mboucher/ffe-cli-ps-plugin
v0.1.3
Published
Photoshop API commands for the Firefly Enterprise CLI
Readme
Photoshop plugin for FFE CLI
An oclif plugin for the Photoshop APIs, using @mboucher/ffe-cli for OAuth, credential
profiles, and authenticated HTTP. Manifest generation implements Photoshop v2
generateManifest:
POST https://photoshop-api.adobe.io/v2/generate-manifest.
Use the resulting manifest to extract tagged layers, check text fonts, and generate
sample data for templates containing {{key}} placeholders. Layer extraction
and font inspection use JSONata.
Commands
All commands run through the host CLI as ffe photoshop <command>.
| Command | Purpose | Stdout |
| --- | --- | --- |
| generate-manifest | Generate a Photoshop v2 manifest from a local PSD, document URL, or request JSON. | Absolute path to a temporary JSON file containing the complete successful job response. --no-wait prints accepted job JSON instead. |
| get-tags | Extract Smart Objects named {{tag}} and text layers tagged in their name or contents, at any depth. | Absolute path to a separate temporary JSON file containing the matching layer array. |
| supported-fonts | List saved supported PostScript fonts; use --update to refresh the reference list from Adobe. | Font-name JSON array by default or with --list; saved file path with --update alone. |
| check-fonts | Compare fonts in extracted layers or a full manifest against the saved supported-font list. | JSON report; custom-font and incomplete-check warnings go to stderr. |
| sample-csv | Create one CSV column per unique {{key}} in extracted layer names and text content, plus one example-data row. | Absolute path to the new CSV file; defaults to sample.csv in the current directory. |
Use ffe photoshop <command> --help for flags and examples:
ffe photoshop generate-manifest --help
ffe photoshop get-tags --help
ffe photoshop supported-fonts --help
ffe photoshop check-fonts --help
ffe photoshop sample-csv --helpExample workflow
After configuring Adobe credentials and the host's local-file upload support (see Generate a manifest), inspect a template and prepare its sample data:
# Initialize or refresh the shared supported-font reference list.
ffe photoshop supported-fonts --update
# Generate the manifest, then extract tagged text and Smart Object layers.
manifest_path="$(ffe photoshop generate-manifest \
--input-file ./document.psd --profile production)"
layers_path="$(ffe photoshop get-tags --manifest "$manifest_path")"
# Inspect all text fonts, including untagged layers.
ffe photoshop check-fonts --input "$manifest_path"
# Generate CSV headers and one example row from {{key}} placeholders.
csv_path="$(ffe photoshop sample-csv \
--input "$layers_path" --output ./data/sample.csv)"
cat "$csv_path"The font reference is persistent and shared across projects. Manifest and layer files are temporary but remain available after the commands exit. The CSV is a persistent file that will not overwrite an existing output; replace its example values before using it in production. CSV generation fails if no placeholders are found.
Only manifest generation requires Adobe API credentials. supported-fonts
--update accesses public Adobe documentation. Font listing and checks use a
saved reference offline, but fetch and cache it when the file is missing.
Layer extraction and CSV generation are offline.
Storage provider selection
For local-file manifest generation, the host also supports AWS S3:
ffe photoshop generate-manifest --input-file ./document.psd --provider aws --bucket inputs --region us-east-1 --aws-profile storage-production --profile adobe-productionThis forwards storage options to ffe storage upload. Install and authenticate
AWS CLI separately; --aws-profile selects AWS credentials while --profile
selects Adobe credentials. Alternatively set FFE_STORAGE_PROVIDER=aws and
AWS_S3_BUCKET for host defaults. Azure remains the default provider. S3 signed
URLs request 12 hours but can expire sooner with temporary AWS credentials.
The CLI does not prompt for a provider. The template-validation skill asks the
user to choose Azure Blob Storage or AWS S3 before a local PSD upload; the MCP
ffe_storage_providers tool supplies guidance without uploading or choosing.
Pass the chosen provider explicitly and authorize uploading and Adobe processing
before running a command. No storage choice is needed for a supplied manifest
or an existing document URL.
Local development
Requires Node.js 22.13+, npm, and the sibling ../ffe-cli checkout.
The host is a peer dependency; the local development dependency links that
checkout rather than bundling a second SDK.
cd ../ffe-cli
npm install
npm --prefix ../ffe-cli-ps-plugin install
npm run build
node bin/run.js photoshop generate-manifest --helpThe host registers this package as a bundled plugin through a versioned npm
dependency and oclif.plugins. When testing unpublished sibling changes, ensure
the host resolves this checkout rather than a registry-installed copy. Its build
compiles the host SDK first, then builds
this plugin and generates its manifest.
If ffe is on your PATH, use it in place of node bin/run.js from the host
checkout. Rebuild after editing TypeScript.
The plugin build also refreshes its generated command manifest so newly added
commands are discoverable by the host.
From the plugin checkout:
npm run typecheck
npm test
npm packTests use synthetic credentials and mocked HTTP/uploads, plus isolated host/plugin integration tests. They do not access the keyring, upload to Azure or AWS, or call Adobe APIs.
Publish to npm
From the plugin checkout, use an npm account authorized to publish
@mboucher/ffe-cli-ps-plugin. For subsequent releases, update the package version first;
npm does not allow overwriting a published version.
npm login
npm run deploy -- --dry-run
npm run deploydeploy publishes the plugin with public access. Both the dry run and actual
publish run typechecking and tests through prepublishOnly, then build the
release files through prepack. The dry run previews the package without
publishing it. Complete any npm authentication or two-factor prompts locally;
do not put tokens in scripts or source control.
The plugin retains its @mboucher/ffe-cli peer dependency; publish a compatible host
version separately before expecting registry-only consumer installation to work.
Update or list supported fonts
Fetch the current built-in Photoshop API font list from Adobe's Technical Usage Notes:
fonts_path="$(ffe photoshop supported-fonts --update)"
# List the saved font names without network access (both forms are equivalent).
ffe photoshop supported-fonts
ffe photoshop supported-fonts --list
# Refresh the file, then print the updated font names.
ffe photoshop supported-fonts --update --list
# Optional: choose a different file for either operation.
ffe photoshop supported-fonts --update --output ./supported-fonts.json
ffe photoshop supported-fonts --list --output ./supported-fonts.jsonThe default file is ~/.ffe/photoshop/supported-fonts.json, a predictable
home-directory location (FFE_DATA_DIR replaces ~/.ffe). Harness access still
depends on sandbox permissions; use --output with an accessible location and
the same file with check-fonts --supported-fonts when needed. It is shared across
projects, not stored in the current working directory or OS temp directory.
--update fetches the documentation and atomically replaces the saved list
after a successful extraction. Parent directories are created as needed;
--output selects the local file to read or update, relative to the current
directory.
- No operation flag, or
--list: read the saved file, or fetch it when missing, and print a pretty-printed JSON array of PostScript font names. The in-process return value is the same array. An existing reference and its timestamp are unchanged. --updatealone: refresh the file and print/return its absolute path.--update --list: refresh and save first, then print/return only the updated font-name array, not the file path.
If the saved file is missing, the command fetches the list from the Adobe URL and
saves it (when the location is writable) before continuing; a malformed file
still fails with guidance to run --update (using the same --output path, if
specified).
No Adobe login, keyring access, or Photoshop API credentials are required.
The JSON contains sourceUrl, an ISO-8601 fetchedAt timestamp, and a fonts
array of exact, case-preserved PostScript names (not family/display names).
Names are deduplicated and sorted for deterministic future comparisons:
{
"sourceUrl": "https://developer.adobe.com/firefly-services/docs/photoshop/getting-started/technical-usage-notes/",
"fetchedAt": "2026-10-23T20:30:00.000Z",
"fonts": ["AcuminVariableConcept", "AdobeArabic-Bold", "..."]
}Extraction is scoped to the supported-fonts table, not unrelated documentation text. HTTP failures, a 30-second request timeout, missing/changed font tables, invalid names, empty lists, and filesystem errors fail explicitly without printing a success path. Failed fetches/parses/writes do not overwrite an existing list.
This command manages the reference list; use check-fonts below to compare text
layer fonts against it.
Adobe also supports some authorized Adobe Fonts and user-supplied fonts, so
check-fonts reports names absent from this built-in list as custom fonts, not
as universally unsupported fonts.
Get tags from a manifest
Use get-tags with a local --manifest JSON file. Both text and Smart Object
layers are checked by default; --text or --smart-objects limits the layer
types. This runs offline, with no credentials, uploads, or API calls.
This command replaces extract-layers.
Smart Object names must consist entirely of a nonempty {{tag}} token, such as
{{hero_image}}. Text layers match when their name is a {{tag}} token or their
text.content contains a token, such as Hello {{first_name}}. Whitespace around
the tag key is allowed. Untagged layers, empty tokens, escaped tokens, and triple
braces are ignored.
For example, a Smart Object named {{hero_image}} matches, but one named
Hero {{hero_image}} does not. A text layer named Headline with contents
Hello {{first_name}} matches even though its name has no tag.
ffe photoshop get-tags --manifest manifest.json --text
ffe photoshop get-tags --manifest manifest.json --smart-objects
layers_path="$(ffe photoshop get-tags --manifest manifest.json)"
cat "$layers_path"
# Capture the generated temporary file path, then extract from its embedded JSON.
manifest_path="$(ffe photoshop generate-manifest --input-file ./document.psd)"
layers_path="$(ffe photoshop get-tags --manifest "$manifest_path")"
cat "$layers_path"Input can be a raw Photoshop v2 manifest with a top-level layers array, or a
successful job response containing embedded JSON manifests under result.outputs.
Hosted/URL-only outputs and embedded string/base64 outputs must first be
downloaded/decoded into a raw JSON manifest file; this command does not fetch URLs.
Stdin is not supported.
JSONata recursively matches the v2 type values text_layer and
smart_object_layer, including layers inside groups (children), artboards
(layers), and deeper nesting, then filters for tags. Tagged linked and embedded
Smart Objects, and hidden layers, are included. There is no configured depth limit.
V1 camelCase layer types are not supported.
The command writes a pretty-printed JSON array of complete, unchanged matching
layer objects, not a list of tag names or just their text or Smart Object metadata,
to a separate,
unique ffe-ps-layers-*/layers.json file under the OS temporary directory.
Stdout and the in-process return value contain only its absolute path, not the
JSON array. The input manifest is never modified. The directory is private and
the file is owner-only (0600 on POSIX). The file remains after the command exits;
delete it when no longer needed, or let OS temporary-directory cleanup apply.
Zero matches save []; one match still saves an array. Using both selectors
combines the results without duplicating a layer. Multiple embedded manifests are
combined in output order. Matching parents retain any nested data in their
original objects.
Missing/unreadable files, invalid JSON, and unsupported manifest structure fail
with a nonzero exit status instead of returning an empty success.
File-write failures remove incomplete output and do not print a success path.
Generate a sample CSV from data placeholders
Scan the layer JSON array returned by get-tags for unique {{key}} data placeholders in
layer names and text.content. This includes Smart Object layer names, hidden
layers, and layers nested under groups/artboards. Other metadata (font names,
URLs, etc.) is not scanned.
# Creates sample.csv in the current directory and prints its absolute path.
ffe photoshop sample-csv --input layers.json
# Choose another output file.
ffe photoshop sample-csv --input layers.json --output ./data/sample.csv
# Use the temporary file returned by get-tags.
layers_path="$(ffe photoshop get-tags --manifest manifest.json)"
ffe photoshop sample-csv --input "$layers_path"For placeholders {{headline}}, {{customer.name}}, and {{hero_image}},
the generated CSV contains:
headline,customer.name,hero_image
Example headline,Example customer.name,Example hero_imageEvery unique key becomes one column, followed by one sample-data row. Replace all example values with real data before production use; image columns need valid image references, not the generated example text. Keys are case-sensitive, whitespace around a key is trimmed, and columns follow first-occurrence order (layer name before text content, then nested layers). Repeated keys produce only one column.
This is literal double-brace placeholder extraction, not a Handlebars engine:
the trimmed contents of each {{...}} token are treated as its key, without
evaluating helpers or block syntax. Escaped tokens (\{{key}}), triple braces
({{{key}}}), and empty placeholders are ignored.
UTF-8 CSV output uses CRLF record endings and quotes/escapes commas, quotes, and
newlines in keys and values. Keys starting with =, +, -, or @ are rejected
to avoid spreadsheet formula execution without silently changing column names.
The command runs offline and prints/returns only the absolute saved file path.
Output directories are created as needed; files use owner-only permissions
(0600 on POSIX). Existing files, including the input, are never overwritten:
choose a different --output path to generate another sample.
Missing/unreadable files, invalid JSON/layer structure, no usable placeholders,
and write failures fail explicitly without printing a success path. Incomplete
new output is removed if writing fails.
Check text layers for custom fonts
Use the saved supported-font reference list to check an extracted-layer array, a raw Photoshop v2 manifest, or a successful generation response containing embedded JSON manifests:
# Only needed to initialize or refresh the reference list.
ffe photoshop supported-fonts --update
ffe photoshop check-fonts --input manifest.json
ffe photoshop check-fonts --input layers.json
# Optional: compare against a different supported-font file.
ffe photoshop check-fonts --input job.json --supported-fonts ./supported-fonts.json
# Check all text layers in the generated manifest, not just tagged layers.
manifest_path="$(ffe photoshop generate-manifest --input-file ./document.psd)"
ffe photoshop check-fonts --input "$manifest_path"The command never updates the input or an existing supported-font reference file.
By default it reads the shared file created by supported-fonts --update; if that
file is missing it fetches the list from Adobe (public documentation, no
authentication) and saves it. A malformed reference file fails with refresh
guidance.
Passing the output of get-tags checks only the text layers in that filtered
array. Pass the full manifest or generation response to check untagged text too.
JSONata selects all text_layer objects recursively, including hidden layers,
groups, and artboards. A second JSONata query collects font.postScriptName
throughout each layer's text object, covering multiple character styles and
nested characterStyle entries. It also includes legacy fontName fields within
the text metadata and the v2 text.missingFonts names. Non-text layers and
unrelated document metadata do not contribute font names.
Comparison uses exact, case-sensitive PostScript names, not display/family names.
Used/custom font names are deduplicated and sorted.
Custom fonts produce a warning on stderr listing every custom font name. Stdout and the in-process return value contain a JSON report:
{
"textLayersChecked": 2,
"fontsUsed": ["BrandSans-Bold", "MyriadPro-Regular"],
"customFonts": ["BrandSans-Bold"],
"complete": true,
"incompleteTextLayers": []
}Text layers with no usable font names or with invalid font-name metadata produce
an additional warning and complete: false. incompleteTextLayers identifies
them by a 1-based index in the selected text-layer list, plus ID/name when present.
Known fonts are still compared and reported; missing information is never treated
as a clean result. An input with no text layers reports zero checked layers.
Custom-font and incomplete-check warnings do not set a failing exit status. Unreadable files, invalid JSON/manifest structure, invalid reference lists, or a failed fetch for a missing reference fail instead of printing a success report. Here "custom" means absent from the saved built-in list, not necessarily unusable: authorized Adobe Fonts and user-supplied fonts may still be available.
Generate a manifest
Authenticate through the host with credentials entitled to the Photoshop API. Use the scopes granted to your Developer Console project; the host's default Firefly scopes may not cover your Photoshop entitlement.
ffe auth login --profile production --scopes YOUR_GRANTED_SCOPES
ffe photoshop generate-manifest \
--input-url 'https://example.com/document.psd?presigned-query' \
--include-layer-thumbnails --include-xmp \
--extract-smart-object-data --maximum-thumbnail-depth 0 \
--profile production--input-url creates this request, with optional flags mapped to their API fields:
{
"image": {"source": {"url": "https://example.com/document.psd"}},
"outputs": [{"destination": {"embedded": "json"}, "mediaType": "application/json"}]
}Other convenience flags: --trim-to-transparency and
--maximum-thumbnail-depth (nonnegative int32; 0 means unlimited).
The API must be able to retrieve the URL. Use --input-file for a local document.
Quote presigned URLs in your shell. For sensitive URLs, prefer a request file
to avoid shell history and process-argument exposure.
Local document upload through the host CLI
--input-file invokes the main CLI's storage upload command in-process, then
uses its read-only signed URL as the Photoshop request's image.source.url.
It reuses the host's selected provider and upload-error handling; the plugin has
no separate cloud storage SDK or credential implementation. Azure is the CLI
default unless FFE_STORAGE_PROVIDER is set; --provider overrides it.
For Azure, require an up-to-date host with storage upload, Azure CLI (az) installed on
PATH, an az login session, and an existing container. Your Azure identity needs
Storage Blob Data Contributor at account scope, or that role at container scope
plus Storage Blob Delegator at account scope for SAS generation.
These Azure permissions are separate from Adobe credentials selected by
--profile.
For AWS, use --provider aws --bucket <bucket> with configured AWS CLI
credentials, the bucket's region, and optional --aws-profile. The identity
needs S3 upload/read permissions and any applicable KMS permissions. Azure CLI
is not required for S3. The requested URL lifetime is 12 hours, but temporary
credentials or policies can shorten access. No buckets, containers, or access
policies are created by manifest generation.
az login
ffe storage configure
ffe photoshop generate-manifest \
--input-file ./document.psd \
--include-layer-thumbnails --include-xmp \
--profile production
# Optional: override the host's Azure destination selection.
ffe photoshop generate-manifest \
--input-file "./design files/document.psd" \
--account mystorageaccount --container inputs \
--no-waitIf --account is omitted, the host uses AZURE_STORAGE_ACCOUNT, or discovers
Storage accounts in the active Azure CLI subscription. If --container is
omitted, it uses AZURE_STORAGE_CONTAINER, then the keyring default saved by
ffe storage configure. Multiple discovered accounts prompt interactively;
noninteractive runs require an explicit/environment account when ambiguous.
Exactly one of --input-file, --input-url, or --request is required.
Storage flags (--provider, --account, --container, --bucket, --region,
and --aws-profile) are accepted only with --input-file.
The same manifest convenience flags work for either file or URL input.
The upload's signed URL is not separately printed; stdout contains only the
temporary JSON file path (or accepted job JSON with --no-wait).
Upload failures stop the command before calling Photoshop.
--timeout covers Photoshop submission/polling, not the preceding storage upload.
Uploaded blobs or S3 objects are not deleted if Photoshop fails or URLs expire.
Full request file
Use --request for all API options, including JPEG/PNG export settings,
Creative Cloud or Lightroom sources, multiple outputs, external URL destinations,
or hosted output with validityPeriod (60–86400 seconds).
The JSON body is passed through without discarding API fields. Core structure,
common option types, and output counts (1–25) are validated locally; the service
performs full API-specific validation.
Example request.json:
{
"image": {"source": {"url": "https://example.com/document.psd"}},
"outputs": [
{"destination": {"embedded": "json"}, "mediaType": "application/json"}
],
"includeLayerThumbnails": true,
"includeXmp": true,
"extractSmartObjectData": true,
"maximumThumbnailDepth": 0,
"trimToTransparency": false,
"exportOptions": {"mediaType": "image/png", "compression": "high"}
}ffe photoshop generate-manifest --request request.json --profile production
ffe photoshop generate-manifest --request request.json --no-wait--request cannot be combined with --input-url, --input-file, or the
convenience-option flags. Request-file sources are passed through as supplied;
the command does not interpret local paths inside JSON as upload instructions.
--profile defaults to default, or the host's FFE_PROFILE environment variable.
Async jobs and output
- By default, the command waits, writes the complete successful job-status
response as pretty-printed JSON to a unique
ffe-ps-manifest-*/manifest.jsonfile under the OS temporary directory, and prints only its absolute path to stdout. In-process command callers also receive that path as the return value. The file includes job metadata andresult.outputs, not just the raw manifest. Embedded output representation follows the API; the command does not flatten it. - Temporary directories are private and files are created with owner-only
permissions (
0600on POSIX). Files remain after the command exits for use byget-tagsor other tools. Delete them when no longer needed; OS temporary-directory cleanup policies also apply. --no-waitprints the accepted{jobId, statusUrl}JSON response without polling or creating a temporary file, since no completed manifest is available.--poll-intervaldefaults to 2 seconds.--timeoutdefaults to 300 seconds and covers submission and polling. Both accept positive integer seconds.- Failed jobs, unknown statuses, malformed responses, timeouts, and HTTP errors fail with a nonzero exit status. Failed-job response bodies are not printed. A timeout does not cancel the remote job; the error includes its ID when known.
- File-creation/write failures also fail without printing a success path. Incomplete output is removed if writing fails.
- Polling always uses
/v2/status/{jobId}on the fixed Photoshop origin. A returnedstatusUrlcannot redirect authenticated requests elsewhere. - No automatic resubmission/retries: avoiding duplicate jobs is more important than hiding transient failures. Hosted/presigned outputs are not downloaded.
Saved JSON responses may contain presigned URLs or document metadata. Treat them as sensitive, along with request files; do not commit either.
Activity indicator
The command uses the host CLI's rotating activity spinner while work is in progress:
- With
--input-file, separate indicators cover reading the saved container default, discovering Azure Storage accounts, and uploading/signing the file, when those steps are needed. Generating Photoshop manifestcovers submission and, by default, waiting for the job to complete. With--no-wait, it covers submission only.- Each indicator stops with
doneorfailed. Indicators stop before interactive prompts and final path/JSON output, including when an operation fails.
The spinner writes only to stderr; stdout contains the temporary file path
(or accepted job JSON with --no-wait). Capture the path while keeping the
animation visible in your terminal:
manifest_path="$(ffe photoshop generate-manifest --input-file ./document.psd)"
cat "$manifest_path"Animation requires interactive stdin and stderr. It is automatically disabled
in CI (when CI is set), when either stream is not a terminal, and for
TERM=dumb or TERM=emacs-color. It indicates activity, not percentage complete
or estimated time remaining.
Publishing and JIT registration
The package is JIT-ready, but the host currently bundles the sibling local dependency rather than using JIT installation. This setup requires both checkouts and is not ready for publishing as-is. To switch the host to JIT after publishing:
Use the npm package name
@mboucher/ffe-cli-ps-pluginand choose the version. Ensure the matching@mboucher/ffe-clipeer version is published before releasing.Run
npm testandnpm pack. Theprepackscript builds TypeScript and generatesoclif.manifest.json; both are included in the package.Publish the plugin to the registry used by the host. The local
file:../ffe-clidevelopment dependency is not a runtime dependency.In the host's
package.json, register the published exact version:{ "oclif": { "jitPlugins": { "@mboucher/ffe-cli-ps-plugin": "0.1.0" } } }
Remove the local @mboucher/ffe-cli-ps-plugin dependency and its oclif.plugins entry
when switching to JIT, and decouple the host build from the sibling checkout.
- Run
npm packin the host to rebuild its merged command manifest, then release the host. Registry access is required for that merge.
The host JIT catalog intentionally remains unchanged until the plugin is published.
Once registered and released, ffe photoshop generate-manifest prompts to
install the plugin on first use. For CI/noninteractive use, preinstall the exact
version with ffe plugins install @mboucher/[email protected].
