workingtree-ftp
v0.4.0
Published
Node.js CLI for FTP or FTPS push, single-file transfer, and Git change reports per project.
Maintainers
Readme
workingtree-ftp
Node.js CLI for pushing Git working tree changes, uploading single files, deleting single remote files, downloading remote files, and exporting change reports from Git. This tool is useful for legacy hosting workflows that still rely on FTP but want Git to decide which files changed.
The main command provided by this package is wftp.
Overview
wftp push reads changes from git status and builds a transfer plan:
modified,added,copied,type-changed, anduntrackedfiles are uploaded.deletedfiles are not removed from the remote server unless you add--delete-removed.- Renamed files upload the new path. The old path is removed only when
--delete-removedis used. - Merge conflicts stop the upload so the tool does not run against an ambiguous repository state.
- Metadata files such as
.gitignore,.gitattributes,.gitmodules,.workingtree-ftp.json, and.uncommit-ftp.jsonare excluded by default. wftp upload <path>uploads a single file directly, as long as the file is inside the current Git repository.wftp delete <path>deletes one remote file using the same repository-relative path mapping.wftp download <path>downloads one remote file into the matching local path inside the repository.wftp download --alldownloads the entireremoteRootinto the local repository root.wftp reportexports a.txtreport of committed or uncommitted Git changes.
Requirements
- Node.js
>= 20 - Git available in
PATH - Access to an FTP or FTPS server
Installation
Install globally from the package:
npm install -g workingtree-ftpFor development from this project folder:
npm install
npm linkAfter that, the wftp command will be available globally.
Quick Start
- Create a global FTP profile:
wftp profile set client-a \
--host ftp.client-a.com \
--user deploy-client-a \
--password-env FTP_CLIENT_A_PASS- Set the password as an environment variable:
export FTP_CLIENT_A_PASS='secret'In PowerShell:
$env:FTP_CLIENT_A_PASS = "secret"- Initialize the current repository:
wftp init --profile client-a --remote-root /public_html/app- Preview which files will be processed:
wftp status- Push the working tree changes:
wftp pushTo upload one file directly:
wftp upload src/app.jsTo download one file directly:
wftp download src/app.jsTo export a report of uncommitted files:
wftp report --uncommitConfiguration Locations
The tool uses two configuration levels.
1. Global profiles
Stored in the user's home directory:
~/.workingtree-ftp/profiles.jsonExample:
{
"profiles": {
"client-a": {
"host": "ftp.client-a.com",
"user": "deploy-client-a",
"passwordEnv": "FTP_CLIENT_A_PASS",
"port": 21,
"secure": false
}
}
}Supported fields:
host: FTP hostnameuser: FTP usernamepassword: plain text password stored in the profilepasswordEnv: environment variable name containing the passwordport: defaults to21secure:trueto use FTPS
2. Local repository config
Stored in the Git repository root:
.workingtree-ftp.jsonExample:
{
"profile": "client-a",
"remoteRoot": "/public_html/app",
"ignore": [
"storage/logs/app.log",
"tmp/debug.txt"
]
}Supported fields:
profile: name of the global profile used by this repositoryremoteRoot: target directory on the serverignore: list of repository-relative paths that should be skipped
When you run wftp init, this file can be added to .gitignore automatically.
Commands
wftp profile set <name>
Creates or updates a global FTP profile.
wftp profile set client-a \
--host ftp.client-a.com \
--user deploy-client-a \
--password-env FTP_CLIENT_A_PASS \
--port 21 \
--secureOptions:
--host <host>: required--user <user>: required--password <password>: store the password directly in the profile file--password-env <envName>: read the password from an environment variable--port <port>: defaults to21--secure: use FTPS
At least one of --password or --password-env must be provided.
wftp profile list
Shows all available global profiles.
wftp profile listwftp profile show <name>
Shows profile details with the password masked.
wftp profile show client-awftp profile remove <name>
Deletes a global profile.
wftp profile remove client-awftp init
Creates the local config file in the active Git repository.
wftp init --profile client-a --remote-root /public_html/appOptions:
--profile <name>: required--remote-root <path>: required--force: overwrite an existing config--gitignore: add the config file to.gitignore(default)--no-gitignore: do not modify.gitignore
wftp config
Prints the local config for the current repository.
wftp configwftp status
Shows a preview of the transfer plan without connecting to FTP.
wftp status
wftp status --delete-removed
wftp status --no-include-untrackedOptions:
--delete-removed: include deleted files or old rename paths as remote deletions--no-include-untracked: ignore untracked files
The output is grouped into:
UploadsDeletesSkippedConflicts
If conflicts are found, this command returns exit code 2.
wftp push
Runs the FTP transfer based on the current working tree.
wftp push
wftp push --dry-run
wftp push --delete-removed
wftp push --delete-same-file
wftp push --verboseOptions:
--dry-run: only print the plan, without connecting to FTP--delete-removed: remove remote files for deleted local files or old rename paths--delete-same-file: if overwriting an existing remote file fails, delete the remote file and retry the upload once--no-include-untracked: ignore untracked files--verbose: show more detailed FTP progress output
Important behavior:
- Upload fails if the working tree contains merge conflicts.
- If there are no changes, the command finishes without opening an FTP connection.
--delete-same-fileonly applies to upload failures that look like an overwrite conflict on the same remote path.- Remote delete failures caused by a missing remote file are reported as
skip-delete, not as fatal errors.
wftp upload <path>
Uploads a single file directly to the remote path derived from the repository root and remoteRoot.
wftp upload src/app.js
wftp upload .\src\app.js --dry-run
wftp upload src/app.js --delete-same-file --verboseOptions:
--dry-run: only print the upload plan, without connecting to FTP--delete-same-file: if overwriting an existing remote file fails, delete the remote file and retry the upload once--verbose: show more detailed FTP progress output
Important behavior:
- The target file must be inside the current Git repository.
- The target must be a regular file.
- This command does not read
git status; it uploads the exact file path you pass in.
wftp delete <path>
Deletes one remote file using the same repository-relative path mapping as upload.
wftp delete src/old-file.js
wftp delete .\src\old-file.js --dry-runOptions:
--dry-run: only print the delete plan, without connecting to FTP
Important behavior:
- The path must resolve inside the current Git repository.
- The local file does not need to exist.
- If the remote file does not exist, the result is reported as
skip-delete, not as a fatal error.
wftp download <path>
Downloads one remote file to the matching local path inside the current repository.
wftp download src/app.js
wftp download .\assets\logo.png --dry-run
wftp download src/app.js --verboseOptions:
--dry-run: only print the download plan, without connecting to FTP--verbose: show more detailed FTP progress output
Important behavior:
- The target path must resolve inside the current Git repository.
- Parent directories are created automatically if needed.
- This command downloads exactly the path you pass in from
remoteRoot.
wftp download --all
Downloads the full contents of remoteRoot into the local repository root.
wftp download --all
wftp download --all --dry-run
wftp download --all --verboseOptions:
--all: required when downloading the entire remote root--dry-run: only print the planned action, without connecting to FTP--verbose: show more detailed FTP progress output
Important behavior:
- This downloads the entire configured remote root recursively.
- Local files inside the repository can be overwritten by the downloaded content.
- Use either
wftp download <path>orwftp download --all, not both.
wftp report
Exports a .txt report describing changed files from Git history or from the current working tree.
wftp report
wftp report --uncommit
wftp report --start abc123 --end def456
wftp report --uncommit --line
wftp report --name client-audit --name-dateOptions:
--uncommit: export files that have not been committed yet--start <id>: starting commit for an explicit commit range--end <id>: ending commit for an explicit commit range--line: include line numbers forEDITandDELentries--name <name>: use a custom output file name--name-date: append_YYYYMMDDto the custom file name, requires--name
Default behavior:
- On the first
wftp reportrun without extra options, the tool exports all committed changes in the repository. - On later plain
wftp reportruns, it exports only commits after the last saved automatic report cursor. - The last automatic report cursor is stored in
.git/workingtree-ftp-report.json.
Naming behavior:
- With
--name report-audit, the file name becomesreport-audit.txt - With
--name report-audit --name-date, the file name becomesreport-audit_YYYYMMDD.txt - Without
--name, the file name becomesREPOSITORYNAME_YYYYMMDD.txt
Format behavior:
- Without
--line, each section contains only[ADD],[EDIT], or[DEL]entries grouped by commit message - With
--line,EDITandDELentries also include aLINE 1,2,3line when line numbers can be derived from Git diff hunks wftp report --uncommituses a single section titledUNCOMMITTED CHANGES
How File Detection Works
The tool reads the output of:
git status --porcelain=1 -z -uallEach entry is mapped to an action:
M,A,C,T: upload??: upload when untracked files are enabledD: delete only when--delete-removedis usedR: upload the new path, delete the old path only when--delete-removedis usedU,AA,DD: treated as conflicts
Before uploading, local paths are checked again to make sure the target is a regular file.
Security
The recommended approach is to use --password-env instead of --password, so the password is not stored directly in the JSON file.
Safer example:
wftp profile set client-a \
--host ftp.client-a.com \
--user deploy-client-a \
--password-env FTP_CLIENT_A_PASSIf a profile uses passwordEnv, the upload fails when the referenced environment variable is not available in the current shell.
Compatibility
The current version still reads older config locations as fallbacks:
- Legacy local config:
.uncommit-ftp.json - Previous global profiles:
~/.workingtree-ftp-uploader/profiles.json - Legacy global profiles:
~/.uncommit-ftp-uploader/profiles.json
This is useful when migrating from the previous tool name.
Development
Run tests:
npm testRun the CLI directly from source:
node src/cli.js --helpTroubleshooting
Failed to run git rev-parse --show-toplevel
This usually means the command was run outside a Git repository, or Git is not available in PATH.
Project config not found
The repository has not been initialized yet. Run:
wftp init --profile <name> --remote-root <path>Profile "<name>" not found
The global profile does not exist yet. Run:
wftp profile set <name> --host <host> --user <user> --password-env <ENV_NAME>Environment variable <NAME> not found
The profile expects its password from an environment variable, but that variable is not set in the current shell.
