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/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..ec62f8e 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -4,15 +4,15 @@ 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 { +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); @@ -47,12 +47,12 @@ 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!; // 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; @@ -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..fba1c2c 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; @@ -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); } @@ -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..cb8950a 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -3,15 +3,15 @@ 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 { +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); @@ -52,11 +52,11 @@ 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 - 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; @@ -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..4b7ad63 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,42 @@ +/** + * 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. + * + * 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, + code: number, +): Promise { + if (collection) { + try { + await collection.close(); + } catch { + // Ignore cleanup errors — we're exiting anyway. + } + } + 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 { 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 6147c8e..70dcd86 100644 Binary files a/test/fixtures/invalid-collection/.mdbase/cache.sqlite and b/test/fixtures/invalid-collection/.mdbase/cache.sqlite differ diff --git a/test/fixtures/valid-collection/.mdbase/cache.sqlite b/test/fixtures/valid-collection/.mdbase/cache.sqlite index 8d8b067..bcc6834 100644 Binary files a/test/fixtures/valid-collection/.mdbase/cache.sqlite and b/test/fixtures/valid-collection/.mdbase/cache.sqlite differ