{"_id":"@belle-wissell/asset-sync","_rev":"2-11d2d03448b6b085c65da9b280a18acf","name":"@belle-wissell/asset-sync","dist-tags":{"latest":"1.1.0"},"versions":{"1.0.0":{"name":"@belle-wissell/asset-sync","version":"1.0.0","author":{"name":"Scott Thiessen","email":"scott@bwco.info"},"license":"UNLICENSED","_id":"@belle-wissell/asset-sync@1.0.0","maintainers":[{"name":"movingobjects","email":"scott@movingobjects.io"}],"homepage":"https://github.com/belle-wissell/asset-sync","bugs":{"url":"https://github.com/belle-wissell/asset-sync/issues"},"bin":{"asset-sync":"asset-sync.js"},"dist":{"shasum":"f48f9c3e52f30124fe2a00cf55d47221eed685d4","tarball":"https://registry.npmjs.org/@belle-wissell/asset-sync/-/asset-sync-1.0.0.tgz","fileCount":8,"integrity":"sha512-Bp5akOpVQyidRDxpOy7j1/yRHI3lrfCzpGvEcEK9Dd/vGBITGW1gBiTMHvcMMkYgGLXno9dgbASHqdA+AI20ag==","signatures":[{"sig":"MEYCIQDORYCLV1d8fI/18Qe4cfLwdGwbYUsJnGNx79r6fbIzTAIhANA05DKXkT0LIVn7RPd7cQ4gVUgAKLlZFk/Q/aJKaooj","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":33895},"type":"module","engines":{"node":">=22"},"gitHead":"098418f3df9ff7a66c16c44f458252064d047ee0","scripts":{"start":"node asset-sync.js"},"_npmUser":{"name":"movingobjects","email":"scott@movingobjects.io"},"repository":{"url":"git+https://github.com/belle-wissell/asset-sync.git","type":"git"},"_npmVersion":"11.11.0","description":"Syncs JSON data sources and their referenced assets into a local folder for offline apps — install as a project dependency or run standalone on a kiosk","directories":{},"_nodeVersion":"22.20.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/asset-sync_1.0.0_1786984474836_0.9439397943359136","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"_id":"@belle-wissell/asset-sync@1.1.0","bin":{"asset-sync":"asset-sync.js"},"bugs":{"url":"https://github.com/belle-wissell/asset-sync/issues"},"dist":{"shasum":"4c6d79277eb3ac89c3b6c3b41948c54f4fa15026","tarball":"https://registry.npmjs.org/@belle-wissell/asset-sync/-/asset-sync-1.1.0.tgz","fileCount":11,"integrity":"sha512-27128/GGW3N0WNDsZ6bS6mDosL9Zuo2p7wWK66HR6wSI0oFadXKxqcVQQcnNCIHBgNkjSg061UH+h9wYBAGS9A==","signatures":[{"sig":"MEYCIQDUO5eBnDSI2OhJUqX//F5pPDpAGAch5sVRRyEubEbT9AIhAOH0WEl/7mLMy5pW5Lb3dqXMVHnCEw+W9QsPWHnf5nfD","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQD6X4ha1i2dBOsHnk/t1gIL7VtOM2Ar/VFB+xpALKr+JgIhAKqQwOrsV/mfbRfAtLpkUZZNRP6qNHGom74d0zTMURXQ"}],"unpackedSize":38175},"name":"@belle-wissell/asset-sync","type":"module","author":{"name":"Scott Thiessen","email":"scott@bwco.info"},"engines":{"node":">=22"},"exports":{".":{"types":"./lib/index.d.ts","default":"./lib/index.js"}},"gitHead":"3d15d8fabb4df9a55bb22dd28c7fe00eb4dc0116","license":"UNLICENSED","scripts":{"start":"node asset-sync.js"},"version":"1.1.0","_npmUser":{"name":"movingobjects","email":"scott@movingobjects.io"},"homepage":"https://github.com/belle-wissell/asset-sync","repository":{"url":"git+https://github.com/belle-wissell/asset-sync.git","type":"git"},"_npmVersion":"11.11.0","description":"Syncs JSON data sources and their referenced assets into a local folder for offline apps — install as a project dependency or run standalone on a kiosk","directories":{},"maintainers":[{"name":"movingobjects","email":"scott@movingobjects.io"}],"_nodeVersion":"22.20.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/asset-sync_1.1.0_1789587891791_0.14558760998113796"}}},"time":{"created":"2026-08-17T16:34:34.541Z","modified":"2026-09-16T19:44:52.158Z","1.0.0":"2026-08-17T16:34:34.975Z","1.1.0":"2026-09-16T19:44:51.876Z"},"bugs":{"url":"https://github.com/belle-wissell/asset-sync/issues"},"author":{"name":"Scott Thiessen","email":"scott@bwco.info"},"license":"UNLICENSED","homepage":"https://github.com/belle-wissell/asset-sync","repository":{"url":"git+https://github.com/belle-wissell/asset-sync.git","type":"git"},"description":"Syncs JSON data sources and their referenced assets into a local folder for offline apps — install as a project dependency or run standalone on a kiosk","maintainers":[{"name":"movingobjects","email":"scott@movingobjects.io"}],"readme":"# Asset Sync\n\nCommand-line tool that keeps data and assets in sync for offline apps. You provide the JSON URLs and which fields hold asset URLs, and it gives you a folder of data and assets your app can read offline.\n\n## Key features\n\n- Local content is only updated after each source downloads successfully. If a file download fails, the source is skipped, leaving the existing content in place.\n- Asset files are renamed to logically follow JSON structure, and JSON data is automatically rewritten to point to the correct asset files.\n- Works as a per-project dependency during development, and can be scheduled to run nightly by the OS on a kiosk machine, keeping content up-to-date and allowing kiosk apps to run offline.\n\n## Installation & usage\n\n### As a project dependency\n\nInstall it into whichever project needs synced assets, and add an npm script pointing at it:\n\n```bash\nnpm install --save-dev @belle-wissell/asset-sync\n```\n\n```json\n{\n  \"scripts\": {\n    \"assets:sync\": \"asset-sync\"\n  }\n}\n```\n\n```bash\nnpm run assets:sync\n```\n\nIf no config is found, a starter `asset-sync.config.json` is written in the project root. Update that file to point to your data sources, then run it again to sync. Each project keeps its own config — commit it or gitignore it, whichever fits that project.\n\n### On a kiosk machine (scheduled nightly)\n\nThere are a number of ways to make this work, but one option is to install the package globally.\n\n```bash\nnpm install -g @belle-wissell/asset-sync\n```\n\nPut `asset-sync.config.json` in a dedicated folder, e.g. `C:\\kiosk\\asset-sync.config.json`, and schedule `asset-sync` to run nightly with the OS scheduler. Since the default config resolves relative to the current working directory (not to wherever the package is installed), the scheduled job must either run with its working directory set to that folder, or pass an explicit absolute `--config` path:\n\n```text\n# Windows Task Scheduler\nProgram:   asset-sync\nArguments: --config C:\\kiosk\\asset-sync.config.json\nStart in:  C:\\kiosk\n```\n\n```bash\n# cron, with an explicit config path\n0 3 * * * /usr/local/bin/asset-sync --config c:/kiosk/asset-sync.config.json\n```\n\n### Options\n\n- `-c, --config <file>` — Config file to read (default: `asset-sync.config.json` in the current directory). Must already exist; only the default is scaffolded when missing.\n- `-j, --json-only` — Skip assets, JSON only. Refreshes the JSON files without touching the assets already on disk.\n- `-n, --concurrency <n>` — Parallel downloads (default: `8`)\n- `--dry-run` — Report without writing. Reports exactly what would be fetched and written, without requesting a single asset or touching your disk.\n- `-h, --help` — Show help message\n- `-v, --version` — Show version\n\n### From code\n\nThe same sync is available as a function, e.g. for an Electron main process. It takes the config as an object, logs nothing, and resolves with what was updated and what was skipped:\n\n```js\nimport { sync } from '@belle-wissell/asset-sync';\n\nconst { updated, skipped } = await sync(config, {\n  concurrency: 8, // optional, like --concurrency\n  jsonOnly: false, // optional, like --json-only\n  dryRun: false, // optional, like --dry-run\n  onProgress: ({ source, sourceCount, url, done, total, bytes }) => {}\n});\n\nfor (const { source, failures } of skipped) {\n  console.warn(source.url, failures.map(({ error }) => error.message));\n}\n```\n\nIt rejects only when the config is invalid or something unexpected fails; a source that couldn't be downloaded is left untouched on disk and listed in `skipped`. Types ship with the package.\n\n## Configuration\n\nAn `asset-sync.config.json` file in the project root (current working directory) controls how content is downloaded.\n\n### Config\n\n| Key         | Default    |                                                                                                    |\n|-------------|------------|----------------------------------------------------------------------------------------------------|\n| `outputDir` | *required* | Shared folder for every source. Absolute, starting with `~`, or relative to the current directory. |\n| `sources`   | *required* | List of sources, each a JSON endpoint to sync  (see below)                                         |\n\n### Source (an entry in `sources`)\n\n| Key          | Default    |                                                                               |\n|--------------|------------|-------------------------------------------------------------------------------|\n| `url`        | *required* | JSON endpoint to fetch                                                        |\n| `outputFile` | *required* | Name for the downloaded JSON                                                  |\n| `assets`     | *none*     | Enables asset downloading for this source. Omit it to download the JSON only. |\n\n### `assets` (on a source, once present)\n\n| Key            | Default    |                                              |\n|----------------|------------|----------------------------------------------|\n| `outputFolder` | *required* | Name of the folder for the downloaded assets |\n| `fields`       | *none*     | Fields in the JSON holding asset URLs        |\n\n### Example\n\n```json\n{\n  \"outputDir\": \"c:/kiosk\",\n  \"sources\": [\n    {\n      \"url\": \"https://example.com/api/categories\",\n      \"outputFile\": \"categories.json\"\n    },\n    {\n      \"url\": \"https://example.com/api/stories\",\n      \"outputFile\": \"stories.json\",\n      \"assets\": {\n        \"outputFolder\": \"story-assets\",\n        \"fields\": [\"stories.img\"]\n      }\n    }\n  ]\n}\n```\n\nThe above `asset-sync.config.json` file will output files like this:\n\n```text\nc:/kiosk/                    ← outputDir\n├─ categories.json           ← outputFile\n├─ stories.json              ← outputFile\n└─ story-assets/             ← assets.outputFolder\n   └─ stories-1-img.jpg      ← assets.fields\n   └─ stories-2-img.jpg      ← assets.fields\n   └─ stories-3-img.jpg      ← assets.fields\n\n\n```\n\n## Handling assets\n\n### Targeting URLs with `assets.fields`\n\nEach string in `assets.fields` digs down through the JSON one field at a time, separated by `.`, until it reaches a URL. Fields along the way may be objects or arrays; the final field may be a URL string or an array of URL strings.\n\nSo this `fields` array captures every image and video URL in the JSON below:\n\n```json\n\"fields\": [\n  \"settings.background.img\",\n  \"stories.img\",\n  \"stories.vid\",\n  \"otherImgs\"\n]\n```\n\n```json\n{\n  \"settings\": {\n    \"background\": {\n      \"enabled\": true,\n      \"img\": \"http://example.com/bg-image.png\",\n      \"opacity\": 0.75\n    }\n  },\n  \"stories\": [\n    {\n      \"title\": \"First story\",\n      \"img\": \"http://example.com/some-image.jpg\"\n    },\n    {\n      \"title\": \"Second story\",\n      \"img\": \"http://example.com/example.jpg\",\n      \"vid\": \"http://example.com/sample-vid-asdf.mp4\"\n    }\n  ],\n  \"otherImgs\": [\n    \"http://example.com/filename_a.jpg\",\n    \"http://example.com/filename_b.jpg\"\n  ]\n}\n```\n\nArrays are stepped through automatically, so you never name them in a field path — you name the fields *inside* them. That holds for the whole document too: if the endpoint returns a bare array, the field path is just `\"imagePath\"`, with nothing in front of it:\n\n```json\n[\n  { \"imagePath\": \"http://example.com/image.jpg\" },\n  { \"imagePath\": \"http://example.com/other.jpg\" }\n]\n```\n\n```json\n\"fields\": [\"imagePath\"]\n```\n\n### Naming downloaded files\n\nDownloaded files are named after the field path that found them, with array items numbered from 1:\n\n```text\nassets/settings-background-img.png\nassets/stories-1-img.jpg\nassets/stories-2-img.jpg\nassets/stories-2-vid.mp4\nassets/otherImgs-1.jpg\nassets/otherImgs-2.jpg\nassets/1-imagePath.jpg\nassets/2-imagePath.jpg\n```\n\nFile extensions come from the asset's final URL after redirects, falling back to its content type. Missing fields are skipped, and a URL referenced more than once is downloaded only once.\n\n### How the JSON is rewritten\n\nThe endpoint returns a story with an asset URL on the internet:\n\n```json\n{\n  \"stories\": [\n    {\n      \"id\": \"uniqueId\",\n      \"img\": \"https://cdn.example.com/x7f2.jpg\"\n    }\n  ]\n}\n```\n\nThe downloaded `c:/kiosk/stories.json` points at the copy on disk instead:\n\n```json\n{\n  \"stories\": [\n    {\n      \"id\": \"uniqueId\",\n      \"img\": \"story-assets/stories-1-img.jpg\"\n    }\n  ]\n}\n```\n\nThat path is always **relative to the JSON file**, and since the assets are a sibling folder, it is just `assets.outputFolder` plus the file name.\n","readmeFilename":"README.md"}