npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mboucher/ffe-cli

v0.1.3

Published

An extensible CLI for Adobe Firefly Services

Readme

Firefly Enterprise CLI

The npm package is @mboucher/ffe-cli; the executable remains ffe.

ffe is a TypeScript CLI built on oclif for Adobe Firefly Services. The core handles OAuth Server-to-Server authentication, secure credential storage, Azure Blob Storage or AWS S3 uploads, and plugin management. Individual API capabilities belong in separate oclif plugins.

Requirements

  • Node.js 22.13+ and npm.
  • macOS or Windows, with an accessible OS credential store for saved Adobe credentials and the default storage container.
  • Azure CLI (az) installed and available on PATH for Azure ffe storage upload (the default provider). It is a separate dependency, not installed by npm install. Uploads require an authenticated Azure CLI session even when --account is supplied. Adobe authentication, plugin management, and ffe storage configure do not require Azure CLI.
  • AWS CLI (aws) installed and configured instead of Azure CLI when using ffe storage upload --provider aws. AWS credentials stay in the AWS CLI flow.

Azure CLI setup

Install Azure CLI using the official installation instructions, or your platform's package manager:

# macOS
brew install azure-cli
# Windows
winget install --exact --id Microsoft.AzureCLI

Open a new terminal after installation if needed, then verify the installation and sign in:

az version
az login
az account show

# Optional: select the subscription containing your Storage account.
az account set --subscription "<subscription-name-or-id>"

The CLI uses AzureCliCredential to obtain a Storage access token from your Azure CLI session. Azure login is separate from ffe auth login; Adobe credentials cannot authorize Azure uploads. FFE does not store Azure passwords, access tokens, storage keys, or connection strings in its keychain entries. Signing in alone is not sufficient: your identity also needs the Blob data permissions described below.

Development

The Photoshop plugin is loaded automatically as a bundled local dependency. Keep both repositories as siblings:

parent/
  ffe-cli/
  ffe-cli-ps-plugin/

From the host checkout:

nvm use
npm ci
npm --prefix ../ffe-cli-ps-plugin install
npm run build
node bin/run.js --help
node bin/run.js photoshop generate-manifest --help
npm link
ffe --help

On Windows, select Node 22+ using your preferred installer/version manager instead of nvm use. npm link makes the ffe executable available locally; this project does not need to be published to try it. The host build first compiles its SDK, then builds the sibling Photoshop plugin and generates that plugin's oclif manifest. No ffe plugins link step is needed for Photoshop. Editing plugin TypeScript requires another build.

CI checks out both repositories in this layout and tests both packages. If the plugin repository is private, configure the PLUGIN_REPO_TOKEN Actions secret with read-only access to it; the default workflow token cannot generally read another private repository. Both repositories' implementation changes must be pushed before CI can use them.

npm run typecheck
npm test
npm pack

npm pack builds the CLI and generates the oclif command manifest included in the published package. Rebuild after editing TypeScript. The test suite uses synthetic credentials and mocked HTTP; it does not call Adobe APIs. Storage tests also use mocks; they do not upload files or access Azure or AWS accounts.

To also test real OS credential storage, set FFE_TEST_KEYRING=1 before running npm test ($env:FFE_TEST_KEYRING = '1' in PowerShell). This writes and removes one synthetic, uniquely named test profile. CI runs on Windows/macOS and Node 22/24; native keyring testing is opt-in because unattended runners may have locked credential stores.

Publish to npm

Use Node.js 22.13+ and an npm account authorized to publish @mboucher/ffe-cli. The current build still requires the sibling Photoshop plugin checkout and its development dependencies. Ensure the declared plugin version is published before releasing the CLI. Increment the CLI version for subsequent releases; npm does not allow overwriting published versions.

npm login
npm run deploy -- --dry-run
npm run deploy

deploy publishes with public access. Both dry-run and real publishing run typechecking and tests through prepublishOnly; failures stop the release. The existing prepack hook builds release files and generates the oclif manifest. The dry run does not publish a package. Complete authentication and two-factor prompts locally; never store npm tokens in scripts or source control.

Command reference

| Command | Purpose | | --- | --- | | ffe auth login | Validate Adobe credentials and save them in the OS keyring. | | ffe auth status | Verify the selected Adobe credential profile. | | ffe auth logout | Remove the selected Adobe credential profile. | | ffe storage configure | Interactively save a default Azure Blob container in the OS keyring. | | ffe storage upload <file> | Upload to Azure Blob Storage or AWS S3 and print a read-only signed URL requesting 12-hour access. | | ffe photoshop generate-manifest | Generate a Photoshop manifest, save the job response to a temporary JSON file, and print its path. | | ffe photoshop supported-fonts | List saved PostScript fonts, fetching a missing reference; --update refreshes the list and prints its path. | | ffe photoshop check-fonts --input <file> | Warn about custom fonts in extracted layers or a full Photoshop manifest, using the saved supported-font list. | | ffe photoshop sample-csv --input <file> | Create a sample CSV with one column per unique {{key}} in extracted layer names and text. | | ffe plugins | List installed plugins. | | ffe plugins install <package> | Install a trusted plugin. | | ffe plugins link <path> | Link a local plugin for development. | | ffe plugins uninstall <package> | Remove a plugin. |

Use --help to see command arguments and options:

ffe --help
ffe storage configure --help
ffe storage upload --help
ffe photoshop generate-manifest --help

Activity indicator

In an interactive terminal, an Ora spinner on stderr indicates work in progress during Adobe authentication, keyring access, Azure account discovery, file upload/URL signing, and Photoshop job submission/polling. It stops before interactive prompts and final command output, and reports failed if the operation throws.

The indicator is disabled in noninteractive sessions, CI, --json mode, and terminals declaring TERM=dumb or TERM=emacs-color. URL and JSON output on stdout stays unchanged. Built-in plugin-management commands retain their own oclif progress indicators. The shared Ora spinner does not change stdin's raw mode or discard input, and nested operations reuse the active indicator.

Authentication

Create OAuth Server-to-Server credentials with the relevant API entitlements in Adobe Developer Console. Then:

ffe auth login
ffe auth status
ffe auth logout

ffe auth login --profile production --client-id YOUR_CLIENT_ID
ffe auth status --profile production

Login prompts for the client secret with masked input. It validates the credentials with Adobe IMS before saving them. There is deliberately no client-secret flag, which would expose the secret in shell history and process arguments. Login requires an interactive terminal. For automation, provision a profile using KeyringCredentialStore or AuthManager.login from the SDK in a trusted setup process; a CI runner must have an accessible OS credential store.

The default scopes follow Adobe's Firefly authentication guide:

openid,AdobeID,session,additional_info,read_organizations,firefly_api,ff_apis

Use --scopes scope1,scope2 during login to match the scopes granted to your Developer Console project, including any additional services required by plugins. --profile defaults to default; FFE_PROFILE can select a profile for all commands that extend the shared base command.

Credential handling

  • @napi-rs/keyring uses macOS Keychain and Windows Credential Manager through native, prebuilt bindings.
  • The client ID, secret, and scopes are stored together under service ffe-cli, with the profile name as the account. No plaintext credential files or fallback.
  • Access tokens stay in process memory, are reused while valid, and are renewed within 60 seconds of expiry. Separate CLI invocations request new tokens.
  • auth status performs a real token exchange without displaying the token or secret. auth logout deletes local credentials; it does not revoke already issued Adobe tokens.
  • Keyring, OAuth, and API errors fail the command. Upstream response bodies are not printed because they may contain sensitive information.
  • Install only trusted plugins: they execute with your user permissions and can access the same credentials as the core CLI.

Choose a storage provider

Use --provider azure or --provider aws for uploads and local-file manifest generation. The CLI defaults to Azure unless FFE_STORAGE_PROVIDER is set; it does not prompt for a provider. The template-validation skill prompts through the AI harness before a local PSD upload, using the optional MCP ffe_storage_providers tool for guidance. Provider selection does not authorize an upload or an Adobe job. Supplied manifests and existing HTTPS URLs do not require a storage-provider choice.

The provider's cloud CLI and credentials are separate from Adobe authentication. ffe storage configure saves only an Azure container, not an AWS bucket or provider preference.

Upload a local file to AWS S3

Select S3 explicitly; Azure remains the default. Use an existing general-purpose bucket and configure AWS CLI credentials locally (for example aws sso login --profile production). No AWS credentials are collected by FFE.

ffe storage upload ./document.psd --provider aws --bucket inputs --region us-east-1 --aws-profile production
ffe storage upload ./document.psd --provider aws --bucket inputs --json
ffe photoshop generate-manifest --input-file ./document.psd --provider aws --bucket inputs --region us-east-1 --aws-profile production

Set FFE_STORAGE_PROVIDER=aws and AWS_S3_BUCKET to select S3 without repeating those flags. Region and credentials otherwise follow AWS CLI configuration, including AWS_PROFILE; --aws-profile is separate from Adobe's --profile. Azure account/container defaults and the Azure keyring are not used by S3.

Uploads use aws s3 cp with a unique object key, then aws s3 presign for a GET URL requesting 12 hours. Temporary AWS credentials, bucket policies, or KMS permissions can shorten usable access; expiresAt is the requested expiry, not a guarantee that the URL remains usable. The identity needs s3:PutObject and s3:GetObject, plus applicable KMS permissions. The Adobe service must be able to reach the S3 endpoint; presigning does not bypass network or bucket policies. The command does not create buckets, change ACLs, or delete uploaded objects. If signing fails after upload, the object remains in S3. --json returns provider, bucket, objectKey, url, and expiresAt; signed URLs are secrets.

Upload a local file to Azure Blob Storage

ffe storage upload <file> uploads to an existing container and prints a read-only, HTTPS-only user-delegation SAS URL expiring 12 hours after upload. It requires the installed, signed-in Azure CLI described in Azure CLI setup, independently of Adobe authentication and --profile. For an explicit destination:

az login
ffe storage upload ./document.psd --provider azure --account mystorageaccount --container inputs

To save a default container in macOS Keychain or Windows Credential Manager:

ffe storage configure

The command interactively prompts for a container name, validates its format, then saves it locally. Run it again to replace the saved default. It does not create or check the existence of a container, contact Azure, or change your Azure login. The default is shared across storage accounts; make sure that container exists in the account you select.

Storage account selection

If --account and AZURE_STORAGE_ACCOUNT are omitted, the command runs az storage account list in your active Azure CLI subscription:

  • One account: automatically use it.
  • Multiple accounts: prompt for a choice in an interactive terminal. The prompt goes to stderr so stdout still contains only the URL.
  • Multiple accounts in noninteractive or --json mode: fail with instructions to supply --account or AZURE_STORAGE_ACCOUNT.
  • No accounts, missing Azure CLI, or discovery failure: fail with actionable guidance; never choose an arbitrary account or change subscriptions.

Use az account show to inspect the active subscription, or az account set --subscription "<name-or-id>" to select another one. Discovery requires management-plane permission to list Storage accounts. If your identity only has blob-data access, pass an explicit Storage account to bypass discovery. Uploads still authenticate using your Azure CLI identity.

az login
ffe storage configure
ffe storage upload ./document.psd

Account selection is --account > AZURE_STORAGE_ACCOUNT > Azure CLI discovery. Explicit settings bypass discovery and the selection prompt.

Alternatively, set defaults using environment variables:

export AZURE_STORAGE_ACCOUNT=mystorageaccount
export AZURE_STORAGE_CONTAINER=inputs
ffe storage upload ./document.psd
ffe storage upload ./document.psd --json

| Upload option | Behavior | | --- | --- | | <file> | Required local file path. | | --account <name> | Override account discovery and AZURE_STORAGE_ACCOUNT. | | --container <name> | Override AZURE_STORAGE_CONTAINER and the saved keyring default. | | --json | Return URL, expiry, account, container, and blob-name metadata instead of URL-only output. |

In PowerShell, use $env:AZURE_STORAGE_ACCOUNT = 'mystorageaccount' and $env:AZURE_STORAGE_CONTAINER = 'inputs'. Container selection is: --container > AZURE_STORAGE_CONTAINER > saved keyring default. If none is set, the command asks you to run ffe storage configure. In noninteractive environments, pass a container flag or environment variable instead of running the interactive configuration command. Explicit flags/environment defaults bypass the keyring. Otherwise, keyring errors fail the command; no plaintext fallback is used. The saved container uses keyring service ffe-cli-storage, entry default-container, separate from Adobe credentials. The command targets public Azure (blob.core.windows.net); sovereign clouds and custom endpoints are not supported.

Without --json, stdout contains only the URL. JSON mode returns url, expiresAt (UTC), account, container, and blobName. Plugins can compose this command with config.runCommand('storage:upload', ['--quiet', '--', file]) to receive the upload result without printing the SAS URL. The internal --quiet flag cannot be combined with --json. Each file is uploaded under a unique <UUID>/<filename> blob name, with an Azure no-overwrite condition. Files are uploaded from disk using the Azure SDK's file-transfer API rather than read into one in-memory buffer.

Your signed-in principal needs permission to upload/read blobs and request a user delegation key. Storage Blob Data Contributor at storage-account scope covers both. For narrower blob access, combine that role at container scope with Storage Blob Delegator at account scope. Azure resource Contributor alone does not grant blob-data access. Storage firewall/private-endpoint rules and public-network settings must also allow access by both the uploader and the service that will retrieve the URL. Keep the container private.

The SAS grants read access to just the uploaded blob, not write/list/delete permissions. Its start time is backdated five minutes for clock skew; expiry is calculated after the upload finishes. Storage policies or early revocation can shorten availability; accounts must permit the requested SAS validity period (12 hours plus the five-minute start-time skew allowance). No storage keys, connection strings, or plaintext credential files are used.

Troubleshooting HTTP 403

Being signed in, or being an Azure Owner or Contributor, does not by itself grant Blob data-write permission. Ask an administrator for Storage Blob Data Contributor on the target storage account, or the narrower container-scoped role plus account-scoped Storage Blob Delegator described above. Allow time for new role assignments to propagate before retrying.

Errors identify the selected account/container and include recognized Azure error codes without printing upstream response bodies or tokens:

  • AuthorizationPermissionMismatch: check data-plane permissions and, for SAS generation, user-delegation-key permissions.
  • AuthorizationFailure: check both RBAC and storage network restrictions.
  • AuthenticationFailed: sign in again and verify tenant/subscription and clock.
  • ContainerNotFound: check the account/container or saved configuration.

An upload-stage failure occurs before a SAS is generated. If SAS generation fails after a successful upload, the error explicitly says the blob remains.

For other setup failures:

  • az not found: install Azure CLI, ensure it is on PATH, and open a new terminal before retrying.
  • Not signed in or session expired: run az login, then check az account show.
  • Cannot list Storage accounts: verify the active subscription and management-plane permissions, or pass --account to bypass discovery. An explicit account still requires Azure CLI authentication and Blob data access.
  • No container configured: run ffe storage configure, pass --container, or set AZURE_STORAGE_CONTAINER.

Use an uploaded file with Photoshop

The bundled Photoshop plugin can invoke the host upload command directly for a local document:

ffe photoshop generate-manifest --input-file ./document.psd
ffe photoshop generate-manifest --input-file ./document.psd --account mystorageaccount --container inputs

The plugin reuses the storage account/container selection above and passes the uploaded file's SAS URL to Photoshop. By default, it saves the complete successful job response in a private temporary JSON file and prints only its absolute path. The file remains available after the command exits; remove it when no longer needed. With --no-wait, stdout is still the accepted job JSON and no temporary file is created.

Capture the path for local layer extraction:

manifest_path="$(ffe photoshop generate-manifest --input-file ./document.psd)"
layers_path="$(ffe photoshop get-tags --manifest "$manifest_path")"
cat "$layers_path"

Alternatively, upload first and supply the URL explicitly:

url="$(ffe storage upload ./document.psd)"
ffe photoshop generate-manifest --input-url "$url"

Treat the URL as a credential: anyone holding it can read the file until expiry. Do not commit it or log it in shared systems. Uploaded blobs are not deleted when the URL expires; configure a storage lifecycle policy or remove them yourself. If upload succeeds but SAS generation fails, the command reports the failure and the blob remains in the container.

Plugins

Bundled local Photoshop plugin

@mboucher/ffe-cli-ps-plugin is registered in oclif.plugins as a versioned npm dependency. Its commands are available after installation; there is no JIT installation prompt. It is a separate package, not a copy of the plugin's implementation in the host.

Consumers install the published plugin through npm without a sibling checkout. The current host build and CI still require the sibling plugin repository and its development dependencies. Publish a compatible plugin version before releasing a host version that depends on it. Optional JIT registration is described below.

The bundled oclif plugin manager supports:

ffe plugins
ffe plugins install <trusted-package>
ffe plugins link /absolute/path/to/local/plugin
ffe plugins uninstall <package>

Just-in-time (JIT) plugins

JIT plugins are not dependencies of the core CLI. The release maintainer registers published plugins in package.json:

{
  "oclif": {
    "jitPlugins": {
      "@your-org/ffe-plugin-firefly": "1.0.0",
      "@your-org/ffe-plugin-photoshop": "1.0.0"
    }
  }
}

These names are illustrative, not published packages. The registry is initially empty so this scaffold does not advertise nonexistent API commands.

  1. Publish each plugin with its own oclif.manifest.json (oclif manifest).
  2. Add its package name and exact version to oclif.jitPlugins.
  3. Run npm pack for the host. Its prepack hook runs oclif manifest, downloads the registered plugin packages, and merges their command metadata into the host manifest. Registry access is needed at release time.
  4. Ship that manifest with the CLI. Command help/discovery works without installing the API plugins.
  5. On first invocation, the JIT hook asks permission before installing the exact registered version, after which oclif runs the original command.

Declining installation fails the command. Non-interactive environments never auto-install; preinstall with ffe plugins install <package>@<version> instead. JIT is an explicit release-time catalog, not automatic discovery of arbitrary registry packages. After changing the catalog, regenerate the host manifest.

Shared TypeScript plugin SDK

The root @mboucher/ffe-cli package exports BaseCommand, AuthManager, KeyringCredentialStore, FireflyClient, withSpinner, and their types. API plugins should declare @mboucher/ffe-cli as a peer dependency and @oclif/core as a dependency so they share the host SDK contract. Commands extending BaseCommand can call this.withSpinner('Working', async () => { /* operation */ }) around long-running work. Other commands can import withSpinner directly and pass !this.jsonEnabled() as its third argument. Wrap work after prompts have completed; nested calls reuse the active indicator rather than starting another.

Example plugin package (replace the name before publishing):

{
  "name": "@your-org/ffe-plugin-firefly",
  "version": "1.0.0",
  "type": "module",
  "engines": {"node": ">=22"},
  "files": ["dist", "oclif.manifest.json"],
  "scripts": {
    "build": "tsc",
    "prepack": "npm run build && oclif manifest"
  },
  "dependencies": {"@oclif/core": "^5.1.9"},
  "peerDependencies": {"@mboucher/ffe-cli": "^0.1.0"},
  "devDependencies": {
    "typescript": "^5.9.3",
    "oclif": "^6.0.10",
    "@mboucher/ffe-cli": "file:../ffe-cli"
  },
  "oclif": {"commands": "./dist/commands"}
}

For local development, point the file: dependency at this checkout. Once the host is published, use the matching host version as the development dependency. Compile with TypeScript module/moduleResolution set to NodeNext, target to ES2022, strict enabled, rootDir set to src, and outDir to dist.

For example, src/commands/firefly/generate.ts in that separate plugin:

import {Flags} from '@oclif/core'
import {BaseCommand} from '@mboucher/ffe-cli'

export default class Generate extends BaseCommand {
  static description = 'Generate an image with Firefly'
  static flags = {
    prompt: Flags.string({required: true, description: 'Image prompt'}),
  }

  async run(): Promise<void> {
    const {flags} = await this.parse(Generate)
    const client = this.createClient(flags.profile)
    const response = await client.request('/v3/images/generate', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({prompt: flags.prompt}),
    })
    this.log(JSON.stringify(await response.json(), null, 2))
  }
}

Build and link the plugin, then run ffe firefly generate --prompt "A mountain lake" --profile production. This is an authoring example, not a bundled API command; production plugins own API schemas, output handling, and job polling.

FireflyClient injects Authorization: Bearer ... and x-api-key, sets request timeouts, and rejects HTTP failures. It accepts only same-origin paths and does not follow redirects, preventing credentials from being forwarded to another origin. Plugins for other services can pass a trusted, fixed HTTPS origin as the second argument to createClient. Never use an untrusted API result or user-supplied upload URL as an authenticated client's base URL. Use a separate, unauthenticated request for presigned uploads/downloads.

Plugins can also use AuthManager.getAuthorization(profile) directly if they need their own transport. Tokens and credentials returned by the SDK are sensitive; never log them.