From d4adfede0bdfdc40f1aafee0a555474ff6004646 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Sun, 1 Mar 2026 01:58:52 +0000 Subject: [PATCH 1/3] =?UTF-8?q?refactor:=20=F0=9F=94=A7=20replace=20proces?= =?UTF-8?q?s.exit=20with=20closeAndExit=20in=20command=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Updated multiple command files to use `closeAndExit` for graceful shutdown. * This change ensures that resources are properly released before exiting, preventing potential assertion failures on Windows. * Affected files include: base.ts, create.ts, delete.ts, diff.ts, export.ts, fmt.ts, graph.ts, import.ts, lint.ts, query.ts, read.ts, rename.ts, schema.ts, stats.ts, types.ts, update.ts, validate.ts, watch.ts. --- src/commands/base.ts | 5 ++-- src/commands/create.ts | 22 +++++++------- src/commands/delete.ts | 7 +++-- src/commands/diff.ts | 9 +++--- src/commands/export.ts | 6 ++-- src/commands/fmt.ts | 7 +++-- src/commands/graph.ts | 11 +++---- src/commands/import.ts | 17 ++++++----- src/commands/lint.ts | 5 ++-- src/commands/query.ts | 10 +++---- src/commands/read.ts | 7 +++-- src/commands/rename.ts | 9 +++--- src/commands/schema.ts | 9 +++--- src/commands/stats.ts | 5 ++-- src/commands/types.ts | 14 +++++---- src/commands/update.ts | 12 ++++---- src/commands/validate.ts | 5 ++-- src/commands/watch.ts | 3 +- src/utils.ts | 28 ++++++++++++++++++ .../invalid-collection/.mdbase/cache.sqlite | Bin 61440 -> 61440 bytes .../valid-collection/.mdbase/cache.sqlite | Bin 61440 -> 61440 bytes 21 files changed, 118 insertions(+), 73 deletions(-) diff --git a/src/commands/base.ts b/src/commands/base.ts index 7498348..62a48e9 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -8,6 +8,7 @@ import type { BaseFile } from "../base/parser.js"; import { executeBase, BaseExecutionError } from "../base/executor.js"; import { printResults } from "../base/formatter.js"; import type { OutputFormat } from "../base/formatter.js"; +import { closeAndExit } from "../utils.js"; export function registerBase(program: Command): void { const base = program @@ -64,7 +65,7 @@ export function registerBase(program: Command): void { }); printResults(results, opts.format as OutputFormat); - process.exit(0); + await closeAndExit(collection, 0); } catch (err) { if (err instanceof BaseExecutionError) { if (opts.format === "json") { @@ -72,7 +73,7 @@ export function registerBase(program: Command): void { } else { console.error(chalk.red(`error: ${err.message}`)); } - process.exit(err.code === "view_not_found" ? 1 : 1); + await closeAndExit(collection, err.code === "view_not_found" ? 1 : 1); } throw err; } diff --git a/src/commands/create.ts b/src/commands/create.ts index 9573049..7949584 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -4,7 +4,7 @@ import chalk from "chalk"; import { Collection } from "@callumalpass/mdbase"; import type { MdbaseError } from "@callumalpass/mdbase"; import yaml from "js-yaml"; -import { parseFieldValue } from "../utils.js"; +import { parseFieldValue, closeAndExit } from "../utils.js"; function parseFields(fieldArgs: string[]): Record { const frontmatter: Record = {}; @@ -47,7 +47,7 @@ export function registerCreate(program: Command): void { } else { console.error(chalk.red(`error: ${openResult.error.message}`)); } - process.exit(3); + await closeAndExit(null, 3); } const collection = openResult.collection!; @@ -86,7 +86,7 @@ export function registerCreate(program: Command): void { } const result = await collection.create(input); - outputResult(result, relativePath, opts); + await outputResult(result, relativePath, opts, collection); }); } @@ -103,17 +103,18 @@ function formatIssue(issue: MdbaseError): string { return ` ${tag}${field}: ${issue.message} ${chalk.dim(`[${issue.code}]`)}`; } -function outputResult( +async function outputResult( result: { valid?: boolean; frontmatter?: Record; body?: string; path?: string; error?: { code: string; message: string }; issues?: MdbaseError[] }, requestedPath: string | undefined, opts: { format: string }, -): void { + collection: { close(): Promise }, +): Promise { if (result.error) { const exitCode = result.error.code === "path_conflict" ? 1 : result.error.code === "unknown_type" ? 1 - : result.error.code === "validation_failed" ? 2 - : result.error.code === "permission_denied" ? 5 - : 1; + : result.error.code === "validation_failed" ? 2 + : result.error.code === "permission_denied" ? 5 + : 1; if (opts.format === "json") { const output: Record = { error: result.error }; @@ -129,7 +130,8 @@ function outputResult( } } } - process.exit(exitCode); + await closeAndExit(collection, exitCode); + return; } const outputPath = result.path ?? requestedPath; @@ -175,5 +177,5 @@ function outputResult( } } - process.exit(0); + await closeAndExit(collection, 0); } diff --git a/src/commands/delete.ts b/src/commands/delete.ts index bb05241..4af507c 100644 --- a/src/commands/delete.ts +++ b/src/commands/delete.ts @@ -3,6 +3,7 @@ import path from "node:path"; import chalk from "chalk"; import { Collection } from "@callumalpass/mdbase"; import yaml from "js-yaml"; +import { closeAndExit } from "../utils.js"; export function registerDelete(program: Command): void { program @@ -35,14 +36,14 @@ export function registerDelete(program: Command): void { if (result.error) { const exitCode = result.error.code === "file_not_found" ? 4 : result.error.code === "permission_denied" ? 5 - : 1; + : 1; if (opts.format === "json") { console.log(JSON.stringify({ error: result.error }, null, 2)); } else { console.error(chalk.red(`error: ${result.error.message}`)); } - process.exit(exitCode); + await closeAndExit(collection, exitCode); } const brokenLinks = result.broken_links ?? []; @@ -85,6 +86,6 @@ export function registerDelete(program: Command): void { } } - process.exit(0); + await closeAndExit(collection, 0); }); } diff --git a/src/commands/diff.ts b/src/commands/diff.ts index 17b6880..7bb7851 100644 --- a/src/commands/diff.ts +++ b/src/commands/diff.ts @@ -2,6 +2,7 @@ import { Command } from "commander"; import path from "node:path"; import chalk from "chalk"; import { Collection } from "@callumalpass/mdbase"; +import { closeAndExit } from "../utils.js"; interface FieldDiff { field: string; @@ -78,7 +79,7 @@ export function registerDiff(program: Command): void { } else { console.error(chalk.red(`error: ${pathA}: ${readA.error.message}`)); } - process.exit(4); + await closeAndExit(collection, 4); } if (readB.error) { if (opts.format === "json") { @@ -86,7 +87,7 @@ export function registerDiff(program: Command): void { } else { console.error(chalk.red(`error: ${pathB}: ${readB.error.message}`)); } - process.exit(4); + await closeAndExit(collection, 4); } const fmA = readA.frontmatter ?? {}; @@ -136,7 +137,7 @@ export function registerDiff(program: Command): void { } else { if (result.identical) { console.log(chalk.green("Files are identical")); - process.exit(0); + await closeAndExit(collection, 0); } console.log(`${chalk.bold(pathA)} ${chalk.dim("vs")} ${chalk.bold(pathB)}`); @@ -169,6 +170,6 @@ export function registerDiff(program: Command): void { } } - process.exit(0); + await closeAndExit(collection, 0); }); } diff --git a/src/commands/export.ts b/src/commands/export.ts index 7db6c58..929ff60 100644 --- a/src/commands/export.ts +++ b/src/commands/export.ts @@ -3,7 +3,7 @@ import fs from "node:fs"; import chalk from "chalk"; import { Collection } from "@callumalpass/mdbase"; import { stringify } from "csv-stringify/sync"; -import { splitList } from "../utils.js"; +import { splitList, closeAndExit } from "../utils.js"; function formatValue(value: unknown): string { if (value === null || value === undefined) return ""; @@ -54,7 +54,7 @@ export function registerExport(program: Command): void { } else { console.error(chalk.red(`error: ${queryResult.error.message}`)); } - process.exit(1); + await closeAndExit(collection, 1); } const results = queryResult.results as Array<{ @@ -120,6 +120,6 @@ export function registerExport(program: Command): void { console.log(output); } - process.exit(0); + await closeAndExit(collection, 0); }); } diff --git a/src/commands/fmt.ts b/src/commands/fmt.ts index 5d9b15c..5f72be4 100644 --- a/src/commands/fmt.ts +++ b/src/commands/fmt.ts @@ -6,6 +6,7 @@ import { Collection, loadConfig, loadTypes } from "@callumalpass/mdbase"; import type { TypeDefinition } from "@callumalpass/mdbase"; import yaml from "js-yaml"; import matter from "gray-matter"; +import { closeAndExit } from "../utils.js"; interface FmtFileResult { path: string; @@ -59,7 +60,7 @@ export function registerFmt(program: Command): void { } else { console.error(chalk.red(`error: ${queryResult.error.message}`)); } - process.exit(1); + await closeAndExit(collection, 1); } filePaths = (queryResult.results ?? []).map((r: { path: string }) => r.path); } @@ -122,9 +123,9 @@ export function registerFmt(program: Command): void { // In --check mode, exit 1 if any files need formatting if (opts.check && changedCount > 0) { - process.exit(1); + await closeAndExit(collection, 1); } - process.exit(0); + await closeAndExit(collection, 0); }); } diff --git a/src/commands/graph.ts b/src/commands/graph.ts index 01ae028..02f52a7 100644 --- a/src/commands/graph.ts +++ b/src/commands/graph.ts @@ -1,6 +1,7 @@ import { Command } from "commander"; import chalk from "chalk"; import { Collection } from "@callumalpass/mdbase"; +import { closeAndExit } from "../utils.js"; const WIKILINK_RE = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g; @@ -178,7 +179,7 @@ export function registerGraph(program: Command): void { } } } - process.exit(0); + await closeAndExit(openResult.collection!, 0); }); graph @@ -216,7 +217,7 @@ export function registerGraph(program: Command): void { } } } - process.exit(0); + await closeAndExit(openResult.collection!, 0); }); graph @@ -246,7 +247,7 @@ export function registerGraph(program: Command): void { } else { console.error(chalk.red(`error: ${readResult.error.message}`)); } - process.exit(4); + await closeAndExit(collection, 4); } const { incoming } = await buildGraph(collection); @@ -264,7 +265,7 @@ export function registerGraph(program: Command): void { } } } - process.exit(0); + await closeAndExit(collection, 0); }); graph @@ -317,6 +318,6 @@ export function registerGraph(program: Command): void { console.log(` ${chalk.dim("Components:")} ${result.connected_components}`); console.log(` ${chalk.dim("Density:")} ${result.density}`); } - process.exit(0); + await closeAndExit(openResult.collection!, 0); }); } diff --git a/src/commands/import.ts b/src/commands/import.ts index b803d9d..76ac685 100644 --- a/src/commands/import.ts +++ b/src/commands/import.ts @@ -4,6 +4,7 @@ import path from "node:path"; import chalk from "chalk"; import { Collection } from "@callumalpass/mdbase"; import { parse } from "csv-parse/sync"; +import { closeAndExit } from "../utils.js"; function coerceValue(value: string): unknown { if (value === "true") return true; @@ -54,7 +55,7 @@ export function registerImport(program: Command): void { } else { console.error(chalk.red(`error: file not found: ${file}`)); } - process.exit(4); + await closeAndExit(collection, 4); } const content = fs.readFileSync(csvPath, "utf-8"); @@ -102,7 +103,7 @@ export function registerImport(program: Command): void { } } - outputResult(result, opts.format); + outputResult(result, opts.format, collection); }); imp @@ -132,7 +133,7 @@ export function registerImport(program: Command): void { } else { console.error(chalk.red(`error: file not found: ${file}`)); } - process.exit(4); + await closeAndExit(collection, 4); } const content = fs.readFileSync(jsonPath, "utf-8"); @@ -145,7 +146,7 @@ export function registerImport(program: Command): void { } else { console.error(chalk.red("error: failed to parse JSON file")); } - process.exit(1); + await closeAndExit(collection, 1); return; } @@ -155,7 +156,7 @@ export function registerImport(program: Command): void { } else { console.error(chalk.red("error: JSON file must contain an array")); } - process.exit(1); + await closeAndExit(collection, 1); return; } @@ -204,11 +205,11 @@ export function registerImport(program: Command): void { } } - outputResult(result, opts.format); + outputResult(result, opts.format, collection); }); } -function outputResult(result: ImportResult, format: string): void { +async function outputResult(result: ImportResult, format: string, collection: { close(): Promise }): Promise { if (format === "json") { console.log(JSON.stringify(result, null, 2)); } else { @@ -228,5 +229,5 @@ function outputResult(result: ImportResult, format: string): void { } } - process.exit(result.failed > 0 ? 1 : 0); + await closeAndExit(collection, result.failed > 0 ? 1 : 0); } diff --git a/src/commands/lint.ts b/src/commands/lint.ts index 085ef91..edf5852 100644 --- a/src/commands/lint.ts +++ b/src/commands/lint.ts @@ -4,6 +4,7 @@ import chalk from "chalk"; import { Collection } from "@callumalpass/mdbase"; import type { MdbaseError } from "@callumalpass/mdbase"; import yaml from "js-yaml"; +import { closeAndExit } from "../utils.js"; interface LintIssue { path: string; @@ -78,7 +79,7 @@ export function registerLint(program: Command): void { } else { console.error(chalk.red(`error: ${queryResult.error.message}`)); } - process.exit(1); + await closeAndExit(collection, 1); } filePaths = (queryResult.results ?? []).map((r: { path: string }) => r.path); } @@ -160,7 +161,7 @@ export function registerLint(program: Command): void { } const hasErrors = result.summary.errors > 0; - process.exit(hasErrors ? 2 : 0); + await closeAndExit(collection, hasErrors ? 2 : 0); }); } diff --git a/src/commands/query.ts b/src/commands/query.ts index 6212c61..be184a5 100644 --- a/src/commands/query.ts +++ b/src/commands/query.ts @@ -2,7 +2,7 @@ import { Command } from "commander"; import chalk from "chalk"; import { Collection } from "@callumalpass/mdbase"; import Table from "cli-table3"; -import { splitList } from "../utils.js"; +import { splitList, closeAndExit } from "../utils.js"; interface ResultRow { path: string; @@ -118,7 +118,7 @@ export function registerQuery(program: Command): void { } else { console.error(chalk.red(`error: ${queryResult.error.message}`)); } - process.exit(1); + await closeAndExit(collection, 1); } const results = queryResult.results as Array<{ @@ -132,7 +132,7 @@ export function registerQuery(program: Command): void { // --count: just print the count if (opts.count) { console.log(String(queryResult.meta?.total_count ?? results.length)); - process.exit(0); + await closeAndExit(collection, 0); } if (results.length === 0) { @@ -141,7 +141,7 @@ export function registerQuery(program: Command): void { } else if (opts.format !== "paths" && opts.format !== "csv" && opts.format !== "jsonl") { console.error(chalk.dim("No results")); } - process.exit(0); + await closeAndExit(collection, 0); } // Collect formula names @@ -229,7 +229,7 @@ export function registerQuery(program: Command): void { } } - process.exit(0); + await closeAndExit(collection, 0); }); } diff --git a/src/commands/read.ts b/src/commands/read.ts index ab015ec..1d8a76f 100644 --- a/src/commands/read.ts +++ b/src/commands/read.ts @@ -3,6 +3,7 @@ import path from "node:path"; import chalk from "chalk"; import { Collection } from "@callumalpass/mdbase"; import yaml from "js-yaml"; +import { closeAndExit } from "../utils.js"; type ReadResultExtras = { warnings?: Array<{ code: string; message: string; field?: string }>; @@ -52,14 +53,14 @@ export function registerRead(program: Command): void { if (result.error) { const exitCode = result.error.code === "file_not_found" ? 4 : result.error.code === "permission_denied" ? 5 - : 1; + : 1; if (opts.format === "json") { console.log(JSON.stringify({ error: result.error }, null, 2)); } else { console.error(chalk.red(`error: ${result.error.message}`)); } - process.exit(exitCode); + await closeAndExit(collection, exitCode); } const frontmatter = opts.raw ? result.rawFrontmatter : result.frontmatter; @@ -147,6 +148,6 @@ export function registerRead(program: Command): void { } } - process.exit(0); + await closeAndExit(collection, 0); }); } diff --git a/src/commands/rename.ts b/src/commands/rename.ts index 3da6524..a00c666 100644 --- a/src/commands/rename.ts +++ b/src/commands/rename.ts @@ -3,6 +3,7 @@ import path from "node:path"; import chalk from "chalk"; import { Collection } from "@callumalpass/mdbase"; import yaml from "js-yaml"; +import { closeAndExit } from "../utils.js"; export function registerRename(program: Command): void { program @@ -46,23 +47,23 @@ export function registerRename(program: Command): void { // but some ref updates failed. Show success output with warnings. if (result.error.code === "rename_ref_update_failed") { outputSuccess(relativeFrom, relativeTo, result, opts.format); - process.exit(0); + await closeAndExit(collection, 0); } const exitCode = result.error.code === "file_not_found" ? 4 : result.error.code === "permission_denied" ? 5 - : 1; + : 1; if (opts.format === "json") { console.log(JSON.stringify({ error: result.error }, null, 2)); } else { console.error(chalk.red(`error: ${result.error.message}`)); } - process.exit(exitCode); + await closeAndExit(collection, exitCode); } outputSuccess(relativeFrom, relativeTo, result, opts.format); - process.exit(0); + await closeAndExit(collection, 0); }); } diff --git a/src/commands/schema.ts b/src/commands/schema.ts index 4013064..b9151f2 100644 --- a/src/commands/schema.ts +++ b/src/commands/schema.ts @@ -4,6 +4,7 @@ import path from "node:path"; import chalk from "chalk"; import { Collection, loadConfig } from "@callumalpass/mdbase"; import yaml from "js-yaml"; +import { closeAndExit } from "../utils.js"; interface InferredField { type: string; @@ -111,7 +112,7 @@ export function registerSchema(program: Command): void { }); if (queryResult.error) { console.error(chalk.red(`error: ${queryResult.error.message}`)); - process.exit(1); + await closeAndExit(collection, 1); } const files = queryResult.results as Array<{ @@ -129,7 +130,7 @@ export function registerSchema(program: Command): void { } else { console.log(chalk.dim("No untyped files found")); } - process.exit(0); + await closeAndExit(collection, 0); } // Group by field signature @@ -159,7 +160,7 @@ export function registerSchema(program: Command): void { } else { console.log(chalk.dim(`No field groups with >= ${minFiles} files found`)); } - process.exit(0); + await closeAndExit(collection, 0); } const inferredTypes: InferredType[] = []; @@ -253,6 +254,6 @@ export function registerSchema(program: Command): void { } } - process.exit(0); + await closeAndExit(collection, 0); }); } diff --git a/src/commands/stats.ts b/src/commands/stats.ts index b6f4b80..d8e737e 100644 --- a/src/commands/stats.ts +++ b/src/commands/stats.ts @@ -1,6 +1,7 @@ import { Command } from "commander"; import chalk from "chalk"; import { Collection } from "@callumalpass/mdbase"; +import { closeAndExit } from "../utils.js"; interface StatsResult { total_files: number; @@ -43,7 +44,7 @@ export function registerStats(program: Command): void { } else { console.error(chalk.red(`error: ${queryResult.error.message}`)); } - process.exit(1); + await closeAndExit(collection, 1); } const files = queryResult.results as Array<{ @@ -164,6 +165,6 @@ export function registerStats(program: Command): void { } } - process.exit(0); + await closeAndExit(collection, 0); }); } diff --git a/src/commands/types.ts b/src/commands/types.ts index f6c1fe6..91cd252 100644 --- a/src/commands/types.ts +++ b/src/commands/types.ts @@ -3,6 +3,7 @@ import chalk from "chalk"; import yaml from "js-yaml"; import { Collection, loadConfig, loadTypes, getType } from "@callumalpass/mdbase"; import type { FieldDefinition, TypeDefinition } from "@callumalpass/mdbase"; +import { closeAndExit } from "../utils.js"; function formatFieldType(field: FieldDefinition): string { let desc = field.type; @@ -282,7 +283,7 @@ export function registerTypes(program: Command): void { } else { console.error(chalk.red(`error: ${openResult.error.message}`)); } - process.exit(3); + await closeAndExit(null, 3); } const collection = openResult.collection!; @@ -294,7 +295,8 @@ export function registerTypes(program: Command): void { const parsed = parseFieldSpec(f); if (!parsed) { console.error(chalk.red(`error: invalid field format: ${f} (expected name:type or name:type:required)`)); - process.exit(1); + await closeAndExit(collection, 1); + return; } fields[parsed.name] = parsed.definition; } @@ -334,15 +336,15 @@ export function registerTypes(program: Command): void { if (result.error) { const exitCode = result.error.code === "path_conflict" ? 1 : result.error.code === "missing_parent_type" ? 1 - : result.error.code === "invalid_type_definition" ? 2 - : 1; + : result.error.code === "invalid_type_definition" ? 2 + : 1; if (opts.format === "json") { console.log(JSON.stringify({ error: result.error }, null, 2)); } else { console.error(chalk.red(`error: ${result.error.message}`)); } - process.exit(exitCode); + await closeAndExit(collection, exitCode); } const typeDef = result.type!; @@ -380,7 +382,7 @@ export function registerTypes(program: Command): void { } } - process.exit(0); + await closeAndExit(collection, 0); }); } diff --git a/src/commands/update.ts b/src/commands/update.ts index bfe6c63..9a3182c 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -3,7 +3,7 @@ import path from "node:path"; import chalk from "chalk"; import { Collection } from "@callumalpass/mdbase"; import yaml from "js-yaml"; -import { parseFieldValue } from "../utils.js"; +import { parseFieldValue, closeAndExit } from "../utils.js"; function parseFields(fieldArgs: string[]): Record { const fields: Record = {}; @@ -52,7 +52,7 @@ export function registerUpdate(program: Command): void { // Need at least one thing to update if (!opts.field && opts.body === undefined && !opts.bodyStdin) { console.error(chalk.red("error: nothing to update (provide --field or --body)")); - process.exit(1); + await closeAndExit(collection, 1); } // Parse field values @@ -89,15 +89,15 @@ export function registerUpdate(program: Command): void { if (result.error) { const exitCode = result.error.code === "file_not_found" ? 4 : result.error.code === "validation_failed" ? 2 - : result.error.code === "permission_denied" ? 5 - : 1; + : result.error.code === "permission_denied" ? 5 + : 1; if (opts.format === "json") { console.log(JSON.stringify({ error: result.error }, null, 2)); } else { console.error(chalk.red(`error: ${result.error.message}`)); } - process.exit(exitCode); + await closeAndExit(collection, exitCode); } switch (opts.format) { @@ -141,6 +141,6 @@ export function registerUpdate(program: Command): void { } } - process.exit(0); + await closeAndExit(collection, 0); }); } diff --git a/src/commands/validate.ts b/src/commands/validate.ts index bab06f1..7b6232d 100644 --- a/src/commands/validate.ts +++ b/src/commands/validate.ts @@ -3,6 +3,7 @@ import path from "node:path"; import chalk from "chalk"; import { Collection } from "@callumalpass/mdbase"; import type { MdbaseError } from "@callumalpass/mdbase"; +import { closeAndExit } from "../utils.js"; function formatIssue(issue: MdbaseError): string { const severity = issue.severity ?? "error"; @@ -87,7 +88,7 @@ export function registerValidate(program: Command): void { } } - process.exit(result.valid ? 0 : 2); + await closeAndExit(collection, result.valid ? 0 : 2); } else { // Validate specific files let allValid = true; @@ -152,7 +153,7 @@ export function registerValidate(program: Command): void { } } - process.exit(allValid ? 0 : 2); + await closeAndExit(collection, allValid ? 0 : 2); } }); } diff --git a/src/commands/watch.ts b/src/commands/watch.ts index 5b6fcb7..da492c1 100644 --- a/src/commands/watch.ts +++ b/src/commands/watch.ts @@ -2,6 +2,7 @@ import { Command } from "commander"; import chalk from "chalk"; import { Collection } from "@callumalpass/mdbase"; import { watch as chokidarWatch } from "chokidar"; +import { closeAndExit } from "../utils.js"; function timestamp(): string { return new Date().toISOString().replace("T", " ").replace(/\.\d+Z$/, ""); @@ -112,7 +113,7 @@ export function registerWatch(program: Command): void { } else { console.log(`\n${chalk.dim(timestamp())} ${chalk.dim("stopped")}`); } - process.exit(0); + closeAndExit(collection, 0); }); }; diff --git a/src/utils.ts b/src/utils.ts index 9ae8d6a..f10b25d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,31 @@ +/** + * Gracefully close a Collection and exit the process. + * + * On Windows, calling process.exit() while a Collection still holds open + * handles (sql.js WASM database, prepared statements) triggers a libuv + * assertion failure: + * + * Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), + * file src\win\async.c, line 76 + * + * Always close the collection before exiting to release all internal + * resources (flush the SQLite cache, free prepared statements, close the + * database) so the event loop can shut down cleanly. + */ +export async function closeAndExit( + collection: { close(): Promise } | null | undefined, + code: number, +): Promise { + if (collection) { + try { + await collection.close(); + } catch { + // Ignore cleanup errors — we're exiting anyway. + } + } + process.exit(code); +} + export function splitList(value: string | undefined): string[] | undefined { if (value === undefined) return undefined; const parts = value.split(",").map((s) => s.trim()).filter((s) => s.length > 0); diff --git a/test/fixtures/invalid-collection/.mdbase/cache.sqlite b/test/fixtures/invalid-collection/.mdbase/cache.sqlite index 6147c8e375f4e6afb2e115ffdc5e4dcdb4d6c74a..70dcd866c3d00d261a92ad4ae19b5a1dcc7ac014 100644 GIT binary patch delta 28 hcmZp8z})bFc|(vq%W}qrf|JAK!@-Qr8|D2k005i93Z(!5 delta 28 jcmZp8z})bFc|(vqi(vJ#=aa+a!-0&&vo>#(_rCxDr}zug diff --git a/test/fixtures/valid-collection/.mdbase/cache.sqlite b/test/fixtures/valid-collection/.mdbase/cache.sqlite index 8d8b06793c01e786344604a7739fc4ab1ac3f5dd..25af44625be918f5836ffe6f2ff405f54e39ec3b 100644 GIT binary patch delta 528 zcmZp8z})bFc>|LGTOWfI>no1Q90L06Hf(oTU$MR0I8lXla+0WzbR#Dtrz|TYL!+&= zU`A?APQG4l3gevRGSag7lRrvJaC(5m6vFe1ChLidvn-cW{XW@K+*~?9fDs0Oc7YZ><0y%B+0r9C2?i?_;SYj5-a+!sJlLaMx zfQ;F%Cnrie0vS`!PhKYJ1Z4DIo%{yO=<1woBxM8?X?`?0L&^`xs4bYhODY7&s8F6P zBpt-MT>4tMBG}YMX$>InA{&HvOj?a)x%8&0qNh~1$PCg?S z0+JF+1}n6ck78XeQ&|LG+dT#;HY>Kt90K~RJ#1F&dK)LIu})4B^d$ShU>qC|zV#GK3&g}nTd07(vI zRz`+WUct=Vg8ZTqT_e5R6vjDfET5m7PhKUatro46lvz3{(8Ve6p9g)?@=Qp2-d3N^BBq_pfJ|1a|cS@u@)W z5lMzQU~aL*ETDN2YbOgz`T!YWOC~2uIszFXlP52ebOJJhIw!vYGXm--8%Y@fMf@r! zXGr-089phKcS(f+8D8;|g`|UkK_kPV2sX7*S_8 Date: Sun, 1 Mar 2026 15:44:38 +0000 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20=F0=9F=94=A7=20replace=20proces?= =?UTF-8?q?s.exit=20with=20closeAndExit=20in=20command=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Updated `parseFields` function in `create.ts`, `query.ts`, and `update.ts` to use `await closeAndExit` instead of `process.exit`. * This change improves the handling of process termination by allowing for cleanup operations. --- src/commands/create.ts | 6 +++--- src/commands/query.ts | 2 +- src/commands/update.ts | 6 +++--- .../valid-collection/.mdbase/cache.sqlite | Bin 61440 -> 61440 bytes 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/commands/create.ts b/src/commands/create.ts index 7949584..ec62f8e 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -6,13 +6,13 @@ import type { MdbaseError } from "@callumalpass/mdbase"; import yaml from "js-yaml"; import { parseFieldValue, closeAndExit } from "../utils.js"; -function parseFields(fieldArgs: string[]): Record { +async function parseFields(fieldArgs: string[], collection: { close(): Promise } | null): Promise> { const frontmatter: Record = {}; for (const f of fieldArgs) { const eqIdx = f.indexOf("="); if (eqIdx === -1) { console.error(chalk.red(`error: invalid field format: ${f} (expected key=value)`)); - process.exit(1); + await closeAndExit(collection, 1); } const key = f.slice(0, eqIdx); const rawValue = f.slice(eqIdx + 1); @@ -52,7 +52,7 @@ export function registerCreate(program: Command): void { const collection = openResult.collection!; // Parse field values - const frontmatter = opts.field ? parseFields(opts.field as string[]) : {}; + const frontmatter = opts.field ? await parseFields(opts.field as string[], collection) : {}; // Read body from stdin if requested let body: string | undefined = opts.body; diff --git a/src/commands/query.ts b/src/commands/query.ts index be184a5..fba1c2c 100644 --- a/src/commands/query.ts +++ b/src/commands/query.ts @@ -95,7 +95,7 @@ export function registerQuery(program: Command): void { const eqIdx = f.indexOf("="); if (eqIdx === -1) { console.error(chalk.red(`error: invalid formula format: ${f} (expected name=expression)`)); - process.exit(1); + await closeAndExit(collection, 1); } formulas[f.slice(0, eqIdx)] = f.slice(eqIdx + 1); } diff --git a/src/commands/update.ts b/src/commands/update.ts index 9a3182c..cb8950a 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -5,13 +5,13 @@ import { Collection } from "@callumalpass/mdbase"; import yaml from "js-yaml"; import { parseFieldValue, closeAndExit } from "../utils.js"; -function parseFields(fieldArgs: string[]): Record { +async function parseFields(fieldArgs: string[], collection: { close(): Promise } | null): Promise> { const fields: Record = {}; for (const f of fieldArgs) { const eqIdx = f.indexOf("="); if (eqIdx === -1) { console.error(chalk.red(`error: invalid field format: ${f} (expected key=value)`)); - process.exit(1); + await closeAndExit(collection, 1); } const key = f.slice(0, eqIdx); const rawValue = f.slice(eqIdx + 1); @@ -56,7 +56,7 @@ export function registerUpdate(program: Command): void { } // Parse field values - const fields = opts.field ? parseFields(opts.field as string[]) : undefined; + const fields = opts.field ? await parseFields(opts.field as string[], collection) : undefined; // Read body from stdin if requested let body: string | undefined = opts.body; diff --git a/test/fixtures/valid-collection/.mdbase/cache.sqlite b/test/fixtures/valid-collection/.mdbase/cache.sqlite index 25af44625be918f5836ffe6f2ff405f54e39ec3b..bcc6834acf494531c7cddaa8852a7111bdfc39c2 100644 GIT binary patch delta 402 zcmZp8z})bFc>}8e6PwLsb^${+b{)1`>~C9~SFM0AC5dCxiWFskKX2UNElQX3JnGN54oxDpbl-cl= z++-o?U>3u&D~e#xH%e==7#7(;ILD;bnGJIrCV!B2WH!u*0Me3^ePy&+43i!~6ifke xf@gVvwceKTXEF3WArI!b$*Qp!dZem=IGa0U1(;Y2pPWbrO98`o^I3W03jhq;fy@8^ delta 384 zcmZp8z})bFc>}8e6YHzV>;i^tY~~2 zv&cxx!UQ;F0;D*USs58hc?C0b3-XIfbdB|LQyAwgms9<2K6#ax)?@{7-pRIN;>>cY z&nAb9nai;2G57&B6v8zaK{fDs7*F;P*Jf8rN-arLs-3JOE&lUIrvGAeDZ7N5+>EVD3hvY@0dv&`(*lM^MKm}RD(pS(=cnOUa)>f|>dx~p@t zk(4pBO!K438B+etGPMPhcS(gZ%Ty>&7LpETk-k>02=;2Dv?h!6MK%cMn6x^x^r=ac zKS(<=OCR+D(vp*XWwcqO_g{f1m;&Oglk@;usRtmbzjtI^D}v=3jmxYdmR7( From c16d21516ac0e57967d7061d21b3539da451b5c1 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Sun, 1 Mar 2026 12:17:38 -0800 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20=F0=9F=94=A7=20update=20exit=20?= =?UTF-8?q?handling=20in=20`closeAndExit`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace direct `process.exit()` call with a delayed exit to allow libuv close-callbacks to complete. - Ensure `closeAndExit` returns a `Promise` to prevent further execution after calling. --- src/cli.ts | 2 +- src/utils.ts | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 9b13603..1d75b04 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -61,7 +61,7 @@ function outputCollectionOptionError(command: Command, code: string, message: st } const exitCode = code === "path_not_found" ? 4 : code === "missing_config" || code === "invalid_config" ? 3 - : 1; + : 1; process.exit(exitCode); } diff --git a/src/utils.ts b/src/utils.ts index f10b25d..4b7ad63 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -11,6 +11,12 @@ * Always close the collection before exiting to release all internal * resources (flush the SQLite cache, free prepared statements, close the * database) so the event loop can shut down cleanly. + * + * After closing, we schedule process.exit() on a short timer so that + * any pending libuv close-callbacks (e.g. the uv_async_t torn down by + * the WASM runtime) can fire before the process terminates. The function + * itself never resolves, so callers see it as Promise and do not + * execute any code after it. */ export async function closeAndExit( collection: { close(): Promise } | null | undefined, @@ -23,7 +29,12 @@ export async function closeAndExit( // Ignore cleanup errors — we're exiting anyway. } } - process.exit(code); + process.exitCode = code; + // Schedule process.exit() after a short delay so libuv close-callbacks + // can finish before the handle list is torn down. + setTimeout(() => process.exit(code), 50); + // Never resolve — the timer above will terminate the process. + return new Promise(() => { }); } export function splitList(value: string | undefined): string[] | undefined {