@johnhenry/sitefile 0.0.0 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +115 -1
  2. package/README.md +62 -24
  3. package/lib/src/checklinks.js +2 -0
  4. package/lib/src/cli/build-image.js +2 -2
  5. package/lib/src/cli/cache.js +15 -5
  6. package/lib/src/cli/images.js +3 -3
  7. package/lib/src/cli/index.d.ts +1 -1
  8. package/lib/src/cli/index.js +1 -1
  9. package/lib/src/cli/network.d.ts +34 -0
  10. package/lib/src/cli/network.js +56 -0
  11. package/lib/src/cli/outdated.js +5 -0
  12. package/lib/src/cli/registry.js +6 -1
  13. package/lib/src/cli/servers.js +23 -7
  14. package/lib/src/cli/vendor.js +5 -0
  15. package/lib/src/cli.js +44 -8
  16. package/lib/src/errors.d.ts +25 -3
  17. package/lib/src/errors.js +9 -0
  18. package/lib/src/executor.d.ts +7 -0
  19. package/lib/src/executor.js +18 -10
  20. package/lib/src/export-formats.d.ts +10 -3
  21. package/lib/src/export-formats.js +17 -6
  22. package/lib/src/index.d.ts +2 -1
  23. package/lib/src/index.js +2 -1
  24. package/lib/src/language/ast.d.ts +9 -2
  25. package/lib/src/outdated.js +1 -1
  26. package/lib/src/planner.d.ts +1 -1
  27. package/lib/src/planner.js +2 -2
  28. package/lib/src/plugins/cache.d.ts +29 -3
  29. package/lib/src/plugins/cache.js +104 -5
  30. package/lib/src/plugins/host.js +3 -2
  31. package/lib/src/registry.d.ts +6 -0
  32. package/lib/src/registry.js +38 -19
  33. package/lib/src/resources/https.d.ts +35 -1
  34. package/lib/src/resources/https.js +175 -18
  35. package/lib/src/resources/network.d.ts +70 -0
  36. package/lib/src/resources/network.js +304 -0
  37. package/lib/src/resources/types.d.ts +6 -0
  38. package/lib/src/secrets.js +1 -1
  39. package/lib/src/serve-rules.js +2 -1
  40. package/lib/src/servers.d.ts +14 -0
  41. package/lib/src/servers.js +89 -3
  42. package/lib/src/vendor.js +1 -1
  43. package/lib/src/wbn.d.ts +21 -0
  44. package/lib/src/wbn.js +111 -17
  45. package/package.json +2 -1
package/CHANGELOG.md CHANGED
@@ -1,8 +1,122 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.2 (2026-11-08)
4
+
5
+ - **Plugin console output keeps its order** (johnhenry/sitefile#164): a
6
+ plugin's console lines are written before its result, in both the
7
+ TypeScript and Rust runners, so they always become build warnings (a race
8
+ dropped some). Fixed in 5689229 (#164).
9
+ - **Docs: the tool is "sitefile".** The product name is lowercase where it
10
+ stands alone; "Sitefile" stays for the file (`Sitefile`, `Sitefile.lock`, "a
11
+ Sitefile") and the language. The desktop app is Sitefile Desktop.
12
+
13
+ ## 0.0.1 (2026-11-08)
14
+
15
+ - **`serve` serves archive files** (johnhenry/sitefile#155): `sitefile serve
16
+ site.zip` (and `serve -d`) serves a zip, tar, tar.gz/tgz, Web Bundle or OCI
17
+ image archive (`build -o x.oci.tar`, `save`) the way it serves a directory.
18
+ The format comes from the bytes; a path-like argument that is a file is an
19
+ archive, and a bare name is an archive file when no image has that name.
20
+ Zip, tar and wbn are extracted with the default archive limits and served
21
+ without hidden files (as a directory is); an OCI archive serves its first
22
+ image as the store would, with every blob checked against its digest. The
23
+ file is read again on every start and `server restart`. Server records gain
24
+ `{"kind": "archive", "path": …}` (older records load unchanged); Rust adds
25
+ `ServerSource::Archive`, `servers::load_archive_tree` and
26
+ `load_archive_tree_with`, with every existing signature unchanged. A file
27
+ that is not an archive is `E_TYPE` naming it. docs/CLI.md, "Site servers".
28
+ - **Explicit network settings: proxy, host rules, connect/read timeouts**
29
+ (johnhenry/sitefile#156), for networks that need a proxy and for sitefile
30
+ Desktop's Settings screen. `--proxy` / `SITEFILE_PROXY` (an `http://` proxy:
31
+ a CONNECT tunnel for https, absolute form for http; `HTTP(S)_PROXY` is still
32
+ never read), `--no-proxy` / `SITEFILE_NO_PROXY` (`NO_PROXY` syntax),
33
+ `--allow-hosts` / `SITEFILE_ALLOW_HOSTS` and `--deny-hosts` /
34
+ `SITEFILE_DENY_HOSTS` (checked on every hop, registries included; deny beats
35
+ allow; the new code `E_HOST_BLOCKED` names the rule), and `--connect-timeout`
36
+ / `--read-timeout` / `SITEFILE_FETCH_TIMEOUT`, on `build`, `validate`,
37
+ `inspect`, `graph`, `why`, `pull`, `push`, `vendor` and `outdated`. A proxy
38
+ never widens the fetch policy: the target is resolved and checked against
39
+ the private-address rule before the proxy is asked. Embedders pass the same
40
+ settings as `FetchPolicy.network` / `RegistryOptions.network`
41
+ (`NetworkSettings`, additive in both implementations). docs/CLI.md,
42
+ "Network"; `test/network.test.ts`, `rust/src/resources/network.rs`, and the
43
+ `net-proxy*`, `net-no-proxy`, `net-allow-hosts` and `net-deny-hosts`
44
+ conformance cases. Added in c131eb3 (#162).
45
+
46
+ - **Library API for embedders** (johnhenry/sitefile#153), found by embedding
47
+ the crate in Sitefile Desktop. Rust: `servers::serve_detached`,
48
+ `restart_detached`, `start_detached`, `stop_server`, `resolve_source`,
49
+ `parse_publish` and `DetachedServe` (the CLI's `serve -d`, `server restart`
50
+ and `server stop` now call them); `executor::store_build_as_image` with
51
+ `ImageInfo` and `StoredImage` (what `build -t` calls);
52
+ `BuildOptions::store_home`, so `FROM <local image>`, `BASE` and the wasm
53
+ module cache use an embedder's store instead of `$SITEFILE_HOME`;
54
+ `BuildOptions::cancel` with `CancelToken`, checked between imports, stages,
55
+ operations and plugin or tool calls, failing with the new code
56
+ `E_CANCELLED`; detached workers are reaped on a background thread, so a
57
+ long-lived host can stop them without waiting out the grace period.
58
+ Both implementations: TRANSFORM cache entries record their PLUGIN or TOOL
59
+ (`"plugin"`, same bytes in both; older entries are still read, keys are
60
+ unchanged), with `usage_by_plugin`/`usageByPlugin`,
61
+ `prune_plugin`/`prunePlugin` and `sitefile cache prune --plugin <name>`; the
62
+ `E_UNSUPPORTED` error of a PLUGIN or TOOL that is not allowed carries
63
+ `needs` (kind, name, flag), with the message unchanged. CLI output is
64
+ otherwise unchanged. docs/RUST.md, "Embedding the crate";
65
+ `rust/tests/embed.rs`. Added in e0c555d (#154).
66
+
67
+ - **`EXPORT … AS wbn COMPRESSION gzip`** (johnhenry/sitefile#143): a Web Bundle
68
+ export can now be gzip(`application/webbundle`), the `.wbn` archive format of
69
+ `@johnhenry/packfile` (`toArchive`/`fromArchive`) and `@johnhenry/fileable`
70
+ (`encode="wbn"`). `COMPRESSION` used to be `E_VALIDATION` on anything but
71
+ `oci`; it now applies to `oci` and `wbn`, and on `wbn` defaults to `none`, so
72
+ existing exports are byte-for-byte unchanged. The gzip layer only wraps the
73
+ finished bundle and uses the shared deterministic encoder (`src/deflate.ts`,
74
+ `rust/src/deflate.rs`), so both CLIs write identical bytes. On the CLI,
75
+ `-o site.wbn.gz` selects a gzipped wbn (also for `sitefile save`), the
76
+ default destination of such a Sitefile is `dist.wbn.gz`, and the new
77
+ `--compression gzip|none` overrides `EXPORT … COMPRESSION` for the export's
78
+ format (oci or wbn; any other format is a usage error); `--oci-compression`
79
+ stays as the oci-only spelling. A Sitefile's `COMPRESSION` belongs to its own
80
+ `AS` format: `--format wbn` on an `AS oci COMPRESSION …` Sitefile does not
81
+ inherit it. `test/packfile-interop.test.ts` now reads a gzip export built with
82
+ `BASE https://packfile.invalid/` through plain `fromArchive(buffer)`; new
83
+ conformance cases compare the bytes of both implementations. Added in
84
+ 3c537de (#149).
85
+
86
+ - **Example: a fileable plugin for TRANSFORM** (`examples/fileable/`,
87
+ johnhenry/sitefile#145). A JavaScript plugin renders a `@johnhenry/fileable`
88
+ tree into the build with fileable's in-memory `toFiles()` and returns
89
+ `{ files }`, so a JSX file tree needs no directory or zip on disk in
90
+ between. fileable stays out of sitefile's dependencies: it is a
91
+ devDependency, and `examples/fileable/vendor.mjs` copies the files its
92
+ browser entry point loads into the plugin folder (gitignored; nothing
93
+ generated is committed), the only folder a plugin can read. Its `cmd`
94
+ attribute and other features that need files, processes or the environment
95
+ fail with a clear `E_PLUGIN`. Checked by `npm run examples` and by
96
+ conformance (both CLIs). docs/PLUGINS.md, "Example: rendering a fileable
97
+ tree". Added in 0b5c4b3 (#147).
98
+
99
+ - **`EXPORT … AS wbn` bakes HEADER and exact REDIRECT rules into the bundle's
100
+ responses** (johnhenry/sitefile#144, #150). Each response gets the headers of the
101
+ `/_headers` blocks matching its URL, with `sitefile serve`'s matching
102
+ (`serve-rules.ts`; a directory URL like `<base>docs/` matches with its
103
+ slash and gets its own response when its headers differ). Exact-path
104
+ `/_redirects` rules with a 3xx status become responses with that status, a
105
+ `location` and an empty body, in `serve`'s order (a forced `301!` replaces
106
+ the file; other rules apply only where `serve` would find nothing). Splats,
107
+ `200` rewrites and `404` rules stay in `/_redirects`, which, like
108
+ `/_headers`, is still in the bundle. Bundles of trees without those files
109
+ are byte-identical to before. packfile's `createWebBundleRouter()` now
110
+ answers with the HEADER values and redirects (`test/packfile-interop.test.ts`);
111
+ conformance cases `ok-wbn-rules` and `ok-wbn-rules-files` compare both
112
+ implementations' bytes. The TypeScript `headersFor` no longer drops a
113
+ header named `__proto__` (the Rust port never did); this also
114
+ changes the TypeScript `sitefile serve`, which now sends such a header as
115
+ the Rust binary does. Added in 429ab24 (#150).
116
+
3
117
  ## 0.0.0 — first npm release (2026-11-06)
4
118
 
5
- `@johnhenry/sitefile` is a new package: Sitefile was never published to npm
119
+ `@johnhenry/sitefile` is a new package: sitefile was never published to npm
6
120
  under any name, and `0.0.0` is its first version. It joins the @johnhenry
7
121
  family as its static build orchestrator, next to `@johnhenry/fileable` (file
8
122
  trees in JSX) and `@johnhenry/packfile` (the Web Bundle reader and server).
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Sitefile
1
+ # sitefile
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/%40johnhenry%2Fsitefile.svg)](https://www.npmjs.com/package/@johnhenry/sitefile)
4
4
  [![CI](https://github.com/johnhenry/sitefile/actions/workflows/ci.yml/badge.svg)](https://github.com/johnhenry/sitefile/actions/workflows/ci.yml)
@@ -84,10 +84,10 @@ no runtime dependencies.
84
84
  | `TOOL name` + `TRANSFORM … WITH name TO <dest> [OPTIONS …] [FOR …] ARGS …` | yes; run an external program (e.g. `ffmpeg`) you allow with `--allow-exec name`: argv only, no shell, temp files, scrubbed environment (see [docs/PLUGINS.md](docs/PLUGINS.md)) |
85
85
  | `SITEMAP BASE <url> [TO <dest>]` | yes; a sitemaps.org `/sitemap.xml` of the `.html` pages so far |
86
86
  | `FEED <import> BASE <url> LINK <path> [TO <dest>] [TITLE <text>]` | yes; an Atom `/feed.xml` from Markdown posts (`title` and `date` required) |
87
- | `REDIRECT <from> <to> [STATUS 301\|302\|303\|307\|308]`, `HEADER <pattern> <Name>: <value>` | yes; `/_redirects` and `/_headers` for Netlify / Cloudflare Pages (and `sitefile serve`) |
87
+ | `REDIRECT <from> <to> [STATUS 301\|302\|303\|307\|308]`, `HEADER <pattern> <Name>: <value>` | yes; `/_redirects` and `/_headers` for Netlify / Cloudflare Pages (and `sitefile serve`); a `wbn` export also bakes HEADER values into each response and exact-path REDIRECTs into 3xx responses (splats stay in `/_redirects`) |
88
88
  | `CHECKLINKS [--warn] [--external]` | yes; fails (or warns) on links to files that do not exist, with file and line; `--external` requests http(s) links under the network policy |
89
89
  | `FINGERPRINT [--overwrite] <pattern>… [MANIFEST <dest>]` | yes; renames assets to `name.<hash>.ext` and rewrites the HTML and CSS that reference them, plus a JSON manifest |
90
- | `EXPORT <path> [AS directory\|zip\|tar\|tgz\|wbn\|oci] [BASE <https-url>] [AT <root>]` | yes; archives are deterministic, `wbn` is an unsigned Web Bundle (needs `BASE` or `--base-url`), `oci` is a single-layer OCI image of the files (under `AT`, default `/srv/www`) |
90
+ | `EXPORT <path> [AS directory\|zip\|tar\|tgz\|wbn\|oci] [BASE <https-url>] [AT <root>] [COMPRESSION gzip\|none]` | yes; archives are deterministic, `wbn` is an unsigned Web Bundle (needs `BASE` or `--base-url`; `COMPRESSION gzip` gzips it, default `none`), `oci` is a single-layer OCI image of the files (under `AT`, default `/srv/www`; `COMPRESSION` sets its layer, default `gzip`) |
91
91
 
92
92
  Templates can also live in the Sitefile itself (see `examples/inline/`):
93
93
 
@@ -163,7 +163,7 @@ build used, `--cache-from <ref>` reuses them (see [docs/CLI.md](docs/CLI.md#regi
163
163
  ## CLI
164
164
 
165
165
  ```
166
- sitefile build [context] [-o dist] [--format directory|zip|tar|tgz|wbn] [--base-url URL]
166
+ sitefile build [context] [-o dist] [--format directory|zip|tar|tgz|wbn] [--base-url URL] [--compression gzip|none]
167
167
  [--build-arg K=V | --build-arg K] [--build-arg-file vars.json|.yaml|.env] [--force]
168
168
  [--frozen-lockfile | --update-lock] [--strict-pins] [--allow-http] [--allow-private-network]
169
169
  [--allow-hardlinks] [--allow-exec name[=/abs/path]]... [--allow-plugin name]... [--allow-plugins]
@@ -181,7 +181,7 @@ sitefile build [context] -t name[:tag] # also store the build as an image
181
181
  sitefile images | image inspect|diff|prune | tag | rmi | save
182
182
  sitefile cache info | cache prune [--older-than days] # TRANSFORM result cache
183
183
  sitefile cache du # cache sizes; SITEFILE_CACHE_MAX bounds them (default 1G)
184
- sitefile serve [-d] [--name n] [-p [host:]port] [--spa] <image|./dir>
184
+ sitefile serve [-d] [--name n] [-p [host:]port] [--spa] <image|./dir|site.zip>
185
185
  sitefile serve --watch [-p [host:]port] [--spa] [context] # build in memory, serve, live reload
186
186
  sitefile servers [-a] | server stop|rm [-f]|restart|logs [-f] <name>
187
187
  ```
@@ -273,6 +273,7 @@ CycloneDX file list to the image as OCI 1.1 referrers (deterministic, pushed wit
273
273
  sitefile build examples/hello --format zip # examples/hello/dist.zip
274
274
  sitefile build examples/hello -o hello.tar.gz # format from the extension
275
275
  sitefile build examples/hello --format wbn --base-url https://example.com/
276
+ sitefile build examples/hello -o hello.wbn.gz --base-url https://example.com/ # gzip(Web Bundle)
276
277
  ```
277
278
 
278
279
  Archives are reproducible: sorted entries, fixed timestamps (or `SOURCE_DATE_EPOCH`),
@@ -324,6 +325,7 @@ stopgap in VS Code.
324
325
  - [docs/RUST.md](docs/RUST.md) — the Rust port: layout, parity checks, known differences
325
326
  - [docs/LSP.md](docs/LSP.md) — the language server for editors (`sitefile lsp`, Rust binary)
326
327
  - [docs/PLUGINS.md](docs/PLUGINS.md) — writing renderer plugins, and the sandbox
328
+ - [docs/DOCKER.md](docs/DOCKER.md) — sitefile next to Docker: site images Docker runs, Dockerfile stages, shared registries
327
329
  - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — invariants and module layout
328
330
  - [docs/MILESTONES.md](docs/MILESTONES.md), [docs/TASKS.md](docs/TASKS.md)
329
331
  - [AGENTS.md](AGENTS.md) — the verification loop and rules for anyone (human or agent) changing this repo
@@ -378,7 +380,7 @@ stage), so building one is treated as running untrusted input. Templates,
378
380
  imports and WebAssembly plugins are data; JavaScript plugins and external
379
381
  tools are code, and run only when you name them on the command line.
380
382
 
381
- **What Sitefile guarantees:**
383
+ **What sitefile guarantees:**
382
384
 
383
385
  - **Local reads stay inside the build context.** No `..`, absolute paths or
384
386
  symlinks out; hard-linked files are refused (`E_HARDLINK`) unless
@@ -388,7 +390,11 @@ tools are code, and run only when you name them on the command line.
388
390
  - **Remote imports are `https:` only, to public addresses only** — checked at
389
391
  connect time and on every redirect (SSRF), size- and time-limited, never
390
392
  decompressed or executed. `--allow-http` and `--allow-private-network` are the
391
- only ways to widen this.
393
+ only ways to widen this. An explicit proxy (`--proxy` / `$SITEFILE_PROXY`;
394
+ `HTTP(S)_PROXY` is never read) does not widen it either: sitefile resolves and
395
+ checks the target itself before handing it to the proxy. `--allow-hosts` and
396
+ `--deny-hosts` narrow it further, on every hop
397
+ ([docs/CLI.md](docs/CLI.md#network-proxy-host-rules-and-timeouts)).
392
398
  - **Pins are enforced.** Unpinned URLs are recorded in `Sitefile.lock` and must
393
399
  match on later builds; `--strict-pins` refuses any remote import or BASE image
394
400
  that is not pinned (`INTEGRITY`, `@sha256:` or a lockfile entry).
@@ -441,35 +447,67 @@ tools are code, and run only when you name them on the command line.
441
447
  `INTEGRITY`, `@sha256:` and `--strict-pins` for builds that must be
442
448
  reproducible.
443
449
  - **Web Bundles are unsigned.** `EXPORT … AS wbn` writes an unsigned b2 bundle;
444
- signing (for example for an Isolated Web App) happens outside Sitefile.
450
+ signing (for example for an Isolated Web App) happens outside sitefile.
445
451
  - **Widening flags are yours to justify.** `--allow-http`,
446
452
  `--allow-private-network`, `--allow-hardlinks`, `--force` and `serve -p
447
453
  0.0.0.0:…` each turn off one of the guarantees above for that build.
448
454
 
449
455
  ## Family
450
456
 
451
- Sitefile is the build orchestrator of the @johnhenry family's static tools. It
457
+ sitefile is the build orchestrator of the @johnhenry family's static tools. It
452
458
  runs the pipeline (pinned imports, stages, templates, sandboxed plugins, checks,
453
459
  exports); its siblings write file trees in code and serve the result. It has no
454
- runtime dependency on either: they meet at file formats, a directory or a `.wbn`
455
- Web Bundle.
460
+ runtime dependency on either: they meet at file formats, a directory, a zip or
461
+ a `.wbn` Web Bundle.
456
462
 
457
463
  - **[`@johnhenry/fileable`](https://github.com/johnhenry/fileable)** -- writes
458
464
  file trees in JSX (`Dir`, `File`, `Rm`), for scaffolding and codegen. A
459
- fileable build's output directory, or its `encode="zip"` archive, is an
460
- ordinary Sitefile input: `COPY ./generated/ /`, or `IMPORT gen FROM
461
- ./site.zip AS tree` and `COPY gen:/ /` (both give the same output). Its
462
- `encode="wbn"` archive is gzip around a Web Bundle, which `AS tree` does not
463
- read yet (johnhenry/sitefile#141). Neither package depends on the other.
465
+ fileable build's output is an ordinary sitefile input. Neither package
466
+ depends on the other.
464
467
  - **[`@johnhenry/packfile`](https://github.com/johnhenry/packfile)** -- the
465
- family's Web Bundle reader and in-memory server. `EXPORT / AS wbn BASE <url>`
466
- writes a plain b2 Web Bundle that packfile's `fromWebBundle(buffer, { baseURL })`
467
- reads back into exactly the exported files, and `createRouter()` serves as a
468
- `(Request) => Response` handler. packfile's own archive format
469
- (`fromArchive()`) is gzip around the same bundle under
470
- `https://packfile.invalid/`: gzip a Sitefile bundle exported with that BASE and
471
- `fromArchive()` reads it as is. `test/packfile-interop.test.ts` checks all
472
- three; `@johnhenry/packfile` is a devDependency for that test only.
468
+ family's Web Bundle reader and in-memory server. It reads what `EXPORT … AS
469
+ wbn` writes and serves it as a `(Request) => Response` handler.
470
+ `@johnhenry/packfile` is a devDependency here for
471
+ `test/packfile-interop.test.ts` only.
472
+
473
+ ### Working together: what works today
474
+
475
+ Checked on 2026-11-07 against the npm releases (`@johnhenry/sitefile` 0.0.0,
476
+ `@johnhenry/fileable` 0.1.1, `@johnhenry/packfile` 0.2.0) and each
477
+ repository's `main`. "Fixed on main, in the next release" means merged but
478
+ not published: the npm versions still behave as the rest of that row says.
479
+
480
+ | From | To | How | Status |
481
+ | --- | --- | --- | --- |
482
+ | fileable, loose output | sitefile | `COPY ./generated/ /` | Works |
483
+ | fileable, `encode="zip"` | sitefile | `IMPORT gen FROM ./site.zip AS tree`, then `COPY gen:/ /` | Works (same output as loose) |
484
+ | fileable, `encode="wbn"` | sitefile | `IMPORT … AS tree` | **Not yet:** it's gzip around a Web Bundle, and `AS tree` reads only plain bundles ([#141](https://github.com/johnhenry/sitefile/issues/141)) |
485
+ | fileable tree, rendered during a build | Sitefile | a JavaScript `TRANSFORM` plugin calling fileable's in-memory `toFiles()` | An example on main, not part of the npm package: [`examples/fileable/`](examples/fileable/) ([#145](https://github.com/johnhenry/sitefile/issues/145)). fileable is vendored into the plugin folder; `cmd`, local `src=` and `env://` fail with `E_PLUGIN` |
486
+ | sitefile, `EXPORT / AS wbn BASE <url>` | packfile | `fromWebBundle(buffer, { baseURL })` from `@johnhenry/packfile/web-bundle`, then `createRouter(files)` | Works; tested here (`test/packfile-interop.test.ts`), and packfile's own tests read a bundle written by sitefile 0.0.0 |
487
+ | sitefile `.wbn` | packfile `fromArchive()` | with 0.0.0 / 0.2.0: gzip it, and export with `BASE https://packfile.invalid/` | Works with that workaround. **Fixed on main, in the next release:** `EXPORT … AS wbn COMPRESSION gzip` (or `-o site.wbn.gz`) writes the gzip ([#143](https://github.com/johnhenry/sitefile/issues/143)), and `fromArchive()` takes a `baseURL` or the origin of the bundle's primary URL, so any `BASE` reads back ([packfile#18](https://github.com/johnhenry/packfile/issues/18)) |
488
+ | sitefile `HEADER` / `REDIRECT` | packfile `createWebBundleRouter()` | -- | **Not applied in 0.0.0:** only the `/_headers` and `/_redirects` files are in the bundle. **Fixed on main, in the next release:** each response carries its HEADER values, and exact-path 301/302/303/307/308 REDIRECTs are 3xx responses with `location` ([#144](https://github.com/johnhenry/sitefile/issues/144)). Splats, `200` rewrites and `404` rules stay in `/_redirects` only |
489
+ | packfile `toArchive()` | sitefile | `IMPORT … AS tree` | **Not yet** (same as fileable's `.wbn`, [#141](https://github.com/johnhenry/sitefile/issues/141)) |
490
+
491
+ Gotchas when serving a Sitefile bundle with packfile:
492
+
493
+ - **Pass `createRouter()` a path string for non-ASCII paths.** A `Request`
494
+ for `/docs/ünï/page.html` returns 404 because the pathname isn't
495
+ percent-decoded; the string `"/docs/ünï/page.html"` works
496
+ ([packfile#20](https://github.com/johnhenry/packfile/issues/20)).
497
+ - **`fromWebBundle` skips the bundle's entry at the bare base URL.** sitefile
498
+ stores the index page there as well as at `/index.html`; you get it from
499
+ `index.html`.
500
+ - **The `baseURL` you pass to `fromWebBundle` must be the `BASE` you exported
501
+ with.** Entries under any other origin are skipped, so the wrong base reads
502
+ back as an empty map, without an error. packfile's `fromArchive()` on its
503
+ main branch (next release) defaults to the origin of the primary URL, which
504
+ sitefile writes only when the site has `/index.html`; with a `BASE` that has
505
+ a path (`https://site.example/blog/`), the keys start with `blog/` unless you
506
+ pass that `baseURL`.
507
+ - **HEADER and REDIRECT need `createWebBundleRouter()`** (on main, next
508
+ release): `createWebBundleRouter(new wbn.Bundle(bytes), { baseURL })` serves
509
+ each exchange's own status and headers. `createRouter()` serves a files map,
510
+ so it answers with the files only.
473
511
 
474
512
  ## License
475
513
 
@@ -44,6 +44,8 @@ function externalReason(e, url, policy) {
44
44
  return /^http:/i.test(url) && !policy.allowHttp ? 'plain http is checked only with --allow-http' : 'not allowed by the network policy';
45
45
  case 'E_SSRF_BLOCKED':
46
46
  return 'a non-public address (checked only with --allow-private-network)';
47
+ case 'E_HOST_BLOCKED':
48
+ return 'refused by the allow-hosts/deny-hosts rules';
47
49
  case 'E_REDIRECT':
48
50
  return 'too many or invalid redirects';
49
51
  default:
@@ -1,6 +1,6 @@
1
1
  import { sourceDateEpoch } from '../archive-writers.js';
2
2
  import { SitefileError } from '../errors.js';
3
- import { imageAttestations } from '../executor.js';
3
+ import { imageAttestations, sitefileCompression } from '../executor.js';
4
4
  import { imageMeta } from '../oci.js';
5
5
  import { parseLocalRef } from '../ref.js';
6
6
  import { shortId, Store } from '../store.js';
@@ -20,7 +20,7 @@ export async function storeBuildAsImage(result, tags, info) {
20
20
  const store = await Store.open();
21
21
  const { id } = await store.addImage(result.output, {
22
22
  root: info.ociRoot ?? result.plan.exportRoot,
23
- compression: info.ociCompression ?? result.plan.exportCompression,
23
+ compression: info.ociCompression ?? sitefileCompression(result.plan, 'oci'),
24
24
  platform: info.platform,
25
25
  epoch: sourceDateEpoch(),
26
26
  base: result.base,
@@ -24,17 +24,24 @@ const info = {
24
24
  const prune = {
25
25
  name: 'cache prune',
26
26
  summary: 'delete cached TRANSFORM results',
27
- usage: 'cache prune [--older-than <days> | --max-size <size>]',
28
- description: 'Delete every cached TRANSFORM result, or only entries not used for the given number of days, and the blobs no\nentry still uses. With --max-size, instead apply the automatic size limit now with that size: least recently\nused evictable data (TRANSFORM results, compiled wasm modules, untagged images and unused blobs) is deleted\nuntil the rest fits. Tagged images are never touched.',
29
- flags: [' --older-than <days> keep entries used within this many days', ' --max-size <size> shrink evictable data to this size (e.g. 500M, 2G)'],
30
- options: { 'older-than': { type: 'string' }, 'max-size': { type: 'string' } },
27
+ usage: 'cache prune [--older-than <days>] [--plugin <name>] | --max-size <size>',
28
+ description: 'Delete every cached TRANSFORM result, or only entries not used for the given number of days, and the blobs no\nentry still uses. With --plugin, only the results of that PLUGIN or TOOL (entries written before results\nrecorded their plugin are kept). With --max-size, instead apply the automatic size limit now with that size:\nleast recently used evictable data (TRANSFORM results, compiled wasm modules, untagged images and unused\nblobs) is deleted until the rest fits. Tagged images are never touched.',
29
+ flags: [
30
+ ' --older-than <days> keep entries used within this many days',
31
+ ' --plugin <name> only the results of this PLUGIN or TOOL',
32
+ ' --max-size <size> shrink evictable data to this size (e.g. 500M, 2G)',
33
+ ],
34
+ options: { 'older-than': { type: 'string' }, plugin: { type: 'string' }, 'max-size': { type: 'string' } },
31
35
  async run(v, pos) {
32
36
  requireArgs(pos, 0, 0, 'no arguments');
33
37
  const raw = v['older-than'];
34
38
  const max = str(v['max-size']);
39
+ const plugin = str(v['plugin']);
35
40
  if (max !== undefined) {
36
41
  if (raw !== undefined)
37
42
  usage('--older-than and --max-size conflict');
43
+ if (plugin !== undefined)
44
+ usage('--plugin and --max-size conflict');
38
45
  const n = parseSize(max);
39
46
  if (n === undefined || n === 0)
40
47
  usage('--max-size expects a size greater than 0, such as 500M or 2G');
@@ -49,7 +56,10 @@ const prune = {
49
56
  usage('--older-than expects a positive number of days');
50
57
  ms = days * 86_400_000;
51
58
  }
52
- const r = await new TransformCache(transformCacheDir()).prune(ms);
59
+ if (plugin !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(plugin))
60
+ usage(`--plugin expects a PLUGIN or TOOL name (an identifier), got '${plugin}'`);
61
+ const cache = new TransformCache(transformCacheDir());
62
+ const r = plugin === undefined ? await cache.prune(ms) : await cache.prunePlugin(plugin, ms);
53
63
  process.stdout.write(`Deleted ${r.entries} entr${r.entries === 1 ? 'y' : 'ies'} and ${r.blobs} blob${r.blobs === 1 ? '' : 's'}\nTotal reclaimed space: ${humanSize(r.bytes)}\n`);
54
64
  return 0;
55
65
  },
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  import { sourceDateEpoch, writeTar } from '../archive-writers.js';
5
5
  import { SitefileError } from '../errors.js';
6
6
  import { exportDirectory, exportFile } from '../exporter.js';
7
- import { encodeTree, formatFromExtension, isArchiveFormat } from '../export-formats.js';
7
+ import { compressionFromExtension, encodeTree, formatFromExtension, isArchiveFormat } from '../export-formats.js';
8
8
  import { EXPORT_FORMATS, parseExportFormat } from '../language/ast.js';
9
9
  import { MEDIA, siteLayerIndex } from '../oci.js';
10
10
  import { looksLikeImageId, parseLocalRef, refKey } from '../ref.js';
@@ -175,7 +175,7 @@ const save = {
175
175
  name: 'save',
176
176
  summary: 'export an image to a directory or archive',
177
177
  usage: 'save <ref|id> -o <out> [--format fmt] [--base-url url] [--force]',
178
- description: 'Write an image\'s files to a directory, or to a zip, tar, tar.gz (tgz) or wbn archive, or the image itself as\nan OCI archive (-o x.oci.tar) or OCI layout directory (--format oci). The format is chosen like "build -o":\nfrom --format, else the extension (.zip .tar .tar.gz .tgz .wbn .oci.tar), else a directory.\nArchives are deterministic (set SOURCE_DATE_EPOCH to stamp them). A non-empty destination needs --force.',
178
+ description: 'Write an image\'s files to a directory, or to a zip, tar, tar.gz (tgz) or wbn archive, or the image itself as\nan OCI archive (-o x.oci.tar) or OCI layout directory (--format oci). The format is chosen like "build -o":\nfrom --format, else the extension (.zip .tar .tar.gz .tgz .wbn .wbn.gz .oci.tar), else a directory; a wbn\nwritten to *.wbn.gz is gzipped.\nArchives are deterministic (set SOURCE_DATE_EPOCH to stamp them). A non-empty destination needs --force.',
179
179
  flags: [
180
180
  '-o, --out <path> destination (required)',
181
181
  ` --format <fmt> ${EXPORT_FORMATS.join(' | ')} (alias tar.gz)`,
@@ -223,7 +223,7 @@ const save = {
223
223
  process.stdout.write(`Saved ${shortId(id)} (${tree.size} files) to ${shown(w.outDir)}\n`);
224
224
  }
225
225
  else {
226
- const data = encodeTree(tree, format, { epoch: sourceDateEpoch(), baseUrl: str(v['base-url']) });
226
+ const data = encodeTree(tree, format, { epoch: sourceDateEpoch(), baseUrl: str(v['base-url']), compression: compressionFromExtension(out, format) });
227
227
  const w = await exportFile(data, out, opts);
228
228
  process.stdout.write(`Saved ${shortId(id)} (${tree.size} files) to ${shown(w.outFile)} (${format === 'tgz' ? 'tar.gz' : format}, ${w.bytes} bytes)\n`);
229
229
  }
@@ -5,6 +5,6 @@ export declare const EXTRA_COMMANDS: readonly string[];
5
5
  * an editor tool. Here `sitefile lsp` is a usage error that points there.
6
6
  */
7
7
  export declare const LSP_UNAVAILABLE = "'sitefile lsp' (the language server) is provided by the Rust implementation: build it with 'cd rust && cargo build --release' and run 'rust/target/release/sitefile lsp' (see docs/LSP.md)";
8
- export declare const EXTRA_HELP = "\nImages (local store: $SITEFILE_HOME or ~/.sitefile):\n sitefile build [context] -t name[:tag] also store the result as an image (repeatable -t)\n sitefile images [--json] [--digests] [-q] list images (alias: image ls)\n sitefile image inspect <ref> [--json] show an image manifest\n sitefile image prune delete unreferenced manifests and blobs\n sitefile image diff <a> <b> added/removed/changed paths\n sitefile tag <src> <dst> add a name:tag\n sitefile rmi [-f] <ref|id>... remove tags / images\n sitefile save <ref> -o out[.zip|.tar|.tar.gz|.wbn] export an image\n sitefile push <registry/name[:tag]> push a (tagged) image to an OCI registry\n sitefile pull <[registry/]name[:tag][@digest]> pull an image (a bare name means Docker Hub)\n\nVariants:\n sitefile bake [targets...] [-f file] [--print] build several targets (matrix, groups) from\n sitefile.bake.yaml; --print shows the plan\n\nOffline builds:\n sitefile vendor [context] [--vendor-dir dir] download remote imports and the BASE image into\n vendor/sitefile; builds then read them from there\n\nPins:\n sitefile outdated [context] [--json] compare remote imports and the BASE image with\n their pins (never changes anything)\n\nBuild cache (TRANSFORM results: $SITEFILE_HOME/cache/transform):\n sitefile cache info [--json] location, entries, size\n sitefile cache prune [--older-than <days>] delete cached results\n sitefile cache prune --max-size <size> shrink evictable data (LRU) to <size>\n sitefile cache du [--json] size of every cache and the store (alias: cache ls)\n Evictable data over $SITEFILE_CACHE_MAX (default 1GiB, 0 = no limit) is deleted after each build.\n\nServing sites (detached servers are tracked by name):\n sitefile serve [--name n] [-p [host:]port] [-d] [--spa] <ref|./dir>\n sitefile serve --watch [-p [host:]port] [ctx] build in memory, serve, rebuild + live reload\n sitefile servers [-a] [--json] list servers (alias: server ls)\n sitefile server stop|rm [-f]|restart|logs [-f] <name|id>\n\nEditor support:\n sitefile lsp [--stdio] language server over stdio (Rust build only; docs/LSP.md)\n\nRun 'sitefile <command> --help' for the options of a command.\n";
8
+ export declare const EXTRA_HELP = "\nImages (local store: $SITEFILE_HOME or ~/.sitefile):\n sitefile build [context] -t name[:tag] also store the result as an image (repeatable -t)\n sitefile images [--json] [--digests] [-q] list images (alias: image ls)\n sitefile image inspect <ref> [--json] show an image manifest\n sitefile image prune delete unreferenced manifests and blobs\n sitefile image diff <a> <b> added/removed/changed paths\n sitefile tag <src> <dst> add a name:tag\n sitefile rmi [-f] <ref|id>... remove tags / images\n sitefile save <ref> -o out[.zip|.tar|.tar.gz|.wbn] export an image\n sitefile push <registry/name[:tag]> push a (tagged) image to an OCI registry\n sitefile pull <[registry/]name[:tag][@digest]> pull an image (a bare name means Docker Hub)\n\nVariants:\n sitefile bake [targets...] [-f file] [--print] build several targets (matrix, groups) from\n sitefile.bake.yaml; --print shows the plan\n\nOffline builds:\n sitefile vendor [context] [--vendor-dir dir] download remote imports and the BASE image into\n vendor/sitefile; builds then read them from there\n\nPins:\n sitefile outdated [context] [--json] compare remote imports and the BASE image with\n their pins (never changes anything)\n\nBuild cache (TRANSFORM results: $SITEFILE_HOME/cache/transform):\n sitefile cache info [--json] location, entries, size\n sitefile cache prune [--older-than <days>] delete cached results\n sitefile cache prune --max-size <size> shrink evictable data (LRU) to <size>\n sitefile cache du [--json] size of every cache and the store (alias: cache ls)\n Evictable data over $SITEFILE_CACHE_MAX (default 1GiB, 0 = no limit) is deleted after each build.\n\nServing sites (detached servers are tracked by name):\n sitefile serve [--name n] [-p [host:]port] [-d] [--spa] <ref|./dir|file>\n sitefile serve --watch [-p [host:]port] [ctx] build in memory, serve, rebuild + live reload\n sitefile servers [-a] [--json] list servers (alias: server ls)\n sitefile server stop|rm [-f]|restart|logs [-f] <name|id>\n\nEditor support:\n sitefile lsp [--stdio] language server over stdio (Rust build only; docs/LSP.md)\n\nRun 'sitefile <command> --help' for the options of a command.\n";
9
9
  export declare function isExtraCommand(name: string | undefined): boolean;
10
10
  export declare function runExtraCommand(argv: readonly string[]): Promise<number>;
@@ -51,7 +51,7 @@ Build cache (TRANSFORM results: $SITEFILE_HOME/cache/transform):
51
51
  Evictable data over $SITEFILE_CACHE_MAX (default 1GiB, 0 = no limit) is deleted after each build.
52
52
 
53
53
  Serving sites (detached servers are tracked by name):
54
- sitefile serve [--name n] [-p [host:]port] [-d] [--spa] <ref|./dir>
54
+ sitefile serve [--name n] [-p [host:]port] [-d] [--spa] <ref|./dir|file>
55
55
  sitefile serve --watch [-p [host:]port] [ctx] build in memory, serve, rebuild + live reload
56
56
  sitefile servers [-a] [--json] list servers (alias: server ls)
57
57
  sitefile server stop|rm [-f]|restart|logs [-f] <name|id>
@@ -0,0 +1,34 @@
1
+ import { type NetworkSettings } from '../resources/network.js';
2
+ /** parseArgs options for the network flags (build, validate, inspect, graph, why, pull, push, vendor, outdated). */
3
+ export declare const NETWORK_OPTIONS: {
4
+ readonly proxy: {
5
+ readonly type: 'string';
6
+ };
7
+ readonly 'no-proxy': {
8
+ readonly type: 'string';
9
+ readonly multiple: true;
10
+ };
11
+ readonly 'allow-hosts': {
12
+ readonly type: 'string';
13
+ readonly multiple: true;
14
+ };
15
+ readonly 'deny-hosts': {
16
+ readonly type: 'string';
17
+ readonly multiple: true;
18
+ };
19
+ readonly 'connect-timeout': {
20
+ readonly type: 'string';
21
+ };
22
+ readonly 'read-timeout': {
23
+ readonly type: 'string';
24
+ };
25
+ };
26
+ /** Help lines for {@link NETWORK_OPTIONS}, at the given flag-column width. */
27
+ export declare function networkHelp(width: number): string[];
28
+ type Values = Record<string, string | boolean | Array<string | boolean> | undefined>;
29
+ /**
30
+ * The network settings for a command: the SITEFILE_* environment, then the flags on top (flags replace
31
+ * the proxy and timeouts and add to the host lists). Throws E_USAGE for a bad value.
32
+ */
33
+ export declare function networkOption(v: Values, env?: NodeJS.ProcessEnv): NetworkSettings | undefined;
34
+ export {};
@@ -0,0 +1,56 @@
1
+ import { SitefileError } from '../errors.js';
2
+ import { mergeNetwork, networkFromEnv, networkProblem, splitList } from '../resources/network.js';
3
+ /** parseArgs options for the network flags (build, validate, inspect, graph, why, pull, push, vendor, outdated). */
4
+ export const NETWORK_OPTIONS = {
5
+ proxy: { type: 'string' },
6
+ 'no-proxy': { type: 'string', multiple: true },
7
+ 'allow-hosts': { type: 'string', multiple: true },
8
+ 'deny-hosts': { type: 'string', multiple: true },
9
+ 'connect-timeout': { type: 'string' },
10
+ 'read-timeout': { type: 'string' },
11
+ };
12
+ /** Help lines for {@link NETWORK_OPTIONS}, at the given flag-column width. */
13
+ export function networkHelp(width) {
14
+ const rows = [
15
+ ['--proxy <url>', ['send requests through this HTTP proxy: http://[user:password@]host[:port]', '(also $SITEFILE_PROXY; HTTP(S)_PROXY are never read)']],
16
+ ['--no-proxy <hosts>', ['hosts reached directly, not through the proxy (NO_PROXY syntax; repeatable,', 'comma-separated; also $SITEFILE_NO_PROXY)']],
17
+ ['--allow-hosts <hosts>', ['contact only hosts matching these patterns (repeatable, comma-separated;', 'also $SITEFILE_ALLOW_HOSTS)']],
18
+ ['--deny-hosts <hosts>', ['never contact hosts matching these patterns; beats --allow-hosts', '(repeatable, comma-separated; also $SITEFILE_DENY_HOSTS)']],
19
+ ['--connect-timeout <ms>', ['time limit for opening each connection (also $SITEFILE_FETCH_TIMEOUT)']],
20
+ ['--read-timeout <ms>', ['longest wait for data on a connection (also $SITEFILE_FETCH_TIMEOUT)']],
21
+ ];
22
+ const out = [];
23
+ for (const [flag, lines] of rows) {
24
+ lines.forEach((l, i) => out.push(`${(i === 0 ? flag : '').padEnd(width)}${l}`));
25
+ }
26
+ return out;
27
+ }
28
+ function ms(raw, flag) {
29
+ if (raw === undefined)
30
+ return undefined;
31
+ const n = Number(raw);
32
+ if (!/^\d+$/.test(raw) || !Number.isSafeInteger(n) || n <= 0)
33
+ throw new SitefileError('E_USAGE', `${flag} expects a positive integer`);
34
+ return n;
35
+ }
36
+ const strings = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string') : []);
37
+ /**
38
+ * The network settings for a command: the SITEFILE_* environment, then the flags on top (flags replace
39
+ * the proxy and timeouts and add to the host lists). Throws E_USAGE for a bad value.
40
+ */
41
+ export function networkOption(v, env = process.env) {
42
+ const flags = {
43
+ ...(typeof v.proxy === 'string' ? { proxy: v.proxy } : {}),
44
+ noProxy: strings(v['no-proxy']).flatMap(splitList),
45
+ allowHosts: strings(v['allow-hosts']).flatMap(splitList),
46
+ denyHosts: strings(v['deny-hosts']).flatMap(splitList),
47
+ connectTimeoutMs: ms(typeof v['connect-timeout'] === 'string' ? v['connect-timeout'] : undefined, '--connect-timeout'),
48
+ readTimeoutMs: ms(typeof v['read-timeout'] === 'string' ? v['read-timeout'] : undefined, '--read-timeout'),
49
+ };
50
+ const merged = mergeNetwork(networkFromEnv(env), flags);
51
+ const problem = networkProblem(merged);
52
+ if (problem)
53
+ throw new SitefileError('E_USAGE', problem);
54
+ return Object.keys(merged).length ? merged : undefined;
55
+ }
56
+ //# sourceMappingURL=network.js.map
@@ -1,3 +1,4 @@
1
+ import { NETWORK_OPTIONS, networkHelp, networkOption } from './network.js';
1
2
  import { insecureRegistriesFromEnv } from '../base-image.js';
2
3
  import { collectBuildArgs } from '../build-args.js';
3
4
  import { outdated } from '../outdated.js';
@@ -47,10 +48,12 @@ const outdatedCommand = {
47
48
  '--allow-private-network permit loopback/private network addresses',
48
49
  `--max-download-bytes <n> per-resource size limit (default ${DEFAULT_FETCH_POLICY.maxBytes})`,
49
50
  `--timeout <ms> per-resource fetch timeout (default ${DEFAULT_FETCH_POLICY.timeoutMs})`,
51
+ ...networkHelp(30),
50
52
  '--base <image> check this BASE image (overrides EXPORT ... AS oci BASE)',
51
53
  '--insecure-registry <h> allow plain http to registry host[:port] (repeatable)',
52
54
  ],
53
55
  options: {
56
+ ...NETWORK_OPTIONS,
54
57
  file: { type: 'string', short: 'f' },
55
58
  json: { type: 'boolean' },
56
59
  'build-arg': { type: 'string', multiple: true },
@@ -68,6 +71,7 @@ const outdatedCommand = {
68
71
  requireArgs(pos, 0, 1, 'a context directory');
69
72
  const maxBytes = positive(str(v['max-download-bytes']), '--max-download-bytes');
70
73
  const timeoutMs = positive(str(v.timeout), '--timeout');
74
+ const network = networkOption(v);
71
75
  const file = str(v.file);
72
76
  const base = str(v.base);
73
77
  const r = await outdated({
@@ -80,6 +84,7 @@ const outdatedCommand = {
80
84
  allowPrivateNetwork: !!v['allow-private-network'],
81
85
  ...(maxBytes ? { maxBytes } : {}),
82
86
  ...(timeoutMs ? { timeoutMs } : {}),
87
+ ...(network ? { network } : {}),
83
88
  },
84
89
  ...(v['no-lockfile'] ? { lockfile: false } : {}),
85
90
  ...(base !== undefined ? { baseImage: base } : {}),
@@ -1,3 +1,4 @@
1
+ import { NETWORK_OPTIONS, networkHelp, networkOption } from './network.js';
1
2
  import { insecureRegistriesFromEnv, makeClient } from '../base-image.js';
2
3
  import { SitefileError } from '../errors.js';
3
4
  import { parseRef, refKey, withDefaultRegistry, canonicalRef } from '../ref.js';
@@ -30,9 +31,11 @@ const COMMON_FLAGS = [
30
31
  ' $SITEFILE_REGISTRY_AUTH or ~/.docker/config.json (auths, credHelpers, credsStore)',
31
32
  '--password-stdin read the password from stdin',
32
33
  '--allow-private-network let redirects/token servers on other hosts resolve to private addresses',
33
- '--timeout <ms> per-request idle timeout (default 30000)',
34
+ '--timeout <ms> per-request idle timeout (default 30000; --read-timeout wins)',
35
+ ...networkHelp(36),
34
36
  ];
35
37
  const COMMON_OPTIONS = {
38
+ ...NETWORK_OPTIONS,
36
39
  'insecure-registry': { type: 'string', multiple: true },
37
40
  username: { type: 'string' },
38
41
  'password-stdin': { type: 'boolean' },
@@ -52,12 +55,14 @@ async function clientOptions(v) {
52
55
  }
53
56
  const timeoutMs = positive(str(v.timeout), '--timeout');
54
57
  const maxBlob = positive(str(v['max-download-bytes']), '--max-download-bytes');
58
+ const network = networkOption(v);
55
59
  return {
56
60
  explicit,
57
61
  options: {
58
62
  insecureRegistries: insecureRegistriesFromEnv(strs(v['insecure-registry'])),
59
63
  allowPrivateNetwork: !!v['allow-private-network'],
60
64
  limits: { ...(timeoutMs ? { timeoutMs } : {}), ...(maxBlob ? { maxBlobBytes: maxBlob } : {}) },
65
+ ...(network ? { network } : {}),
61
66
  },
62
67
  };
63
68
  }