@johnhenry/sitefile 0.0.0-stage → 0.0.1

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 (178) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/LICENSE +21 -0
  3. package/README.md +513 -2
  4. package/lib/src/archive-writers.d.ts +31 -0
  5. package/lib/src/archive-writers.js +197 -0
  6. package/lib/src/attest.d.ts +126 -0
  7. package/lib/src/attest.js +220 -0
  8. package/lib/src/bake.d.ts +74 -0
  9. package/lib/src/bake.js +372 -0
  10. package/lib/src/base-image.d.ts +76 -0
  11. package/lib/src/base-image.js +138 -0
  12. package/lib/src/build-args.d.ts +26 -0
  13. package/lib/src/build-args.js +164 -0
  14. package/lib/src/cache-limit.d.ts +54 -0
  15. package/lib/src/cache-limit.js +239 -0
  16. package/lib/src/casefold-data.d.ts +2 -0
  17. package/lib/src/casefold-data.js +217 -0
  18. package/lib/src/cbor.d.ts +39 -0
  19. package/lib/src/cbor.js +174 -0
  20. package/lib/src/checklinks.d.ts +42 -0
  21. package/lib/src/checklinks.js +120 -0
  22. package/lib/src/cli/bake.d.ts +2 -0
  23. package/lib/src/cli/bake.js +179 -0
  24. package/lib/src/cli/build-image.d.ts +21 -0
  25. package/lib/src/cli/build-image.js +42 -0
  26. package/lib/src/cli/cache.d.ts +8 -0
  27. package/lib/src/cli/cache.js +119 -0
  28. package/lib/src/cli/common.d.ts +29 -0
  29. package/lib/src/cli/common.js +100 -0
  30. package/lib/src/cli/git-context.d.ts +18 -0
  31. package/lib/src/cli/git-context.js +48 -0
  32. package/lib/src/cli/images.d.ts +6 -0
  33. package/lib/src/cli/images.js +285 -0
  34. package/lib/src/cli/index.d.ts +10 -0
  35. package/lib/src/cli/index.js +95 -0
  36. package/lib/src/cli/network.d.ts +34 -0
  37. package/lib/src/cli/network.js +56 -0
  38. package/lib/src/cli/outdated.d.ts +5 -0
  39. package/lib/src/cli/outdated.js +99 -0
  40. package/lib/src/cli/registry.d.ts +2 -0
  41. package/lib/src/cli/registry.js +127 -0
  42. package/lib/src/cli/serve-worker.d.ts +2 -0
  43. package/lib/src/cli/serve-worker.js +42 -0
  44. package/lib/src/cli/servers.d.ts +15 -0
  45. package/lib/src/cli/servers.js +467 -0
  46. package/lib/src/cli/vendor.d.ts +2 -0
  47. package/lib/src/cli/vendor.js +97 -0
  48. package/lib/src/cli/watch.d.ts +26 -0
  49. package/lib/src/cli/watch.js +81 -0
  50. package/lib/src/cli.d.ts +12 -0
  51. package/lib/src/cli.js +522 -0
  52. package/lib/src/csv.d.ts +27 -0
  53. package/lib/src/csv.js +117 -0
  54. package/lib/src/deflate.d.ts +76 -0
  55. package/lib/src/deflate.js +604 -0
  56. package/lib/src/digest.d.ts +3 -0
  57. package/lib/src/digest.js +7 -0
  58. package/lib/src/errors.d.ts +56 -0
  59. package/lib/src/errors.js +29 -0
  60. package/lib/src/executor.d.ts +393 -0
  61. package/lib/src/executor.js +2201 -0
  62. package/lib/src/explain.d.ts +64 -0
  63. package/lib/src/explain.js +445 -0
  64. package/lib/src/export-formats.d.ts +23 -0
  65. package/lib/src/export-formats.js +63 -0
  66. package/lib/src/exporter.d.ts +103 -0
  67. package/lib/src/exporter.js +676 -0
  68. package/lib/src/fingerprint.d.ts +40 -0
  69. package/lib/src/fingerprint.js +168 -0
  70. package/lib/src/frontmatter.d.ts +22 -0
  71. package/lib/src/frontmatter.js +45 -0
  72. package/lib/src/index.d.ts +13 -0
  73. package/lib/src/index.js +13 -0
  74. package/lib/src/language/ast.d.ts +360 -0
  75. package/lib/src/language/ast.js +57 -0
  76. package/lib/src/language/parser.d.ts +3 -0
  77. package/lib/src/language/parser.js +612 -0
  78. package/lib/src/language/scanner.d.ts +110 -0
  79. package/lib/src/language/scanner.js +449 -0
  80. package/lib/src/language/validate.d.ts +13 -0
  81. package/lib/src/language/validate.js +367 -0
  82. package/lib/src/links.d.ts +79 -0
  83. package/lib/src/links.js +583 -0
  84. package/lib/src/lint.d.ts +10 -0
  85. package/lib/src/lint.js +223 -0
  86. package/lib/src/markdown.d.ts +29 -0
  87. package/lib/src/markdown.js +612 -0
  88. package/lib/src/media.d.ts +82 -0
  89. package/lib/src/media.js +650 -0
  90. package/lib/src/memo.d.ts +57 -0
  91. package/lib/src/memo.js +93 -0
  92. package/lib/src/oci.d.ts +178 -0
  93. package/lib/src/oci.js +380 -0
  94. package/lib/src/outdated.d.ts +46 -0
  95. package/lib/src/outdated.js +195 -0
  96. package/lib/src/pages.d.ts +10 -0
  97. package/lib/src/pages.js +54 -0
  98. package/lib/src/paths.d.ts +39 -0
  99. package/lib/src/paths.js +101 -0
  100. package/lib/src/planner.d.ts +297 -0
  101. package/lib/src/planner.js +410 -0
  102. package/lib/src/plugins/cache.d.ts +96 -0
  103. package/lib/src/plugins/cache.js +325 -0
  104. package/lib/src/plugins/exec.d.ts +51 -0
  105. package/lib/src/plugins/exec.js +144 -0
  106. package/lib/src/plugins/host.d.ts +75 -0
  107. package/lib/src/plugins/host.js +277 -0
  108. package/lib/src/plugins/wasm.d.ts +23 -0
  109. package/lib/src/plugins/wasm.js +249 -0
  110. package/lib/src/profile.d.ts +42 -0
  111. package/lib/src/profile.js +68 -0
  112. package/lib/src/ref.d.ts +40 -0
  113. package/lib/src/ref.js +79 -0
  114. package/lib/src/registry-auth.d.ts +32 -0
  115. package/lib/src/registry-auth.js +151 -0
  116. package/lib/src/registry-cache.d.ts +71 -0
  117. package/lib/src/registry-cache.js +333 -0
  118. package/lib/src/registry-ops.d.ts +51 -0
  119. package/lib/src/registry-ops.js +248 -0
  120. package/lib/src/registry.d.ts +115 -0
  121. package/lib/src/registry.js +524 -0
  122. package/lib/src/render.d.ts +134 -0
  123. package/lib/src/render.js +576 -0
  124. package/lib/src/resources/archive.d.ts +11 -0
  125. package/lib/src/resources/archive.js +306 -0
  126. package/lib/src/resources/bundle-data.d.ts +16 -0
  127. package/lib/src/resources/bundle-data.js +50 -0
  128. package/lib/src/resources/convert.d.ts +12 -0
  129. package/lib/src/resources/convert.js +63 -0
  130. package/lib/src/resources/documents.d.ts +35 -0
  131. package/lib/src/resources/documents.js +122 -0
  132. package/lib/src/resources/git.d.ts +103 -0
  133. package/lib/src/resources/git.js +720 -0
  134. package/lib/src/resources/https.d.ts +74 -0
  135. package/lib/src/resources/https.js +413 -0
  136. package/lib/src/resources/ignore.d.ts +33 -0
  137. package/lib/src/resources/ignore.js +119 -0
  138. package/lib/src/resources/local.d.ts +64 -0
  139. package/lib/src/resources/local.js +248 -0
  140. package/lib/src/resources/lockfile.d.ts +52 -0
  141. package/lib/src/resources/lockfile.js +172 -0
  142. package/lib/src/resources/netpolicy.d.ts +6 -0
  143. package/lib/src/resources/netpolicy.js +82 -0
  144. package/lib/src/resources/network.d.ts +70 -0
  145. package/lib/src/resources/network.js +304 -0
  146. package/lib/src/resources/types.d.ts +69 -0
  147. package/lib/src/resources/types.js +13 -0
  148. package/lib/src/resources/vendor.d.ts +94 -0
  149. package/lib/src/resources/vendor.js +296 -0
  150. package/lib/src/resources/wbn-reader.d.ts +44 -0
  151. package/lib/src/resources/wbn-reader.js +302 -0
  152. package/lib/src/schema.d.ts +25 -0
  153. package/lib/src/schema.js +169 -0
  154. package/lib/src/secrets.d.ts +21 -0
  155. package/lib/src/secrets.js +103 -0
  156. package/lib/src/serve-rules.d.ts +29 -0
  157. package/lib/src/serve-rules.js +123 -0
  158. package/lib/src/server.d.ts +45 -0
  159. package/lib/src/server.js +246 -0
  160. package/lib/src/servers.d.ts +85 -0
  161. package/lib/src/servers.js +225 -0
  162. package/lib/src/site.d.ts +60 -0
  163. package/lib/src/site.js +169 -0
  164. package/lib/src/store.d.ts +151 -0
  165. package/lib/src/store.js +596 -0
  166. package/lib/src/tree.d.ts +50 -0
  167. package/lib/src/tree.js +134 -0
  168. package/lib/src/values.d.ts +7 -0
  169. package/lib/src/values.js +37 -0
  170. package/lib/src/vendor.d.ts +19 -0
  171. package/lib/src/vendor.js +145 -0
  172. package/lib/src/watch.d.ts +44 -0
  173. package/lib/src/watch.js +137 -0
  174. package/lib/src/wbn.d.ts +26 -0
  175. package/lib/src/wbn.js +241 -0
  176. package/lib/src/yaml.d.ts +34 -0
  177. package/lib/src/yaml.js +562 -0
  178. package/package.json +59 -4
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import { type BuildMemo } from './memo.js';
3
+ /**
4
+ * Run the CLI. `run.once` runs a --watch command a single time (the watch loop's
5
+ * builds); `run.wrote` then receives the export destination, and `run.memo` carries
6
+ * results between those builds (incremental rebuilds).
7
+ */
8
+ export declare function main(argv: readonly string[], run?: {
9
+ readonly once?: boolean;
10
+ readonly wrote?: string[];
11
+ readonly memo?: BuildMemo | undefined;
12
+ }): Promise<number>;
package/lib/src/cli.js ADDED
@@ -0,0 +1,522 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ import path from 'node:path';
4
+ import { SitefileError, isSitefileError } from './errors.js';
5
+ import { build, buildToDirectory, checkPlanPins, lintBuild, planBuild, sitefileCompression } from './executor.js';
6
+ import { LINT_RULES } from './lint.js';
7
+ import { compressionFromExtension, defaultOutName, formatFromExtension } from './export-formats.js';
8
+ import { collectBuildArgs } from './build-args.js';
9
+ import { insecureRegistriesFromEnv, makeClient } from './base-image.js';
10
+ import { cacheLimitFromEnv } from './cache-limit.js';
11
+ import { parseCacheRef, RegistryCache } from './registry-cache.js';
12
+ import { parseLabels } from './oci.js';
13
+ import { EXPORT_FORMATS, parseExportFormat, parseOciCompression } from './language/ast.js';
14
+ import { DEFAULT_FETCH_POLICY } from './resources/types.js';
15
+ import { parseLocalRef, refKey } from './ref.js';
16
+ import { EXTRA_HELP, isExtraCommand, runExtraCommand } from './cli/index.js';
17
+ import { evictAfterBuild, transformCacheDir } from './cli/cache.js';
18
+ import { storeBuildAsImage, validateImageTags } from './cli/build-image.js';
19
+ import { SITEFILE_VERSION } from './attest.js';
20
+ import { watchLoop } from './cli/watch.js';
21
+ import { Profiler } from './profile.js';
22
+ import { watchMemo } from './memo.js';
23
+ import { promises as fs } from 'node:fs';
24
+ import { graphDot, graphText, planGraph, why, whyText } from './explain.js';
25
+ import { isGitContext, materializeGitContext } from './cli/git-context.js';
26
+ import { loadSecrets } from './secrets.js';
27
+ import { NETWORK_OPTIONS, networkHelp, networkOption } from './cli/network.js';
28
+ const VERSION = SITEFILE_VERSION;
29
+ const HELP = `sitefile ${VERSION} — Docker-inspired static artifact builder
30
+
31
+ Usage:
32
+ sitefile build [context] [options] build and export to --out (default: <context>/dist, or dist.<ext>)
33
+ sitefile validate [context] [options] parse, validate and evaluate variables (no IO)
34
+ sitefile inspect [context] [options] build in memory and list the exported files
35
+ sitefile graph [context] [options] print the dependency graph of imports, stages and operations
36
+ (text, --json or --dot; no IO, like validate)
37
+ sitefile why <path> [context] [opts] build in memory and show which instructions wrote an exported
38
+ file, from what inputs, and what overwrote it
39
+ The context may be a Git URL, https://host/repo.git#ref:dir (fetched to a temporary directory;
40
+ the default --out is then ./dist; see docs/CLI.md).
41
+
42
+ Options:
43
+ -f, --file <path> Sitefile path (default: <context>/Sitefile)
44
+ --target <stage> stop after the named stage and export its whole tree instead of
45
+ the EXPORT path (EXPORT's format still applies; no _redirects/_headers)
46
+ -o, --out <path> export destination (build only): a directory for 'directory',
47
+ a file for zip, tar, tgz, wbn (*.wbn.gz: a gzipped bundle)
48
+ -t, --tag <name[:tag]> also store the build as a local image (build only, repeatable);
49
+ with -t and no -o nothing is written to the context
50
+ --format <fmt> directory | zip | tar | tgz (alias tar.gz) | wbn | oci (build only).
51
+ oci writes an OCI archive for -o *.tar (e.g. site.oci.tar), else an
52
+ OCI image layout directory.
53
+ Default: the --out extension (.zip .tar .tar.gz .tgz .wbn .wbn.gz .oci.tar),
54
+ else the Sitefile's EXPORT ... AS <fmt>
55
+ --oci-root <path> directory inside an oci image layer (default /srv/www; overrides EXPORT ... AT)
56
+ --compression <c> gzip or none (build only; overrides EXPORT ... COMPRESSION): the oci site layer
57
+ (default gzip; none: plain tar), or the whole wbn (default none; gzip:
58
+ gzip(application/webbundle), also chosen by -o *.wbn.gz)
59
+ --oci-compression <c> the same as --compression, for oci only
60
+ --label <key=value> add an OCI image config label (repeatable)
61
+ --git-labels add org.opencontainers.image.source/revision from the git checkout
62
+ (off by default: they change the image ID on every commit)
63
+ --provenance attach a SLSA provenance attestation to the oci image (an in-toto
64
+ statement: Sitefile digest, build args, imports, BASE, builder)
65
+ --sbom attach an SBOM attestation (CycloneDX: every file with its SHA-256);
66
+ both are OCI 1.1 referrers of the image (its ID does not change),
67
+ written into the oci export and kept with -t images for push
68
+ --platform <os/arch> oci image platform (default linux/amd64); with a BASE, the platform pulled
69
+ --base <image> build the oci image on <image> (runnable; overrides EXPORT ... AS oci BASE);
70
+ a bare name means Docker Hub. Pin it: nginx:alpine@sha256:...
71
+ --insecure-registry <h> allow plain http to registry host[:port] (repeatable; localhost always may)
72
+ --base-url <url> https URL ending in '/' the files are mounted at (wbn only);
73
+ overrides EXPORT ... BASE
74
+ --build-arg <NAME=value> set an ARG (repeatable); --build-arg NAME reads $NAME
75
+ --secret id=<id>,env=<VAR> | id=<id>,src=<file>
76
+ a build secret for IMPORT ... SECRET <id> AS HEADER <Name> | AS BEARER
77
+ (repeatable); sent only to that import's origin, never recorded
78
+ --plugin-timeout <ms> per-call time limit for TRANSFORM plugins and tools (default 60000)
79
+ --allow-exec <name[=path]> allow a TOOL (external program) to run, resolved on PATH or at
80
+ the absolute path (repeatable); programs run with your privileges
81
+ --allow-plugin <name> allow a JavaScript PLUGIN to run (repeatable; wasm plugins need no flag)
82
+ --allow-plugins allow every JavaScript PLUGIN in the Sitefile
83
+ --no-cache do not read or write the TRANSFORM result cache
84
+ ($SITEFILE_HOME/cache/transform; see 'sitefile cache --help')
85
+ --cache-from <ref> also look up TRANSFORM results in this registry cache (build only,
86
+ repeatable, first match wins); results are fetched only on a hit
87
+ --cache-to <ref> after a successful build, push the TRANSFORM results it used to this
88
+ registry cache (build only, once); registry problems only warn
89
+ --build-arg-file <path> read ARGs from a .json, .yaml/.yml or .env file (repeatable;
90
+ later files win, --build-arg wins over files)
91
+ --force replace a non-empty --out outside the context
92
+ --frozen-lockfile fail instead of changing Sitefile.lock
93
+ --update-lock re-record unpinned URLs whose content changed
94
+ --strict-pins fail when a remote IMPORT has no INTEGRITY and no lockfile entry, or the
95
+ BASE image is neither @sha256:-pinned nor locked (build, validate, inspect)
96
+ --lint-disable <rules> turn lint rules off (validate; comma-separated rule ids, or all; repeatable)
97
+ --no-lockfile do not read or write Sitefile.lock
98
+ --vendor-dir <dir> read remote imports and BASE blobs from this vendor directory (relative
99
+ to the context; default vendor/sitefile when it exists; see 'sitefile vendor')
100
+ --allow-http permit plain http:// imports
101
+ --allow-private-network permit loopback/private network addresses
102
+ --allow-hardlinks permit hard-linked files in the context (you assert they are safe)
103
+ --max-download-bytes <n> per-resource size limit (default ${DEFAULT_FETCH_POLICY.maxBytes})
104
+ --timeout <ms> per-resource fetch timeout (default ${DEFAULT_FETCH_POLICY.timeoutMs})
105
+ ${networkHelp(25).map((l) => ` ${l}`).join('\n')}
106
+ --json machine-readable output (validate, inspect, graph, why)
107
+ --dot Graphviz DOT output (graph only)
108
+ --watch build, then rebuild when the context, the Sitefile or a
109
+ --build-arg-file changes (build only; Ctrl-C stops)
110
+ --profile <file> write a timing trace of the build (Chrome trace event JSON:
111
+ chrome://tracing, ui.perfetto.dev); --watch rewrites it per build
112
+ -h, --help show this help
113
+ -v, --version show the version
114
+ ${EXTRA_HELP}`;
115
+ /**
116
+ * Run the CLI. `run.once` runs a --watch command a single time (the watch loop's
117
+ * builds); `run.wrote` then receives the export destination, and `run.memo` carries
118
+ * results between those builds (incremental rebuilds).
119
+ */
120
+ export async function main(argv, run = {}) {
121
+ if (isExtraCommand(argv[0]))
122
+ return runExtraCommand(argv);
123
+ let parsed;
124
+ try {
125
+ parsed = parseArgs({
126
+ args: [...argv],
127
+ allowPositionals: true,
128
+ options: {
129
+ ...NETWORK_OPTIONS,
130
+ file: { type: 'string', short: 'f' },
131
+ target: { type: 'string' },
132
+ out: { type: 'string', short: 'o' },
133
+ tag: { type: 'string', short: 't', multiple: true },
134
+ format: { type: 'string' },
135
+ 'base-url': { type: 'string' },
136
+ 'oci-root': { type: 'string' },
137
+ 'oci-compression': { type: 'string' },
138
+ compression: { type: 'string' },
139
+ label: { type: 'string', multiple: true },
140
+ 'git-labels': { type: 'boolean' },
141
+ provenance: { type: 'boolean' },
142
+ sbom: { type: 'boolean' },
143
+ platform: { type: 'string' },
144
+ base: { type: 'string' },
145
+ 'insecure-registry': { type: 'string', multiple: true },
146
+ 'build-arg': { type: 'string', multiple: true },
147
+ 'build-arg-file': { type: 'string', multiple: true },
148
+ secret: { type: 'string', multiple: true },
149
+ 'plugin-timeout': { type: 'string' },
150
+ 'allow-exec': { type: 'string', multiple: true },
151
+ 'allow-plugin': { type: 'string', multiple: true },
152
+ 'allow-plugins': { type: 'boolean' },
153
+ 'no-cache': { type: 'boolean' },
154
+ 'cache-from': { type: 'string', multiple: true },
155
+ 'cache-to': { type: 'string', multiple: true },
156
+ force: { type: 'boolean' },
157
+ 'frozen-lockfile': { type: 'boolean' },
158
+ 'update-lock': { type: 'boolean' },
159
+ 'strict-pins': { type: 'boolean' },
160
+ 'lint-disable': { type: 'string', multiple: true },
161
+ 'no-lockfile': { type: 'boolean' },
162
+ 'vendor-dir': { type: 'string' },
163
+ 'allow-http': { type: 'boolean' },
164
+ 'allow-private-network': { type: 'boolean' },
165
+ 'allow-hardlinks': { type: 'boolean' },
166
+ 'max-download-bytes': { type: 'string' },
167
+ timeout: { type: 'string' },
168
+ json: { type: 'boolean' },
169
+ dot: { type: 'boolean' },
170
+ watch: { type: 'boolean' },
171
+ profile: { type: 'string' },
172
+ help: { type: 'boolean', short: 'h' },
173
+ version: { type: 'boolean', short: 'v' },
174
+ },
175
+ });
176
+ }
177
+ catch (e) {
178
+ return usageError(e.message);
179
+ }
180
+ const { values: o, positionals } = parsed;
181
+ if (o.version) {
182
+ process.stdout.write(`${VERSION}\n`);
183
+ return 0;
184
+ }
185
+ const [command, ...rest] = positionals;
186
+ if (o.help || !command) {
187
+ process.stdout.write(HELP);
188
+ return o.help ? 0 : 2;
189
+ }
190
+ // why <path> [context]
191
+ const whyPath = command === 'why' ? rest.shift() : undefined;
192
+ if (command === 'why' && whyPath === undefined)
193
+ return usageError('why expects a path in the build output: sitefile why <path> [context]');
194
+ const [contextArg = '.', ...extra] = rest;
195
+ if (extra.length)
196
+ return usageError(`unexpected arguments: ${extra.join(' ')}`);
197
+ const gitContext = isGitContext(contextArg);
198
+ if (gitContext && o.watch)
199
+ return usageError('--watch needs a local context directory, not a Git URL');
200
+ if (gitContext && o.file !== undefined && (path.isAbsolute(o.file) || o.file.split(/[\\/]/).includes('..'))) {
201
+ return usageError('with a Git URL context, -f/--file is a path inside the repository');
202
+ }
203
+ if (o.dot && command !== 'graph')
204
+ return usageError('--dot is only for graph');
205
+ if (o.dot && o.json)
206
+ return usageError('--dot and --json conflict');
207
+ if (o['frozen-lockfile'] && o['update-lock'])
208
+ return usageError('--frozen-lockfile and --update-lock conflict');
209
+ if (o['strict-pins'] && o['update-lock'])
210
+ return usageError('--strict-pins and --update-lock conflict');
211
+ const lintDisabled = (o['lint-disable'] ?? []).flatMap((v) => v.split(',').map((s) => s.trim()));
212
+ if (o['lint-disable'] && command !== 'validate')
213
+ return usageError('--lint-disable is only for validate');
214
+ for (const id of lintDisabled) {
215
+ if (id !== 'all' && !LINT_RULES.includes(id)) {
216
+ return usageError(`--lint-disable: unknown lint rule '${id}' (rules: ${LINT_RULES.join(', ')}, or all)`);
217
+ }
218
+ }
219
+ const cacheFrom = o['cache-from'] ?? [];
220
+ const cacheTo = o['cache-to'] ?? [];
221
+ if (cacheFrom.length || cacheTo.length) {
222
+ if (command !== 'build')
223
+ return usageError('--cache-from and --cache-to are only for build');
224
+ if (o.watch)
225
+ return usageError('--watch cannot be combined with --cache-from or --cache-to');
226
+ if (o['no-cache'])
227
+ return usageError('--no-cache cannot be combined with --cache-from or --cache-to');
228
+ if (cacheTo.length > 1)
229
+ return usageError('--cache-to can be given only once');
230
+ }
231
+ if (o.watch && !run.once) {
232
+ if (command !== 'build')
233
+ return usageError(`--watch is only for build (and serve --watch), not ${command}`);
234
+ if ((o.tag ?? []).length)
235
+ return usageError('--watch cannot be combined with -t');
236
+ try {
237
+ const memo = watchMemo();
238
+ return await watchLoop({
239
+ context: contextArg,
240
+ ...(o.file ? { sitefile: o.file } : {}),
241
+ argFiles: o['build-arg-file'] ?? [],
242
+ build: async () => {
243
+ const wrote = [];
244
+ return { code: await main(argv, { once: true, wrote, memo }), wrote };
245
+ },
246
+ });
247
+ }
248
+ catch (e) {
249
+ if (!isSitefileError(e))
250
+ throw e;
251
+ process.stderr.write(`error[${e.code}]: ${e.message}\n`);
252
+ return 1;
253
+ }
254
+ }
255
+ if (o.profile === '')
256
+ return usageError('--profile expects a file path');
257
+ // --profile: a trace of this command (build, validate, inspect, graph, why), written when it ends.
258
+ const profiler = o.profile !== undefined ? new Profiler(command) : undefined;
259
+ if (o.profile !== undefined)
260
+ run.wrote?.push(o.profile);
261
+ const commandSpan = profiler?.begin(command, 'command');
262
+ let failed = false;
263
+ let fetched;
264
+ try {
265
+ const network = networkOption(o);
266
+ const fetchPolicy = {
267
+ allowHttp: !!o['allow-http'],
268
+ allowPrivateNetwork: !!o['allow-private-network'],
269
+ ...(o['max-download-bytes'] ? { maxBytes: positiveInt(o['max-download-bytes'], '--max-download-bytes') } : {}),
270
+ ...(o.timeout ? { timeoutMs: positiveInt(o.timeout, '--timeout') } : {}),
271
+ ...(network ? { network } : {}),
272
+ };
273
+ if (gitContext) {
274
+ // A Git URL context: fetched into a temporary directory, built from there, then removed.
275
+ fetched = await materializeGitContext(contextArg, { ...DEFAULT_FETCH_POLICY, ...fetchPolicy });
276
+ process.stderr.write(`context: ${contextArg} at commit ${fetched.commit}\n`);
277
+ }
278
+ const context = fetched?.dir ?? contextArg;
279
+ const file = fetched && o.file !== undefined ? path.join(context, o.file) : o.file;
280
+ const remoteCache = cacheFrom.length || cacheTo.length
281
+ ? new RegistryCache({
282
+ from: cacheFrom.map((r) => parseCacheRef('--cache-from', r)),
283
+ to: cacheTo[0] !== undefined ? parseCacheRef('--cache-to', cacheTo[0]) : undefined,
284
+ client: (registry) => makeClient(registry, { insecureRegistries: insecureRegistriesFromEnv(o['insecure-registry'] ?? []), allowPrivateNetwork: !!o['allow-private-network'], network }),
285
+ limit: cacheLimitFromEnv(),
286
+ warn: (m) => process.stderr.write(`warning: ${m}\n`),
287
+ notice: (m) => process.stderr.write(`${m}\n`),
288
+ })
289
+ : undefined;
290
+ // Precedence: Sitefile defaults < --build-arg-file (later files win) < --build-arg.
291
+ const buildArgs = await collectBuildArgs(o['build-arg-file'] ?? [], o['build-arg'] ?? []);
292
+ const secrets = await loadSecrets(o.secret ?? []);
293
+ let formatOverride;
294
+ if (o.format !== undefined) {
295
+ formatOverride = parseExportFormat(o.format);
296
+ if (!formatOverride)
297
+ return usageError(`--format expects one of ${EXPORT_FORMATS.join(', ')} (or tar.gz), got '${o.format}'`);
298
+ }
299
+ let ociCompression;
300
+ if (o['oci-compression'] !== undefined) {
301
+ ociCompression = parseOciCompression(o['oci-compression']);
302
+ if (!ociCompression)
303
+ return usageError(`--oci-compression expects gzip or none, got '${o['oci-compression']}'`);
304
+ }
305
+ let compression;
306
+ if (o.compression !== undefined) {
307
+ compression = parseOciCompression(o.compression);
308
+ if (!compression)
309
+ return usageError(`--compression expects gzip or none, got '${o.compression}'`);
310
+ if (ociCompression !== undefined)
311
+ return usageError('--compression and --oci-compression set the same thing; give one');
312
+ }
313
+ // No --format: an archive extension on --out picks the format, so `-o site.zip`
314
+ // writes a zip instead of a directory named site.zip.
315
+ if (!formatOverride && o.out)
316
+ formatOverride = formatFromExtension(o.out);
317
+ // The default destination depends on the format, which the Sitefile may set.
318
+ // (A wbn's default name also depends on its COMPRESSION: dist.wbn or dist.wbn.gz.)
319
+ const sitefilePlan = command === 'build' && !o.out && (!formatOverride || formatOverride === 'wbn') ? await planBuild({ contextDir: context, ...(file ? { sitefile: file } : {}), buildArgs }) : undefined;
320
+ const sitefileFormat = formatOverride ? undefined : sitefilePlan?.exportFormat;
321
+ const defaultFormat = formatOverride ?? sitefileFormat ?? 'directory';
322
+ const defaultCompression = defaultFormat === 'wbn' ? (compression ?? (sitefilePlan ? sitefileCompression(sitefilePlan, 'wbn') : undefined)) : undefined;
323
+ // A Git context's default destination is in the current directory, not the temporary checkout.
324
+ const out = o.out ?? path.join(fetched ? '.' : context, defaultOutName(defaultFormat, defaultCompression));
325
+ run.wrote?.push(out);
326
+ const options = {
327
+ contextDir: context,
328
+ ...(file ? { sitefile: file } : {}),
329
+ ...(fetched ? { label: o.file ?? 'Sitefile' } : {}),
330
+ ...(o.target !== undefined ? { target: o.target } : {}),
331
+ buildArgs,
332
+ ...(secrets.size ? { secrets } : {}),
333
+ outDir: out,
334
+ fetchPolicy,
335
+ ...(o['no-lockfile'] ? { lockfile: false } : {}),
336
+ ...(o['vendor-dir'] !== undefined ? { vendorDir: o['vendor-dir'] } : {}),
337
+ ...(o['plugin-timeout']
338
+ ? { pluginPolicy: { timeoutMs: positiveInt(o['plugin-timeout'], '--plugin-timeout') }, toolPolicy: { timeoutMs: positiveInt(o['plugin-timeout'], '--plugin-timeout') } }
339
+ : {}),
340
+ ...(o['allow-exec'] ? { allowExec: o['allow-exec'] } : {}),
341
+ ...(o['allow-plugin'] ? { allowPlugin: o['allow-plugin'] } : {}),
342
+ allowAllPlugins: !!o['allow-plugins'],
343
+ ...(o['no-cache'] ? {} : { cacheDir: transformCacheDir() }),
344
+ ...(remoteCache ? { remoteCache } : {}),
345
+ onNotice: (m) => process.stderr.write(`${m}\n`),
346
+ allowHardlinks: !!o['allow-hardlinks'],
347
+ ...(o.platform !== undefined ? { platform: o.platform } : {}),
348
+ ...(o.base !== undefined ? { baseImage: o.base } : {}),
349
+ // inspect never pulls; a non-oci export (with no image tag) has no use for a BASE
350
+ ...(command === 'inspect' || command === 'why' || (command === 'build' && formatOverride !== undefined && formatOverride !== 'oci' && !(o.tag ?? []).length) ? { skipBase: true } : {}),
351
+ registry: { insecureRegistries: insecureRegistriesFromEnv(o['insecure-registry'] ?? []) },
352
+ frozenLockfile: !!o['frozen-lockfile'],
353
+ updateLock: !!o['update-lock'],
354
+ strictPins: !!o['strict-pins'],
355
+ onWarning: (m) => process.stderr.write(`warning: ${m}\n`),
356
+ ...(profiler ? { profiler, alsoWrites: [o.profile] } : {}),
357
+ ...(run.memo ? { memo: run.memo } : {}),
358
+ };
359
+ // --compression belongs to the export's own format, oci or wbn (-t without -o stores an oci image);
360
+ // a *.wbn.gz destination means a gzipped bundle.
361
+ let wbnCompression;
362
+ if (command === 'build' && compression !== undefined) {
363
+ const fmt = (o.tag ?? []).length && !o.out ? 'oci' : (formatOverride ?? sitefileFormat ?? (await planBuild(options)).exportFormat);
364
+ if (fmt === 'oci')
365
+ ociCompression = compression;
366
+ else if (fmt === 'wbn')
367
+ wbnCompression = compression;
368
+ else
369
+ return usageError(`--compression applies only to oci and wbn exports, not ${fmt}`);
370
+ }
371
+ if (formatOverride === 'wbn' && o.out)
372
+ wbnCompression ??= compressionFromExtension(o.out, 'wbn');
373
+ switch (command) {
374
+ case 'build': {
375
+ const tags = o.tag ?? [];
376
+ validateImageTags(tags);
377
+ const labels = parseLabels(o.label ?? []);
378
+ const attest = { provenance: !!o.provenance, sbom: !!o.sbom };
379
+ if ((attest.provenance || attest.sbom) && !tags.length && (formatOverride ?? sitefileFormat ?? (await planBuild(options)).exportFormat) !== 'oci') {
380
+ return usageError('--provenance and --sbom attach attestations to an oci image: export one (--format oci, -o *.oci.tar, EXPORT ... AS oci) or store one with -t');
381
+ }
382
+ const imageInfo = {
383
+ contextDir: context,
384
+ ociRoot: o['oci-root'],
385
+ ociCompression,
386
+ platform: o.platform,
387
+ labels,
388
+ gitLabels: !!o['git-labels'],
389
+ attest,
390
+ build: { contextDir: context, ...(file ? { sitefile: file } : {}), ...(fetched ? { label: o.file ?? 'Sitefile' } : {}), buildArgs },
391
+ };
392
+ await remoteCache?.load();
393
+ if (tags.length && !o.out) {
394
+ // -t without -o: store the image only; nothing is written to the context.
395
+ await storeBuildAsImage(await build(options), tags, imageInfo);
396
+ await remoteCache?.push(transformCacheDir());
397
+ return 0;
398
+ }
399
+ const r = await buildToDirectory({
400
+ ...options,
401
+ outDir: out,
402
+ force: !!o.force,
403
+ ...(formatOverride ? { format: formatOverride } : {}),
404
+ ...(o['base-url'] !== undefined ? { baseUrl: o['base-url'] } : {}),
405
+ ...(o['oci-root'] !== undefined ? { ociRoot: o['oci-root'] } : {}),
406
+ ...(ociCompression !== undefined ? { ociCompression } : {}),
407
+ ...(wbnCompression !== undefined ? { wbnCompression } : {}),
408
+ ...(o.platform !== undefined ? { platform: o.platform } : {}),
409
+ imageLabels: labels,
410
+ gitLabels: !!o['git-labels'],
411
+ ...(tags[0] ? { ociRefName: refKey(parseLocalRef(tags[0])) } : {}),
412
+ attest,
413
+ });
414
+ const where = path.relative(process.cwd(), r.outDir) || '.';
415
+ const n = `${r.output.size} file${r.output.size === 1 ? '' : 's'}`;
416
+ process.stdout.write(r.format === 'directory'
417
+ ? `Exported ${n} to ${where} (${r.output.digest()})\n`
418
+ : r.format === 'oci'
419
+ ? `Exported ${n} to ${where} (oci image ${r.manifestDigest}${r.bytes !== undefined ? `, ${r.bytes} bytes` : ''})\n`
420
+ : `Exported ${n} to ${where} (${r.format === 'tgz' ? 'tar.gz' : r.format}, ${r.bytes} bytes, ${r.fileDigest})\n`);
421
+ // A Git context's lockfile lives in the temporary checkout: it is used, never written back.
422
+ if (r.lockfileWritten && !fetched)
423
+ process.stdout.write('Updated Sitefile.lock\n');
424
+ if (tags.length)
425
+ await storeBuildAsImage(r, tags, imageInfo);
426
+ await remoteCache?.push(transformCacheDir());
427
+ return 0;
428
+ }
429
+ case 'validate': {
430
+ const span = profiler?.begin('parse', 'build');
431
+ const p = await planBuild(options);
432
+ span?.end({ imports: p.imports.length, stages: p.stages.length, ...(p.target !== undefined ? { target: p.target } : {}) });
433
+ p.warnings.forEach((m) => process.stderr.write(`warning: ${m}\n`));
434
+ (await lintBuild(options, lintDisabled)).forEach((m) => process.stderr.write(`warning: ${m}\n`));
435
+ if (o['strict-pins'])
436
+ await checkPlanPins(options, p);
437
+ if (o.json) {
438
+ process.stdout.write(JSON.stringify({ file: p.file, vars: Object.fromEntries(p.vars), imports: p.imports, stages: p.stages, export: p.exportPath, ...(p.target !== undefined ? { target: p.target } : {}) }, null, 2) + '\n');
439
+ }
440
+ else {
441
+ const opCount = p.stages.reduce((n, st) => n + st.operations.length, 0);
442
+ process.stdout.write(`${p.file}: OK (${p.imports.length} import${p.imports.length === 1 ? '' : 's'}, ${opCount} operation${opCount === 1 ? '' : 's'}${p.stages.length > 1 ? `, ${p.stages.length} stages` : ''}${p.target !== undefined ? `, target ${p.target}` : ''})\n`);
443
+ }
444
+ return 0;
445
+ }
446
+ case 'inspect': {
447
+ const r = await build({ ...options, frozenLockfile: true });
448
+ const files = r.output.entries().map(([p, n]) => ({ path: p, mode: n.mode.toString(8), size: n.bytes.byteLength, digest: n.digest, origin: n.origin }));
449
+ if (o.json) {
450
+ process.stdout.write(JSON.stringify({ digest: r.output.digest(), files }, null, 2) + '\n');
451
+ }
452
+ else {
453
+ for (const f of files)
454
+ process.stdout.write(`${f.mode} ${String(f.size).padStart(8)} ${f.path} (${f.origin})\n`);
455
+ process.stdout.write(`${files.length} files, ${r.output.digest()}\n`);
456
+ }
457
+ return 0;
458
+ }
459
+ case 'graph': {
460
+ const span = profiler?.begin('parse', 'build');
461
+ const p = await planBuild(options);
462
+ span?.end({ imports: p.imports.length, stages: p.stages.length, ...(p.target !== undefined ? { target: p.target } : {}) });
463
+ p.warnings.forEach((m) => process.stderr.write(`warning: ${m}\n`));
464
+ if (o['strict-pins'])
465
+ await checkPlanPins(options, p);
466
+ const g = planGraph(p);
467
+ process.stdout.write(o.json ? JSON.stringify(g, null, 2) + '\n' : o.dot ? graphDot(g) : graphText(g));
468
+ return 0;
469
+ }
470
+ case 'why': {
471
+ const r = await build({ ...options, frozenLockfile: true, recordSteps: true });
472
+ const report = why(r, whyPath, context);
473
+ process.stdout.write(o.json ? JSON.stringify(report, null, 2) + '\n' : whyText(report));
474
+ return 0;
475
+ }
476
+ default:
477
+ return usageError(`unknown command '${command}'`);
478
+ }
479
+ }
480
+ catch (e) {
481
+ failed = true;
482
+ if (isSitefileError(e)) {
483
+ if (e.code === 'E_USAGE')
484
+ return usageError(e.detail);
485
+ process.stderr.write(`error[${e.code}]: ${e.message}\n`);
486
+ return 1;
487
+ }
488
+ throw e;
489
+ }
490
+ finally {
491
+ // Keep $SITEFILE_HOME under $SITEFILE_CACHE_MAX (never fails the command).
492
+ if (command === 'build' || command === 'inspect' || command === 'why')
493
+ await evictAfterBuild();
494
+ await fetched?.dispose();
495
+ commandSpan?.end();
496
+ if (profiler && !(await fs.writeFile(o.profile, profiler.toJSON()).then(() => true, () => false))) {
497
+ // A failed build already reported its error; otherwise this is the command's error.
498
+ if (!failed) {
499
+ process.stderr.write(`error[E_EXPORT]: cannot write the --profile file '${o.profile}'\n`);
500
+ // eslint-disable-next-line no-unsafe-finally
501
+ return 1;
502
+ }
503
+ }
504
+ }
505
+ }
506
+ function positiveInt(raw, flag) {
507
+ const n = Number(raw);
508
+ if (!Number.isSafeInteger(n) || n <= 0)
509
+ throw new SitefileError('E_USAGE', `${flag} expects a positive integer`);
510
+ return n;
511
+ }
512
+ function usageError(message) {
513
+ process.stderr.write(`error: ${message}\nRun 'sitefile --help' for usage.\n`);
514
+ return 2;
515
+ }
516
+ main(process.argv.slice(2)).then((code) => {
517
+ process.exitCode = code;
518
+ }, (e) => {
519
+ process.stderr.write(`internal error: ${e?.stack ?? String(e)}\n`);
520
+ process.exitCode = 70;
521
+ });
522
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * RFC 4180 CSV parser (no dependencies).
3
+ *
4
+ * - Fields may be double-quoted; inside quotes `""` is a literal quote and
5
+ * delimiters / newlines are data. Records end at LF or CRLF; a trailing
6
+ * newline is optional; a leading UTF-8 BOM is stripped.
7
+ * - Completely empty lines are skipped.
8
+ * - A quote inside an unquoted field, text after a closing quote, or an
9
+ * unterminated quote is E_DATA with line and column.
10
+ * - `header: true` (default): the first record names the columns (non-empty,
11
+ * unique) and every record must have the same number of fields. Values are
12
+ * always strings. With `header: false`, records are returned as string
13
+ * arrays and may have differing lengths.
14
+ * - Rows are ordinary objects; a `__proto__` column is stored as plain data.
15
+ */
16
+ export interface CsvOptions {
17
+ header?: boolean;
18
+ delimiter?: ',' | ';' | '\t';
19
+ }
20
+ export declare function parseCsv(text: string, file: string, options?: CsvOptions & {
21
+ header?: true;
22
+ }): Array<Record<string, string>>;
23
+ export declare function parseCsv(text: string, file: string, options: CsvOptions & {
24
+ header: false;
25
+ }): string[][];
26
+ export declare function parseCsv(text: string, file: string, options?: CsvOptions): Array<Record<string, string>> | string[][];
27
+ export declare function parseCsvRecords(text: string, file: string): Array<Record<string, string>>;