@johnhenry/sitefile 0.0.0-stage → 0.0.0

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 (174) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/LICENSE +21 -0
  3. package/README.md +475 -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 +118 -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 +109 -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/outdated.d.ts +5 -0
  37. package/lib/src/cli/outdated.js +94 -0
  38. package/lib/src/cli/registry.d.ts +2 -0
  39. package/lib/src/cli/registry.js +122 -0
  40. package/lib/src/cli/serve-worker.d.ts +2 -0
  41. package/lib/src/cli/serve-worker.js +42 -0
  42. package/lib/src/cli/servers.d.ts +15 -0
  43. package/lib/src/cli/servers.js +451 -0
  44. package/lib/src/cli/vendor.d.ts +2 -0
  45. package/lib/src/cli/vendor.js +92 -0
  46. package/lib/src/cli/watch.d.ts +26 -0
  47. package/lib/src/cli/watch.js +81 -0
  48. package/lib/src/cli.d.ts +12 -0
  49. package/lib/src/cli.js +486 -0
  50. package/lib/src/csv.d.ts +27 -0
  51. package/lib/src/csv.js +117 -0
  52. package/lib/src/deflate.d.ts +76 -0
  53. package/lib/src/deflate.js +604 -0
  54. package/lib/src/digest.d.ts +3 -0
  55. package/lib/src/digest.js +7 -0
  56. package/lib/src/errors.d.ts +34 -0
  57. package/lib/src/errors.js +20 -0
  58. package/lib/src/executor.d.ts +386 -0
  59. package/lib/src/executor.js +2193 -0
  60. package/lib/src/explain.d.ts +64 -0
  61. package/lib/src/explain.js +445 -0
  62. package/lib/src/export-formats.d.ts +16 -0
  63. package/lib/src/export-formats.js +52 -0
  64. package/lib/src/exporter.d.ts +103 -0
  65. package/lib/src/exporter.js +676 -0
  66. package/lib/src/fingerprint.d.ts +40 -0
  67. package/lib/src/fingerprint.js +168 -0
  68. package/lib/src/frontmatter.d.ts +22 -0
  69. package/lib/src/frontmatter.js +45 -0
  70. package/lib/src/index.d.ts +12 -0
  71. package/lib/src/index.js +12 -0
  72. package/lib/src/language/ast.d.ts +353 -0
  73. package/lib/src/language/ast.js +57 -0
  74. package/lib/src/language/parser.d.ts +3 -0
  75. package/lib/src/language/parser.js +612 -0
  76. package/lib/src/language/scanner.d.ts +110 -0
  77. package/lib/src/language/scanner.js +449 -0
  78. package/lib/src/language/validate.d.ts +13 -0
  79. package/lib/src/language/validate.js +367 -0
  80. package/lib/src/links.d.ts +79 -0
  81. package/lib/src/links.js +583 -0
  82. package/lib/src/lint.d.ts +10 -0
  83. package/lib/src/lint.js +223 -0
  84. package/lib/src/markdown.d.ts +29 -0
  85. package/lib/src/markdown.js +612 -0
  86. package/lib/src/media.d.ts +82 -0
  87. package/lib/src/media.js +650 -0
  88. package/lib/src/memo.d.ts +57 -0
  89. package/lib/src/memo.js +93 -0
  90. package/lib/src/oci.d.ts +178 -0
  91. package/lib/src/oci.js +380 -0
  92. package/lib/src/outdated.d.ts +46 -0
  93. package/lib/src/outdated.js +195 -0
  94. package/lib/src/pages.d.ts +10 -0
  95. package/lib/src/pages.js +54 -0
  96. package/lib/src/paths.d.ts +39 -0
  97. package/lib/src/paths.js +101 -0
  98. package/lib/src/planner.d.ts +297 -0
  99. package/lib/src/planner.js +410 -0
  100. package/lib/src/plugins/cache.d.ts +70 -0
  101. package/lib/src/plugins/cache.js +226 -0
  102. package/lib/src/plugins/exec.d.ts +51 -0
  103. package/lib/src/plugins/exec.js +144 -0
  104. package/lib/src/plugins/host.d.ts +75 -0
  105. package/lib/src/plugins/host.js +277 -0
  106. package/lib/src/plugins/wasm.d.ts +23 -0
  107. package/lib/src/plugins/wasm.js +249 -0
  108. package/lib/src/profile.d.ts +42 -0
  109. package/lib/src/profile.js +68 -0
  110. package/lib/src/ref.d.ts +40 -0
  111. package/lib/src/ref.js +79 -0
  112. package/lib/src/registry-auth.d.ts +32 -0
  113. package/lib/src/registry-auth.js +151 -0
  114. package/lib/src/registry-cache.d.ts +71 -0
  115. package/lib/src/registry-cache.js +333 -0
  116. package/lib/src/registry-ops.d.ts +51 -0
  117. package/lib/src/registry-ops.js +248 -0
  118. package/lib/src/registry.d.ts +109 -0
  119. package/lib/src/registry.js +505 -0
  120. package/lib/src/render.d.ts +134 -0
  121. package/lib/src/render.js +576 -0
  122. package/lib/src/resources/archive.d.ts +11 -0
  123. package/lib/src/resources/archive.js +306 -0
  124. package/lib/src/resources/bundle-data.d.ts +16 -0
  125. package/lib/src/resources/bundle-data.js +50 -0
  126. package/lib/src/resources/convert.d.ts +12 -0
  127. package/lib/src/resources/convert.js +63 -0
  128. package/lib/src/resources/documents.d.ts +35 -0
  129. package/lib/src/resources/documents.js +122 -0
  130. package/lib/src/resources/git.d.ts +103 -0
  131. package/lib/src/resources/git.js +720 -0
  132. package/lib/src/resources/https.d.ts +40 -0
  133. package/lib/src/resources/https.js +256 -0
  134. package/lib/src/resources/ignore.d.ts +33 -0
  135. package/lib/src/resources/ignore.js +119 -0
  136. package/lib/src/resources/local.d.ts +64 -0
  137. package/lib/src/resources/local.js +248 -0
  138. package/lib/src/resources/lockfile.d.ts +52 -0
  139. package/lib/src/resources/lockfile.js +172 -0
  140. package/lib/src/resources/netpolicy.d.ts +6 -0
  141. package/lib/src/resources/netpolicy.js +82 -0
  142. package/lib/src/resources/types.d.ts +63 -0
  143. package/lib/src/resources/types.js +13 -0
  144. package/lib/src/resources/vendor.d.ts +94 -0
  145. package/lib/src/resources/vendor.js +296 -0
  146. package/lib/src/resources/wbn-reader.d.ts +44 -0
  147. package/lib/src/resources/wbn-reader.js +302 -0
  148. package/lib/src/schema.d.ts +25 -0
  149. package/lib/src/schema.js +169 -0
  150. package/lib/src/secrets.d.ts +21 -0
  151. package/lib/src/secrets.js +103 -0
  152. package/lib/src/serve-rules.d.ts +29 -0
  153. package/lib/src/serve-rules.js +122 -0
  154. package/lib/src/server.d.ts +45 -0
  155. package/lib/src/server.js +246 -0
  156. package/lib/src/servers.d.ts +71 -0
  157. package/lib/src/servers.js +139 -0
  158. package/lib/src/site.d.ts +60 -0
  159. package/lib/src/site.js +169 -0
  160. package/lib/src/store.d.ts +151 -0
  161. package/lib/src/store.js +596 -0
  162. package/lib/src/tree.d.ts +50 -0
  163. package/lib/src/tree.js +134 -0
  164. package/lib/src/values.d.ts +7 -0
  165. package/lib/src/values.js +37 -0
  166. package/lib/src/vendor.d.ts +19 -0
  167. package/lib/src/vendor.js +145 -0
  168. package/lib/src/watch.d.ts +44 -0
  169. package/lib/src/watch.js +137 -0
  170. package/lib/src/wbn.d.ts +5 -0
  171. package/lib/src/wbn.js +147 -0
  172. package/lib/src/yaml.d.ts +34 -0
  173. package/lib/src/yaml.js +562 -0
  174. package/package.json +58 -4
@@ -0,0 +1,109 @@
1
+ import path from 'node:path';
2
+ import { CACHE_MAX_ENV, cacheLimitFromEnv, cacheUsage, enforceCacheLimit, formatLimit, parseSize } from '../cache-limit.js';
3
+ import { TransformCache } from '../plugins/cache.js';
4
+ import { resolveStoreHome } from '../store.js';
5
+ import { humanSize, requireArgs, str, table, usage } from './common.js';
6
+ /** Where TRANSFORM results are cached: beside the image store, in `$SITEFILE_HOME/cache/transform`. */
7
+ export function transformCacheDir(env = process.env) {
8
+ return path.join(resolveStoreHome(env), 'cache', 'transform');
9
+ }
10
+ const info = {
11
+ name: 'cache info',
12
+ summary: 'show the TRANSFORM result cache location and size',
13
+ usage: 'cache info [--json]',
14
+ description: 'Print where plugin/tool results are cached, and how many entries and bytes it holds.',
15
+ flags: [' --json machine-readable output'],
16
+ options: { json: { type: 'boolean' } },
17
+ async run(v, pos) {
18
+ requireArgs(pos, 0, 0, 'no arguments');
19
+ const i = await new TransformCache(transformCacheDir()).info();
20
+ process.stdout.write(v['json'] ? JSON.stringify(i, null, 2) + '\n' : `Cache: ${i.dir}\nEntries: ${i.entries}\nBlobs: ${i.blobs}\nSize: ${humanSize(i.bytes)}\n`);
21
+ return 0;
22
+ },
23
+ };
24
+ const prune = {
25
+ name: 'cache prune',
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' } },
31
+ async run(v, pos) {
32
+ requireArgs(pos, 0, 0, 'no arguments');
33
+ const raw = v['older-than'];
34
+ const max = str(v['max-size']);
35
+ if (max !== undefined) {
36
+ if (raw !== undefined)
37
+ usage('--older-than and --max-size conflict');
38
+ const n = parseSize(max);
39
+ if (n === undefined || n === 0)
40
+ usage('--max-size expects a size greater than 0, such as 500M or 2G');
41
+ const r = await enforceCacheLimit(resolveStoreHome(), n);
42
+ process.stdout.write(`Evicted ${r.items} item${r.items === 1 ? '' : 's'}\nTotal reclaimed space: ${humanSize(r.bytes)}\n`);
43
+ return 0;
44
+ }
45
+ let ms = 0;
46
+ if (typeof raw === 'string') {
47
+ const days = Number(raw);
48
+ if (!Number.isFinite(days) || days <= 0)
49
+ usage('--older-than expects a positive number of days');
50
+ ms = days * 86_400_000;
51
+ }
52
+ const r = await new TransformCache(transformCacheDir()).prune(ms);
53
+ 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
+ return 0;
55
+ },
56
+ };
57
+ const du = {
58
+ name: 'cache du',
59
+ summary: 'show the size of every cache under $SITEFILE_HOME',
60
+ usage: 'cache du [--json]',
61
+ description: 'Print the size of each cache and store under $SITEFILE_HOME, how much of it may be evicted automatically\n(EVICTABLE), and the size limit ($SITEFILE_CACHE_MAX). Alias: cache ls.',
62
+ flags: [' --json machine-readable output (sizes in bytes; limit 0 = unlimited)'],
63
+ options: { json: { type: 'boolean' } },
64
+ async run(v, pos) {
65
+ requireArgs(pos, 0, 0, 'no arguments');
66
+ const home = resolveStoreHome();
67
+ const limit = cacheLimitFromEnv(process.env, (m) => process.stderr.write(`warning: ${m}\n`));
68
+ const caches = await cacheUsage(home);
69
+ const bytes = caches.reduce((a, c) => a + c.bytes, 0);
70
+ const evictable = caches.reduce((a, c) => a + c.evictable, 0);
71
+ if (v['json']) {
72
+ process.stdout.write(JSON.stringify({ home, limit, caches, bytes, evictable }, null, 2) + '\n');
73
+ return 0;
74
+ }
75
+ const rows = [
76
+ ['CACHE', 'ITEMS', 'SIZE', 'EVICTABLE', 'PATH'],
77
+ ...caches.map((c) => [c.name, String(c.items), humanSize(c.bytes), humanSize(c.evictable), c.path]),
78
+ ['total', '', humanSize(bytes), humanSize(evictable), home],
79
+ ];
80
+ process.stdout.write(table(rows));
81
+ process.stdout.write(`Limit: ${formatLimit(limit)} (${CACHE_MAX_ENV}; evictable data over it is deleted after each build, least recently used first)\n`);
82
+ return 0;
83
+ },
84
+ };
85
+ /**
86
+ * End of `build`/`inspect`: apply $SITEFILE_CACHE_MAX. Never fails: problems are warnings.
87
+ */
88
+ export async function evictAfterBuild(env = process.env) {
89
+ const warn = (m) => void process.stderr.write(`warning: ${m}\n`);
90
+ try {
91
+ const limit = cacheLimitFromEnv(env, warn);
92
+ if (limit === 0)
93
+ return;
94
+ const r = await enforceCacheLimit(resolveStoreHome(env), limit);
95
+ if (r.items > 0) {
96
+ process.stderr.write(`Evicted ${r.items} cache item${r.items === 1 ? '' : 's'} (${humanSize(r.bytes)}) to stay under ${CACHE_MAX_ENV}=${formatLimit(limit)}\n`);
97
+ }
98
+ }
99
+ catch (e) {
100
+ warn(`cache size limit not applied: ${e.message}`);
101
+ }
102
+ }
103
+ export const cacheSubcommands = new Map([
104
+ ['info', info],
105
+ ['prune', prune],
106
+ ['du', du],
107
+ ['ls', du],
108
+ ]);
109
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1,29 @@
1
+ import { type ParseArgsConfig } from 'node:util';
2
+ type Options = NonNullable<ParseArgsConfig['options']>;
3
+ export type ParsedValues = Record<string, string | boolean | Array<string | boolean> | undefined>;
4
+ export interface Command {
5
+ readonly name: string;
6
+ /** One line for the command list. */
7
+ readonly summary: string;
8
+ readonly usage: string;
9
+ readonly description: string;
10
+ /** Help lines for the options, already formatted. */
11
+ readonly flags: readonly string[];
12
+ readonly options: Options;
13
+ /** Positional arguments allowed (default true). */
14
+ readonly run: (values: ParsedValues, positionals: string[]) => Promise<number>;
15
+ }
16
+ export declare function commandHelp(c: Command): string;
17
+ export declare function usageError(message: string): number;
18
+ /** Parse `args` for `command` and run it, mapping SitefileError to the CLI exit codes (1, or 2 for E_USAGE). */
19
+ export declare function runCommand(command: Command, args: readonly string[]): Promise<number>;
20
+ export declare function usage(message: string): never;
21
+ export declare function requireArgs(positionals: string[], min: number, max: number, what: string): void;
22
+ /** Left-aligned columns separated by two spaces, like `docker images`. */
23
+ export declare function table(rows: readonly (readonly string[])[]): string;
24
+ /** 1.2kB, 34.5MB (decimal units, as Docker prints them). */
25
+ export declare function humanSize(bytes: number): string;
26
+ export declare function relativeTime(iso: string, now?: number): string;
27
+ export declare function str(v: ParsedValues[string]): string | undefined;
28
+ export declare function strs(v: ParsedValues[string]): string[];
29
+ export {};
@@ -0,0 +1,100 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { SitefileError } from '../errors.js';
3
+ export function commandHelp(c) {
4
+ return `Usage: sitefile ${c.usage}\n\n${c.description}\n${c.flags.length ? `\nOptions:\n${c.flags.map((f) => ` ${f}`).join('\n')}\n` : ''}`;
5
+ }
6
+ export function usageError(message) {
7
+ process.stderr.write(`error: ${message}\nRun 'sitefile --help' for usage.\n`);
8
+ return 2;
9
+ }
10
+ /** Parse `args` for `command` and run it, mapping SitefileError to the CLI exit codes (1, or 2 for E_USAGE). */
11
+ export async function runCommand(command, args) {
12
+ let parsed;
13
+ try {
14
+ parsed = parseArgs({ args: [...args], allowPositionals: true, options: { ...command.options, help: { type: 'boolean', short: 'h' } } });
15
+ }
16
+ catch (e) {
17
+ return usageError(`${command.name}: ${e.message}`);
18
+ }
19
+ if (parsed.values.help) {
20
+ process.stdout.write(commandHelp(command));
21
+ return 0;
22
+ }
23
+ try {
24
+ return await command.run(parsed.values, parsed.positionals);
25
+ }
26
+ catch (e) {
27
+ if (e instanceof SitefileError) {
28
+ if (e.code === 'E_USAGE')
29
+ return usageError(e.detail);
30
+ process.stderr.write(`error[${e.code}]: ${e.message}\n`);
31
+ return 1;
32
+ }
33
+ throw e;
34
+ }
35
+ }
36
+ export function usage(message) {
37
+ throw new SitefileError('E_USAGE', message);
38
+ }
39
+ export function requireArgs(positionals, min, max, what) {
40
+ if (positionals.length < min)
41
+ usage(`expected ${what}`);
42
+ if (positionals.length > max)
43
+ usage(`unexpected arguments: ${positionals.slice(max).join(' ')}`);
44
+ }
45
+ /** Left-aligned columns separated by two spaces, like `docker images`. */
46
+ export function table(rows) {
47
+ if (rows.length === 0)
48
+ return '';
49
+ const widths = [];
50
+ for (const r of rows)
51
+ r.forEach((c, i) => (widths[i] = Math.max(widths[i] ?? 0, c.length)));
52
+ return rows.map((r) => r.map((c, i) => (i === r.length - 1 ? c : c.padEnd(widths[i] ?? 0))).join(' ').trimEnd()).join('\n') + '\n';
53
+ }
54
+ /** Drop trailing zeros only after a decimal point ("1.50" -> "1.5", "100" stays). */
55
+ function stripDecimalZeros(s) {
56
+ return s.includes('.') ? s.replace(/\.?0+$/, '') : s;
57
+ }
58
+ /** 1.2kB, 34.5MB (decimal units, as Docker prints them). */
59
+ export function humanSize(bytes) {
60
+ if (bytes < 1000)
61
+ return `${bytes}B`;
62
+ const units = ['kB', 'MB', 'GB', 'TB'];
63
+ let v = bytes;
64
+ let i = -1;
65
+ do {
66
+ v /= 1000;
67
+ i++;
68
+ } while (v >= 1000 && i < units.length - 1);
69
+ return `${v >= 100 ? v.toFixed(0) : stripDecimalZeros(v.toPrecision(3))}${units[i]}`;
70
+ }
71
+ export function relativeTime(iso, now = Date.now()) {
72
+ const s = Math.max(0, Math.round((now - Date.parse(iso)) / 1000));
73
+ if (Number.isNaN(s))
74
+ return 'unknown';
75
+ if (s < 5)
76
+ return 'Less than a second ago';
77
+ const units = [
78
+ [31536000, 'year'],
79
+ [2592000, 'month'],
80
+ [604800, 'week'],
81
+ [86400, 'day'],
82
+ [3600, 'hour'],
83
+ [60, 'minute'],
84
+ [1, 'second'],
85
+ ];
86
+ for (const [n, name] of units) {
87
+ if (s >= n) {
88
+ const k = Math.floor(s / n);
89
+ return `${k} ${name}${k === 1 ? '' : 's'} ago`;
90
+ }
91
+ }
92
+ return 'Less than a second ago';
93
+ }
94
+ export function str(v) {
95
+ return typeof v === 'string' ? v : undefined;
96
+ }
97
+ export function strs(v) {
98
+ return Array.isArray(v) ? v.filter((x) => typeof x === 'string') : typeof v === 'string' ? [v] : [];
99
+ }
100
+ //# sourceMappingURL=common.js.map
@@ -0,0 +1,18 @@
1
+ import { type GitLocation } from '../resources/git.js';
2
+ import { type ArchiveLimits, type FetchPolicy } from '../resources/types.js';
3
+ /**
4
+ * `sitefile build https://host/repo.git#ref:dir` (issue #91): a build context fetched from Git.
5
+ * A context argument with a URL scheme (`https://`, `http://`, `git+https://`, `git+http://`) is a
6
+ * Git URL. Its tree is fetched like a Git IMPORT (same network policy; symlinks resolved inside the
7
+ * tree, submodules refused), written to a temporary directory, built from there, and removed.
8
+ */
9
+ export declare function isGitContext(context: string): boolean;
10
+ /** The location of a Git context argument (`https://…` is read as `git+https://…`). */
11
+ export declare function gitContextLocation(context: string): GitLocation;
12
+ export interface GitContext {
13
+ readonly dir: string;
14
+ readonly commit: string;
15
+ dispose(): Promise<void>;
16
+ }
17
+ /** Fetch the context's tree and write it to a new temporary directory. */
18
+ export declare function materializeGitContext(context: string, policy: FetchPolicy, limits?: ArchiveLimits): Promise<GitContext>;
@@ -0,0 +1,48 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { SitefileError } from '../errors.js';
5
+ import { fetchGitCommit, parseGitUri, selectGitPath } from '../resources/git.js';
6
+ import { DEFAULT_ARCHIVE_LIMITS } from '../resources/types.js';
7
+ import { Tree, makeFile } from '../tree.js';
8
+ /**
9
+ * `sitefile build https://host/repo.git#ref:dir` (issue #91): a build context fetched from Git.
10
+ * A context argument with a URL scheme (`https://`, `http://`, `git+https://`, `git+http://`) is a
11
+ * Git URL. Its tree is fetched like a Git IMPORT (same network policy; symlinks resolved inside the
12
+ * tree, submodules refused), written to a temporary directory, built from there, and removed.
13
+ */
14
+ export function isGitContext(context) {
15
+ return /^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(context);
16
+ }
17
+ /** The location of a Git context argument (`https://…` is read as `git+https://…`). */
18
+ export function gitContextLocation(context) {
19
+ const uri = /^git\+/i.test(context) ? context : `git+${context}`;
20
+ const loc = parseGitUri(uri);
21
+ return { ...loc, url: context };
22
+ }
23
+ /** Fetch the context's tree and write it to a new temporary directory. */
24
+ export async function materializeGitContext(context, policy, limits = DEFAULT_ARCHIVE_LIMITS) {
25
+ const loc = gitContextLocation(context);
26
+ const objs = await fetchGitCommit(loc.repo, loc.ref, loc.commit, { policy }, limits);
27
+ const sel = selectGitPath(objs, loc, limits);
28
+ if (sel.kind !== 'tree')
29
+ throw new SitefileError('E_TYPE', `build context '${context}' is a file; a Git context must name a directory`);
30
+ // Same checks as an IMPORT of the tree (NFC, case collisions), before anything is written.
31
+ const tree = Tree.fromFiles(sel.files.map(([p, f]) => [p, makeFile(f.bytes, context, f.mode)]));
32
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'sitefile-context-'));
33
+ const dispose = () => fs.rm(dir, { recursive: true, force: true });
34
+ try {
35
+ for (const [p, n] of tree.entries()) {
36
+ const dest = path.join(dir, ...p.slice(1).split('/'));
37
+ await fs.mkdir(path.dirname(dest), { recursive: true });
38
+ await fs.writeFile(dest, n.bytes, { mode: n.mode });
39
+ await fs.chmod(dest, n.mode);
40
+ }
41
+ }
42
+ catch (e) {
43
+ await dispose();
44
+ throw new SitefileError('E_EXPORT', `cannot write the Git context to a temporary directory: ${e.message}`, undefined, { cause: e });
45
+ }
46
+ return { dir, commit: objs.commit, dispose };
47
+ }
48
+ //# sourceMappingURL=git-context.js.map
@@ -0,0 +1,6 @@
1
+ import { type ImageSummary } from '../store.js';
2
+ import { type Command } from './common.js';
3
+ export declare const imageCommands: ReadonlyMap<string, Command>;
4
+ /** `sitefile image <sub>` → command; `ls`, `rm`, `tag` and `save` map onto the top-level commands. */
5
+ export declare const imageSubcommands: ReadonlyMap<string, Command>;
6
+ export type { ImageSummary };
@@ -0,0 +1,285 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { sourceDateEpoch, writeTar } from '../archive-writers.js';
5
+ import { SitefileError } from '../errors.js';
6
+ import { exportDirectory, exportFile } from '../exporter.js';
7
+ import { encodeTree, formatFromExtension, isArchiveFormat } from '../export-formats.js';
8
+ import { EXPORT_FORMATS, parseExportFormat } from '../language/ast.js';
9
+ import { MEDIA, siteLayerIndex } from '../oci.js';
10
+ import { looksLikeImageId, parseLocalRef, refKey } from '../ref.js';
11
+ import { shortId, Store } from '../store.js';
12
+ import { humanSize, relativeTime, requireArgs, str, table, usage } from './common.js';
13
+ function split(key) {
14
+ const i = key.lastIndexOf(':');
15
+ return [key.slice(0, i), key.slice(i + 1)];
16
+ }
17
+ /** A path relative to the cwd when it is inside it, else absolute. */
18
+ function shown(p) {
19
+ const rel = path.relative(process.cwd(), p);
20
+ return rel === '' ? '.' : rel.startsWith('..') ? p : rel;
21
+ }
22
+ const images = {
23
+ name: 'images',
24
+ summary: 'list images in the local store',
25
+ usage: 'images [--json] [--digests] [-q]',
26
+ description: 'List the images in the local store (SITEFILE_HOME or ~/.sitefile), newest first.\nImages without a tag show as <none>. Alias: sitefile image ls.',
27
+ flags: ['--json machine-readable output (full IDs, sizes in bytes)', '--digests add the full image ID (sha256:...) column', '-q, --quiet only print short image IDs'],
28
+ options: { json: { type: 'boolean' }, digests: { type: 'boolean' }, quiet: { type: 'boolean', short: 'q' } },
29
+ async run(v, pos) {
30
+ requireArgs(pos, 0, 0, 'no arguments');
31
+ const store = await Store.open();
32
+ const list = await store.list();
33
+ const rows = list.flatMap((im) => (im.tags.length ? im.tags : [undefined]).map((t) => ({ im, tag: t })));
34
+ if (v.json) {
35
+ const out = rows.map(({ im, tag }) => {
36
+ const [repository, t] = tag ? split(tag) : [null, null];
37
+ return { repository, tag: t, id: im.id, created: im.created, files: im.files, size: im.size };
38
+ });
39
+ process.stdout.write(JSON.stringify(out, null, 2) + '\n');
40
+ return 0;
41
+ }
42
+ if (v.quiet) {
43
+ const seen = new Set();
44
+ for (const { im } of rows)
45
+ if (!seen.has(im.id))
46
+ (seen.add(im.id), process.stdout.write(`${shortId(im.id)}\n`));
47
+ return 0;
48
+ }
49
+ const head = ['REPOSITORY', 'TAG', 'IMAGE ID', ...(v.digests ? ['DIGEST'] : []), 'CREATED', 'FILES', 'SIZE'];
50
+ const body = rows.map(({ im, tag }) => {
51
+ const [repo, t] = tag ? split(tag) : ['<none>', '<none>'];
52
+ return [repo, t, shortId(im.id), ...(v.digests ? [im.id] : []), relativeTime(im.created), String(im.files), humanSize(im.size)];
53
+ });
54
+ process.stdout.write(table([head, ...body]));
55
+ return 0;
56
+ },
57
+ };
58
+ const inspect = {
59
+ name: 'image inspect',
60
+ summary: 'show an image manifest',
61
+ usage: 'image inspect <ref|id> [--json]',
62
+ description: 'Show an image: its ID, tags, creation time, build metadata (ARG names only) and files.',
63
+ flags: ['--json machine-readable output'],
64
+ options: { json: { type: 'boolean' } },
65
+ async run(v, pos) {
66
+ requireArgs(pos, 1, 1, 'an image reference');
67
+ const store = await Store.open();
68
+ const id = await store.resolve(pos[0]);
69
+ const d = await store.details(id);
70
+ const tree = await store.loadTree(id); // verifies every digest
71
+ const files = tree.entries().map(([p, n]) => ({ path: p, mode: n.mode.toString(8), size: n.bytes.byteLength, digest: n.digest }));
72
+ const layer = d.manifest.layers[0];
73
+ if (v.json) {
74
+ const out = {
75
+ id,
76
+ tags: d.tags,
77
+ created: d.created,
78
+ os: d.config.os,
79
+ architecture: d.config.architecture,
80
+ root: d.root,
81
+ size: d.size,
82
+ labels: d.config.labels,
83
+ config: d.manifest.config,
84
+ layers: d.manifest.layers,
85
+ diffIds: d.config.diffIds,
86
+ files,
87
+ };
88
+ process.stdout.write(JSON.stringify(out, null, 2) + '\n');
89
+ return 0;
90
+ }
91
+ const labels = Object.entries(d.config.labels).map(([k, val]) => ` ${k}=${val}`);
92
+ const out = [
93
+ `ID: ${id}`,
94
+ `Tags: ${d.tags.join(', ') || '<none>'}`,
95
+ `Stored: ${d.created} (${relativeTime(d.created)})`,
96
+ `Platform: ${d.config.os}/${d.config.architecture}`,
97
+ `Root: ${d.root}`,
98
+ `Layer: ${layer?.digest ?? '-'} (${humanSize(d.layerSize)} ${d.manifest.layers[siteLayerIndex(d.manifest, d.config)]?.mediaType === MEDIA.layerTar ? 'tar' : 'gzip'})`,
99
+ `Diff ID: ${d.config.diffIds[0] ?? '-'}`,
100
+ `Config: ${d.manifest.config.digest}`,
101
+ 'Labels:',
102
+ ...labels,
103
+ `Files: ${files.length} (${humanSize(d.size)})`,
104
+ '',
105
+ ...files.map((f) => `${f.mode} ${String(f.size).padStart(8)} ${f.path}`),
106
+ ];
107
+ process.stdout.write(out.join('\n') + '\n');
108
+ return 0;
109
+ },
110
+ };
111
+ const tag = {
112
+ name: 'tag',
113
+ summary: 'add a name:tag to an image',
114
+ usage: 'tag <src> <dst>',
115
+ description: 'Create the local reference <dst> (name[:tag]) for the image <src> (reference or ID).\nAn existing <dst> is moved to the new image.',
116
+ flags: [],
117
+ options: {},
118
+ async run(_v, pos) {
119
+ requireArgs(pos, 2, 2, '<src> and <dst>');
120
+ const dst = parseLocalRef(pos[1]);
121
+ const store = await Store.open();
122
+ const id = await store.resolve(pos[0]);
123
+ const key = await store.tag(id, dst);
124
+ process.stdout.write(`${key} -> ${shortId(id)}\n`);
125
+ return 0;
126
+ },
127
+ };
128
+ const rmi = {
129
+ name: 'rmi',
130
+ summary: 'remove tags / images',
131
+ usage: 'rmi [-f] <ref|id>...',
132
+ description: 'Remove the tag <ref>. The image manifest is deleted when no other tag references it (blobs are\nreclaimed by "sitefile image prune"). Removing by image ID untags every tag and needs -f when there is more than one.',
133
+ flags: ['-f, --force remove an image by ID even if it has several tags'],
134
+ options: { force: { type: 'boolean', short: 'f' } },
135
+ async run(v, pos) {
136
+ requireArgs(pos, 1, Infinity, 'an image reference');
137
+ const store = await Store.open();
138
+ let failed = false;
139
+ for (const ref of pos) {
140
+ try {
141
+ const r = await store.removeImage(ref, !!v.force);
142
+ for (const t of r.untagged)
143
+ process.stdout.write(`Untagged: ${t}\n`);
144
+ if (r.deleted)
145
+ process.stdout.write(`Deleted: ${r.deleted}\n`);
146
+ }
147
+ catch (e) {
148
+ if (!(e instanceof SitefileError))
149
+ throw e;
150
+ if (e.code === 'E_USAGE')
151
+ throw e;
152
+ process.stderr.write(`error[${e.code}]: ${e.message}\n`);
153
+ failed = true;
154
+ }
155
+ }
156
+ return failed ? 1 : 0;
157
+ },
158
+ };
159
+ const prune = {
160
+ name: 'image prune',
161
+ summary: 'delete unreferenced manifests and blobs',
162
+ usage: 'image prune',
163
+ description: 'Delete untagged image manifests and every blob no remaining image uses, then print the space reclaimed.',
164
+ flags: [],
165
+ options: {},
166
+ async run(_v, pos) {
167
+ requireArgs(pos, 0, 0, 'no arguments');
168
+ const store = await Store.open();
169
+ const r = await store.prune();
170
+ process.stdout.write(`Deleted ${r.manifests} image${r.manifests === 1 ? '' : 's'} and ${r.blobs} blob${r.blobs === 1 ? '' : 's'}\nTotal reclaimed space: ${humanSize(r.bytes)}\n`);
171
+ return 0;
172
+ },
173
+ };
174
+ const save = {
175
+ name: 'save',
176
+ summary: 'export an image to a directory or archive',
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.',
179
+ flags: [
180
+ '-o, --out <path> destination (required)',
181
+ ` --format <fmt> ${EXPORT_FORMATS.join(' | ')} (alias tar.gz)`,
182
+ ' --base-url <url> https URL ending in "/" (wbn only; required for wbn)',
183
+ ' --force replace a non-empty destination or an existing file',
184
+ ],
185
+ options: { out: { type: 'string', short: 'o' }, format: { type: 'string' }, 'base-url': { type: 'string' }, force: { type: 'boolean' } },
186
+ async run(v, pos) {
187
+ requireArgs(pos, 1, 1, 'an image reference');
188
+ const out = str(v.out);
189
+ if (!out)
190
+ usage('save requires -o <out>');
191
+ let format;
192
+ if (v.format !== undefined) {
193
+ format = parseExportFormat(str(v.format) ?? '');
194
+ if (!format)
195
+ usage(`--format expects one of ${EXPORT_FORMATS.join(', ')} (or tar.gz)`);
196
+ }
197
+ format ??= formatFromExtension(out) ?? 'directory';
198
+ const store = await Store.open();
199
+ const id = await store.resolve(pos[0]);
200
+ // An empty scratch directory as the "context": the destination is then always
201
+ // outside it, so a non-empty directory or an existing file needs --force.
202
+ const scratch = await fs.mkdtemp(path.join(os.tmpdir(), 'sitefile-save-'));
203
+ try {
204
+ const opts = { contextDir: scratch, ...(v.force ? { force: true } : {}) };
205
+ if (format === 'oci') {
206
+ // The stored image as-is (same digests): an OCI archive for *.tar, else a layout directory.
207
+ const input = pos[0];
208
+ const refName = looksLikeImageId(input) ? undefined : refKey(parseLocalRef(input));
209
+ const layout = await store.layoutOf(id, refName);
210
+ if (out.toLowerCase().endsWith('.tar')) {
211
+ const w = await exportFile(writeTar(layout, { epoch: sourceDateEpoch() ?? 0 }), out, opts);
212
+ process.stdout.write(`Saved ${shortId(id)} to ${shown(w.outFile)} (oci archive, ${w.bytes} bytes)\n`);
213
+ }
214
+ else {
215
+ const w = await exportDirectory(layout, out, opts);
216
+ process.stdout.write(`Saved ${shortId(id)} to ${shown(w.outDir)} (oci layout)\n`);
217
+ }
218
+ return 0;
219
+ }
220
+ const tree = await store.loadTree(id);
221
+ if (!isArchiveFormat(format)) {
222
+ const w = await exportDirectory(tree, out, opts);
223
+ process.stdout.write(`Saved ${shortId(id)} (${tree.size} files) to ${shown(w.outDir)}\n`);
224
+ }
225
+ else {
226
+ const data = encodeTree(tree, format, { epoch: sourceDateEpoch(), baseUrl: str(v['base-url']) });
227
+ const w = await exportFile(data, out, opts);
228
+ process.stdout.write(`Saved ${shortId(id)} (${tree.size} files) to ${shown(w.outFile)} (${format === 'tgz' ? 'tar.gz' : format}, ${w.bytes} bytes)\n`);
229
+ }
230
+ }
231
+ finally {
232
+ await fs.rm(scratch, { recursive: true, force: true });
233
+ }
234
+ return 0;
235
+ },
236
+ };
237
+ const diff = {
238
+ name: 'image diff',
239
+ summary: 'compare two images',
240
+ usage: 'image diff <ref|id> <ref|id>',
241
+ description: 'List paths added (A), removed (D) or changed (C) going from the first image to the second.\nExit status is 0 whether or not they differ.',
242
+ flags: [],
243
+ options: {},
244
+ async run(_v, pos) {
245
+ requireArgs(pos, 2, 2, 'two image references');
246
+ const store = await Store.open();
247
+ const [a, b] = await Promise.all(pos.map(async (p) => store.loadTree(await store.resolve(p))));
248
+ const am = new Map(a?.entries());
249
+ const bm = new Map(b?.entries());
250
+ const lines = [];
251
+ for (const p of [...new Set([...am.keys(), ...bm.keys()])].sort()) {
252
+ const x = am.get(p);
253
+ const y = bm.get(p);
254
+ if (!x)
255
+ lines.push(`A ${p}`);
256
+ else if (!y)
257
+ lines.push(`D ${p}`);
258
+ else if (x.digest !== y.digest || x.mode !== y.mode)
259
+ lines.push(`C ${p}`);
260
+ }
261
+ process.stdout.write(lines.length ? lines.join('\n') + '\n' : '');
262
+ return 0;
263
+ },
264
+ };
265
+ export const imageCommands = new Map([
266
+ ['images', images],
267
+ ['tag', tag],
268
+ ['rmi', rmi],
269
+ ['save', save],
270
+ ['image inspect', inspect],
271
+ ['image prune', prune],
272
+ ['image diff', diff],
273
+ ]);
274
+ /** `sitefile image <sub>` → command; `ls`, `rm`, `tag` and `save` map onto the top-level commands. */
275
+ export const imageSubcommands = new Map([
276
+ ['ls', images],
277
+ ['list', images],
278
+ ['rm', rmi],
279
+ ['tag', tag],
280
+ ['save', save],
281
+ ['inspect', inspect],
282
+ ['prune', prune],
283
+ ['diff', diff],
284
+ ]);
285
+ //# sourceMappingURL=images.js.map
@@ -0,0 +1,10 @@
1
+ export declare const EXTRA_COMMANDS: readonly string[];
2
+ /**
3
+ * The language server exists only in the Rust implementation (a tooling
4
+ * exception, docs/RUST.md): the language is the same in both, the server is
5
+ * an editor tool. Here `sitefile lsp` is a usage error that points there.
6
+ */
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";
9
+ export declare function isExtraCommand(name: string | undefined): boolean;
10
+ export declare function runExtraCommand(argv: readonly string[]): Promise<number>;