@marcuwynu23/jsdaffodil
v2.1.0
Published
Cross-platform deployment automation framework for Node.js with SSH, archive-based file transfer, and step-by-step task execution. Supports Windows, Linux, and macOS.
Maintainers
Readme
Overview
JSDaffodil is the Node.js implementation in the Daffodil family. It simplifies remote deployments over SSH with a step-by-step API, archive-based transfer, optional watch() triggers, and multi-host inventory.ini support—the same concepts as PyDaffodil (Python) and GoDaffodil (Go). See Sister projects for links and CLI equivalents.
Key Features
- Archive-Based File Transfer - Efficient tar.gz compression for fast bulk file transfers
- Cross-Platform Support - Works seamlessly on Windows, Linux, and macOS
- Multi-Key SSH Authentication - Automatic fallback across multiple SSH key types
- Dual Module Support - Full support for both CommonJS and ESM
- Step-by-Step Execution - Declarative task execution with clear progress tracking
- Ignore Pattern Support -
.scpignorefile for excluding files from transfers - Beautiful CLI Output - Styled terminal output with progress bars and spinners
- Zero External Dependencies - Pure Node.js implementation for archive creation
- Watch-Based CI/CD (
watch()) - Automatically trigger deployments on local file changes or Git events (commits, merges, tags) - Multi-Host Deployments via
inventory.ini- Run the same deployment steps across multiple servers defined in an inventory file
Sister projects
The Daffodil line shares the same ideas: SSH, archive transfer, .scpignore, watch() (files + Git), Ansible-style inventory.ini, and a shared .daffodil.yml schema for the official CLIs.
| Project | Language | Install | YAML CLI |
|---------|----------|---------|----------|
| JSDaffodil (this repo) | Node.js | @marcuwynu23/jsdaffodil · source | jsdaffodil --config .daffodil.yml |
| PyDaffodil | Python | pydaffodil · source | pydaffodil --config .daffodil.yml |
| GoDaffodil | Go | module | godaffodil run --config .daffodil.yml |
Use --watch with each CLI when your YAML defines a watch: block (Go uses godaffodil run … --watch).
Documentation
This project includes comprehensive documentation:
- GUIDELINES.md - Complete usage guide with examples, best practices, troubleshooting, and real-world scenarios. Includes sample code from the
samples/directory. - DOCUMENTATION.md - Developer documentation covering architecture, code organization, testing, and extension points for contributors.
- CONTRIBUTING.md - Contribution guidelines, code review process, and collaboration best practices.
For quick examples, check the samples/ directory:
samples/sample.mjs- ESM module examplesamples/sample.cjs- CommonJS module examplesamples/watch-sample.mjs- ESM example usingwatch()for file + Git triggerssamples/watch-sample.cjs- CommonJS example usingwatch()samples/inventory-sample.mjs- ESM multi-host deployment withinventory.inisamples/inventory-sample.cjs- CommonJS multi-host deployment withinventory.ini
Installation
npm install @marcuwynu23/jsdaffodilQuick Start
ESM Example
// test.mjs or test.js (with "type": "module" in package.json)
import { Daffodil } from "@marcuwynu23/jsdaffodil";
const deployer = new Daffodil({
remoteUser: "deployer",
remoteHost: "231.142.34.222",
remotePath: "/var/www/myapp",
port: 22, // Optional, defaults to 22
});
const steps = [
{
step: "Transfer application files",
command: async () => {
await deployer.transferFiles("./dist", "/var/www/myapp");
},
},
{
step: "Install dependencies",
command: () => deployer.sshCommand("cd /var/www/myapp && npm install"),
},
{
step: "Restart application",
command: () => deployer.sshCommand("pm2 restart myapp"),
},
];
await deployer.deploy(steps);CommonJS Example
// test.cjs or test.js (without "type": "module")
const { Daffodil } = require("@marcuwynu23/jsdaffodil");
const deployer = new Daffodil({
remoteUser: "deployer",
remoteHost: "231.142.34.222",
remotePath: "/var/www/myapp",
});
const steps = [
{
step: "Transfer files",
command: async () => {
await deployer.transferFiles("./dist", "/var/www/myapp");
},
},
{
step: "Run remote command",
command: () => deployer.sshCommand("ls -la /var/www/myapp"),
},
];
deployer.deploy(steps).catch(console.error);API Reference
Constructor
new Daffodil({
remoteUser: string, // SSH username (single-host mode)
remoteHost: string, // Server hostname or IP (single-host mode)
remotePath?: string, // Default remote path (default: ".")
port?: number, // SSH port (default: 22)
ignoreFile?: string, // Ignore file path (default: ".scpignore")
verbose?: boolean, // Verbose logging (default: false)
inventory?: string, // Path to inventory.ini (multi-host mode)
group?: string, // Group name inside inventory.ini
})Methods
async connect()
Establishes SSH connection using available SSH keys. Automatically tries multiple key types (id_rsa, id_ed25519, id_ecdsa, id_dsa) in order.
async transferFiles(localPath, destinationPath?)
Transfers files from local directory to remote server using archive-based compression.
localPath(string): Local directory path to transferdestinationPath(string, optional): Remote destination path (defaults toremotePath)
Features:
- Creates tar.gz archive locally (cross-platform)
- Transfers single archive file for efficiency
- Automatically extracts on remote server
- Respects
.scpignorepatterns - Cleans up archives after successful transfer
async runCommand(cmd)
Executes a command locally and returns the output.
cmd(string): Command to execute
async sshCommand(cmd)
Executes a command on the remote server via SSH.
cmd(string): Command to execute remotely
async makeDirectory(dirName)
Creates a directory on the remote server.
dirName(string): Directory name to create
async deploy(steps)
Executes a series of deployment steps sequentially.
steps(Array): Array of step objects withstep(description) andcommand(async function)
-#### watch(options)
Creates a watcher that can trigger deployments based on file system or Git changes. Returns an internal watcher with a .deploy(steps) method.
deployer.watch({
paths?: string[], // Local files/folders to watch
debounce?: number, // Debounce in ms before triggering deploy
repoPath?: string, // Local Git repo path
branch?: string, // Single branch to watch
branches?: string[], // Multiple branches to watch
tags?: boolean, // Watch tags
tagPattern?: RegExp, // Filter tag names
events?: ("commit" | "merge" | "tag")[], // Git events
interval?: number, // Poll interval in ms
}).deploy(steps);Advanced Features
Archive-Based File Transfer
JSDaffodil uses an efficient archive-based transfer method:
- Local Archive Creation - Files are compressed into a tar.gz archive using cross-platform Node.js libraries
- Single File Transfer - Only one archive file is transferred, significantly faster than individual file transfers
- Remote Extraction - Archive is automatically extracted on the remote server
- Automatic Cleanup - Both local and remote archives are cleaned up after successful transfer
This approach is especially beneficial for:
- Large projects with many files
- Slow network connections
- Reducing SSH connection overhead
Cross-Platform Support
JSDaffodil works seamlessly across all major operating systems:
- ✅ Windows - Uses Node.js tar library (no external dependencies)
- ✅ Linux - Native support
- ✅ macOS - Native support
SSH Key Management
The framework automatically attempts to connect using multiple SSH key types in order:
id_rsaid_ed25519id_ecdsaid_dsa
This ensures compatibility with various SSH key configurations.
Ignore Patterns (.scpignore)
Create a .scpignore file in your project root to exclude files from transfers:
# Dependencies
node_modules
vendor
# Environment files
.env
.env.local
.env.production
# Logs
*.log
logs/
# Build artifacts
dist/
build/
*.map
# IDE
.vscode/
.idea/
# OS
.DS_Store
Thumbs.dbBest Practices
1. SSH Key Setup
Ensure your SSH key is properly configured:
# Generate SSH key if needed
ssh-keygen -t ed25519 -C "[email protected]"
# Copy public key to server
ssh-copy-id deployer@your-server-ip
# Test connection
ssh deployer@your-server-ip2. Error Handling
Always handle errors in your deployment scripts:
try {
await deployer.deploy(steps);
} catch (error) {
console.error("Deployment failed:", error.message);
process.exit(1);
}3. Environment Variables
Use environment variables for sensitive information:
const deployer = new Daffodil({
remoteUser: process.env.DEPLOY_USER,
remoteHost: process.env.DEPLOY_HOST,
remotePath: process.env.DEPLOY_PATH,
});4. Conditional Steps
Add conditional logic to your deployment steps:
const steps = [
{
step: "Transfer files",
command: async () => {
await deployer.transferFiles("./dist", "/var/www/myapp");
},
},
{
step: "Run migrations",
command: () => deployer.sshCommand("cd /var/www/myapp && npm run migrate"),
},
...(process.env.NODE_ENV === "production"
? [
{
step: "Restart production server",
command: () => deployer.sshCommand("pm2 restart myapp"),
},
]
: []),
];Configuration Options
| Option | Type | Default | Description |
| ------------ | --------- | -------------- | -------------------------------------------------- |
| remoteUser | string | Required* | SSH username for remote server (single-host mode) |
| remoteHost | string | Required* | Remote server hostname or IP address (single-host) |
| remotePath | string | "." | Default remote directory path |
| port | number | 22 | SSH port number |
| ignoreFile | string | ".scpignore" | Path to ignore patterns file |
| verbose | boolean | false | Enable verbose logging |
| inventory | string | undefined | Path to inventory.ini for multi-host deployments |
| group | string | undefined | Inventory group name (e.g. "webservers") |
*
remoteUserandremoteHostare required in single-host mode. Wheninventoryis provided, hosts are taken frominventory.iniinstead.
Watch-Based CI/CD with watch()
Use watch() to automatically trigger deployments when files change or Git state updates.
Example: Watch local files
import { Daffodil } from "@marcuwynu23/jsdaffodil";
const deployer = new Daffodil({
remoteUser: "deployer",
remoteHost: "231.142.34.222",
remotePath: "/var/www/myapp",
});
const steps = [
{
step: "Build project",
command: () => deployer.runCommand("npm run build"),
},
{
step: "Upload build",
command: () => deployer.transferFiles("./dist", "/var/www/myapp"),
},
{
step: "Restart application",
command: () => deployer.sshCommand("pm2 restart myapp"),
},
];
await deployer
.watch({
paths: ["./dist", "./src"],
debounce: 2000, // ms
})
.deploy(steps);Example: Watch Git repository (commits/merges/tags)
await deployer
.watch({
repoPath: "/home/user/projects/myapp",
branches: ["main", "staging"],
tags: true,
tagPattern: /^v\d+\.\d+\.\d+$/,
events: ["commit", "merge", "tag"],
interval: 5000, // ms
})
.deploy(steps);See samples/watch-sample.mjs and samples/watch-sample.cjs for complete examples.
Multi-Host Deployments with inventory.ini
Deploy to multiple servers with a single script using an inventory.ini file.
Example inventory.ini
[webservers]
server1 host=231.142.34.222 user=deployer port=22
server2 host=231.142.34.223 user=deployer ; uses default port 22
server3 host=231.142.34.224 user=ubuntu port=2200Usage
import { Daffodil } from "@marcuwynu23/jsdaffodil";
const deployer = new Daffodil({
inventory: "./inventory.ini",
group: "webservers",
remotePath: "/var/www/myapp",
});
const steps = [
{
step: "Transfer application files",
command: () => deployer.transferFiles("./dist"),
},
{
step: "Install dependencies",
command: () =>
deployer.sshCommand(
"cd /var/www/myapp && npm install --production=false"
),
},
{
step: "Restart application",
command: () => deployer.sshCommand("pm2 restart myapp"),
},
];
await deployer.deploy(steps);This will sequentially run the same steps for each host in the webservers group, with logs like:
==== Starting deployment for [server1] (231.142.34.222) ====
...
==== Finished deployment for [server1] (231.142.34.222) ====See samples/inventory-sample.mjs and samples/inventory-sample.cjs for full examples.
Requirements
- Node.js >= 14.0.0
- SSH access to remote server
- SSH key configured in
~/.ssh/(or%USERPROFILE%\.ssh\on Windows)
YAML CLI Deployment
JSDaffodil now includes a CLI entrypoint with YAML config support.
jsdaffodil --config samples/.daffodil.yml
jsdaffodil --config samples/.daffodil.yml --watchUse samples/.daffodil.yml as the reference schema (single-host or hosts[] multi-host). The filename is required to be exactly .daffodil.yml. The same schema works with PyDaffodil and GoDaffodil—see Sister projects.
You can also reference a separate inventory file:
inventoryFile: inventory.ini
inventoryGroup: webserversContributing
Contributions are welcome! Please read our CONTRIBUTING.md guide for contribution guidelines, code review process, and best practices.
For developers, see DOCUMENTATION.md for architecture details and development setup.
License
MIT License - feel free to use this project for any purpose.
Acknowledgments
Part of the Daffodil family alongside PyDaffodil and GoDaffodil—see Sister projects.
