@gitpagedocs/cli 1.1.67 → 1.1.69
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/ai/application/ai-command.ts +1 -1
- package/ai/application/docs-pattern.ts +9 -5
- package/ai/application/resolve-chat-credentials.ts +13 -6
- package/ai/core/models/parse-ai-cli-config.ts +6 -8
- package/application/home/handler.mjs +3 -1
- package/application/ports/cli-runtime-ports.ts +2 -0
- package/application/report/config-only-reporter.mjs +9 -5
- package/builders/config-orchestrator.mjs +7 -6
- package/builders/language-bundles-builder.mjs +24 -0
- package/builders/root-config-builder.mjs +1 -6
- package/content/docs-en.mjs +11 -4
- package/content/docs-es.mjs +11 -4
- package/content/docs-pt.mjs +11 -4
- package/contracts/doc-versions.mjs +1 -1
- package/contracts/langs-paths.mjs +28 -0
- package/contracts/languages.mjs +6 -0
- package/contracts/layouts-paths.mjs +4 -4
- package/contracts/path-segments.d.mts +2 -0
- package/contracts/path-segments.mjs +15 -0
- package/data/default-site-config.mjs +9 -2
- package/domain/services/layouts-migration.mjs +15 -1
- package/home/home-file-writer.mjs +2 -3
- package/home/templates.mjs +5 -2
- package/infrastructure/home/runtime.mjs +2 -2
- package/package.json +3 -3
- package/presentation/commands/config-info.ts +54 -6
- package/presentation/commands/diagnostics.ts +30 -20
- package/presentation/commands/docs.ts +7 -2
- package/presentation/commands/pages.ts +4 -2
- package/presentation/options/parser.ts +46 -45
- package/presentation/ui/prompts.ts +6 -5
- package/runtime/doc-path-resolver.mjs +8 -6
- package/runtime/exec.mjs +168 -0
- package/runtime/git-ops.mjs +22 -20
- package/runtime/layouts-migrator.mjs +15 -1
- package/runtime/output.mjs +99 -57
- package/runtime/workflow.mjs +3 -1
- package/start.mjs +8 -6
package/runtime/git-ops.mjs
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { runExecutable, runExecutableCapture } from "./exec.mjs";
|
|
4
4
|
|
|
5
5
|
/** Detect { owner, repo } from the git `origin` remote URL, or null. */
|
|
6
6
|
export function detectRepoFromGit(root) {
|
|
7
7
|
try {
|
|
8
|
-
const url =
|
|
8
|
+
const url = runExecutableCapture("git", ["remote", "get-url", "origin"], { cwd: root });
|
|
9
9
|
const match = url.match(/[:/]([^/:]+)\/([^/]+?)(?:\.git)?$/);
|
|
10
10
|
if (match) return { owner: match[1], repo: match[2] };
|
|
11
11
|
} catch {
|
|
12
|
-
// no remote / not a repo
|
|
12
|
+
// no remote / not a repo / git not installed
|
|
13
13
|
}
|
|
14
14
|
return null;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export function tryConfigurePagesToGitHubActions(owner, repo, branch, root) {
|
|
18
18
|
try {
|
|
19
|
-
|
|
19
|
+
runExecutable("gh", ["--version"], { cwd: root, stdio: "ignore" });
|
|
20
20
|
} catch {
|
|
21
21
|
console.warn(
|
|
22
22
|
"GitHub CLI (gh) not found. Could not auto-configure Pages source. Set GitHub Pages source to 'GitHub Actions' manually in repository settings.",
|
|
@@ -24,16 +24,19 @@ export function tryConfigurePagesToGitHubActions(owner, repo, branch, root) {
|
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// Each candidate is an argv array: owner/repo/branch travel as discrete
|
|
28
|
+
// arguments, never through a shell string.
|
|
29
|
+
const pagesEndpoint = `repos/${owner}/${repo}/pages`;
|
|
27
30
|
const candidates = [
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
["api", "-X", "PUT", pagesEndpoint, "-f", "build_type=workflow"],
|
|
32
|
+
["api", "-X", "POST", pagesEndpoint, "-f", "build_type=workflow"],
|
|
33
|
+
["api", "-X", "POST", pagesEndpoint, "-f", `source[branch]=${branch}`, "-f", "source[path]=/"],
|
|
34
|
+
["api", "-X", "PUT", pagesEndpoint, "-f", `source[branch]=${branch}`, "-f", "source[path]=/", "-f", "build_type=workflow"],
|
|
32
35
|
];
|
|
33
36
|
|
|
34
|
-
for (const
|
|
37
|
+
for (const args of candidates) {
|
|
35
38
|
try {
|
|
36
|
-
|
|
39
|
+
runExecutable("gh", args, { cwd: root, stdio: "ignore" });
|
|
37
40
|
console.log("GitHub Pages source configured for GitHub Actions.");
|
|
38
41
|
return;
|
|
39
42
|
} catch {
|
|
@@ -48,7 +51,7 @@ export function tryConfigurePagesToGitHubActions(owner, repo, branch, root) {
|
|
|
48
51
|
|
|
49
52
|
export function getCurrentGitBranch(root) {
|
|
50
53
|
try {
|
|
51
|
-
const branch =
|
|
54
|
+
const branch = runExecutableCapture("git", ["branch", "--show-current"], { cwd: root });
|
|
52
55
|
return branch || "main";
|
|
53
56
|
} catch {
|
|
54
57
|
return "main";
|
|
@@ -67,17 +70,16 @@ export function runGitPushForGeneratedArtifacts(options, root, sanitizeSegment)
|
|
|
67
70
|
|
|
68
71
|
const repoUrl = `https://github.com/${owner}/${repo}.git`;
|
|
69
72
|
try {
|
|
70
|
-
|
|
73
|
+
runExecutable("git", ["remote", "get-url", "origin"], { cwd: root, stdio: "ignore" });
|
|
71
74
|
} catch {
|
|
72
|
-
|
|
75
|
+
runExecutable("git", ["remote", "add", "origin", repoUrl], { cwd: root, stdio: "inherit" });
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
|
|
78
|
+
runExecutable("git", ["add", "gitpagedocs", ".github/workflows/gitpagedocs-pages.yml"], { cwd: root, stdio: "inherit" });
|
|
76
79
|
|
|
77
80
|
let hasStagedChanges = false;
|
|
78
81
|
try {
|
|
79
|
-
|
|
80
|
-
hasStagedChanges = false;
|
|
82
|
+
runExecutable("git", ["diff", "--cached", "--quiet"], { cwd: root, stdio: "ignore" });
|
|
81
83
|
} catch {
|
|
82
84
|
hasStagedChanges = true;
|
|
83
85
|
}
|
|
@@ -86,11 +88,11 @@ export function runGitPushForGeneratedArtifacts(options, root, sanitizeSegment)
|
|
|
86
88
|
return;
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
|
|
91
|
+
runExecutable("git", ["commit", "-m", "chore: setup gitpagedocs pages workflow"], { cwd: root, stdio: "inherit" });
|
|
90
92
|
|
|
91
93
|
const currentBranch = getCurrentGitBranch(root);
|
|
92
94
|
try {
|
|
93
|
-
|
|
95
|
+
runExecutable("git", ["push", "-u", "origin", currentBranch], { cwd: root, stdio: "inherit" });
|
|
94
96
|
tryConfigurePagesToGitHubActions(owner, repo, currentBranch, root);
|
|
95
97
|
return;
|
|
96
98
|
} catch {
|
|
@@ -98,8 +100,8 @@ export function runGitPushForGeneratedArtifacts(options, root, sanitizeSegment)
|
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
runExecutable("git", ["pull", "--rebase", "origin", currentBranch], { cwd: root, stdio: "inherit" });
|
|
104
|
+
runExecutable("git", ["push", "-u", "origin", currentBranch], { cwd: root, stdio: "inherit" });
|
|
103
105
|
} catch {
|
|
104
106
|
throw new Error(
|
|
105
107
|
`Failed to push after automatic rebase on branch '${currentBranch}'. Resolve conflicts and run 'git push -u origin ${currentBranch}' manually.`,
|
|
@@ -48,7 +48,21 @@ export function listFilesRecursively(absoluteDir) {
|
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
walk(absoluteDir, "");
|
|
51
|
-
return found.sort();
|
|
51
|
+
return found.sort(compareCodeUnits);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Plain UTF-16 code-unit order (what `sort()` does by default), spelled out so
|
|
56
|
+
* the listing never depends on the runtime locale.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} a
|
|
59
|
+
* @param {string} b
|
|
60
|
+
* @returns {number}
|
|
61
|
+
*/
|
|
62
|
+
function compareCodeUnits(a, b) {
|
|
63
|
+
if (a < b) return -1;
|
|
64
|
+
if (a > b) return 1;
|
|
65
|
+
return 0;
|
|
52
66
|
}
|
|
53
67
|
|
|
54
68
|
/**
|
package/runtime/output.mjs
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
normalizeToOutputPath,
|
|
10
10
|
} from "./doc-path-resolver.mjs";
|
|
11
11
|
import { layoutsArtifactPaths } from "../contracts/layouts-paths.mjs";
|
|
12
|
+
import { languageArtifactPaths } from "../contracts/langs-paths.mjs";
|
|
13
|
+
import { SUPPORTED_LANGUAGES } from "../contracts/languages.mjs";
|
|
12
14
|
|
|
13
15
|
const DEFAULT_ICON_SVG = `<?xml version="1.0" encoding="utf-8"?>
|
|
14
16
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
@@ -29,7 +31,7 @@ function listExistingVersionIds(root, outputDir) {
|
|
|
29
31
|
function removeLegacySourceViewerFiles(root, outputDir) {
|
|
30
32
|
const versionIds = listExistingVersionIds(root, outputDir);
|
|
31
33
|
for (const versionId of versionIds) {
|
|
32
|
-
for (const language of
|
|
34
|
+
for (const language of SUPPORTED_LANGUAGES) {
|
|
33
35
|
for (const fileName of ["source-viewer", "source-viewer.html"]) {
|
|
34
36
|
const legacyPath = path.join(root, outputDir, "docs", "versions", versionId, language, fileName);
|
|
35
37
|
if (existsSync(legacyPath)) {
|
|
@@ -75,10 +77,101 @@ async function writeLayoutArtifacts(options) {
|
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Write one UI-strings bundle per shipped language. Which ones are on is
|
|
82
|
+
* `site.languages` in config.json, so the 1.1.68 `langs.json` manifest is
|
|
83
|
+
* removed: a stale copy would only mislead.
|
|
84
|
+
*/
|
|
85
|
+
async function writeLanguageArtifacts(root, outputDir, artifacts) {
|
|
86
|
+
const paths = languageArtifactPaths(outputDir);
|
|
87
|
+
rmSync(path.join(root, paths.legacyManifest), { force: true });
|
|
88
|
+
for (const [language, bundle] of Object.entries(artifacts.languageBundles)) {
|
|
89
|
+
await writeJson(root, paths.bundle(language), bundle);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Keep only versioned docs output in docs/, removing legacy root language folders. */
|
|
94
|
+
function removeLegacyLanguageDirs(root, outputDir) {
|
|
95
|
+
for (const legacyLanguageDir of SUPPORTED_LANGUAGES) {
|
|
96
|
+
const legacyPath = path.join(root, outputDir, "docs", legacyLanguageDir);
|
|
97
|
+
if (existsSync(legacyPath)) {
|
|
98
|
+
rmSync(legacyPath, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function writeVersionConfigs(root, outputDir, versionConfigs) {
|
|
104
|
+
for (const [versionId, versionConfig] of Object.entries(versionConfigs)) {
|
|
105
|
+
await writeJson(root, `${outputDir}/docs/versions/${versionId}/config.json`, versionConfig);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function writeMarkdownDoc(root, outputDir, versionId, docPath, docs) {
|
|
110
|
+
const key = parseDocFileToKey(path.basename(docPath));
|
|
111
|
+
const language = extractLanguageFromPath(docPath);
|
|
112
|
+
const content = key && language ? docs?.[language]?.[key] : undefined;
|
|
113
|
+
if (!content) return;
|
|
114
|
+
const versionedContent = withVersionBadge(content, versionId, language);
|
|
115
|
+
await writeText(root, normalizeToOutputPath(outputDir, docPath), versionedContent);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function writeVersionMarkdownDocs(root, outputDir, versionId, versionConfig, docs) {
|
|
119
|
+
const versionRoutesMd = versionConfig["routes-md"] ?? versionConfig.routes ?? [];
|
|
120
|
+
for (const route of versionRoutesMd) {
|
|
121
|
+
for (const docPath of Object.values(route.path ?? {})) {
|
|
122
|
+
await writeMarkdownDoc(root, outputDir, versionId, docPath, docs);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function writeHtmlDoc(root, outputDir, docPath, docsHtml) {
|
|
128
|
+
const key = parseHtmlFileToKey(path.basename(docPath));
|
|
129
|
+
const language = extractLanguageFromPath(docPath);
|
|
130
|
+
const content = key && language ? docsHtml[key]?.[language] : undefined;
|
|
131
|
+
if (!content) return;
|
|
132
|
+
const normalizedHtmlPath = normalizeToOutputPath(outputDir, docPath);
|
|
133
|
+
await writeText(root, normalizedHtmlPath, content);
|
|
134
|
+
// Backward-compatible alias for tools or users that still expect ".html".
|
|
135
|
+
if (!path.extname(docPath)) {
|
|
136
|
+
await writeText(root, `${normalizedHtmlPath}.html`, content);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function writeVersionHtmlDocs(root, outputDir, versionConfig, docsHtml) {
|
|
141
|
+
const versionRoutesHtml = versionConfig["routes-html"] ?? [];
|
|
142
|
+
for (const route of versionRoutesHtml) {
|
|
143
|
+
if (route.url) continue;
|
|
144
|
+
for (const docPath of Object.values(route.path ?? {})) {
|
|
145
|
+
await writeHtmlDoc(root, outputDir, docPath, docsHtml);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Ensure version folders always include an index file. */
|
|
151
|
+
async function writeVersionFallbackIndexes(root, outputDir, versionId, docs) {
|
|
152
|
+
for (const language of SUPPORTED_LANGUAGES) {
|
|
153
|
+
const fallbackIndex = docs?.[language]?.index;
|
|
154
|
+
if (!fallbackIndex) continue;
|
|
155
|
+
const versionIndexPath = `${outputDir}/docs/versions/${versionId}/${language}/index.md`;
|
|
156
|
+
if (!existsSync(path.join(root, versionIndexPath))) {
|
|
157
|
+
const versionedFallbackIndex = withVersionBadge(fallbackIndex, versionId, language);
|
|
158
|
+
await writeText(root, versionIndexPath, versionedFallbackIndex);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function writeVersionedDocs(root, outputDir, artifacts) {
|
|
164
|
+
const docsHtml = artifacts.docsHtml ?? {};
|
|
165
|
+
for (const [versionId, versionConfig] of Object.entries(artifacts.versionConfigs)) {
|
|
166
|
+
await writeVersionMarkdownDocs(root, outputDir, versionId, versionConfig, artifacts.docs);
|
|
167
|
+
await writeVersionHtmlDocs(root, outputDir, versionConfig, docsHtml);
|
|
168
|
+
await writeVersionFallbackIndexes(root, outputDir, versionId, artifacts.docs);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
78
172
|
export async function writeConfigOnlyOutput(options) {
|
|
79
173
|
const {
|
|
80
174
|
root,
|
|
81
|
-
pkgRoot,
|
|
82
175
|
outputDir,
|
|
83
176
|
artifacts,
|
|
84
177
|
useLocalLayoutConfig,
|
|
@@ -87,15 +180,10 @@ export async function writeConfigOnlyOutput(options) {
|
|
|
87
180
|
createThemeTemplate,
|
|
88
181
|
} = options;
|
|
89
182
|
|
|
90
|
-
|
|
91
|
-
for (const legacyLanguageDir of ["en", "pt", "es"]) {
|
|
92
|
-
const legacyPath = path.join(root, outputDir, "docs", legacyLanguageDir);
|
|
93
|
-
if (existsSync(legacyPath)) {
|
|
94
|
-
rmSync(legacyPath, { recursive: true, force: true });
|
|
95
|
-
}
|
|
96
|
-
}
|
|
183
|
+
removeLegacyLanguageDirs(root, outputDir);
|
|
97
184
|
|
|
98
185
|
await writeJson(root, `${outputDir}/config.json`, artifacts.rootConfig);
|
|
186
|
+
await writeLanguageArtifacts(root, outputDir, artifacts);
|
|
99
187
|
await writeText(root, `${outputDir}/icon.svg`, DEFAULT_ICON_SVG);
|
|
100
188
|
await writeLayoutArtifacts({
|
|
101
189
|
root,
|
|
@@ -105,55 +193,9 @@ export async function writeConfigOnlyOutput(options) {
|
|
|
105
193
|
artifacts,
|
|
106
194
|
createThemeTemplate,
|
|
107
195
|
});
|
|
108
|
-
|
|
109
|
-
for (const [versionId, versionConfig] of Object.entries(artifacts.versionConfigs)) {
|
|
110
|
-
await writeJson(root, `${outputDir}/docs/versions/${versionId}/config.json`, versionConfig);
|
|
111
|
-
}
|
|
196
|
+
await writeVersionConfigs(root, outputDir, artifacts.versionConfigs);
|
|
112
197
|
|
|
113
198
|
removeLegacySourceViewerFiles(root, outputDir);
|
|
114
199
|
|
|
115
|
-
|
|
116
|
-
const versionRoutesMd = versionConfig["routes-md"] ?? versionConfig.routes ?? [];
|
|
117
|
-
for (const route of versionRoutesMd) {
|
|
118
|
-
for (const docPath of Object.values(route.path ?? {})) {
|
|
119
|
-
const fileName = path.basename(docPath);
|
|
120
|
-
const key = parseDocFileToKey(fileName);
|
|
121
|
-
const language = extractLanguageFromPath(docPath);
|
|
122
|
-
const content = key && language ? artifacts.docs?.[language]?.[key] : undefined;
|
|
123
|
-
if (!content) continue;
|
|
124
|
-
const versionedContent = withVersionBadge(content, versionId, language);
|
|
125
|
-
await writeText(root, normalizeToOutputPath(outputDir, docPath), versionedContent);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const versionRoutesHtml = versionConfig["routes-html"] ?? [];
|
|
130
|
-
const docsHtml = artifacts.docsHtml ?? {};
|
|
131
|
-
for (const route of versionRoutesHtml) {
|
|
132
|
-
if (route.url) continue;
|
|
133
|
-
for (const docPath of Object.values(route.path ?? {})) {
|
|
134
|
-
const fileName = path.basename(docPath);
|
|
135
|
-
const key = parseHtmlFileToKey(fileName);
|
|
136
|
-
const language = extractLanguageFromPath(docPath);
|
|
137
|
-
const content = key && language ? docsHtml[key]?.[language] : undefined;
|
|
138
|
-
if (!content) continue;
|
|
139
|
-
const normalizedHtmlPath = normalizeToOutputPath(outputDir, docPath);
|
|
140
|
-
await writeText(root, normalizedHtmlPath, content);
|
|
141
|
-
// Backward-compatible alias for tools or users that still expect ".html".
|
|
142
|
-
if (!path.extname(docPath)) {
|
|
143
|
-
await writeText(root, `${normalizedHtmlPath}.html`, content);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Ensure version folders always include an index file.
|
|
149
|
-
for (const language of ["pt", "en", "es"]) {
|
|
150
|
-
const fallbackIndex = artifacts.docs?.[language]?.index;
|
|
151
|
-
if (!fallbackIndex) continue;
|
|
152
|
-
const versionIndexPath = `${outputDir}/docs/versions/${versionId}/${language}/index.md`;
|
|
153
|
-
if (!existsSync(path.join(root, versionIndexPath))) {
|
|
154
|
-
const versionedFallbackIndex = withVersionBadge(fallbackIndex, versionId, language);
|
|
155
|
-
await writeText(root, versionIndexPath, versionedFallbackIndex);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
200
|
+
await writeVersionedDocs(root, outputDir, artifacts);
|
|
159
201
|
}
|
package/runtime/workflow.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { trimSlashes } from "../contracts/path-segments.mjs";
|
|
2
|
+
|
|
1
3
|
function sanitizeDocsPath(docsPath) {
|
|
2
4
|
if (!docsPath || typeof docsPath !== "string") return "";
|
|
3
|
-
const segment = docsPath
|
|
5
|
+
const segment = trimSlashes(docsPath).trim();
|
|
4
6
|
return /^[A-Za-z0-9._-]+$/.test(segment) ? segment : "";
|
|
5
7
|
}
|
|
6
8
|
|
package/start.mjs
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { spawnExecutable } from "./runtime/exec.mjs";
|
|
3
3
|
|
|
4
4
|
const env = { ...process.env, GITPAGEDOCS_REPOSITORY_SEARCH: "true" };
|
|
5
5
|
// The Next app lives in frontend/, so `next build` emits to frontend/.next —
|
|
6
6
|
// point `next start` at the same directory (matches `next build frontend`).
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
env
|
|
10
|
-
|
|
11
|
-
});
|
|
7
|
+
let child;
|
|
8
|
+
try {
|
|
9
|
+
child = spawnExecutable("npx", ["next", "start", "frontend"], { stdio: "inherit", env });
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error(`[gitpagedocs] ${error instanceof Error ? error.message : String(error)}`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
12
14
|
child.on("exit", (code) => process.exit(code ?? 0));
|