XFIX CLI is a command-line tool for PHP application deployment, code obfuscation, Flutter builds, Android APK/AAB signing, and development automation. It is built with Node.js and designed to streamline the workflow from local development to production release without requiring complex CI/CD pipelines.
XFIX CLI provides a unified workflow that combines packaging, secure deployment, code protection, mobile builds, and Android signing into a single command interface.
It is suitable for:
- SaaS developers managing multiple deployments
- PHP developers without dedicated DevOps pipelines
- Flutter developers who need signed Android artifacts without handling keystores by hand
- Teams that need fast, repeatable release processes
- Automated packaging into optimized ZIP archives
- FTP, FTPS, and SFTP upload with retry mechanism
- Remote deployment via HTTP endpoint
- Branch restriction to prevent unintended deployments
- File filtering using
.updateignore - Multi-distribution deployment from a single project
- Per-distribution deploy markers for incremental releases
- Optional secure mode with full obfuscation
- JavaScript obfuscation with domain locking
- PHP obfuscation using Yakpro-PO
- Selective obfuscation (JS, PHP, or both)
- Automatic backup before obfuscation
- Revert capability to restore original code
- Android APK builds via
flutter build apk - Android App Bundle (AAB) builds for Play Store
- iOS archive builds
- Web bundle builds
- Raw Flutter CLI passthrough for any subcommand
- Build-tool detection (flutter, keytool, jarsigner, zipalign, apksigner)
- Local JKS keystore generation
- Keystore inspection and APK signature verification
- Two-phase provisioning that registers a keystore with a platform API
- Credentials fetch API with env-var override and retry
- Automatic zipalign and apksigner (v1/v2/v3) pipeline
- AAB signing with jarsigner
- Support for API-provided keystores (no local
.jksneeded in CI)
- Controller generation
- Service generation (general and migration type)
- Verbose logging for debugging
- Environment variable support
- Structured error handling
- Create, run, rollback, and reset migrations
- Seeder generation and execution
- Migration status inspection
- Support for JavaScript and PHP migration templates
- Node.js >= 18.x
- PHP >= 7.4 (for PHP obfuscation)
- Composer (for Yakpro-PO)
- FTP/FTPS or SFTP server with write access (deployment only)
- Git repository (deployment only)
- Flutter SDK (build and signing only)
- JDK 11+ (for keytool and jarsigner)
- Android SDK build-tools (for zipalign and apksigner)
git clone https://github.com/Gicehajunior/xfix-cli.git
cd xfix-cli
npm install
npm link
composer global require pk-fr/yakpro-po
export PATH="$HOME/.composer/vendor/bin:$PATH"Create a .xfixrc.json file in your project root. Every section is optional; the CLI only requires the sections relevant to the command you run.
{
"protocol": "ftp",
"host": "ftp.yourdomain.com",
"username": "your-ftp-username",
"password": "${DEPLOY_PASSWORD}",
"remotePath": "public_html/",
"deployPath": "public_html/",
"branch": "main",
"cleanupLocal": true,
"secure": false,
"rejectUnauthorized": false,
"maxRetries": 3,
"retryDelay": 2000,
"verbose": false,
"deployUrl": "https://yourdomain.com",
"allowBackup": true,
"runMigrations": false,
"clearCache": false,
"runComposer": false,
"clientId": "${CLIENT_ID}",
"apiKey": "${API_KEY}",
"obfuscateJs": false,
"obfuscatePhp": false,
"jsSrcPath": "public/js",
"jsDestPath": "public/orig",
"preserveOriginal": "public/original_js_asset_folder",
"domainLock": [
"http://localhost",
"http://127.0.0.1",
"https://yourdomain.com",
"https://www.yourdomain.com"
],
"domainLockRedirectUrl": "https://yourdomain.com",
"exclude": [
"vendor",
"node_modules",
".git",
".env"
]
}Set protocol to sftp to upload over SSH instead of FTP. Port defaults to 22, and the SFTP-specific connection options (readyTimeout, retryFactor, retryMinTimeout) apply.
{
"protocol": "sftp",
"host": "workspace.xfixglobal.com",
"username": "deploy-user",
"password": "${DEPLOY_PASSWORD}",
"port": 22,
"remotePath": "workspace.xfixglobal.com/public/store/deploy/",
"deployPath": "workspace.xfixglobal.com/",
"branch": "develop",
"cleanupLocal": false,
"secure": true,
"rejectUnauthorized": false,
"maxRetries": 3,
"readyTimeout": 30000,
"retryFactor": 2,
"retryDelay": 2000,
"retryMinTimeout": 2000,
"verbose": false,
"deployUrl": "https://xfixglobal.com/api/v1/deploy",
"allowBackup": false,
"runMigrations": false,
"clearCache": false,
"runComposer": false,
"clientId": "${CLIENT_ID}",
"apiKey": "${API_KEY}",
"obfuscateJs": false,
"obfuscatePhp": false,
"jsSrcPath": "public/js",
"jsDestPath": "public/orig",
"preserveOriginal": "public/original_js_asset_folder",
"domainLock": [
"http://localhost",
"http://127.0.0.1",
"https://workspace.xfixglobal.com",
"https://www.workspace.xfixglobal.com"
],
"domainLockRedirectUrl": "https://workspace.xfixglobal.com",
"exclude": [
"tests",
"vendor.js",
"vendor",
"resources",
"node_modules",
".git",
".env"
]
}Notes on SFTP:
remotePathis the directory the ZIP is uploaded into over SFTP.deployPathis passed to the remote deployment endpoint so the server knows where to extract.secure: trueis still required to enable full obfuscation. It has no effect on the SFTP transport itself (SFTP is already encrypted).rejectUnauthorizedis not used by the SFTP client.- Set
verbose: trueto log SFTP protocol messages and per-file transfer progress.
Use a distributions block to deploy to several servers from the same project. Each distribution can use a different protocol.
{
"distributions": {
"production": {
"protocol": "sftp",
"host": "workspace.xfixglobal.com",
"username": "prod-user",
"password": "${PROD_DEPLOY_PASSWORD}",
"port": 22,
"remotePath": "workspace.xfixglobal.com/public/store/deploy/",
"deployPath": "workspace.xfixglobal.com/",
"branch": "main"
},
"staging": {
"protocol": "ftp",
"host": "staging.yourdomain.com",
"username": "staging-user",
"password": "${STAGING_DEPLOY_PASSWORD}",
"remotePath": "staging/",
"deployPath": "staging/",
"branch": "develop"
}
}
}The preferred shape uses a single android.apiUrl base URL plus a shared auth block and an optional releaseBody:
{
"android": {
"appId": "app_your_app_identifier",
"apiUrl": "https://api.xfixglobal.com",
"keystorePath": "android/app/release.jks",
"keyAlias": "release",
"auth": {
"headers": {
"XFIX-APP-ID": { "env": "XFIX_APP_ID" },
"XFIX-API-KEY": { "env": "XFIX_API_KEY" }
}
},
"releaseBody": {
"project": "your-project-slug",
"environment": "production"
},
"timeout": 10000,
"retries": 2
},
"flutter": {
"projectDir": ".",
"flavor": null
}
}The auth block accepts several shapes (all optional, combinable):
{ "type": "bearer", "tokenEnv": "XFIX_SECRETS_TOKEN" }
{ "type": "basic", "userEnv": "XFIX_USER", "passEnv": "XFIX_PASS" }
{ "type": "header", "headerName": "X-API-Key", "tokenEnv": "XFIX_API_KEY" }
{
"headers": {
"X-Tenant": "acme",
"XFIX-APP-ID": { "env": "XFIX_APP_ID" },
"XFIX-API-KEY": { "env": "XFIX_API_KEY" },
"Authorization": { "template": "Token ${XFIX_API_KEY}" }
}
}{ "env": "NAME" }reads the header value from the given environment variable. Missing values are a hard error.{ "template": "…" }interpolates${VAR}placeholders from the environment. Missing values are a hard error.- Plain strings are sent as-is.
Base URL resolution order: android.apiUrl → XFIX_API_URL → URL derived from a legacy credentialsApi.url / provisioningApi.url → https://api.xfixglobal.com. The path suffixes (/api/v1/android-signing/release, /provision/prepare, /provision/commit) are a fixed part of the API contract and are not configurable.
Legacy shape (still honoured): if your config uses credentialsApi and provisioningApi, the CLI derives the base URL from the first non-empty url, and uses that block's auth and body as defaults when android.auth / android.releaseBody are absent.
{
"android": {
"appId": "app_your_app_identifier",
"keystorePath": "android/app/release.jks",
"keyAlias": "release",
"credentialsApi": {
"url": "https://api.yourcompany.com/api/v1/android-signing/release",
"method": "POST",
"timeout": 10000,
"retries": 2,
"auth": {
"headers": {
"XFIX-APP-ID": { "env": "XFIX_APP_ID" },
"XFIX-API-KEY": { "env": "XFIX_API_KEY" }
}
},
"body": { "project": "your-project-slug" }
},
"provisioningApi": {
"url": "https://api.yourcompany.com/api/v1/android-signing/provision",
"auth": {
"headers": {
"XFIX-API-KEY": { "env": "XFIX_API_KEY" }
}
}
}
}
}Do not store credentials directly in .xfixrc.json. Use environment variables.
# Deployment
export DEPLOY_PASSWORD="your-ftp-or-sftp-password"
export CLIENT_ID="your-client-id"
export API_KEY="your-api-key"
# Android signing
export XFIX_APP_ID="app_your_app_identifier"
export XFIX_API_KEY="sk_live_..."
export XFIX_API_URL="https://api.xfixglobal.com"
export ANDROID_KEYSTORE_PASSWORD="your-keystore-password"
# Optional keystore overrides
export ANDROID_KEYSTORE_PATH="android/app/release.jks"
export ANDROID_KEY_ALIAS="release"
export ANDROID_KEY_PASSWORD="your-key-password"XFIX CLI resolves these values at runtime. The auth blocks in .xfixrc.json describe which headers to send and which environment variables hold the values; the values themselves never appear in the config file.
Precedence for any value that can be set multiple ways is always CLI flag → environment variable → .xfixrc.json → built-in default.
| Option | Type | Description |
|---|---|---|
| protocol | string | Transport: ftp (default), ftps, or sftp |
| host | string | FTP/FTPS/SFTP server hostname |
| port | number | Server port. Default 21 for ftp/ftps, 22 for sftp |
| username | string | Login username |
| password | string | Login password or env reference |
| remotePath | string | Remote directory the ZIP is uploaded to |
| deployPath | string | Server deploy path sent to the deploy endpoint |
| branch | string | Allowed deployment branch |
| cleanupLocal | boolean | Remove ZIP after upload |
| secure | boolean | Enable FTPS and full obfuscation |
| rejectUnauthorized | boolean | SSL validation (ftps only) |
| maxRetries | number | Upload retry attempts |
| retryDelay | number | Delay between retries (ms) |
| readyTimeout | number | SFTP connection timeout in ms (default 30000) |
| retryFactor | number | SFTP retry backoff factor (default 2) |
| retryMinTimeout | number | SFTP minimum retry timeout in ms (default 2000) |
| verbose | boolean | Debug logging (SFTP protocol trace when sftp) |
| deployUrl | string | Deployment endpoint |
| allowBackup | boolean | Backup before deployment |
| runMigrations | boolean | Execute migrations |
| clearCache | boolean | Clear application cache |
| runComposer | boolean | Run composer install |
| clientId | string | API client ID |
| apiKey | string | API key |
| obfuscateJs | boolean | Enable JS obfuscation |
| obfuscatePhp | boolean | Enable PHP obfuscation |
| jsSrcPath | string | JS source directory |
| jsDestPath | string | JS output directory |
| preserveOriginal | string | Backup directory |
| domainLock | array | Allowed domains |
| domainLockRedirectUrl | string | Redirect for blocked domains |
| exclude | array | Excluded files |
| Option | Type | Description |
|---|---|---|
| android.appId | string | Application identifier used as XFIX-APP-ID |
| android.apiUrl | string | Base URL of the signing API (overrides anything derived below) |
| android.keystorePath | string | Path to the local .jks file |
| android.keyAlias | string | Alias to sign with |
| android.auth | object | Auth block — see "Auth shapes" above |
| android.releaseBody | object | JSON body sent to the credentials endpoint |
| android.timeout | number | Request timeout in ms (default 10000) |
| android.retries | number | Retry count for API calls (default 2) |
| android.credentialsApi.url | string | Legacy. Base URL derived from this when apiUrl is unset |
| android.credentialsApi.auth | object | Legacy. Used when android.auth is unset |
| android.credentialsApi.body | object | Legacy. Used as releaseBody fallback |
| android.provisioningApi.url | string | Legacy. Base URL derived from this when apiUrl is unset |
| android.provisioningApi.auth | object | Legacy. Used when android.auth is unset |
Define files to exclude from packaging and obfuscation.
node_modules/
vendor/
.git/
.env
storage/
cache/
logs/
public/storage/
public/build/
obfuscated/
deploy.zipxfix runis the base command for PHP deployments and obfuscationxfix deployis shorthand forxfix run --deployxfix buildhandles Flutter builds and Android signingxfix keystoremanages Android JKS keystoresxfix dbhandles database migrations and seedersxfix devhouses development generatorsxfix flutterpasses any arguments straight to the Flutter CLI
xfix run --deploy
xfix deploy
xfix deploy --secure
xfix run --deploy --verbose
xfix run --deploy --include-dependencies
xfix run --deploy --fullMulti-distribution:
xfix distributions list
xfix distributions status
xfix distributions deploy production
xfix distributions deploy-all
xfix distributions validate production
xfix distributions reset-marker productionxfix obfuscate --all
xfix obfuscate --js
xfix obfuscate --php
xfix revert --allxfix build doctor
xfix build apk
xfix build apk --flavor prod --split-per-abi
xfix build apk --target lib/main_prod.dart
xfix build apk --no-sign
xfix build apk --sign-only --apk build/app/outputs/flutter-apk/app-release.apk
xfix build aab
xfix build aab --target lib/main_prod.dart
xfix build ios
xfix build ios --no-codesign
xfix build webRaw Flutter passthrough:
xfix flutter pub get
xfix flutter clean
xfix flutter doctor -v
xfix flutter pub run build_runner buildxfix keystore generate --path android/app/release.jks --alias release
xfix keystore generate --path android/app/release.jks --alias release --force
xfix keystore info --path android/app/release.jks
xfix keystore verify build/app/outputs/flutter-apk/app-release.apkProvision a keystore and register it with a platform API in one command:
xfix keystore provision
xfix keystore provision --app-id app_other_identifier
xfix keystore provision --skip-upload
xfix keystore provision --api-url https://api.example.com--api-url overrides android.apiUrl (or XFIX_API_URL). It is a base URL only — the CLI appends the fixed contract paths (/api/v1/android-signing/provision/prepare and /api/v1/android-signing/provision/commit) itself. Passing a full endpoint path will not work.
The provision command runs in four phases:
- Call the provisioning API's
prepareendpoint to obtain the Distinguished Name, key alias, and package name for the target application. The server derives these from its own records so every app under the same organisation gets a consistent certificate identity. - Generate the JKS locally with those values.
- Encode the file as base64.
- Upload it to the provisioning API's
commitendpoint. The server stores the file, encrypts the passwords, and links it to the application row.
App IDs resolve in this order: --app-id flag, then android.appId in .xfixrc.json, then the XFIX_APP_ID environment variable.
Keystore paths resolve in this order: --path flag, then android.keystorePath in .xfixrc.json, then ANDROID_KEYSTORE_PATH.
Passwords resolve in this order: --storepass flag, then the ANDROID_KEYSTORE_PASSWORD environment variable, then an interactive prompt. If the server reports passwordOwner: "admin", the CLI uses the server-supplied value (after verifying it satisfies keytool's six-character minimum) and skips the prompt entirely.
xfix db migrate
xfix db migrate --step 5
xfix db migrate --dry-run
xfix db rollback
xfix db rollback --step 2
xfix db rollback --target 20260101_create_users_table
xfix db create create_users_table --table users
xfix db create add_status_to_orders --table orders --template alter
xfix db status
xfix db reset
xfix db reset --seed
xfix db seed
xfix db seed --class UserSeeder
xfix db generate:seeder UserSeederxfix dev generate controller UserController
xfix dev generate controller UserController AdminController
xfix dev generate service PaymentGateway EmailService
xfix dev generate service MigrationRunner --type migration
xfix dev generate service UserService RoleService PermissionService- Load configuration
- Validate branch
- Apply obfuscation if enabled
- Scan files using
.updateignore - Create ZIP archive
- Upload over the configured protocol:
ftp/ftpsviabasic-ftpsftpviassh2-sftp-client
- Trigger remote deployment endpoint
- Cleanup local artifacts
- Log results
The signing pipeline depends on where the signing credentials live.
Set ANDROID_KEYSTORE_PASSWORD and the CLI uses it directly, bypassing the credentials API.
If no environment override is present, the CLI calls the release endpoint derived from android.apiUrl (or its legacy equivalent) with the headers described in android.auth. The server responds with the keystore password, key alias, and optionally a base64-encoded copy of the keystore file. If a base64 keystore is returned, the CLI writes it to a temporary file, signs with it, and deletes the file. This is what allows CI runners to sign without a .jks ever existing on disk.
If neither the environment override nor the credentials API produces a keystore, the CLI falls back to the local file at android.keystorePath (or ANDROID_KEYSTORE_PATH).
Once credentials are resolved:
- Build the APK or AAB with Flutter
- Run
zipalignon the artifact (APK only) - Run
apksignerwith v1, v2, and v3 signature schemes (APK) - Run
jarsignerwith SHA256withRSA (AAB) - Replace the original artifact with the signed version
- Delete any temporary keystore file
Connecting to server...
Connected to server
Upload attempt 1/3...
Upload failed, retrying...
Upload attempt 2/3...
Upload successful
Triggering remote deployment...
Deployment completed
Cleanup complete
Flutter build (APK):
----------------------------------------
$ flutter build apk --release
...
build/app/outputs/flutter-apk/app-release.apk
Android signing:
----------------------------------------
Fetching signing credentials from API (attempt 1/3)...
Signing credentials fetched
Credentials source: api
zipalign...
apksigner...
Signed: build/app/outputs/flutter-apk/app-release.apk
Build + signing completed
The deployment server must expose a POST endpoint such as:
POST /api/v1/deploy
Expected responsibilities:
- Validate API credentials
- Locate the uploaded ZIP file
- Extract files to the target directory
- Optionally run migrations, clear caches, or run composer install
- Return a JSON response
Example response:
{
"success": true,
"message": "Deployment completed"
}Example controller:
class DeploymentApiController
{
public function deploy()
{
// Validate headers
// Extract ZIP
// Execute tasks
return [
'success' => true,
'message' => 'Deployment completed'
];
}
}The signing server must expose three POST endpoints to support the two-phase provisioning flow and credential fetch.
POST /api/v1/android-signing/{profile} # fetch signing credentials
POST /api/v1/android-signing/provision/prepare # obtain DN, alias, package name
POST /api/v1/android-signing/provision/commit # upload generated keystore
The prepare endpoint should derive the Distinguished Name from a company settings record so the certificate identity is consistent across every application in the organisation. It must refuse to run if a keystore already exists for the application, because the DN is baked into the certificate at generation time and cannot be changed afterwards.
The commit endpoint should decode the base64 keystore, verify the JKS magic bytes, write the file to private storage, and update the application row. It must refuse to overwrite an existing keystore.
If a deployment fails:
- Restore from the server backup
- Revert local obfuscation:
xfix revert --all- Redeploy:
xfix deploy --secureIf a build or signing step fails, no artifact is published. Correct the underlying issue and re-run xfix build. The keystore and its credentials are not affected by build failures.
- Ensure the remote directory exists
- Verify the correct
remotePath - Check write permissions
- Verify
port(default is22forprotocol: "sftp") - Ensure the server allows password authentication, or configure key-based auth on the SFTP server
- Increase
readyTimeoutif the handshake times out on slow links - Run with
verbose: trueto see the SFTP protocol trace - Confirm
remotePathexists and the SSH user has write access
- Increase
max_execution_time - Check server logs
- Confirm ZIP file presence
Error: Cannot find a specific module
Fix:
npm install
# or
npm updateThe credentials API responded successfully but the server could not locate the keystore file on disk. Check that android_keystore_path on the application row points at a file that exists inside the configured storage disk.
The password used to create the .jks does not match what the server or environment currently holds. Either the keystore was generated with a different password or the value was changed after generation. If the app has never been published to Play Store, delete the local keystore and re-run xfix keystore provision. If the app has been published, the keystore is the source of truth and the server value must be corrected to match it.
The JDK's keytool refuses passwords shorter than six characters. The value stored in the application record must be updated to a longer string before provisioning can succeed.
Building an APK whose applicationId does not match the package name the server expects will cause Android to reject the update. Verify that android/app/build.gradle and the application record agree on the package name before uploading.
--api-url expects a base URL, not a full endpoint. Use https://api.example.com, not https://api.example.com/api/v1/android-signing/provision. The CLI appends the fixed contract paths itself.
An android.auth block references an environment variable that is not set in the current shell. Export the variable (or add it to .env if you load dotenv before invoking the CLI) and re-run.
Local Machine
|
XFIX CLI
|
+---> FTP / FTPS / SFTP Upload (ZIP) ---> Remote Server ---> Deployment Endpoint ---> Extraction and Execution
|
+---> Flutter Build ---> zipalign ---> apksigner ---> Signed APK
|
+---> Flutter Build ---> jarsigner ---> Signed AAB
|
+---> Credentials API ---> Temp Keystore ---> apksigner ---> Signed APK (CI mode)
xfix-cli/
|-- bin/
|-- src/
| |-- app.js
| |-- index.js
| |-- config/
| | |-- index.js
| |-- partials/
|-- .xfixrc.json
|-- .updateignore
|-- package.json
|-- README.md
- commander
- dotenv
- fs-extra
- archiver
- basic-ftp
- ssh2-sftp-client
- execa
- node-fetch
- ignore
- glob
- fast-glob
- simple-git
- javascript-obfuscator
- mysql2
- pg
- sqlite3
- Yakpro-PO
- keytool (JDK 11+)
- jarsigner (JDK 11+)
- zipalign (Android SDK build-tools)
- apksigner (Android SDK build-tools)
This project follows semantic versioning:
- MAJOR for breaking changes
- MINOR for new features
- PATCH for fixes
Open an issue on the repository for bugs or feature requests.