{"id":31441299,"url":"https://github.com/marcuwynu23/jsdaffodil","last_synced_at":"2026-07-14T23:32:23.526Z","repository":{"id":299259392,"uuid":"1002470900","full_name":"marcuwynu23/jsdaffodil","owner":"marcuwynu23","description":"Cross-Platform Deployment Automation Framework for Node.js","archived":false,"fork":false,"pushed_at":"2026-04-12T02:22:19.000Z","size":207,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-12T04:16:41.600Z","etag":null,"topics":["ansible","cloud","deployment","deployment-automation","godaffodil","javascript","jsdaffodil","pydaffodil"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/@marcuwynu23/jsdaffodil","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/marcuwynu23.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":"FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"patreon":"marcuwynu23","custom":["https://www.paypal.me/wynumarcu23"]}},"created_at":"2025-06-15T14:48:24.000Z","updated_at":"2026-04-12T02:22:26.000Z","dependencies_parsed_at":"2025-06-15T16:56:58.712Z","dependency_job_id":null,"html_url":"https://github.com/marcuwynu23/jsdaffodil","commit_stats":null,"previous_names":["marcuwynu23/jsdaffodil"],"tags_count":14,"template":false,"template_full_name":null,"purl":"pkg:github/marcuwynu23/jsdaffodil","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marcuwynu23%2Fjsdaffodil","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marcuwynu23%2Fjsdaffodil/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marcuwynu23%2Fjsdaffodil/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marcuwynu23%2Fjsdaffodil/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/marcuwynu23","download_url":"https://codeload.github.com/marcuwynu23/jsdaffodil/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marcuwynu23%2Fjsdaffodil/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35483857,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-14T02:00:06.603Z","response_time":114,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ansible","cloud","deployment","deployment-automation","godaffodil","javascript","jsdaffodil","pydaffodil"],"created_at":"2025-09-30T16:41:13.032Z","updated_at":"2026-07-14T23:32:23.520Z","avatar_url":"https://github.com/marcuwynu23.png","language":"JavaScript","funding_links":["https://patreon.com/marcuwynu23","https://www.paypal.me/wynumarcu23"],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003ch1\u003eJSDaffodil\u003c/h1\u003e\n  \u003cp\u003e\u003cstrong\u003eCross-Platform Deployment Automation Framework for Node.js\u003c/strong\u003e\u003c/p\u003e\n  \u003cp\u003e\n    \u003cimg src=\"https://img.shields.io/npm/v/@marcuwynu23/jsdaffodil.svg\" alt=\"npm version\"/\u003e\n    \u003cimg src=\"https://img.shields.io/npm/dm/@marcuwynu23/jsdaffodil.svg\" alt=\"npm downloads\"/\u003e\n    \u003cimg src=\"https://img.shields.io/github/stars/marcuwynu23/jsdaffodil.svg\" alt=\"Stars Badge\"/\u003e\n    \u003cimg src=\"https://img.shields.io/github/license/marcuwynu23/jsdaffodil.svg\" alt=\"License Badge\"/\u003e\n  \u003c/p\u003e\n\u003c/div\u003e\n\n---\n\n## Overview\n\n**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](https://pypi.org/project/pydaffodil/) (Python) and [GoDaffodil](https://github.com/marcuwynu23/godaffodil) (Go). See **[Sister projects](#sister-projects)** for links and CLI equivalents.\n\n### What are Daffodil tools?\n\n**Daffodil tools** are the three sibling deployment projects:\n\n- **JSDaffodil** for Node.js\n- **PyDaffodil** for Python\n- **GoDaffodil** for Go\n\nAll three share the same core workflow: define deployment steps, transfer files as archives, run commands over SSH, support `.scpignore`, scale to multiple hosts using `inventory.ini`, and use the same `.daffodil.yml` schema for CLI-driven deployments.\n\n### Key Features\n\n- **Archive-Based File Transfer** - Efficient tar.gz compression for fast bulk file transfers\n- **Cross-Platform Support** - Works seamlessly on Windows, Linux, and macOS\n- **Multi-Key SSH Authentication** - Automatic fallback across multiple SSH key types\n- **Dual Module Support** - Full support for both CommonJS and ESM\n- **Step-by-Step Execution** - Declarative task execution with clear progress tracking\n- **Ignore Pattern Support** - `.scpignore` file for excluding files from transfers\n- **Beautiful CLI Output** - Styled terminal output with progress bars and spinners\n- **Zero External Dependencies** - Pure Node.js implementation for archive creation\n- **Watch-Based CI/CD (`watch()`)** - Automatically trigger deployments on local file changes or Git events (commits, merges, tags)\n- **Multi-Host Deployments via `inventory.ini`** - Run the same deployment steps across multiple servers defined in an inventory file\n\n---\n\n## Sister projects\n\nThe **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.\n\n| Project                    | Language | Install                                                                                                                                  | YAML CLI                                |\n| -------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |\n| **JSDaffodil** (this repo) | Node.js  | [`@marcuwynu23/jsdaffodil`](https://www.npmjs.com/package/@marcuwynu23/jsdaffodil) · [source](https://github.com/marcuwynu23/jsdaffodil) | `jsdaffodil --config .daffodil.yml`     |\n| **PyDaffodil**             | Python   | [`pydaffodil`](https://pypi.org/project/pydaffodil/) · [source](https://github.com/marcuwynu23/pydaffodil)                               | `pydaffodil --config .daffodil.yml`     |\n| **GoDaffodil**             | Go       | [module](https://github.com/marcuwynu23/godaffodil)                                                                                      | `godaffodil run --config .daffodil.yml` |\n\nUse **`--watch`** with each CLI when your YAML defines a `watch:` block (Go uses `godaffodil run … --watch`).\n\n---\n\n## Documentation\n\n| Resource | Description |\n| -------- | ----------- |\n| [GUIDELINES.md](./GUIDELINES.md) | Usage: API, `inventory.ini`, `.daffodil.yml` CLI, `watch()`, troubleshooting |\n| [DOCUMENTATION.md](./DOCUMENTATION.md) | Developers: architecture, package layout, tests |\n| [CONTRIBUTING.md](./CONTRIBUTING.md) | Contribution workflow and PR expectations |\n| [LICENSE](./LICENSE) | MIT License |\n\nAligned with [PyDaffodil](https://github.com/marcuwynu23/pydaffodil) and [GoDaffodil](https://github.com/marcuwynu23/godaffodil) documentation set.\n\n---\n\n## Documentation and Examples\n\nFor quick examples, check the **`samples/`** directory:\n\n- `samples/sample.mjs` - ESM module example\n- `samples/sample.cjs` - CommonJS module example\n- `samples/watch-sample.mjs` - ESM example using `watch()` for file + Git triggers\n- `samples/watch-sample.cjs` - CommonJS example using `watch()`\n- `samples/inventory-sample.mjs` - ESM multi-host deployment with `inventory.ini`\n- `samples/inventory-sample.cjs` - CommonJS multi-host deployment with `inventory.ini`\n\n---\n\n## Installation\n\n```bash\nnpm install @marcuwynu23/jsdaffodil\n```\n\n---\n\n## Quick Start\n\n### ESM Example\n\n```javascript\n// test.mjs or test.js (with \"type\": \"module\" in package.json)\nimport { Daffodil } from \"@marcuwynu23/jsdaffodil\";\n\nconst deployer = new Daffodil({\n  remoteUser: \"deployer\",\n  remoteHost: \"231.142.34.222\",\n  remotePath: \"/var/www/myapp\",\n  port: 22, // Optional, defaults to 22\n});\n\nconst steps = [\n  {\n    step: \"Transfer application files\",\n    command: async () =\u003e {\n      await deployer.transferFiles(\"./dist\", \"/var/www/myapp\");\n    },\n  },\n  {\n    step: \"Install dependencies\",\n    command: () =\u003e deployer.sshCommand(\"cd /var/www/myapp \u0026\u0026 npm install\"),\n  },\n  {\n    step: \"Restart application\",\n    command: () =\u003e deployer.sshCommand(\"pm2 restart myapp\"),\n  },\n];\n\nawait deployer.deploy(steps);\n```\n\n### CommonJS Example\n\n```javascript\n// test.cjs or test.js (without \"type\": \"module\")\nconst { Daffodil } = require(\"@marcuwynu23/jsdaffodil\");\n\nconst deployer = new Daffodil({\n  remoteUser: \"deployer\",\n  remoteHost: \"231.142.34.222\",\n  remotePath: \"/var/www/myapp\",\n});\n\nconst steps = [\n  {\n    step: \"Transfer files\",\n    command: async () =\u003e {\n      await deployer.transferFiles(\"./dist\", \"/var/www/myapp\");\n    },\n  },\n  {\n    step: \"Run remote command\",\n    command: () =\u003e deployer.sshCommand(\"ls -la /var/www/myapp\"),\n  },\n];\n\ndeployer.deploy(steps).catch(console.error);\n```\n\n---\n\n## API Reference\n\n### Constructor\n\n```javascript\nnew Daffodil({\n  remoteUser: string,        // SSH username (single-host mode)\n  remoteHost: string,        // Server hostname or IP (single-host mode)\n  remotePath?: string,       // Default remote path (default: \".\")\n  port?: number,             // SSH port (default: 22)\n  ignoreFile?: string,       // Ignore file path (default: \".scpignore\")\n  verbose?: boolean,         // Verbose logging (default: false)\n  inventory?: string,        // Path to inventory.ini (multi-host mode)\n  group?: string,            // Group name inside inventory.ini\n})\n```\n\n### Methods\n\n#### `async connect()`\n\nEstablishes SSH connection using available SSH keys. Automatically tries multiple key types (`id_rsa`, `id_ed25519`, `id_ecdsa`, `id_dsa`) in order.\n\n#### `async transferFiles(localPath, destinationPath?)`\n\nTransfers files from local directory to remote server using archive-based compression.\n\n- **`localPath`** (string): Local directory path to transfer\n- **`destinationPath`** (string, optional): Remote destination path (defaults to `remotePath`)\n\n**Features:**\n\n- Creates tar.gz archive locally (cross-platform)\n- Transfers single archive file for efficiency\n- Automatically extracts on remote server\n- Respects `.scpignore` patterns\n- Cleans up archives after successful transfer\n\n#### `async runCommand(cmd)`\n\nExecutes a command locally and returns the output.\n\n- **`cmd`** (string): Command to execute\n\n#### `async sshCommand(cmd)`\n\nExecutes a command on the remote server via SSH.\n\n- **`cmd`** (string): Command to execute remotely\n\n#### `async makeDirectory(dirName)`\n\nCreates a directory on the remote server.\n\n- **`dirName`** (string): Directory name to create\n\n#### `async deploy(steps)`\n\nExecutes a series of deployment steps sequentially.\n\n- **`steps`** (Array): Array of step objects with `step` (description) and `command` (async function)\n\n#### `watch(options)`\n\nCreates a watcher that can trigger deployments based on file system or Git changes. Returns an internal watcher with a `.deploy(steps)` method.\n\n```javascript\ndeployer.watch({\n  paths?: string[],           // Local files/folders to watch\n  debounce?: number,          // Debounce in ms before triggering deploy\n  repoPath?: string,          // Local Git repo path\n  branch?: string,            // Single branch to watch\n  branches?: string[],        // Multiple branches to watch\n  tags?: boolean,             // Watch tags\n  tagPattern?: RegExp,        // Filter tag names\n  events?: (\"commit\" | \"merge\" | \"tag\")[], // Git events\n  interval?: number,          // Poll interval in ms\n}).deploy(steps);\n```\n\n---\n\n## Advanced Features\n\n### Archive-Based File Transfer\n\nJSDaffodil uses an efficient archive-based transfer method:\n\n1. **Local Archive Creation** - Files are compressed into a tar.gz archive using cross-platform Node.js libraries\n2. **Single File Transfer** - Only one archive file is transferred, significantly faster than individual file transfers\n3. **Remote Extraction** - Archive is automatically extracted on the remote server\n4. **Automatic Cleanup** - Both local and remote archives are cleaned up after successful transfer\n\nThis approach is especially beneficial for:\n\n- Large projects with many files\n- Slow network connections\n- Reducing SSH connection overhead\n\n### Cross-Platform Support\n\nJSDaffodil works seamlessly across all major operating systems:\n\n- ✅ **Windows** - Uses Node.js tar library (no external dependencies)\n- ✅ **Linux** - Native support\n- ✅ **macOS** - Native support\n\n### SSH Key Management\n\nThe framework automatically attempts to connect using multiple SSH key types in order:\n\n1. `id_rsa`\n2. `id_ed25519`\n3. `id_ecdsa`\n4. `id_dsa`\n\nThis ensures compatibility with various SSH key configurations.\n\n### Ignore Patterns (`.scpignore`)\n\nCreate a `.scpignore` file in your project root to exclude files from transfers:\n\n```plaintext\n# Dependencies\nnode_modules\nvendor\n\n# Environment files\n.env\n.env.local\n.env.production\n\n# Logs\n*.log\nlogs/\n\n# Build artifacts\ndist/\nbuild/\n*.map\n\n# IDE\n.vscode/\n.idea/\n\n# OS\n.DS_Store\nThumbs.db\n```\n\n---\n\n## Best Practices\n\n### 1. SSH Key Setup\n\nEnsure your SSH key is properly configured:\n\n```bash\n# Generate SSH key if needed\nssh-keygen -t ed25519 -C \"your_email@example.com\"\n\n# Copy public key to server\nssh-copy-id deployer@your-server-ip\n\n# Test connection\nssh deployer@your-server-ip\n```\n\n### 2. Error Handling\n\nAlways handle errors in your deployment scripts:\n\n```javascript\ntry {\n  await deployer.deploy(steps);\n} catch (error) {\n  console.error(\"Deployment failed:\", error.message);\n  process.exit(1);\n}\n```\n\n### 3. Environment Variables\n\nUse environment variables for sensitive information:\n\n```javascript\nconst deployer = new Daffodil({\n  remoteUser: process.env.DEPLOY_USER,\n  remoteHost: process.env.DEPLOY_HOST,\n  remotePath: process.env.DEPLOY_PATH,\n});\n```\n\n### 4. Conditional Steps\n\nAdd conditional logic to your deployment steps:\n\n```javascript\nconst steps = [\n  {\n    step: \"Transfer files\",\n    command: async () =\u003e {\n      await deployer.transferFiles(\"./dist\", \"/var/www/myapp\");\n    },\n  },\n  {\n    step: \"Run migrations\",\n    command: () =\u003e deployer.sshCommand(\"cd /var/www/myapp \u0026\u0026 npm run migrate\"),\n  },\n  ...(process.env.NODE_ENV === \"production\"\n    ? [\n        {\n          step: \"Restart production server\",\n          command: () =\u003e deployer.sshCommand(\"pm2 restart myapp\"),\n        },\n      ]\n    : []),\n];\n```\n\n---\n\n## Configuration Options\n\n| Option       | Type      | Default        | Description                                        |\n| ------------ | --------- | -------------- | -------------------------------------------------- |\n| `remoteUser` | `string`  | **Required\\*** | SSH username for remote server (single-host mode)  |\n| `remoteHost` | `string`  | **Required\\*** | Remote server hostname or IP address (single-host) |\n| `remotePath` | `string`  | `\".\"`          | Default remote directory path                      |\n| `port`       | `number`  | `22`           | SSH port number                                    |\n| `ignoreFile` | `string`  | `\".scpignore\"` | Path to ignore patterns file                       |\n| `verbose`    | `boolean` | `false`        | Enable verbose logging                             |\n| `inventory`  | `string`  | `undefined`    | Path to `inventory.ini` for multi-host deployments |\n| `group`      | `string`  | `undefined`    | Inventory group name (e.g. `\"webservers\"`)         |\n\n\u003e \\* `remoteUser` and `remoteHost` are required in single-host mode. When `inventory` is provided, hosts are taken from `inventory.ini` instead.\n\n---\n\n## Watch-Based CI/CD with `watch()`\n\nUse `watch()` to automatically trigger deployments when files change or Git state updates.\n\n### Example: Watch local files\n\n```javascript\nimport { Daffodil } from \"@marcuwynu23/jsdaffodil\";\n\nconst deployer = new Daffodil({\n  remoteUser: \"deployer\",\n  remoteHost: \"231.142.34.222\",\n  remotePath: \"/var/www/myapp\",\n});\n\nconst steps = [\n  {\n    step: \"Build project\",\n    command: () =\u003e deployer.runCommand(\"npm run build\"),\n  },\n  {\n    step: \"Upload build\",\n    command: () =\u003e deployer.transferFiles(\"./dist\", \"/var/www/myapp\"),\n  },\n  {\n    step: \"Restart application\",\n    command: () =\u003e deployer.sshCommand(\"pm2 restart myapp\"),\n  },\n];\n\nawait deployer\n  .watch({\n    paths: [\"./dist\", \"./src\"],\n    debounce: 2000, // ms\n  })\n  .deploy(steps);\n```\n\n### Example: Watch Git repository (commits/merges/tags)\n\n```javascript\nawait deployer\n  .watch({\n    repoPath: \"/home/user/projects/myapp\",\n    branches: [\"main\", \"staging\"],\n    tags: true,\n    tagPattern: /^v\\d+\\.\\d+\\.\\d+$/,\n    events: [\"commit\", \"merge\", \"tag\"],\n    interval: 5000, // ms\n  })\n  .deploy(steps);\n```\n\nSee `samples/watch-sample.mjs` and `samples/watch-sample.cjs` for complete examples.\n\n---\n\n## Multi-Host Deployments with `inventory.ini`\n\nDeploy to multiple servers with a single script using an `inventory.ini` file.\n\n### Example `inventory.ini`\n\n```ini\n[webservers]\nserver1 host=231.142.34.222 user=deployer port=22\nserver2 host=231.142.34.223 user=deployer        ; uses default port 22\nserver3 host=231.142.34.224 user=ubuntu port=2200\n```\n\n### Usage\n\n```javascript\nimport { Daffodil } from \"@marcuwynu23/jsdaffodil\";\n\nconst deployer = new Daffodil({\n  inventory: \"./inventory.ini\",\n  group: \"webservers\",\n  remotePath: \"/var/www/myapp\",\n});\n\nconst steps = [\n  {\n    step: \"Transfer application files\",\n    command: () =\u003e deployer.transferFiles(\"./dist\"),\n  },\n  {\n    step: \"Install dependencies\",\n    command: () =\u003e\n      deployer.sshCommand(\n        \"cd /var/www/myapp \u0026\u0026 npm install --production=false\"\n      ),\n  },\n  {\n    step: \"Restart application\",\n    command: () =\u003e deployer.sshCommand(\"pm2 restart myapp\"),\n  },\n];\n\nawait deployer.deploy(steps);\n```\n\nThis will sequentially run the same steps for each host in the `webservers` group, with logs like:\n\n```text\n==== Starting deployment for [server1] (231.142.34.222) ====\n...\n==== Finished deployment for [server1] (231.142.34.222) ====\n```\n\nSee `samples/inventory-sample.mjs` and `samples/inventory-sample.cjs` for full examples.\n\n---\n\n## Requirements\n\n- **Node.js** \u003e= 14.0.0\n- **SSH access** to remote server\n- **SSH key** configured in `~/.ssh/` (or `%USERPROFILE%\\.ssh\\` on Windows)\n\n---\n\n## YAML CLI Deployment\n\nJSDaffodil now includes a CLI entrypoint with YAML config support.\n\n```bash\njsdaffodil --config samples/.daffodil.yml\njsdaffodil --config samples/.daffodil.yml --watch\n```\n\nUse `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](#sister-projects)**.\n\n### Example `.daffodil.yml`\n\n```yaml\nremotePath: /var/www/myapp\nignoreFile: .scpignore\nverbose: false\ninventoryFile: inventory.ini\ninventoryGroup: webservers\nsteps:\n  - name: Build app\n    type: local\n    command: npm run build\n  - name: Upload dist\n    type: transfer\n    localPath: dist\n    destinationPath: /var/www/myapp\n  - name: Restart app\n    type: ssh\n    command: pm2 restart myapp\nwatch:\n  paths: [\"./dist\", \"./src\"]\n  repoPath: .\n  branch: main\n  events: [\"commit\", \"merge\", \"tag\"]\n  tags: true\n  tagPattern: \"^v\\\\d+\\\\.\\\\d+\\\\.\\\\d+$\"\n  interval: 5000\n  debounce: 2000\n```\n\nYou can also reference a separate inventory file:\n\n```yaml\ninventoryFile: inventory.ini\ninventoryGroup: webservers\n```\n\n---\n\n## Contributing\n\nContributions are welcome! Please read our [CONTRIBUTING.md](./CONTRIBUTING.md) guide for contribution guidelines, code review process, and best practices.\n\nFor developers, see [DOCUMENTATION.md](./DOCUMENTATION.md) for architecture details and development setup.\n\n---\n\n## License\n\n[MIT License](./LICENSE) - feel free to use this project for any purpose.\n\n---\n\n## Acknowledgments\n\nPart of the **Daffodil** family alongside [PyDaffodil](https://pypi.org/project/pydaffodil/) and [GoDaffodil](https://github.com/marcuwynu23/godaffodil)—see **[Sister projects](#sister-projects)**.\n\n---\n\n\u003cdiv align=\"center\"\u003e\n  \u003cp\u003eMade with love by \u003ca href=\"https://github.com/marcuwynu23\"\u003eMark Wayne B. Menorca\u003c/a\u003e\u003c/p\u003e\n\u003c/div\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarcuwynu23%2Fjsdaffodil","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmarcuwynu23%2Fjsdaffodil","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarcuwynu23%2Fjsdaffodil/lists"}