vastgrid-cli 0.0.0-stage → 0.2.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/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `vastgrid [--save] [--port <n>] [--no-open] [<file or folder>…]` (docs/cli.md): starts DuckDB and the page that
4
+ * shows the files in 0.2's grid, on 127.0.0.1, and opens the page in the browser. Ctrl+C stops both, and the engine
5
+ * deletes its temporary folder.
6
+ */
7
+ import { spawn } from "node:child_process";
8
+ import { parseArgs } from "node:util";
9
+ import { serve } from "./server.js";
10
+ const USAGE = `Usage: vastgrid [options] [<file or folder>…]
11
+
12
+ Opens CSV, TSV, Parquet, JSON Lines and Excel files in a grid in the browser, read by DuckDB on this computer.
13
+ A folder gives its files, two folders deep; the current folder when nothing is named.
14
+
15
+ Options:
16
+ --save Let the page save edited cells into CSV and TSV files (⌘S or Ctrl+S).
17
+ --port <n> The port on 127.0.0.1; a free one by default.
18
+ --no-open Print the address without opening the browser.
19
+ -h, --help Show this help.
20
+
21
+ VASTGRID_TOKEN sets the token in the address, instead of a random one.
22
+ `;
23
+ function fail(message, code) {
24
+ process.stderr.write(`vastgrid: ${message}\n`);
25
+ process.exit(code);
26
+ }
27
+ let parsed;
28
+ function readArgs() {
29
+ return parseArgs({
30
+ allowPositionals: true,
31
+ options: {
32
+ save: { type: "boolean" },
33
+ port: { type: "string" },
34
+ "no-open": { type: "boolean" },
35
+ help: { type: "boolean", short: "h" },
36
+ },
37
+ });
38
+ }
39
+ try {
40
+ parsed = readArgs();
41
+ }
42
+ catch (error) {
43
+ fail(`${error.message}\nRun vastgrid --help for the options.`, 2);
44
+ }
45
+ const { values, positionals } = parsed;
46
+ if (values.help) {
47
+ process.stdout.write(USAGE);
48
+ process.exit(0);
49
+ }
50
+ const port = values.port === undefined ? 0 : Number(values.port);
51
+ if (!Number.isInteger(port) || port < 0 || port > 65535)
52
+ fail(`${values.port} is not a port.`, 2);
53
+ /** Opens the address in the browser of the system; the address is printed either way. */
54
+ function openBrowser(url) {
55
+ const [command, args] = process.platform === "darwin"
56
+ ? ["open", [url]]
57
+ : process.platform === "win32"
58
+ ? ["rundll32", ["url.dll,FileProtocolHandler", url]]
59
+ : ["xdg-open", [url]];
60
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
61
+ child.on("error", () => process.stderr.write("vastgrid: Open the address in a browser.\n"));
62
+ child.unref();
63
+ }
64
+ const served = await serve({
65
+ paths: positionals.length > 0 ? positionals : ["."],
66
+ port,
67
+ token: process.env.VASTGRID_TOKEN || undefined,
68
+ save: values.save ?? false,
69
+ log: (message, error) => process.stderr.write(`vastgrid: ${message}: ${String(error ?? "")}\n`),
70
+ }).catch((error) => fail(error instanceof Error ? error.message : String(error), 1));
71
+ const count = (await served.files()).length;
72
+ process.stdout.write(`vastgrid shows ${count === 1 ? "1 file" : `${count} files`}${values.save ? ", and saves edited cells," : ""} at\n` +
73
+ ` ${served.url}\nPress Ctrl+C to stop.\n`);
74
+ if (!values["no-open"])
75
+ openBrowser(served.url);
76
+ let stopping = false;
77
+ function stop() {
78
+ // A second Ctrl+C does not wait.
79
+ if (stopping)
80
+ process.exit(130);
81
+ stopping = true;
82
+ void served.close().finally(() => process.exit(0));
83
+ }
84
+ process.on("SIGINT", stop);
85
+ process.on("SIGTERM", stop);
86
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ /** `vastgrid` (docs/cli.md): files in 0.2's grid in the browser, read by DuckDB on this computer. */
2
+ export { type Served, type ServeOptions, serve } from "./server.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,qGAAqG;AACrG,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,YAAY,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ /** `vastgrid` (docs/cli.md): files in 0.2's grid in the browser, read by DuckDB on this computer. */
2
+ export { serve } from "./server.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,qGAAqG;AACrG,OAAO,EAAkC,KAAK,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,30 @@
1
+ export interface ServeOptions {
2
+ /** The files and folders to open: a folder gives its table files, up to two folders deep. */
3
+ paths: string[];
4
+ /** The port on 127.0.0.1; 0, the default, takes a free one. */
5
+ port?: number;
6
+ /** The token of the address: 16 or more letters, digits, `-` and `_`. A random one by default. */
7
+ token?: string;
8
+ /** Lets the page save written cells into the CSV and TSV files they came from (§7.5). Off by default. */
9
+ save?: boolean;
10
+ /** The folder that the page's names of files are relative to: the current one by default. */
11
+ cwd?: string;
12
+ /** Where the engine keeps its database and spilled data, in a folder of its own that it deletes when it stops. */
13
+ tempDirectory?: string;
14
+ /** The page: `dist/app.html` of the package by default. */
15
+ html?: () => Promise<string>;
16
+ /** Receives problems that no request caused. */
17
+ log?: (message: string, error?: unknown) => void;
18
+ }
19
+ export interface Served {
20
+ /** The address that opens the page, with the token. */
21
+ url: string;
22
+ port: number;
23
+ /** The files that the page lists, by the names that it opens them with. */
24
+ files(): Promise<string[]>;
25
+ /** Stops the server and the engine, which deletes its temporary folder. */
26
+ close(): Promise<void>;
27
+ }
28
+ /** Starts the engine and the server of the page, which shows the files of `paths`. */
29
+ export declare function serve(options: ServeOptions): Promise<Served>;
30
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,YAAY;IAC3B,6FAA6F;IAC7F,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,+DAA+D;IAC/D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kGAAkG;IAClG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yGAAyG;IACzG,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,6FAA6F;IAC7F,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kHAAkH;IAClH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7B,gDAAgD;IAChD,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;CAClD;AAED,MAAM,WAAW,MAAM;IACrB,uDAAuD;IACvD,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,KAAK,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC3B,2EAA2E;IAC3E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AA+DD,sFAAsF;AACtF,wBAAsB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAgHlE"}
package/dist/server.js ADDED
@@ -0,0 +1,191 @@
1
+ /**
2
+ * The server of `vastgrid` (docs/cli.md): the page, the list of the files that it opens, and the DuckDB engine over
3
+ * HTTP (docs/spec.md §8.8), on 127.0.0.1 only. The address that the command prints holds a token, which the page
4
+ * keeps in a cookie: other sites in the browser, other people on the computer and names that point at 127.0.0.1
5
+ * (DNS rebinding) reach nothing.
6
+ */
7
+ import { randomBytes, timingSafeEqual } from "node:crypto";
8
+ import { mkdirSync } from "node:fs";
9
+ import { readFile, stat } from "node:fs/promises";
10
+ import { createServer } from "node:http";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+ import { createHttpHandler, DuckDBEngine, findTableFiles, tableFormatOf } from "vastgrid-duckdb";
14
+ /** Files that the list of a folder holds at most. */
15
+ const LIST_LIMIT = 1000;
16
+ /**
17
+ * The page's script and styles are inside it, and it reaches nothing but this server; it is in no other site's
18
+ * frame.
19
+ */
20
+ const PAGE_HEADERS = {
21
+ "content-type": "text/html; charset=utf-8",
22
+ "cache-control": "no-store",
23
+ "content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; " +
24
+ "font-src data:; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
25
+ "x-content-type-options": "nosniff",
26
+ "referrer-policy": "no-referrer",
27
+ };
28
+ /** The page as the package builds it (`npm run build`), next to `src` and `dist`. */
29
+ async function builtHtml() {
30
+ return readFile(new URL("../dist/app.html", import.meta.url), "utf8");
31
+ }
32
+ function text(response, status, body) {
33
+ response.writeHead(status, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
34
+ response.end(body);
35
+ }
36
+ function sameText(a, b) {
37
+ const given = Buffer.from(a ?? "");
38
+ const expected = Buffer.from(b);
39
+ return given.length === expected.length && timingSafeEqual(given, expected);
40
+ }
41
+ function cookieOf(request, name) {
42
+ for (const part of String(request.headers.cookie ?? "").split(";")) {
43
+ const at = part.indexOf("=");
44
+ if (at > 0 && part.slice(0, at).trim() === name)
45
+ return part.slice(at + 1).trim();
46
+ }
47
+ return null;
48
+ }
49
+ function isInside(folder, file) {
50
+ const relative = path.relative(folder, file);
51
+ return relative !== "" && !path.isAbsolute(relative) && relative.split(path.sep)[0] !== "..";
52
+ }
53
+ /** The files and folders of `paths`, or why one of them cannot be opened. */
54
+ async function sortPaths(paths, cwd) {
55
+ const files = [];
56
+ const folders = [];
57
+ for (const named of paths) {
58
+ const full = path.resolve(cwd, named);
59
+ const found = await stat(full).catch(() => null);
60
+ if (!found)
61
+ throw new Error(`${named} does not exist.`);
62
+ if (found.isDirectory())
63
+ folders.push(full);
64
+ else if (tableFormatOf(full))
65
+ files.push(full);
66
+ else
67
+ throw new Error(`${named} is not a CSV, TSV, Parquet, JSON Lines or Excel file.`);
68
+ }
69
+ return { files: [...new Set(files)], folders: [...new Set(folders)] };
70
+ }
71
+ /** Starts the engine and the server of the page, which shows the files of `paths`. */
72
+ export async function serve(options) {
73
+ const cwd = path.resolve(options.cwd ?? process.cwd());
74
+ const token = options.token ?? randomBytes(24).toString("base64url");
75
+ if (!/^[\w-]{16,}$/.test(token))
76
+ throw new Error("The token is 16 or more letters, digits, - and _.");
77
+ const { files, folders } = await sortPaths(options.paths, cwd);
78
+ const nameOf = (file) => (isInside(cwd, file) ? path.relative(cwd, file).split(path.sep).join("/") : file);
79
+ // Only the files named, and the table files of the folders named, open; DuckDB reads nothing outside their folders.
80
+ const opens = (file) => files.includes(file) || (tableFormatOf(file) !== null && folders.some((folder) => isInside(folder, file)));
81
+ const tempDirectory = options.tempDirectory ?? path.join(os.tmpdir(), "vastgrid-cli");
82
+ mkdirSync(tempDirectory, { recursive: true });
83
+ const engine = await DuckDBEngine.create({
84
+ allowedDirectories: [...new Set([...folders, ...files.map((file) => path.dirname(file))])],
85
+ resolve: (ref) => {
86
+ const file = path.resolve(cwd, ref);
87
+ if (!opens(file))
88
+ throw new Error(`vastgrid was not started with ${ref}.`);
89
+ return file;
90
+ },
91
+ tempDirectory,
92
+ saveFiles: options.save ?? false,
93
+ log: options.log,
94
+ });
95
+ const list = async () => {
96
+ const found = await findTableFiles(folders, LIST_LIMIT);
97
+ return [...new Set([...files, ...found])].map(nameOf);
98
+ };
99
+ const html = options.html ?? builtHtml;
100
+ const api = createHttpHandler(engine, { path: "/api" });
101
+ let hosts = new Set();
102
+ let cookie = "";
103
+ const route = async (request, response) => {
104
+ // A name of another site that points at this computer is not this server's.
105
+ if (!hosts.has(String(request.headers.host))) {
106
+ text(response, 403, "vastgrid answers at 127.0.0.1 and localhost only.\n");
107
+ return;
108
+ }
109
+ const url = new URL(request.url ?? "/", "http://localhost");
110
+ if (url.pathname === "/" && url.searchParams.has("token")) {
111
+ if (!sameText(url.searchParams.get("token"), token)) {
112
+ text(response, 401, "This is not the address that vastgrid printed.\n");
113
+ return;
114
+ }
115
+ // The page keeps the token, which leaves the address.
116
+ url.searchParams.delete("token");
117
+ response.writeHead(303, {
118
+ location: `/${url.search}`,
119
+ "set-cookie": `${cookie}=${token}; HttpOnly; SameSite=Strict; Path=/`,
120
+ "cache-control": "no-store",
121
+ });
122
+ response.end();
123
+ return;
124
+ }
125
+ if (!sameText(cookieOf(request, cookie), token)) {
126
+ text(response, 401, "Open the address that vastgrid printed when it started.\n");
127
+ return;
128
+ }
129
+ if (url.pathname === "/" && request.method === "GET") {
130
+ const page = await html().catch(() => null);
131
+ if (page === null)
132
+ text(response, 500, "The page is not built: run `npm run build`.\n");
133
+ else {
134
+ response.writeHead(200, PAGE_HEADERS);
135
+ response.end(page);
136
+ }
137
+ }
138
+ else if (url.pathname === "/files" && request.method === "GET") {
139
+ response.writeHead(200, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
140
+ response.end(JSON.stringify({ files: await list(), save: options.save ?? false }));
141
+ }
142
+ else if (url.pathname === "/api" || url.pathname.startsWith("/api/")) {
143
+ await api(request, response);
144
+ }
145
+ else {
146
+ text(response, 404, "There is nothing at this path.\n");
147
+ }
148
+ };
149
+ const server = createServer((request, response) => {
150
+ route(request, response).catch((error) => {
151
+ options.log?.("A request failed", error);
152
+ if (response.headersSent)
153
+ response.destroy();
154
+ else
155
+ text(response, 500, "The request failed.\n");
156
+ });
157
+ });
158
+ try {
159
+ await new Promise((resolve, reject) => {
160
+ server.once("error", reject);
161
+ server.listen(options.port ?? 0, "127.0.0.1", resolve);
162
+ });
163
+ }
164
+ catch (error) {
165
+ await engine.dispose();
166
+ if (error.code === "EADDRINUSE")
167
+ throw new Error(`Port ${options.port} is in use.`);
168
+ throw error;
169
+ }
170
+ const { port } = server.address();
171
+ hosts = new Set([`127.0.0.1:${port}`, `localhost:${port}`]);
172
+ // Cookies are not kept apart by port: each server of the computer has its own.
173
+ cookie = `vastgrid-${port}`;
174
+ let closed = null;
175
+ return {
176
+ url: `http://127.0.0.1:${port}/?token=${token}`,
177
+ port,
178
+ files: list,
179
+ close() {
180
+ closed ??= (async () => {
181
+ const stopped = new Promise((resolve) => server.close(() => resolve()));
182
+ // Streams of events stay open until they are closed.
183
+ server.closeAllConnections();
184
+ await stopped;
185
+ await engine.dispose();
186
+ })();
187
+ return closed;
188
+ },
189
+ };
190
+ }
191
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,55 @@
1
1
  {
2
2
  "name": "vastgrid-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Open huge CSV, TSV, Parquet, JSON Lines and Excel files in a grid in the browser with one command, read by DuckDB on this computer: npx vastgrid-cli data.parquet",
5
+ "keywords": [
6
+ "csv",
7
+ "tsv",
8
+ "parquet",
9
+ "excel",
10
+ "xlsx",
11
+ "duckdb",
12
+ "viewer",
13
+ "cli",
14
+ "data grid"
15
+ ],
16
+ "license": "MIT",
17
+ "author": "yhay81",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/yhay81/vastgrid.git",
21
+ "directory": "packages/cli"
22
+ },
23
+ "homepage": "https://vastgrid.yhay81.com",
24
+ "bugs": "https://github.com/yhay81/vastgrid/issues",
25
+ "type": "module",
26
+ "sideEffects": false,
27
+ "bin": {
28
+ "vastgrid": "./dist/cli.js"
29
+ },
30
+ "exports": {
31
+ ".": {
32
+ "vastgrid-source": "./src/index.ts",
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ }
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "src"
40
+ ],
41
+ "engines": {
42
+ "node": ">=20"
43
+ },
44
+ "scripts": {
45
+ "build": "tsc -b && node scripts/build-app.mjs"
46
+ },
47
+ "dependencies": {
48
+ "vastgrid-duckdb": "0.2.0"
49
+ },
50
+ "devDependencies": {
51
+ "react": "19.3.1",
52
+ "react-dom": "19.3.1",
53
+ "vastgrid": "0.2.0"
54
+ }
55
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `vastgrid [--save] [--port <n>] [--no-open] [<file or folder>…]` (docs/cli.md): starts DuckDB and the page that
4
+ * shows the files in 0.2's grid, on 127.0.0.1, and opens the page in the browser. Ctrl+C stops both, and the engine
5
+ * deletes its temporary folder.
6
+ */
7
+ import { spawn } from "node:child_process";
8
+ import { parseArgs } from "node:util";
9
+ import { serve } from "./server.js";
10
+
11
+ const USAGE = `Usage: vastgrid [options] [<file or folder>…]
12
+
13
+ Opens CSV, TSV, Parquet, JSON Lines and Excel files in a grid in the browser, read by DuckDB on this computer.
14
+ A folder gives its files, two folders deep; the current folder when nothing is named.
15
+
16
+ Options:
17
+ --save Let the page save edited cells into CSV and TSV files (⌘S or Ctrl+S).
18
+ --port <n> The port on 127.0.0.1; a free one by default.
19
+ --no-open Print the address without opening the browser.
20
+ -h, --help Show this help.
21
+
22
+ VASTGRID_TOKEN sets the token in the address, instead of a random one.
23
+ `;
24
+
25
+ function fail(message: string, code: number): never {
26
+ process.stderr.write(`vastgrid: ${message}\n`);
27
+ process.exit(code);
28
+ }
29
+
30
+ let parsed: ReturnType<typeof readArgs>;
31
+ function readArgs() {
32
+ return parseArgs({
33
+ allowPositionals: true,
34
+ options: {
35
+ save: { type: "boolean" },
36
+ port: { type: "string" },
37
+ "no-open": { type: "boolean" },
38
+ help: { type: "boolean", short: "h" },
39
+ },
40
+ });
41
+ }
42
+ try {
43
+ parsed = readArgs();
44
+ } catch (error) {
45
+ fail(`${(error as Error).message}\nRun vastgrid --help for the options.`, 2);
46
+ }
47
+ const { values, positionals } = parsed;
48
+ if (values.help) {
49
+ process.stdout.write(USAGE);
50
+ process.exit(0);
51
+ }
52
+ const port = values.port === undefined ? 0 : Number(values.port);
53
+ if (!Number.isInteger(port) || port < 0 || port > 65535) fail(`${values.port} is not a port.`, 2);
54
+
55
+ /** Opens the address in the browser of the system; the address is printed either way. */
56
+ function openBrowser(url: string) {
57
+ const [command, args] =
58
+ process.platform === "darwin"
59
+ ? ["open", [url]]
60
+ : process.platform === "win32"
61
+ ? ["rundll32", ["url.dll,FileProtocolHandler", url]]
62
+ : ["xdg-open", [url]];
63
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
64
+ child.on("error", () => process.stderr.write("vastgrid: Open the address in a browser.\n"));
65
+ child.unref();
66
+ }
67
+
68
+ const served = await serve({
69
+ paths: positionals.length > 0 ? positionals : ["."],
70
+ port,
71
+ token: process.env.VASTGRID_TOKEN || undefined,
72
+ save: values.save ?? false,
73
+ log: (message, error) => process.stderr.write(`vastgrid: ${message}: ${String(error ?? "")}\n`),
74
+ }).catch((error: unknown) => fail(error instanceof Error ? error.message : String(error), 1));
75
+
76
+ const count = (await served.files()).length;
77
+ process.stdout.write(
78
+ `vastgrid shows ${count === 1 ? "1 file" : `${count} files`}${values.save ? ", and saves edited cells," : ""} at\n` +
79
+ ` ${served.url}\nPress Ctrl+C to stop.\n`,
80
+ );
81
+ if (!values["no-open"]) openBrowser(served.url);
82
+
83
+ let stopping = false;
84
+ function stop() {
85
+ // A second Ctrl+C does not wait.
86
+ if (stopping) process.exit(130);
87
+ stopping = true;
88
+ void served.close().finally(() => process.exit(0));
89
+ }
90
+ process.on("SIGINT", stop);
91
+ process.on("SIGTERM", stop);
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** `vastgrid` (docs/cli.md): files in 0.2's grid in the browser, read by DuckDB on this computer. */
2
+ export { type Served, type ServeOptions, serve } from "./server.js";