@quillmark/quiver 0.18.1 → 0.19.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.
package/README.md CHANGED
@@ -62,7 +62,7 @@ import { build } from '@quillmark/quiver/node';
62
62
  await build('./node_modules/@org/my-quiver', './public/quivers/my-quiver');
63
63
  ```
64
64
 
65
- `build` clears its output directory before writing, so it owns that path outright. An `outDir` that is, or contains, the source quiver or the working directory is refused with a `transport_error` rather than deleted.
65
+ `build` owns its output path outright: it assembles a generation in `<outDir>.stage`, moves it in whole, and deletes the one it replaced. A reader fetching mid-build sees the previous generation rather than a torn tree, and a build that throws leaves it serving. An `outDir` that is, or contains, the source quiver or the working directory is refused with a `transport_error` rather than deleted.
66
66
 
67
67
  ```ts
68
68
  // browser runtime
@@ -134,32 +134,17 @@ A consumer reaches an installed quiver by resolving `<specifier>/Quiver.yaml`, w
134
134
  }
135
135
  ```
136
136
 
137
- Gate the quiver in CI so a validation failure surfaces on publish rather than on a consumer's build. `quillmark-quiver test` loads with `fromDir`, compiles every quill, and renders each quill's example document. One door, so what you run locally is what CI runs.
137
+ ## Authoring is [quillkit](../quillkit#readme)'s
138
138
 
139
- The CLI needs no file. Installing the package links `quillmark-quiver` into `node_modules/.bin`, which npm puts on PATH for every script:
139
+ This package is a library: it loads a quiver and packs one, and it has no CLI. The verbs a quill author runs (gate, pack, look at, deploy) are `quillkit`'s, and it resolves this package out of the collection's own `node_modules`. Depend on it here and the version you pin is the format your quiver is packed in:
140
140
 
141
141
  ```sh
142
- npm install --save-dev @quillmark/quiver @quillmark/wasm
142
+ npm install --save-dev @quillmark/quiver @quillmark/wasm quillkit
143
143
  ```
144
144
 
145
145
  ```jsonc
146
146
  // package.json
147
147
  {
148
- "scripts": { "test": "quillmark-quiver test" }
148
+ "scripts": { "test": "quillkit test" }
149
149
  }
150
150
  ```
151
-
152
- It finds the engine itself: a named `engine` export from `quiver.config.js` at the collection root, else `@quillmark/wasm` from your own `node_modules`. `quillmark-quiver build [--out <dir>]` packs the same source into a servable artifact.
153
-
154
- On vitest, jest or `node:test`, spawn the bin from a test rather than rebuilding the loop against the main API. The gate stays one implementation, and the case gates what CI gates:
155
-
156
- ```ts
157
- // quiver.test.ts
158
- import { execFileSync } from 'node:child_process';
159
-
160
- it('gates the quiver', () => {
161
- execFileSync('quillmark-quiver', ['test'], { stdio: 'inherit' });
162
- });
163
- ```
164
-
165
- It answers _does it work_, and that is the whole of what this package offers a quill author: _what is it like to use_ is answered by working a real document through the quill live, which is an app rather than a subpath.
package/dist/build.d.ts CHANGED
@@ -17,6 +17,10 @@
17
17
  * store/
18
18
  * <sha256> # dehydrated font bytes (full hash, no ext)
19
19
  *
20
+ * A generation is assembled beside outDir and moved in whole, so a reader
21
+ * fetching mid-build reads the previous one rather than a torn tree, and a
22
+ * build that throws leaves the previous one serving.
23
+ *
20
24
  * Throws:
21
25
  * - `quiver_invalid` on source validation failures (propagated from scanner)
22
26
  * - `transport_error` on I/O failures, and on an outDir the build would have
package/dist/build.js CHANGED
@@ -11,12 +11,20 @@ import { NAME_DIGEST_LENGTH } from './digest.js';
11
11
  import { POINTER_FORMAT } from './format.js';
12
12
  /** Font file extensions recognised by the builder (case-insensitive). */
13
13
  const FONT_EXT = /\.(ttf|otf|woff|woff2)$/i;
14
+ /** The two trees a build owns beside its output: where a generation is assembled, and
15
+ * where the one it replaces waits to be deleted. Siblings of outDir, so the swap is a
16
+ * rename rather than a copy across filesystems. */
17
+ const stagesOf = (out) => ({
18
+ stage: `${out}.stage`,
19
+ prev: `${out}.prev`
20
+ });
14
21
  /**
15
- * The build's first act is `rm(outDir, { recursive: true })`, so an outDir that
16
- * is, or contains, the source quiver or the working directory deletes the
17
- * thing the caller was building from. `quillmark-quiver build --out .` and a
18
- * mistyped `--out ..` are both one keystroke away, and the failure is
19
- * unrecoverable, so an outDir that owns the caller is refused up front.
22
+ * The build deletes each of the three trees it owns, so an outDir that is, or contains,
23
+ * the source quiver or the working directory deletes the thing the caller was building
24
+ * from. `quillkit build --out .` and a mistyped `--out ..` are both one keystroke away,
25
+ * and the failure is unrecoverable, so an outDir that owns the caller is refused up
26
+ * front. The staging siblings are checked too: they are named off outDir and cleared
27
+ * with it, so a caller who avoids one spelling has not avoided the others.
20
28
  *
21
29
  * An outDir *inside* sourceDir stays allowed: the scan reads the source before
22
30
  * any write, and `dist/` under the quiver root is the ordinary layout.
@@ -25,18 +33,19 @@ const FONT_EXT = /\.(ttf|otf|woff|woff2)$/i;
25
33
  */
26
34
  function assertSafeOutDir(path, sourceDir, outDir) {
27
35
  const out = path.resolve(outDir);
28
- /** True when `dir` is `target` or an ancestor of it. */
29
- const owns = (target) => {
30
- const rel = path.relative(out, path.resolve(target));
36
+ const { stage, prev } = stagesOf(out);
37
+ /** True when one of the owned trees is `target` or an ancestor of it. */
38
+ const owns = (target) => [out, stage, prev].some((dir) => {
39
+ const rel = path.relative(dir, path.resolve(target));
31
40
  return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
32
- };
41
+ });
33
42
  const what = owns(sourceDir)
34
43
  ? 'the source quiver'
35
44
  : owns(process.cwd())
36
45
  ? 'the working directory'
37
46
  : undefined;
38
47
  if (what !== undefined) {
39
- throw new QuiverError('transport_error', `Refusing to build into "${outDir}": the build clears its output directory, and this one holds ${what} ("${out}"). Point --out at a directory the build owns.`);
48
+ throw new QuiverError('transport_error', `Refusing to build into "${outDir}": the build clears that directory and two staging siblings beside it, and this one holds ${what} ("${out}"). Point --out somewhere the build can own outright.`);
40
49
  }
41
50
  }
42
51
  /**
@@ -51,6 +60,10 @@ function assertSafeOutDir(path, sourceDir, outDir) {
51
60
  * store/
52
61
  * <sha256> # dehydrated font bytes (full hash, no ext)
53
62
  *
63
+ * A generation is assembled beside outDir and moved in whole, so a reader
64
+ * fetching mid-build reads the previous one rather than a torn tree, and a
65
+ * build that throws leaves the previous one serving.
66
+ *
54
67
  * Throws:
55
68
  * - `quiver_invalid` on source validation failures (propagated from scanner)
56
69
  * - `transport_error` on I/O failures, and on an outDir the build would have
@@ -59,107 +72,139 @@ function assertSafeOutDir(path, sourceDir, outDir) {
59
72
  export async function buildQuiver(sourceDir, outDir) {
60
73
  // Dynamic imports keep this module safe to type-import from browser contexts.
61
74
  const path = await import('node:path');
62
- const { join } = path;
63
- const { mkdir, rm, writeFile } = await import('node:fs/promises');
75
+ const { dirname, join, resolve } = path;
76
+ const { mkdir, rename, rm, writeFile } = await import('node:fs/promises');
77
+ const { existsSync } = await import('node:fs');
64
78
  const { createHash } = await import('node:crypto');
65
79
  const { scanSourceQuiver, readQuillTree } = await import('./source-loader.js');
66
- // 0. The first write is a recursive delete of outDir; refuse the paths where
67
- // that deletes the caller instead of a previous build.
80
+ // 0. Three trees get cleared here; refuse the paths where that deletes the
81
+ // caller instead of a previous build.
68
82
  assertSafeOutDir(path, sourceDir, outDir);
83
+ const out = resolve(outDir);
84
+ const { stage, prev } = stagesOf(out);
69
85
  // 1. Scan + validate source quiver (throws quiver_invalid on bad input).
70
86
  const { meta, catalog } = await scanSourceQuiver(sourceDir);
71
- // 2. Clear and recreate outDir + outDir/store/.
72
87
  try {
73
- await rm(outDir, { recursive: true, force: true });
74
- await mkdir(join(outDir, 'store'), { recursive: true });
88
+ await packGeneration();
75
89
  }
76
- catch (err) {
77
- throw new QuiverError('transport_error', `Failed to prepare output directory "${outDir}": ${err.message}`, { cause: err });
90
+ finally {
91
+ // A throw anywhere above leaves a partial generation staged, and outDir
92
+ // untouched. Neither tree outlives the call either way. Swept rather than
93
+ // checked: a sweep that threw would answer for the build, and what a caller
94
+ // needs to hear is why the build failed.
95
+ for (const at of [stage, prev])
96
+ await rm(at, { recursive: true, force: true }).catch(() => { });
78
97
  }
79
- // 3. Process each quill version.
80
- const manifestQuills = [];
81
- for (const [quillName, versions] of catalog) {
82
- for (const version of versions) {
83
- const quillDir = join(sourceDir, 'quills', quillName, version);
84
- // a. Read quill file tree.
85
- const tree = await readQuillTree(quillDir);
86
- // b. Partition fonts vs content.
87
- const fontEntries = [];
88
- const contentEntries = [];
89
- for (const [path, bytes] of tree) {
90
- if (FONT_EXT.test(path)) {
91
- fontEntries.push([path, bytes]);
98
+ /**
99
+ * Write a whole generation into `stage`, then move it to outDir.
100
+ *
101
+ * The move is two renames: a directory rename refuses a non-empty target, so the
102
+ * outgoing generation steps aside first. That is the window, and it is two syscalls
103
+ * wide against the seconds a build takes.
104
+ */
105
+ async function packGeneration() {
106
+ // 2. Clear and recreate the staged tree + its store/.
107
+ try {
108
+ await rm(stage, { recursive: true, force: true });
109
+ await mkdir(join(stage, 'store'), { recursive: true });
110
+ }
111
+ catch (err) {
112
+ throw new QuiverError('transport_error', `Failed to prepare output directory "${outDir}": ${err.message}`, { cause: err });
113
+ }
114
+ // 3. Process each quill version.
115
+ const manifestQuills = [];
116
+ for (const [quillName, versions] of catalog) {
117
+ for (const version of versions) {
118
+ const quillDir = join(sourceDir, 'quills', quillName, version);
119
+ // a. Read quill file tree.
120
+ const tree = await readQuillTree(quillDir);
121
+ // b. Partition fonts vs content.
122
+ const fontEntries = [];
123
+ const contentEntries = [];
124
+ for (const [path, bytes] of tree) {
125
+ if (FONT_EXT.test(path)) {
126
+ fontEntries.push([path, bytes]);
127
+ }
128
+ else {
129
+ contentEntries.push([path, bytes]);
130
+ }
92
131
  }
93
- else {
94
- contentEntries.push([path, bytes]);
132
+ // c. Dehydrate fonts into store/.
133
+ const fonts = {};
134
+ for (const [path, bytes] of fontEntries) {
135
+ // Full width: the store is keyed by hash, so two distinct fonts
136
+ // sharing a prefix would merge into one entry.
137
+ const hash = createHash('sha256').update(bytes).digest('hex');
138
+ try {
139
+ await writeFile(join(stage, 'store', hash), bytes);
140
+ }
141
+ catch (err) {
142
+ throw new QuiverError('transport_error', `Failed to write font store entry "${join(outDir, 'store', hash)}": ${err.message}`, { cause: err });
143
+ }
144
+ fonts[path] = hash;
95
145
  }
96
- }
97
- // c. Dehydrate fonts into store/.
98
- const fonts = {};
99
- for (const [path, bytes] of fontEntries) {
100
- // Full width: the store is keyed by hash, so two distinct fonts
101
- // sharing a prefix would merge into one entry.
102
- const hash = createHash('sha256').update(bytes).digest('hex');
103
- const storePath = join(outDir, 'store', hash);
146
+ // d. Zip content files (deterministic: sorted paths, fixed mtime).
147
+ const contentRecord = {};
148
+ for (const [path, bytes] of contentEntries) {
149
+ contentRecord[path] = bytes;
150
+ }
151
+ const zipBytes = packFiles(contentRecord);
152
+ // e–f. Compute bundle hash and name.
153
+ const bundleHash = createHash('sha256')
154
+ .update(zipBytes)
155
+ .digest('hex')
156
+ .slice(0, NAME_DIGEST_LENGTH);
157
+ const bundleName = `${quillName}@${version}.${bundleHash}.zip`;
158
+ // g. Write bundle zip.
104
159
  try {
105
- await writeFile(storePath, bytes);
160
+ await writeFile(join(stage, bundleName), zipBytes);
106
161
  }
107
162
  catch (err) {
108
- throw new QuiverError('transport_error', `Failed to write font store entry "${storePath}": ${err.message}`, { cause: err });
163
+ throw new QuiverError('transport_error', `Failed to write bundle "${join(outDir, bundleName)}": ${err.message}`, { cause: err });
109
164
  }
110
- fonts[path] = hash;
165
+ // h. Record manifest entry.
166
+ manifestQuills.push({ name: quillName, version, bundle: bundleName, fonts });
111
167
  }
112
- // d. Zip content files (deterministic: sorted paths, fixed mtime).
113
- const contentRecord = {};
114
- for (const [path, bytes] of contentEntries) {
115
- contentRecord[path] = bytes;
116
- }
117
- const zipBytes = packFiles(contentRecord);
118
- // e–f. Compute bundle hash and name.
119
- const bundleHash = createHash('sha256')
120
- .update(zipBytes)
121
- .digest('hex')
122
- .slice(0, NAME_DIGEST_LENGTH);
123
- const bundleName = `${quillName}@${version}.${bundleHash}.zip`;
124
- // g. Write bundle zip.
125
- const bundlePath = join(outDir, bundleName);
126
- try {
127
- await writeFile(bundlePath, zipBytes);
128
- }
129
- catch (err) {
130
- throw new QuiverError('transport_error', `Failed to write bundle "${bundlePath}": ${err.message}`, { cause: err });
131
- }
132
- // h. Record manifest entry.
133
- manifestQuills.push({ name: quillName, version, bundle: bundleName, fonts });
134
168
  }
135
- }
136
- // 4–8. Build and write hashed manifest.
137
- const manifest = {
138
- version: 1,
139
- name: meta.name,
140
- quills: manifestQuills
141
- };
142
- const manifestJson = JSON.stringify(manifest, null, 2);
143
- const manifestHash = createHash('sha256')
144
- .update(manifestJson)
145
- .digest('hex')
146
- .slice(0, NAME_DIGEST_LENGTH);
147
- const manifestFileName = `manifest.${manifestHash}.json`;
148
- const manifestPath = join(outDir, manifestFileName);
149
- try {
150
- await writeFile(manifestPath, manifestJson, 'utf-8');
151
- }
152
- catch (err) {
153
- throw new QuiverError('transport_error', `Failed to write manifest "${manifestPath}": ${err.message}`, { cause: err });
154
- }
155
- // 9–10. Write stable pointer latest.json. The format is stamped here and read first,
156
- // so a client older than the tree says so rather than misreading it.
157
- const pointer = { format: POINTER_FORMAT, manifest: manifestFileName };
158
- const pointerPath = join(outDir, 'latest.json');
159
- try {
160
- await writeFile(pointerPath, JSON.stringify(pointer), 'utf-8');
161
- }
162
- catch (err) {
163
- throw new QuiverError('transport_error', `Failed to write pointer "${pointerPath}": ${err.message}`, { cause: err });
169
+ // 4–8. Build and write hashed manifest.
170
+ const manifest = {
171
+ version: 1,
172
+ name: meta.name,
173
+ quills: manifestQuills
174
+ };
175
+ const manifestJson = JSON.stringify(manifest, null, 2);
176
+ const manifestHash = createHash('sha256')
177
+ .update(manifestJson)
178
+ .digest('hex')
179
+ .slice(0, NAME_DIGEST_LENGTH);
180
+ const manifestFileName = `manifest.${manifestHash}.json`;
181
+ try {
182
+ await writeFile(join(stage, manifestFileName), manifestJson, 'utf-8');
183
+ }
184
+ catch (err) {
185
+ throw new QuiverError('transport_error', `Failed to write manifest "${join(outDir, manifestFileName)}": ${err.message}`, { cause: err });
186
+ }
187
+ // 9–10. Write stable pointer latest.json. The format is stamped here and read first,
188
+ // so a client older than the tree says so rather than misreading it.
189
+ const pointer = { format: POINTER_FORMAT, manifest: manifestFileName };
190
+ try {
191
+ await writeFile(join(stage, 'latest.json'), JSON.stringify(pointer), 'utf-8');
192
+ }
193
+ catch (err) {
194
+ throw new QuiverError('transport_error', `Failed to write pointer "${join(outDir, 'latest.json')}": ${err.message}`, { cause: err });
195
+ }
196
+ // 11. Move the generation in. `prev` is cleared first: a rename onto a
197
+ // non-empty directory fails, and the tree left by an interrupted run is
198
+ // one.
199
+ try {
200
+ await mkdir(dirname(out), { recursive: true });
201
+ await rm(prev, { recursive: true, force: true });
202
+ if (existsSync(out))
203
+ await rename(out, prev);
204
+ await rename(stage, out);
205
+ }
206
+ catch (err) {
207
+ throw new QuiverError('transport_error', `Failed to move the build into "${outDir}": ${err.message}`, { cause: err });
208
+ }
164
209
  }
165
210
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@quillmark/quiver",
3
- "version": "0.18.1",
4
- "description": "Quiver registry and build tooling for Quillmark",
3
+ "version": "0.19.0",
4
+ "description": "A quiver of Quillmark quills: load one, pack one, hand out quills.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -53,9 +53,6 @@
53
53
  "typescript": "^5.9.3",
54
54
  "vitest": "^4.0.18"
55
55
  },
56
- "bin": {
57
- "quillmark-quiver": "./dist/bin/quiver.js"
58
- },
59
56
  "scripts": {
60
57
  "build": "tsc",
61
58
  "prepare": "npm run build",
@@ -1,16 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Quiver CLI — build and test a quiver collection. Two verbs, and they are the
4
- * whole of what a quiver author gets from here: `build` packs, `test` gates.
5
- * Looking at rendered output is studio's, an app rather than a verb.
6
- *
7
- * Commands:
8
- * quillmark-quiver build [--out <dir>]
9
- * quillmark-quiver test
10
- *
11
- * Engine discovery (for `test`):
12
- * 1. Named export `engine` from `quiver.config.js` at the collection root,
13
- * if the file exists.
14
- * 2. Auto-imported `@quillmark/wasm` from the collection's node_modules.
15
- */
16
- export {};
@@ -1,141 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Quiver CLI — build and test a quiver collection. Two verbs, and they are the
4
- * whole of what a quiver author gets from here: `build` packs, `test` gates.
5
- * Looking at rendered output is studio's, an app rather than a verb.
6
- *
7
- * Commands:
8
- * quillmark-quiver build [--out <dir>]
9
- * quillmark-quiver test
10
- *
11
- * Engine discovery (for `test`):
12
- * 1. Named export `engine` from `quiver.config.js` at the collection root,
13
- * if the file exists.
14
- * 2. Auto-imported `@quillmark/wasm` from the collection's node_modules.
15
- */
16
- import { createRequire } from 'node:module';
17
- import { join } from 'node:path';
18
- import { pathToFileURL } from 'node:url';
19
- import { build as buildQuiver, fromDir } from '../node.js';
20
- // ---------------------------------------------------------------------------
21
- // Arg parsing helpers
22
- // ---------------------------------------------------------------------------
23
- const argv = process.argv.slice(2);
24
- const command = argv[0];
25
- /** One flag with a value. `build --out` is the only one either verb takes. */
26
- function flag(name) {
27
- const i = argv.indexOf(name);
28
- return i !== -1 && i + 1 < argv.length ? argv[i + 1] : undefined;
29
- }
30
- // ---------------------------------------------------------------------------
31
- // Engine discovery
32
- // ---------------------------------------------------------------------------
33
- async function loadEngine(cwd) {
34
- // Resolve @quillmark/wasm from the collection's node_modules (best-effort).
35
- // Its `Engine` renders every quill (cloning the core Quill/Document into the
36
- // backend itself). A quiver.config.js may export a custom `engine`.
37
- let wasm;
38
- try {
39
- const req = createRequire(pathToFileURL(join(cwd, 'package.json')).href);
40
- const wasmPath = req.resolve('@quillmark/wasm');
41
- wasm = (await import(pathToFileURL(wasmPath).href));
42
- // Instantiate the core. Every `@quillmark/wasm` export throws
43
- // `runtime::not_initialized` until `init()` resolves, and `new Engine()` is
44
- // lazy, so without this the throw arrives at the first render and reads as a
45
- // failing quill rather than an uninitialized runtime.
46
- await wasm.init();
47
- }
48
- catch {
49
- // Not installed — a quiver.config.js may still provide an engine.
50
- }
51
- // 1. Named `engine` export from quiver.config.js, if present.
52
- try {
53
- const cfg = await import(pathToFileURL(join(cwd, 'quiver.config.js')).href);
54
- if (cfg.engine != null) {
55
- return cfg.engine;
56
- }
57
- }
58
- catch {
59
- // File absent or incomplete — fall through to auto-discovery.
60
- }
61
- // 2. Auto-discover from @quillmark/wasm.
62
- if (wasm == null) {
63
- throw new Error("Cannot find @quillmark/wasm in this collection's node_modules.\n" +
64
- ' Install it: npm install @quillmark/wasm\n' +
65
- ' Or export { engine } from quiver.config.js for a custom engine.');
66
- }
67
- return new wasm.Engine();
68
- }
69
- // ---------------------------------------------------------------------------
70
- // Commands
71
- // ---------------------------------------------------------------------------
72
- async function build() {
73
- const cwd = process.cwd();
74
- const outDir = flag('--out') ?? 'dist';
75
- console.log(`quillmark-quiver build: ${cwd} → ${outDir}`);
76
- await buildQuiver(cwd, outDir);
77
- console.log('done.');
78
- }
79
- async function test() {
80
- const cwd = process.cwd();
81
- const engine = await loadEngine(cwd);
82
- const quiver = await fromDir(cwd);
83
- const names = quiver.quillNames();
84
- if (names.length === 0) {
85
- console.error('error: quiver has no quills');
86
- process.exit(1);
87
- }
88
- let pass = 0;
89
- let fail = 0;
90
- for (const name of names) {
91
- for (const version of quiver.versionsOf(name)) {
92
- const ref = `${name}@${version}`;
93
- try {
94
- const quill = await quiver.getQuill(ref);
95
- const doc = quill.seedDocument();
96
- let result;
97
- try {
98
- result = await engine.render(quill, doc);
99
- }
100
- finally {
101
- doc.free();
102
- }
103
- if (!Array.isArray(result.artifacts) || result.artifacts.length === 0) {
104
- throw new Error('example render produced no artifacts');
105
- }
106
- console.log(`pass ${ref}`);
107
- pass++;
108
- }
109
- catch (err) {
110
- console.error(`FAIL ${ref} — ${err.message}`);
111
- fail++;
112
- }
113
- }
114
- }
115
- const total = pass + fail;
116
- console.log(`\n${pass}/${total} passed`);
117
- if (fail > 0)
118
- process.exit(1);
119
- }
120
- // ---------------------------------------------------------------------------
121
- // Dispatch
122
- // ---------------------------------------------------------------------------
123
- function usage() {
124
- console.error(['Usage:', ' quillmark-quiver build [--out <dir>]', ' quillmark-quiver test'].join('\n'));
125
- }
126
- function die(err) {
127
- const msg = err instanceof Error ? err.message : String(err);
128
- console.error(`error: ${msg}`);
129
- process.exit(1);
130
- }
131
- switch (command) {
132
- case 'build':
133
- build().catch(die);
134
- break;
135
- case 'test':
136
- test().catch(die);
137
- break;
138
- default:
139
- usage();
140
- process.exit(1);
141
- }