@diy-accounting-uk/diya-gl 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -0
- package/bin/diya-gl-mcp.js +10 -0
- package/bin/diya-gl-read-workbook.js +11 -0
- package/bin/diya-gl-recalc.js +10 -0
- package/bin/diya-gl-write-workbook.js +12 -0
- package/bin/diya-gl.js +24 -0
- package/bin/run-dist-bin.js +23 -0
- package/dist/app/bin/build-diya-gl-spec.js +680 -0
- package/dist/app/bin/build-packages.js +149 -0
- package/dist/app/bin/build-reconciliation-pages.js +1052 -0
- package/dist/app/bin/build-sitemaps.js +51 -0
- package/dist/app/bin/compliance-report.js +125 -0
- package/dist/app/bin/cross-package-reconciliation.js +92 -0
- package/dist/app/bin/diya-gl-mcp.js +27 -0
- package/dist/app/bin/export.js +318 -0
- package/dist/app/bin/extract-scenarios.js +1348 -0
- package/dist/app/bin/generate.js +353 -0
- package/dist/app/bin/judge-reconciliation.js +649 -0
- package/dist/app/bin/reconcile.js +247 -0
- package/dist/app/bin/report.js +291 -0
- package/dist/app/bin/verify-roundtrip.js +941 -0
- package/dist/app/bin/verify-stability.js +429 -0
- package/dist/app/bin/write-workbook.js +106 -0
- package/dist/app/data/filing/ct-computation-v1.1.toml +1033 -0
- package/dist/app/data/filing/ct600-v3.toml +2329 -0
- package/dist/app/data/filing/frs105-formats.toml +244 -0
- package/dist/app/data/fixture-master-gaps.json +247 -0
- package/dist/app/data/hmrc/SOURCE.md +14 -0
- package/dist/app/data/hmrc/form-layouts/bst.json +116 -0
- package/dist/app/data/hmrc/form-layouts/ltd.json +452 -0
- package/dist/app/data/hmrc/form-layouts/se.json +485 -0
- package/dist/app/data/hmrc/form-layouts/taxi.json +117 -0
- package/dist/app/data/hmrc/sa103-mtd-mapping.json +1078 -0
- package/dist/app/data/hmrc/sa103f_mapping_v3.csv +111 -0
- package/dist/app/data/judge-rubric.md +52 -0
- package/dist/app/data/ltd-2020.toml +49 -0
- package/dist/app/data/ltd-2021.toml +49 -0
- package/dist/app/data/ltd-2022.toml +49 -0
- package/dist/app/data/ltd-2023.toml +49 -0
- package/dist/app/data/ltd-2024.toml +49 -0
- package/dist/app/data/ltd-2025.toml +49 -0
- package/dist/app/data/ltd-2026.toml +53 -0
- package/dist/app/data/ltd-2027.toml +51 -0
- package/dist/app/data/render-unrepresentable/bst.json +24 -0
- package/dist/app/data/render-unrepresentable/ltd.json +863 -0
- package/dist/app/data/render-unrepresentable/se.json +455 -0
- package/dist/app/data/render-unrepresentable/taxi.json +16 -0
- package/dist/app/data/roundtrip-budget.json +38 -0
- package/dist/app/data/roundtrip-matrix-budget.json +26 -0
- package/dist/app/data/roundtrip-unrepresentable.json +401 -0
- package/dist/app/data/se-2020-2021.toml +52 -0
- package/dist/app/data/se-2021-2022.toml +52 -0
- package/dist/app/data/se-2022-2023.toml +55 -0
- package/dist/app/data/se-2023-2024.toml +54 -0
- package/dist/app/data/se-2024-2025.toml +51 -0
- package/dist/app/data/se-2025-2026.toml +51 -0
- package/dist/app/data/se-2026-2027.toml +51 -0
- package/dist/app/data/volatile-cells.json +16 -0
- package/dist/app/lib/anchors/bst.js +79 -0
- package/dist/app/lib/anchors/ltd.js +626 -0
- package/dist/app/lib/anchors/run.js +100 -0
- package/dist/app/lib/anchors/se.js +456 -0
- package/dist/app/lib/anchors/taxi.js +164 -0
- package/dist/app/lib/app-resources.js +80 -0
- package/dist/app/lib/book-checks/ltd.js +408 -0
- package/dist/app/lib/book-checks/se.js +687 -0
- package/dist/app/lib/book-checks/taxi.js +215 -0
- package/dist/app/lib/book-checks.js +872 -0
- package/dist/app/lib/books-engine.js +197 -0
- package/dist/app/lib/books-interchange.js +533 -0
- package/dist/app/lib/calculators/bst.js +381 -0
- package/dist/app/lib/calculators/ltd.js +1938 -0
- package/dist/app/lib/calculators/se.js +1239 -0
- package/dist/app/lib/calculators/shared.js +131 -0
- package/dist/app/lib/calculators/taxi.js +461 -0
- package/dist/app/lib/canonical-report-value.js +77 -0
- package/dist/app/lib/compliance-report.js +341 -0
- package/dist/app/lib/diya-gl-calculator.js +52 -0
- package/dist/app/lib/diya-gl-canonical.js +355 -0
- package/dist/app/lib/diya-gl-edits-ltd.js +144 -0
- package/dist/app/lib/diya-gl-edits.js +361 -0
- package/dist/app/lib/diya-gl-loader.js +774 -0
- package/dist/app/lib/diya-gl-schema.js +212 -0
- package/dist/app/lib/generator.js +1912 -0
- package/dist/app/lib/guide.js +33 -0
- package/dist/app/lib/headlines.js +293 -0
- package/dist/app/lib/link-caches.js +421 -0
- package/dist/app/lib/ltd-layout.js +139 -0
- package/dist/app/lib/mcp/diya-gl-tools.js +330 -0
- package/dist/app/lib/mcp/jsonrpc-stdio.js +66 -0
- package/dist/app/lib/mcp/server.js +65 -0
- package/dist/app/lib/overtype-sidecar.js +180 -0
- package/dist/app/lib/package-builder.js +202 -0
- package/dist/app/lib/payslips-date-chain.js +481 -0
- package/dist/app/lib/payslips-layout.js +235 -0
- package/dist/app/lib/period-shift.js +111 -0
- package/dist/app/lib/product-workbook.js +317 -0
- package/dist/app/lib/products.js +27 -0
- package/dist/app/lib/provenance-data.js +36 -0
- package/dist/app/lib/provenance.js +92 -0
- package/dist/app/lib/report-generator.js +374 -0
- package/dist/app/lib/report-indicators.js +468 -0
- package/dist/app/lib/report-serializer.js +392 -0
- package/dist/app/lib/scenario-extractor.js +1403 -0
- package/dist/app/lib/scenario-loader.js +84 -0
- package/dist/app/lib/sitemap-builder.js +59 -0
- package/dist/app/lib/spreadsheet-runner.js +566 -0
- package/dist/app/lib/tax/capital-allowances.js +77 -0
- package/dist/app/lib/tax/corporation-tax.js +144 -0
- package/dist/app/lib/tax/income-tax.js +122 -0
- package/dist/app/lib/tax/mileage.js +69 -0
- package/dist/app/lib/tax/national-insurance.js +31 -0
- package/dist/app/lib/tax/vat.js +188 -0
- package/dist/app/lib/tax-year.js +31 -0
- package/dist/app/lib/template-formula-map.js +253 -0
- package/dist/app/lib/workbook-set.js +153 -0
- package/dist/app/lib/xlsx-exporter.js +2815 -0
- package/dist/app/lib/xlsx-parts.js +111 -0
- package/dist/app/lib/xlsx-reader.js +84 -0
- package/dist/app/products/bst.js +727 -0
- package/dist/app/products/ltd.js +4270 -0
- package/dist/app/products/se.js +3403 -0
- package/dist/app/products/taxi.js +912 -0
- package/dist/app/templates/bst/bst-excel.xlsx +0 -0
- package/dist/app/templates/bst/meta.toml +27 -0
- package/dist/app/templates/ltd/Cashaccount.xlsx +0 -0
- package/dist/app/templates/ltd/Companysecretary.xlsx +0 -0
- package/dist/app/templates/ltd/Creditcardaccount.xlsx +0 -0
- package/dist/app/templates/ltd/Currentaccount.xlsx +0 -0
- package/dist/app/templates/ltd/Dividend Voucher.docx +0 -0
- package/dist/app/templates/ltd/Financialaccounts.xlsx +0 -0
- package/dist/app/templates/ltd/Fixedassets.xlsx +0 -0
- package/dist/app/templates/ltd/Payslips.xlsx +0 -0
- package/dist/app/templates/ltd/Purchases.xlsx +0 -0
- package/dist/app/templates/ltd/Sales.xlsx +0 -0
- package/dist/app/templates/ltd/Salesinvoice.xlsx +0 -0
- package/dist/app/templates/ltd/Savingaccount.xlsx +0 -0
- package/dist/app/templates/ltd/Vatreturns.xlsx +0 -0
- package/dist/app/templates/ltd/expensesform.xlsx +0 -0
- package/dist/app/templates/ltd/meta.toml +54 -0
- package/dist/app/templates/meta.toml +12 -0
- package/dist/app/templates/se/Bank.xlsx +0 -0
- package/dist/app/templates/se/Cash.xlsx +0 -0
- package/dist/app/templates/se/Financialaccounts.xlsx +0 -0
- package/dist/app/templates/se/Fixedassets.xlsx +0 -0
- package/dist/app/templates/se/Payslips.xlsx +0 -0
- package/dist/app/templates/se/Purchases.xlsx +0 -0
- package/dist/app/templates/se/Sales.xlsx +0 -0
- package/dist/app/templates/se/Salesinvoice.xlsx +0 -0
- package/dist/app/templates/se/Vat.xlsx +0 -0
- package/dist/app/templates/se/meta.toml +43 -0
- package/dist/app/templates/taxi/meta.toml +43 -0
- package/dist/app/templates/taxi/taxi-excel.xlsx +0 -0
- package/dist/web/spreadsheets.diyaccounting.co.uk/public/schema/diya-gl-book-v2.schema.json +713 -0
- package/dist/web/spreadsheets.diyaccounting.co.uk/public/schema/diya-gl-docs.md +930 -0
- package/dist/web/spreadsheets.diyaccounting.co.uk/public/schema/diya-gl-lines-v2.schema.json +220 -0
- package/package.json +37 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
// Copyright (C) 2025-2026 DIY Accounting Ltd
|
|
4
|
+
//
|
|
5
|
+
// build-sitemaps.js — Generate sitemap.xml for spreadsheets site
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// node app/bin/build-sitemaps.js
|
|
9
|
+
//
|
|
10
|
+
// Reads: web/spreadsheets.diyaccounting.co.uk/public/knowledge-base.toml
|
|
11
|
+
// web/spreadsheets.diyaccounting.co.uk/public/catalogue.toml
|
|
12
|
+
// Writes: web/spreadsheets.diyaccounting.co.uk/public/sitemap.xml
|
|
13
|
+
|
|
14
|
+
import { parse as parseTOML } from "smol-toml";
|
|
15
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync } from "fs";
|
|
16
|
+
import { resolve, dirname } from "path";
|
|
17
|
+
import { fileURLToPath } from "url";
|
|
18
|
+
import { buildSitemapXml } from "../lib/sitemap-builder.js";
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const ROOT = resolve(__dirname, "..", "..");
|
|
22
|
+
const PUBLIC_DIR = resolve(ROOT, "web", "spreadsheets.diyaccounting.co.uk", "public");
|
|
23
|
+
const KB_TOML = resolve(PUBLIC_DIR, "knowledge-base.toml");
|
|
24
|
+
const CATALOGUE_TOML = resolve(PUBLIC_DIR, "catalogue.toml");
|
|
25
|
+
const SITEMAP_PATH = resolve(PUBLIC_DIR, "sitemap.xml");
|
|
26
|
+
|
|
27
|
+
let articles = [];
|
|
28
|
+
if (existsSync(KB_TOML)) {
|
|
29
|
+
const kb = parseTOML(readFileSync(KB_TOML, "utf8"));
|
|
30
|
+
articles = kb.article || [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let products = [];
|
|
34
|
+
if (existsSync(CATALOGUE_TOML)) {
|
|
35
|
+
const cat = parseTOML(readFileSync(CATALOGUE_TOML, "utf8"));
|
|
36
|
+
products = cat.products || [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Reconciliation pages, discovered from the metadata files the page builder writes
|
|
40
|
+
const RECONCILIATION_DIR = resolve(PUBLIC_DIR, "reconciliation");
|
|
41
|
+
let reconciliationPages = [];
|
|
42
|
+
if (existsSync(RECONCILIATION_DIR)) {
|
|
43
|
+
reconciliationPages = readdirSync(RECONCILIATION_DIR)
|
|
44
|
+
.filter((f) => f.endsWith(".json"))
|
|
45
|
+
.map((f) => f.replace(/\.json$/, ""))
|
|
46
|
+
.sort();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { xml, urlCount } = buildSitemapXml(products, articles, reconciliationPages);
|
|
50
|
+
writeFileSync(SITEMAP_PATH, xml, "utf8");
|
|
51
|
+
console.log(`Spreadsheets sitemap: ${SITEMAP_PATH} (${urlCount} URLs)`);
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
// Copyright (C) 2026 DIY Accounting Ltd
|
|
4
|
+
//
|
|
5
|
+
// compliance-report.js — Generate markdown compliance report from existing test reports.
|
|
6
|
+
// Does NOT run tests — expects reports to already exist.
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// node app/bin/compliance-report.js --target URL [--output FILE]
|
|
10
|
+
|
|
11
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
12
|
+
import { join, resolve, dirname } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import {
|
|
15
|
+
parseNpmAudit,
|
|
16
|
+
parseEslintSecurity,
|
|
17
|
+
parsePa11yReport,
|
|
18
|
+
parseAxeResults,
|
|
19
|
+
parseLighthouseResults,
|
|
20
|
+
parseTextSpacingResults,
|
|
21
|
+
parseRetireResults,
|
|
22
|
+
generateReport,
|
|
23
|
+
} from "../lib/compliance-report.js";
|
|
24
|
+
|
|
25
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const projectRoot = resolve(__dirname, "..", "..");
|
|
27
|
+
|
|
28
|
+
const args = process.argv.slice(2);
|
|
29
|
+
const getArg = (name, defaultValue) => {
|
|
30
|
+
const idx = args.indexOf(name);
|
|
31
|
+
if (idx !== -1 && args[idx + 1]) return args[idx + 1];
|
|
32
|
+
return defaultValue;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const targetUrl = getArg("--target", null);
|
|
36
|
+
const outputFile = getArg("--output", "REPORT_ACCESSIBILITY_PENETRATION.md");
|
|
37
|
+
|
|
38
|
+
if (!targetUrl) {
|
|
39
|
+
console.error("Error: --target URL is required");
|
|
40
|
+
console.error("Usage: node app/bin/compliance-report.js --target URL [--output FILE]");
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const targetDir = join(projectRoot, "web/spreadsheets.diyaccounting.co.uk/public/tests");
|
|
45
|
+
const accessibilityDir = join(targetDir, "accessibility");
|
|
46
|
+
const penetrationDir = join(targetDir, "penetration");
|
|
47
|
+
|
|
48
|
+
function readJsonFile(path) {
|
|
49
|
+
try {
|
|
50
|
+
if (existsSync(path)) return JSON.parse(readFileSync(path, "utf8"));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.warn(`Warning: Could not parse ${path}: ${error.message}`);
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readTextFile(path) {
|
|
58
|
+
try {
|
|
59
|
+
if (existsSync(path)) return readFileSync(path, "utf8");
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.warn(`Warning: Could not read ${path}: ${error.message}`);
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getPackageVersion() {
|
|
67
|
+
const pkg = readJsonFile(join(projectRoot, "package.json"));
|
|
68
|
+
return pkg?.version || "unknown";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function main() {
|
|
72
|
+
console.log("Compliance Report Generator");
|
|
73
|
+
console.log("===========================");
|
|
74
|
+
console.log(`Target: ${targetUrl}`);
|
|
75
|
+
console.log(`Output: ${outputFile}`);
|
|
76
|
+
console.log("");
|
|
77
|
+
|
|
78
|
+
const reportFiles = [
|
|
79
|
+
join(penetrationDir, "npm-audit.json"),
|
|
80
|
+
join(penetrationDir, "eslint-security.txt"),
|
|
81
|
+
join(penetrationDir, "retire.json"),
|
|
82
|
+
join(accessibilityDir, "pa11y-report.txt"),
|
|
83
|
+
join(accessibilityDir, "axe-results.json"),
|
|
84
|
+
join(accessibilityDir, "axe-wcag22-results.json"),
|
|
85
|
+
join(accessibilityDir, "lighthouse-results.json"),
|
|
86
|
+
join(accessibilityDir, "text-spacing-results.json"),
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const sourceFiles = [];
|
|
90
|
+
let foundCount = 0;
|
|
91
|
+
for (const file of reportFiles) {
|
|
92
|
+
const exists = existsSync(file);
|
|
93
|
+
const relativePath = file.replace(projectRoot + "/", "");
|
|
94
|
+
console.log(` ${exists ? "+" : "-"} ${relativePath}`);
|
|
95
|
+
sourceFiles.push({ path: relativePath, exists });
|
|
96
|
+
if (exists) foundCount++;
|
|
97
|
+
}
|
|
98
|
+
console.log(`\nFound ${foundCount}/${reportFiles.length} report files.`);
|
|
99
|
+
|
|
100
|
+
if (foundCount === 0) {
|
|
101
|
+
console.error("\nError: No report files found. Run compliance tests first:");
|
|
102
|
+
console.error(" npm run compliance:ci-report");
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log("\nGenerating compliance report...");
|
|
107
|
+
const report = generateReport({
|
|
108
|
+
targetUrl,
|
|
109
|
+
version: getPackageVersion(),
|
|
110
|
+
sourceFiles,
|
|
111
|
+
npmAudit: parseNpmAudit(readJsonFile(join(penetrationDir, "npm-audit.json"))),
|
|
112
|
+
eslint: parseEslintSecurity(readTextFile(join(penetrationDir, "eslint-security.txt"))),
|
|
113
|
+
pa11y: parsePa11yReport(readTextFile(join(accessibilityDir, "pa11y-report.txt"))),
|
|
114
|
+
axe: parseAxeResults(readJsonFile(join(accessibilityDir, "axe-results.json"))),
|
|
115
|
+
axeWcag22: parseAxeResults(readJsonFile(join(accessibilityDir, "axe-wcag22-results.json"))),
|
|
116
|
+
lighthouse: parseLighthouseResults(readJsonFile(join(accessibilityDir, "lighthouse-results.json"))),
|
|
117
|
+
textSpacing: parseTextSpacingResults(readJsonFile(join(accessibilityDir, "text-spacing-results.json"))),
|
|
118
|
+
retire: parseRetireResults(readJsonFile(join(penetrationDir, "retire.json"))),
|
|
119
|
+
});
|
|
120
|
+
const outputPath = join(projectRoot, outputFile);
|
|
121
|
+
writeFileSync(outputPath, report);
|
|
122
|
+
console.log(`\nReport written to: ${outputPath}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
main();
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
// Copyright (C) 2026 DIY Accounting Ltd
|
|
4
|
+
//
|
|
5
|
+
// cross-package-reconciliation.js — Check that values which should be
|
|
6
|
+
// consistent between packages are consistent across reconciliation reports.
|
|
7
|
+
//
|
|
8
|
+
// For a non-VAT scenario with the same transactions, pre-tax profit should
|
|
9
|
+
// be identical in BST, SE, and Ltd (since amounts are face value, no VAT split).
|
|
10
|
+
//
|
|
11
|
+
// Usage:
|
|
12
|
+
// node app/bin/cross-package-reconciliation.js
|
|
13
|
+
// npm run cross-package-reconciliation
|
|
14
|
+
|
|
15
|
+
import { readFileSync, readdirSync, existsSync } from "fs";
|
|
16
|
+
import { resolve, dirname } from "path";
|
|
17
|
+
import { fileURLToPath } from "url";
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const REPORTS_DIR = resolve(__dirname, "..", "..", "reports");
|
|
21
|
+
|
|
22
|
+
if (!existsSync(REPORTS_DIR)) {
|
|
23
|
+
console.error("No reports/ directory. Run reconciliation first.");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const reports = readdirSync(REPORTS_DIR)
|
|
28
|
+
.filter((f) => f.endsWith(".md"))
|
|
29
|
+
.map((f) => {
|
|
30
|
+
const content = readFileSync(resolve(REPORTS_DIR, f), "utf-8");
|
|
31
|
+
return { name: f, content };
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
console.log(`Found ${reports.length} reconciliation reports`);
|
|
35
|
+
|
|
36
|
+
// Extract scenario name and key values from each report
|
|
37
|
+
function extractValues(content) {
|
|
38
|
+
const scenario = content.match(/Scenario: (.+)/)?.[1] || "";
|
|
39
|
+
const status = content.match(/Status: (.+)/)?.[1] || "";
|
|
40
|
+
|
|
41
|
+
// Extract cell values from appendix
|
|
42
|
+
const values = {};
|
|
43
|
+
const cellMatches = [...content.matchAll(/\| ([A-Z]+\d+) \| .+? \| ([^\|]+?) \|/g)];
|
|
44
|
+
for (const m of cellMatches) {
|
|
45
|
+
const val = parseFloat(m[2].trim());
|
|
46
|
+
if (!isNaN(val)) values[m[1]] = val;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { scenario, status, values };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Group reports by scenario
|
|
53
|
+
const byScenario = {};
|
|
54
|
+
for (const r of reports) {
|
|
55
|
+
const { scenario, status, values } = extractValues(r.content);
|
|
56
|
+
if (!byScenario[scenario]) byScenario[scenario] = [];
|
|
57
|
+
|
|
58
|
+
// Detect product from filename
|
|
59
|
+
let product = "unknown";
|
|
60
|
+
if (r.name.includes("Basic_Sole_Trader")) product = "bst";
|
|
61
|
+
else if (r.name.includes("Self_Employed")) product = "se";
|
|
62
|
+
else if (r.name.includes("Company")) product = "ltd";
|
|
63
|
+
else if (r.name.includes("Taxi")) product = "taxi";
|
|
64
|
+
|
|
65
|
+
byScenario[scenario].push({ name: r.name, product, status, values });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let failures = 0;
|
|
69
|
+
|
|
70
|
+
// For each scenario that appears in multiple products, check consistency
|
|
71
|
+
for (const [scenario, entries] of Object.entries(byScenario)) {
|
|
72
|
+
if (entries.length < 2) continue;
|
|
73
|
+
|
|
74
|
+
console.log(`\nScenario: ${scenario} (${entries.length} reports)`);
|
|
75
|
+
for (const e of entries) {
|
|
76
|
+
console.log(` ${e.product}: ${e.status}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check: all reports should RECONCILE
|
|
80
|
+
const anomalies = entries.filter((e) => e.status.includes("ANOMALY"));
|
|
81
|
+
if (anomalies.length > 0) {
|
|
82
|
+
console.log(` CROSS-CHECK FAIL: ${anomalies.length} anomalies detected`);
|
|
83
|
+
failures++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (failures > 0) {
|
|
88
|
+
console.log(`\n=== ${failures} cross-package failures ===`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
} else {
|
|
91
|
+
console.log(`\n=== All cross-package checks passed ===`);
|
|
92
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
// Copyright (C) 2026 DIY Accounting Ltd
|
|
4
|
+
//
|
|
5
|
+
// diya-gl-mcp.js — a stdio MCP server exposing the diya-gl pipeline as
|
|
6
|
+
// four tools: extract_book, report, edit_lines, save_workbook, over any of
|
|
7
|
+
// the four products (Basic Sole Trader, Taxi Driver, Self Employed, Company).
|
|
8
|
+
//
|
|
9
|
+
// Transport: hand-rolled newline-delimited JSON-RPC 2.0
|
|
10
|
+
// (app/lib/mcp/jsonrpc-stdio.js), not @modelcontextprotocol/sdk. The SDK's
|
|
11
|
+
// Node package pulls in express, hono, cors, jose and zod -- an HTTP
|
|
12
|
+
// server framework and an HTTP-transport dependency set -- to reach the
|
|
13
|
+
// three JSON-RPC methods a stdio-only server actually receives here
|
|
14
|
+
// (initialize, tools/list, tools/call). This repo carries no server
|
|
15
|
+
// framework at all (see CLAUDE.md's "what this repo does NOT have"): adding
|
|
16
|
+
// one to speak three methods over stdin/stdout would be exactly the heavy
|
|
17
|
+
// dependency the plan's own escape hatch names. The protocol surface this
|
|
18
|
+
// server implements is small enough to write directly and test without a
|
|
19
|
+
// mock of someone else's transport.
|
|
20
|
+
//
|
|
21
|
+
// Run directly: node app/bin/diya-gl-mcp.js
|
|
22
|
+
// Registered for this repo's own Claude Code sessions in .mcp.json.
|
|
23
|
+
|
|
24
|
+
import { createMethods } from "../lib/mcp/server.js";
|
|
25
|
+
import { serveJsonRpc } from "../lib/mcp/jsonrpc-stdio.js";
|
|
26
|
+
|
|
27
|
+
serveJsonRpc({ input: process.stdin, output: process.stdout, methods: createMethods() });
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
// Copyright (C) 2026 DIY Accounting Ltd
|
|
4
|
+
//
|
|
5
|
+
// export.js — Extract diya-gl data from a populated Excel package.
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// node app/bin/export.js --package bst --source-dir examples/bst-latest --output-dir /tmp/exported
|
|
9
|
+
// node app/bin/export.js --package se --source-dir examples/se-latest --output-dir /tmp/exported
|
|
10
|
+
// node app/bin/export.js --file my-file.xlsx
|
|
11
|
+
// node app/bin/export.js --file my-package.zip --output-dir /tmp/exported
|
|
12
|
+
// node app/bin/export.js --package se --file my-package.zip --output-dir /tmp/exported
|
|
13
|
+
//
|
|
14
|
+
// --source-dir reads a package the pipeline already unpacked into its own
|
|
15
|
+
// directory, and always needs --package to say which product's extractors
|
|
16
|
+
// to run. --file takes any of the kinds books-interchange.js reads -- a
|
|
17
|
+
// customer's own download, not a directory this repo laid out -- and writes
|
|
18
|
+
// beside that input unless --output-dir says otherwise; --package is
|
|
19
|
+
// optional there, since the file itself says which product it is (the
|
|
20
|
+
// anchor guard for a single workbook, the sheets a package zip carries for
|
|
21
|
+
// the rest). Given anyway, --package is checked against what the file
|
|
22
|
+
// sniffs as and a disagreement is refused by name.
|
|
23
|
+
//
|
|
24
|
+
// book.toml and lines.jsonl are written through app/lib/diya-gl-canonical.js,
|
|
25
|
+
// the one form D is compared in, so a re-ordered line, a re-ordered field or
|
|
26
|
+
// a formatting difference can never register as a data difference. The
|
|
27
|
+
// exported book is validated against the published v2 schemas before it is
|
|
28
|
+
// written. --file also emits report.json: R, computed by the JS engine from
|
|
29
|
+
// the D just extracted, through the same modules report.js --data uses, so
|
|
30
|
+
// one run is the whole extract-recalculate-report loop. Alongside it goes
|
|
31
|
+
// overtyped.json, every sum the template computes that this copy of it
|
|
32
|
+
// carries as a typed value instead -- present only when the input carried an
|
|
33
|
+
// actual workbook to compare against a computed value in the first place.
|
|
34
|
+
// --file also emits bookchecks.json: the book checks and warnings
|
|
35
|
+
// app/lib/book-checks.js runs over D, for whichever of the five kinds the
|
|
36
|
+
// input sniffed as -- the tax data behind the VAT threshold warning comes
|
|
37
|
+
// from loadTaxDataForBook, the book's own declared year read straight off
|
|
38
|
+
// app/data/, so the figure is real rather than a stand-in constant.
|
|
39
|
+
|
|
40
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
41
|
+
import { resolve, dirname, basename } from "path";
|
|
42
|
+
import { extractBook, extractLines } from "../lib/xlsx-exporter.js";
|
|
43
|
+
import { workbookSetFromDirectory } from "../lib/workbook-set.js";
|
|
44
|
+
import { canonicalBookToml, canonicalLinesJsonl } from "../lib/diya-gl-canonical.js";
|
|
45
|
+
import { stampBook } from "../lib/provenance.js";
|
|
46
|
+
import { validateBook, validateLines } from "../lib/diya-gl-schema.js";
|
|
47
|
+
import { extractTaxDataFromBook, diyaGlToScenario } from "../lib/diya-gl-loader.js";
|
|
48
|
+
import { calculateFromDiyaGl } from "../lib/diya-gl-calculator.js";
|
|
49
|
+
import { calculateExpectedTax } from "../lib/tax/income-tax.js";
|
|
50
|
+
import { buildReportDocument, serializeReportDocument } from "../lib/report-serializer.js";
|
|
51
|
+
import { runBookChecks, bookChecksJson } from "../lib/book-checks.js";
|
|
52
|
+
import { loadTaxDataForBook, productOf } from "../lib/product-workbook.js";
|
|
53
|
+
import {
|
|
54
|
+
readBookSource,
|
|
55
|
+
AnchorError,
|
|
56
|
+
XlsBookSourceError,
|
|
57
|
+
UnknownBookSourceError,
|
|
58
|
+
InvalidDiyaGlBookError,
|
|
59
|
+
InvalidDiyaGlJsonError,
|
|
60
|
+
PackagePartError,
|
|
61
|
+
ProductNotAvailableError,
|
|
62
|
+
} from "../lib/books-interchange.js";
|
|
63
|
+
import { PRODUCTS } from "../lib/products.js";
|
|
64
|
+
|
|
65
|
+
// A book read through --file whose sniffed product disagrees with the
|
|
66
|
+
// --package it was given, so the CLI reports it by name rather than
|
|
67
|
+
// building R for the wrong product.
|
|
68
|
+
export class PackageMismatchError extends Error {
|
|
69
|
+
constructor(given, actualProductName) {
|
|
70
|
+
super(`--package ${given} was given but the file is a ${actualProductName} workbook`);
|
|
71
|
+
this.name = "PackageMismatchError";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Every error a book source can refuse a read with, named rather than a
|
|
76
|
+
// stack trace: --file prints the message and exits, exactly as the anchor
|
|
77
|
+
// guard already did before books-interchange.js grew four more ways a file
|
|
78
|
+
// can be rejected.
|
|
79
|
+
const NAMED_BOOK_SOURCE_ERRORS = [
|
|
80
|
+
AnchorError,
|
|
81
|
+
XlsBookSourceError,
|
|
82
|
+
UnknownBookSourceError,
|
|
83
|
+
InvalidDiyaGlBookError,
|
|
84
|
+
InvalidDiyaGlJsonError,
|
|
85
|
+
PackagePartError,
|
|
86
|
+
ProductNotAvailableError,
|
|
87
|
+
PackageMismatchError,
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
function isNamedBookSourceError(err) {
|
|
91
|
+
return NAMED_BOOK_SOURCE_ERRORS.some((ErrorClass) => err instanceof ErrorClass);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseArgs(argv) {
|
|
95
|
+
const args = argv.slice(2);
|
|
96
|
+
const getArg = (name) => {
|
|
97
|
+
const idx = args.indexOf(name);
|
|
98
|
+
return idx !== -1 && args[idx + 1] ? args[idx + 1] : null;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const packageName = getArg("--package");
|
|
102
|
+
const sourceDir = getArg("--source-dir");
|
|
103
|
+
const file = getArg("--file");
|
|
104
|
+
const outputDir = getArg("--output-dir");
|
|
105
|
+
|
|
106
|
+
const usage =
|
|
107
|
+
"Usage: node app/bin/export.js --package <bst|taxi|se|ltd> --source-dir <path> --output-dir <path>\n" +
|
|
108
|
+
" or: node app/bin/export.js --file <path.xlsx|path.zip> [--package <bst|taxi|se|ltd>] [--output-dir <path>]";
|
|
109
|
+
|
|
110
|
+
if (!sourceDir && !file) {
|
|
111
|
+
console.error(usage);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
if (sourceDir && file) {
|
|
115
|
+
console.error("Error: --source-dir and --file are mutually exclusive");
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
if (sourceDir && !packageName) {
|
|
119
|
+
console.error(usage);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
if (sourceDir && !outputDir) {
|
|
123
|
+
console.error("Error: --output-dir is required with --source-dir");
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { packageName, sourceDir, file, outputDir };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// R for a (book, lines) pair: the JS engine's own D -> R loop --
|
|
131
|
+
// diyaGlToScenario -> calculateFromDiyaGl -> checkCompliance ->
|
|
132
|
+
// buildReportDocument -- the same loop report.js's --data mode runs, so the
|
|
133
|
+
// CLI's --file mode, report.js and the MCP server's tools never diverge on
|
|
134
|
+
// how R is built from the same D. Pure: no disk access, no console output.
|
|
135
|
+
export function buildFileReportDocument(book, lines, packageName, productMod) {
|
|
136
|
+
const taxData = extractTaxDataFromBook(book, packageName);
|
|
137
|
+
const scenario = diyaGlToScenario(book, lines, packageName);
|
|
138
|
+
const results = calculateFromDiyaGl(book, lines, packageName, taxData, scenario);
|
|
139
|
+
const mergedScenario = { ...scenario, ...scenario.expected };
|
|
140
|
+
const periodEnd = book.documentInfo?.periodCoveredEnd;
|
|
141
|
+
const yearEnd = periodEnd ? new Date(periodEnd).toISOString().slice(0, 10) : null;
|
|
142
|
+
const checks =
|
|
143
|
+
typeof productMod.checkCompliance === "function"
|
|
144
|
+
? productMod.checkCompliance({ ...results }, mergedScenario, taxData, calculateExpectedTax, yearEnd)
|
|
145
|
+
: [];
|
|
146
|
+
return buildReportDocument({
|
|
147
|
+
packageName,
|
|
148
|
+
engine: "js",
|
|
149
|
+
results,
|
|
150
|
+
productMod,
|
|
151
|
+
scenario: mergedScenario,
|
|
152
|
+
checks,
|
|
153
|
+
scenarioName: book.documentInfo?.entriesComment,
|
|
154
|
+
yearEnd,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function writeReportJson(outputDir, document) {
|
|
159
|
+
writeFileSync(resolve(outputDir, "report.json"), serializeReportDocument(document));
|
|
160
|
+
console.log(` report.json: ${document.values.length} values`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function writeOvertypedJson(outputDir, overtyped) {
|
|
164
|
+
writeFileSync(resolve(outputDir, "overtyped.json"), `${JSON.stringify(overtyped, null, 2)}\n`);
|
|
165
|
+
const count = Object.keys(overtyped).length;
|
|
166
|
+
console.log(` overtyped.json: ${count} ${count === 1 ? "cell" : "cells"} typed over a template formula`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// R's calculated cell map alone, the same D -> R half buildFileReportDocument
|
|
170
|
+
// runs before it builds a whole report document -- for a caller (the book
|
|
171
|
+
// checks, which only read a handful of R's cells for a warning like Ltd's
|
|
172
|
+
// distributable-profits one) that has no use for the rest of it.
|
|
173
|
+
export function calculatedResultsFor(book, lines, taxData) {
|
|
174
|
+
const packageName = productOf(book);
|
|
175
|
+
const scenario = diyaGlToScenario(book, lines, packageName);
|
|
176
|
+
return calculateFromDiyaGl(book, lines, packageName, taxData, scenario);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// The book checks and warnings, run over D with the book's own tax year's
|
|
180
|
+
// data behind the VAT threshold warning -- loadTaxDataForBook resolves that
|
|
181
|
+
// year from book.documentInfo alone, so this runs the same way whichever of
|
|
182
|
+
// the five kinds --file read the book from. Also carries R, so a
|
|
183
|
+
// product's own warning that reads the calculated accounts (Ltd's dividend
|
|
184
|
+
// warning against distributable profits) sees them here too, not just
|
|
185
|
+
// through the report.
|
|
186
|
+
async function writeBookChecksJson(outputDir, book, lines) {
|
|
187
|
+
const taxData = await loadTaxDataForBook(book);
|
|
188
|
+
const calculatedResults = calculatedResultsFor(book, lines, taxData);
|
|
189
|
+
const { results } = runBookChecks({ book, lines, taxData, results: calculatedResults });
|
|
190
|
+
writeFileSync(resolve(outputDir, "bookchecks.json"), bookChecksJson(results));
|
|
191
|
+
const failing = results.filter((r) => r.result === "fail").length;
|
|
192
|
+
console.log(` bookchecks.json: ${results.length} rules, ${failing} failing`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// The whole --file extraction: books-interchange.js sniffs the input and
|
|
196
|
+
// turns it into D (a workbook or a package zip runs the anchor guard and
|
|
197
|
+
// the extractors over a workbook set; a diya-gl zip, a JSON file or that
|
|
198
|
+
// JSON zipped validate straight against the published schemas), then this
|
|
199
|
+
// builds R from that D. --source-dir's CLI path and the MCP server's
|
|
200
|
+
// extract_book tool both call this rather than each reaching into
|
|
201
|
+
// books-interchange.js on their own, so a change to how D or R are produced
|
|
202
|
+
// can only ever happen in one place.
|
|
203
|
+
//
|
|
204
|
+
// The product is whichever of the four the file sniffs as; a caller that
|
|
205
|
+
// already knows which product it expects (--package, or the MCP tool's own
|
|
206
|
+
// argument) passes it as `product` and gets a named refusal when the file
|
|
207
|
+
// disagrees, rather than silently reading it as something it is not.
|
|
208
|
+
export async function extractBookFromFile(filePath, { product } = {}) {
|
|
209
|
+
const resolvedFile = resolve(filePath);
|
|
210
|
+
const bytes = readFileSync(resolvedFile);
|
|
211
|
+
const { product: sniffedProduct, book, lines, overtyped } = await readBookSource(bytes, basename(resolvedFile), { products: PRODUCTS });
|
|
212
|
+
if (product && product !== sniffedProduct) {
|
|
213
|
+
throw new PackageMismatchError(product, PRODUCTS[sniffedProduct].PRODUCT.name);
|
|
214
|
+
}
|
|
215
|
+
const productMod = PRODUCTS[sniffedProduct];
|
|
216
|
+
const document = buildFileReportDocument(book, lines, sniffedProduct, productMod);
|
|
217
|
+
return { product: sniffedProduct, book, lines, document, overtyped };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// The v2 schema validation, then book.toml + lines.jsonl written through
|
|
221
|
+
// diya-gl-canonical.js, shared by both --source-dir and --file so the two
|
|
222
|
+
// can only ever differ in how they got to (book, lines), never in how that
|
|
223
|
+
// pair reaches disk -- which is what the byte-for-byte verification rests on.
|
|
224
|
+
function writeDiyaGlData(outputDir, book, lines) {
|
|
225
|
+
const stamped = stampBook(book);
|
|
226
|
+
const bookErrors = validateBook(stamped);
|
|
227
|
+
if (!bookErrors.valid) {
|
|
228
|
+
console.error(`The exported book does not conform to the published v2 book schema:`);
|
|
229
|
+
for (const error of bookErrors.errors) console.error(` ${error}`);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
const lineErrors = validateLines(lines, stamped);
|
|
233
|
+
if (!lineErrors.valid) {
|
|
234
|
+
console.error(`The exported lines do not conform to the published v2 lines schema:`);
|
|
235
|
+
for (const error of lineErrors.errors.slice(0, 20)) console.error(` ${error}`);
|
|
236
|
+
if (lineErrors.errors.length > 20) console.error(` ... and ${lineErrors.errors.length - 20} more`);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
mkdirSync(outputDir, { recursive: true });
|
|
241
|
+
writeFileSync(resolve(outputDir, "lines.jsonl"), canonicalLinesJsonl(lines));
|
|
242
|
+
writeFileSync(resolve(outputDir, "book.toml"), canonicalBookToml(stamped));
|
|
243
|
+
|
|
244
|
+
console.log(` lines.jsonl: ${lines.length} entries`);
|
|
245
|
+
console.log(` book.toml: ${Object.keys(stamped).length} tables`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function runFileMode(filePath, outputDirArg, packageName) {
|
|
249
|
+
const resolvedFile = resolve(filePath);
|
|
250
|
+
const resolvedOutput = resolve(outputDirArg || dirname(resolvedFile));
|
|
251
|
+
|
|
252
|
+
console.log(`=== export.js ===`);
|
|
253
|
+
console.log(`File: ${resolvedFile}`);
|
|
254
|
+
console.log(`Output: ${resolvedOutput}`);
|
|
255
|
+
|
|
256
|
+
let extracted;
|
|
257
|
+
try {
|
|
258
|
+
extracted = await extractBookFromFile(resolvedFile, { product: packageName });
|
|
259
|
+
} catch (err) {
|
|
260
|
+
if (isNamedBookSourceError(err)) {
|
|
261
|
+
console.error(err.message);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
throw err;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const { product, book, lines, document, overtyped } = extracted;
|
|
268
|
+
console.log(`Package: ${product}`);
|
|
269
|
+
writeDiyaGlData(resolvedOutput, book, lines);
|
|
270
|
+
writeReportJson(resolvedOutput, document);
|
|
271
|
+
if (overtyped) writeOvertypedJson(resolvedOutput, overtyped);
|
|
272
|
+
await writeBookChecksJson(resolvedOutput, book, lines);
|
|
273
|
+
|
|
274
|
+
console.log(`\nExported ${lines.length} transactions to ${resolvedOutput}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function main() {
|
|
278
|
+
const { packageName, sourceDir, file, outputDir } = parseArgs(process.argv);
|
|
279
|
+
if (packageName && !PRODUCTS[packageName]) {
|
|
280
|
+
console.error(`Unknown package: ${packageName}. Available: ${Object.keys(PRODUCTS).join(", ")}`);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (file) {
|
|
285
|
+
await runFileMode(file, outputDir, packageName);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const productMod = PRODUCTS[packageName];
|
|
290
|
+
const resolvedSource = resolve(sourceDir);
|
|
291
|
+
const resolvedOutput = resolve(outputDir);
|
|
292
|
+
|
|
293
|
+
console.log(`=== export.js ===`);
|
|
294
|
+
console.log(`Package: ${packageName}`);
|
|
295
|
+
console.log(`Source: ${resolvedSource}`);
|
|
296
|
+
console.log(`Output: ${resolvedOutput}`);
|
|
297
|
+
|
|
298
|
+
const set = await workbookSetFromDirectory(resolvedSource);
|
|
299
|
+
if (set.names().length === 0) {
|
|
300
|
+
console.error(`No xlsx file found in ${resolvedSource}`);
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const lines = await extractLines(set, packageName);
|
|
305
|
+
const book = await extractBook(set, packageName, lines, productMod.CELL_MAP);
|
|
306
|
+
writeDiyaGlData(resolvedOutput, book, lines);
|
|
307
|
+
console.log(`\nExported ${lines.length} transactions to ${resolvedOutput}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Only run as a CLI when invoked directly; a caller importing
|
|
311
|
+
// extractBookFromFile or buildFileReportDocument (the MCP server does both)
|
|
312
|
+
// must never trigger a second, argv-driven run of this file's own main().
|
|
313
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
314
|
+
main().catch((err) => {
|
|
315
|
+
console.error(err);
|
|
316
|
+
process.exit(1);
|
|
317
|
+
});
|
|
318
|
+
}
|