@bensandee/tooling 0.17.0 → 0.19.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
CHANGED
|
@@ -162,17 +162,17 @@ import type {
|
|
|
162
162
|
| `DetectedProjectState` | Detected existing project state (package manager, CI, etc.) |
|
|
163
163
|
| `LegacyConfig` | Legacy config detection for migration |
|
|
164
164
|
|
|
165
|
-
## Docker
|
|
165
|
+
## Docker check
|
|
166
166
|
|
|
167
|
-
The `@bensandee/tooling/docker-
|
|
167
|
+
The `@bensandee/tooling/docker-check` export provides utilities for checking Docker Compose stacks via health checks.
|
|
168
168
|
|
|
169
169
|
### Quick start
|
|
170
170
|
|
|
171
171
|
```ts
|
|
172
|
-
import { createRealExecutor,
|
|
173
|
-
import type {
|
|
172
|
+
import { createRealExecutor, runDockerCheck } from "@bensandee/tooling/docker-check";
|
|
173
|
+
import type { CheckConfig } from "@bensandee/tooling/docker-check";
|
|
174
174
|
|
|
175
|
-
const config:
|
|
175
|
+
const config: CheckConfig = {
|
|
176
176
|
compose: {
|
|
177
177
|
cwd: "./deploy",
|
|
178
178
|
composeFiles: ["docker-compose.yaml"],
|
|
@@ -190,7 +190,7 @@ const config: VerifyConfig = {
|
|
|
190
190
|
pollIntervalMs: 5_000,
|
|
191
191
|
};
|
|
192
192
|
|
|
193
|
-
const result = await
|
|
193
|
+
const result = await runDockerCheck(createRealExecutor(), config);
|
|
194
194
|
if (!result.success) {
|
|
195
195
|
console.error(result.reason, result.message);
|
|
196
196
|
}
|
|
@@ -200,7 +200,7 @@ if (!result.success) {
|
|
|
200
200
|
|
|
201
201
|
| Export | Description |
|
|
202
202
|
| -------------------------------------- | ----------------------------------------------------------------- |
|
|
203
|
-
| `
|
|
203
|
+
| `runDockerCheck(executor, config)` | Full lifecycle: build, compose up, health check polling, teardown |
|
|
204
204
|
| `createRealExecutor()` | Production executor (real shell, fetch, timers) |
|
|
205
205
|
| `composeUp(executor, config)` | Start compose services |
|
|
206
206
|
| `composeDown(executor, config)` | Stop and remove compose services |
|
|
@@ -211,11 +211,11 @@ if (!result.success) {
|
|
|
211
211
|
|
|
212
212
|
### Types
|
|
213
213
|
|
|
214
|
-
| Type
|
|
215
|
-
|
|
|
216
|
-
| `
|
|
217
|
-
| `ComposeConfig`
|
|
218
|
-
| `HttpHealthCheck`
|
|
219
|
-
| `
|
|
220
|
-
| `
|
|
221
|
-
| `ContainerInfo`
|
|
214
|
+
| Type | Description |
|
|
215
|
+
| --------------------- | ------------------------------------------------------------------------------------------ |
|
|
216
|
+
| `CheckConfig` | Full check config (compose settings, build command, health checks, timeouts) |
|
|
217
|
+
| `ComposeConfig` | Docker Compose settings (cwd, compose files, env file, services) |
|
|
218
|
+
| `HttpHealthCheck` | Health check definition (name, URL, validate function) |
|
|
219
|
+
| `CheckResult` | Result: `{ success: true, elapsedMs }` or `{ success: false, reason, message, elapsedMs }` |
|
|
220
|
+
| `DockerCheckExecutor` | Side-effect abstraction (exec, fetch, timers) for testability |
|
|
221
|
+
| `ContainerInfo` | Container status info from `composePs` |
|
package/dist/bin.mjs
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as isExecSyncError } from "./
|
|
2
|
+
import { l as createRealExecutor$1, t as runDockerCheck, u as isExecSyncError } from "./check-VAgrEX2D.mjs";
|
|
3
3
|
import { defineCommand, runMain } from "citty";
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7
7
|
import JSON5 from "json5";
|
|
8
8
|
import { parse } from "jsonc-parser";
|
|
9
9
|
import { z } from "zod";
|
|
10
|
-
import { isMap, isSeq, parseDocument } from "yaml";
|
|
10
|
+
import { isMap, isSeq, parse as parse$1, parseDocument, stringify } from "yaml";
|
|
11
11
|
import { execSync } from "node:child_process";
|
|
12
12
|
import { FatalError, TransientError, UnexpectedError } from "@bensandee/common";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
13
14
|
//#region src/types.ts
|
|
14
15
|
const LEGACY_TOOLS = [
|
|
15
16
|
"eslint",
|
|
@@ -41,7 +42,7 @@ const TsconfigSchema = z.object({
|
|
|
41
42
|
include: z.array(z.string()).optional(),
|
|
42
43
|
exclude: z.array(z.string()).optional(),
|
|
43
44
|
files: z.array(z.string()).optional(),
|
|
44
|
-
references: z.array(z.object({ path: z.string() }).
|
|
45
|
+
references: z.array(z.object({ path: z.string() }).loose()).optional(),
|
|
45
46
|
compilerOptions: z.record(z.string(), z.unknown()).optional()
|
|
46
47
|
}).loose();
|
|
47
48
|
const RenovateSchema = z.object({
|
|
@@ -456,7 +457,7 @@ const DeclarativeHealthCheckSchema = z.object({
|
|
|
456
457
|
url: z.string(),
|
|
457
458
|
status: z.number().int().optional()
|
|
458
459
|
});
|
|
459
|
-
const
|
|
460
|
+
const DockerCheckConfigSchema = z.object({
|
|
460
461
|
composeFiles: z.array(z.string()).optional(),
|
|
461
462
|
envFile: z.string().optional(),
|
|
462
463
|
services: z.array(z.string()).optional(),
|
|
@@ -495,7 +496,7 @@ const ToolingConfigSchema = z.object({
|
|
|
495
496
|
dockerfile: z.string(),
|
|
496
497
|
context: z.string().default(".")
|
|
497
498
|
})).optional(),
|
|
498
|
-
|
|
499
|
+
dockerCheck: z.union([z.literal(false), DockerCheckConfigSchema]).optional()
|
|
499
500
|
});
|
|
500
501
|
/** Load saved tooling config from the target directory. Returns undefined if missing or invalid. */
|
|
501
502
|
function loadToolingConfig(targetDir) {
|
|
@@ -656,7 +657,7 @@ function getAddedDevDepNames(config) {
|
|
|
656
657
|
const deps = { ...ROOT_DEV_DEPS };
|
|
657
658
|
if (config.structure !== "monorepo") Object.assign(deps, PER_PACKAGE_DEV_DEPS);
|
|
658
659
|
deps["@bensandee/config"] = "0.8.2";
|
|
659
|
-
deps["@bensandee/tooling"] = "0.
|
|
660
|
+
deps["@bensandee/tooling"] = "0.19.0";
|
|
660
661
|
if (config.formatter === "oxfmt") deps["oxfmt"] = "0.35.0";
|
|
661
662
|
if (config.formatter === "prettier") deps["prettier"] = "3.8.1";
|
|
662
663
|
addReleaseDeps(deps, config);
|
|
@@ -677,7 +678,7 @@ async function generatePackageJson(ctx) {
|
|
|
677
678
|
const devDeps = { ...ROOT_DEV_DEPS };
|
|
678
679
|
if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
|
|
679
680
|
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.8.2";
|
|
680
|
-
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.
|
|
681
|
+
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.19.0";
|
|
681
682
|
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.9.2";
|
|
682
683
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = "0.35.0";
|
|
683
684
|
if (ctx.config.formatter === "prettier") devDeps["prettier"] = "3.8.1";
|
|
@@ -1266,6 +1267,34 @@ function mergeWorkflowSteps(existing, jobName, requiredSteps) {
|
|
|
1266
1267
|
* Returns unchanged content if the job already exists, the file has an opt-out comment,
|
|
1267
1268
|
* or the document can't be parsed.
|
|
1268
1269
|
*/
|
|
1270
|
+
/**
|
|
1271
|
+
* Ensure a `concurrency` block exists at the workflow top level.
|
|
1272
|
+
* Adds it if missing — never modifies an existing one.
|
|
1273
|
+
* Returns unchanged content if the file has an opt-out comment or can't be parsed.
|
|
1274
|
+
*/
|
|
1275
|
+
function ensureWorkflowConcurrency(existing, concurrency) {
|
|
1276
|
+
if (isToolingIgnored(existing)) return {
|
|
1277
|
+
content: existing,
|
|
1278
|
+
changed: false
|
|
1279
|
+
};
|
|
1280
|
+
try {
|
|
1281
|
+
const doc = parseDocument(existing);
|
|
1282
|
+
if (doc.has("concurrency")) return {
|
|
1283
|
+
content: existing,
|
|
1284
|
+
changed: false
|
|
1285
|
+
};
|
|
1286
|
+
doc.set("concurrency", doc.createNode(concurrency));
|
|
1287
|
+
return {
|
|
1288
|
+
content: doc.toString(),
|
|
1289
|
+
changed: true
|
|
1290
|
+
};
|
|
1291
|
+
} catch {
|
|
1292
|
+
return {
|
|
1293
|
+
content: existing,
|
|
1294
|
+
changed: false
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1269
1298
|
function addWorkflowJob(existing, jobName, jobConfig) {
|
|
1270
1299
|
if (isToolingIgnored(existing)) return {
|
|
1271
1300
|
content: existing,
|
|
@@ -1296,6 +1325,14 @@ function addWorkflowJob(existing, jobName, jobConfig) {
|
|
|
1296
1325
|
}
|
|
1297
1326
|
//#endregion
|
|
1298
1327
|
//#region src/generators/ci.ts
|
|
1328
|
+
/** Build a GitHub Actions expression like `${{ expr }}` without triggering no-template-curly-in-string. */
|
|
1329
|
+
function actionsExpr$2(expr) {
|
|
1330
|
+
return `\${{ ${expr} }}`;
|
|
1331
|
+
}
|
|
1332
|
+
const CI_CONCURRENCY = {
|
|
1333
|
+
group: `ci-${actionsExpr$2("github.ref")}`,
|
|
1334
|
+
"cancel-in-progress": actionsExpr$2("github.ref != 'refs/heads/main'")
|
|
1335
|
+
};
|
|
1299
1336
|
function hasEnginesNode$2(ctx) {
|
|
1300
1337
|
const raw = ctx.read("package.json");
|
|
1301
1338
|
if (!raw) return false;
|
|
@@ -1309,6 +1346,10 @@ ${emailNotifications}on:
|
|
|
1309
1346
|
branches: [main]
|
|
1310
1347
|
pull_request:
|
|
1311
1348
|
|
|
1349
|
+
concurrency:
|
|
1350
|
+
group: ci-${actionsExpr$2("github.ref")}
|
|
1351
|
+
cancel-in-progress: ${actionsExpr$2("github.ref != 'refs/heads/main'")}
|
|
1352
|
+
|
|
1312
1353
|
jobs:
|
|
1313
1354
|
check:
|
|
1314
1355
|
runs-on: ubuntu-latest
|
|
@@ -1371,8 +1412,9 @@ async function generateCi(ctx) {
|
|
|
1371
1412
|
const existing = ctx.read(filePath);
|
|
1372
1413
|
if (existing) {
|
|
1373
1414
|
const merged = mergeWorkflowSteps(existing, "check", requiredCheckSteps(nodeVersionYaml));
|
|
1374
|
-
const
|
|
1375
|
-
|
|
1415
|
+
const withConcurrency = ensureWorkflowConcurrency(merged.content, CI_CONCURRENCY);
|
|
1416
|
+
const withComment = ensureSchemaComment(withConcurrency.content, isGitHub ? "github" : "forgejo");
|
|
1417
|
+
if (merged.changed || withConcurrency.changed || withComment !== withConcurrency.content) {
|
|
1376
1418
|
ctx.write(filePath, withComment);
|
|
1377
1419
|
return {
|
|
1378
1420
|
filePath,
|
|
@@ -2312,7 +2354,7 @@ const SCHEMA_NPM_PATH = "@bensandee/config/schemas/forgejo-workflow.schema.json"
|
|
|
2312
2354
|
const SCHEMA_LOCAL_PATH = ".vscode/forgejo-workflow.schema.json";
|
|
2313
2355
|
const SETTINGS_PATH = ".vscode/settings.json";
|
|
2314
2356
|
const SCHEMA_GLOB = ".forgejo/workflows/*.{yml,yaml}";
|
|
2315
|
-
const VscodeSettingsSchema = z.object({ "yaml.schemas": z.record(z.string(), z.unknown()).default({}) }).
|
|
2357
|
+
const VscodeSettingsSchema = z.object({ "yaml.schemas": z.record(z.string(), z.unknown()).default({}) }).loose();
|
|
2316
2358
|
function readSchemaFromNodeModules(targetDir) {
|
|
2317
2359
|
const candidate = path.join(targetDir, "node_modules", SCHEMA_NPM_PATH);
|
|
2318
2360
|
if (!existsSync(candidate)) return void 0;
|
|
@@ -4073,7 +4115,7 @@ function generateTags(version) {
|
|
|
4073
4115
|
function imageRef(namespace, imageName, tag) {
|
|
4074
4116
|
return `${namespace}/${imageName}:${tag}`;
|
|
4075
4117
|
}
|
|
4076
|
-
function log(message) {
|
|
4118
|
+
function log$1(message) {
|
|
4077
4119
|
console.log(message);
|
|
4078
4120
|
}
|
|
4079
4121
|
function debug(verbose, message) {
|
|
@@ -4114,22 +4156,22 @@ function runDockerBuild(executor, config) {
|
|
|
4114
4156
|
const repoName = readRepoName(executor, config.cwd);
|
|
4115
4157
|
if (config.packageDir) {
|
|
4116
4158
|
const pkg = readSinglePackageDocker(executor, config.cwd, config.packageDir, repoName);
|
|
4117
|
-
log(`Building image for ${pkg.dir} (${pkg.imageName}:latest)...`);
|
|
4159
|
+
log$1(`Building image for ${pkg.dir} (${pkg.imageName}:latest)...`);
|
|
4118
4160
|
buildImage(executor, pkg, config.cwd, config.verbose, config.extraArgs);
|
|
4119
|
-
log(`Built ${pkg.imageName}:latest`);
|
|
4161
|
+
log$1(`Built ${pkg.imageName}:latest`);
|
|
4120
4162
|
return { packages: [pkg] };
|
|
4121
4163
|
}
|
|
4122
4164
|
const packages = detectDockerPackages(executor, config.cwd, repoName);
|
|
4123
4165
|
if (packages.length === 0) {
|
|
4124
|
-
log("No packages with docker config found");
|
|
4166
|
+
log$1("No packages with docker config found");
|
|
4125
4167
|
return { packages: [] };
|
|
4126
4168
|
}
|
|
4127
|
-
log(`Found ${packages.length} Docker package(s): ${packages.map((p) => p.dir).join(", ")}`);
|
|
4169
|
+
log$1(`Found ${packages.length} Docker package(s): ${packages.map((p) => p.dir).join(", ")}`);
|
|
4128
4170
|
for (const pkg of packages) {
|
|
4129
|
-
log(`Building image for ${pkg.dir} (${pkg.imageName}:latest)...`);
|
|
4171
|
+
log$1(`Building image for ${pkg.dir} (${pkg.imageName}:latest)...`);
|
|
4130
4172
|
buildImage(executor, pkg, config.cwd, config.verbose, config.extraArgs);
|
|
4131
4173
|
}
|
|
4132
|
-
log(`Built ${packages.length} image(s)`);
|
|
4174
|
+
log$1(`Built ${packages.length} image(s)`);
|
|
4133
4175
|
return { packages };
|
|
4134
4176
|
}
|
|
4135
4177
|
/**
|
|
@@ -4153,35 +4195,35 @@ function runDockerPublish(executor, config) {
|
|
|
4153
4195
|
};
|
|
4154
4196
|
for (const pkg of packages) if (!pkg.version) throw new FatalError(`Package ${pkg.dir} has docker config but no version in package.json`);
|
|
4155
4197
|
if (!config.dryRun) {
|
|
4156
|
-
log(`Logging in to ${config.registryHost}...`);
|
|
4198
|
+
log$1(`Logging in to ${config.registryHost}...`);
|
|
4157
4199
|
const loginResult = executor.exec(`echo "${config.password}" | docker login ${config.registryHost} -u ${config.username} --password-stdin`);
|
|
4158
4200
|
if (loginResult.exitCode !== 0) throw new FatalError(`Docker login failed: ${loginResult.stderr}`);
|
|
4159
|
-
} else log("[dry-run] Skipping docker login");
|
|
4201
|
+
} else log$1("[dry-run] Skipping docker login");
|
|
4160
4202
|
const allTags = [];
|
|
4161
4203
|
try {
|
|
4162
4204
|
for (const pkg of packages) {
|
|
4163
4205
|
const tags = generateTags(pkg.version ?? "");
|
|
4164
|
-
log(`${pkg.dir} v${pkg.version} → tags: ${tags.join(", ")}`);
|
|
4206
|
+
log$1(`${pkg.dir} v${pkg.version} → tags: ${tags.join(", ")}`);
|
|
4165
4207
|
for (const tag of tags) {
|
|
4166
4208
|
const ref = imageRef(config.registryNamespace, pkg.imageName, tag);
|
|
4167
4209
|
allTags.push(ref);
|
|
4168
|
-
log(`Tagging ${pkg.imageName} → ${ref}`);
|
|
4210
|
+
log$1(`Tagging ${pkg.imageName} → ${ref}`);
|
|
4169
4211
|
const tagResult = executor.exec(`docker tag ${pkg.imageName} ${ref}`);
|
|
4170
4212
|
if (tagResult.exitCode !== 0) throw new FatalError(`docker tag failed: ${tagResult.stderr}`);
|
|
4171
4213
|
if (!config.dryRun) {
|
|
4172
|
-
log(`Pushing ${ref}...`);
|
|
4214
|
+
log$1(`Pushing ${ref}...`);
|
|
4173
4215
|
const pushResult = executor.exec(`docker push ${ref}`);
|
|
4174
4216
|
if (pushResult.exitCode !== 0) throw new FatalError(`docker push failed: ${pushResult.stderr}`);
|
|
4175
|
-
} else log(`[dry-run] Skipping push for ${ref}`);
|
|
4217
|
+
} else log$1(`[dry-run] Skipping push for ${ref}`);
|
|
4176
4218
|
}
|
|
4177
4219
|
}
|
|
4178
4220
|
} finally {
|
|
4179
4221
|
if (!config.dryRun) {
|
|
4180
|
-
log(`Logging out from ${config.registryHost}...`);
|
|
4222
|
+
log$1(`Logging out from ${config.registryHost}...`);
|
|
4181
4223
|
executor.exec(`docker logout ${config.registryHost}`);
|
|
4182
4224
|
}
|
|
4183
4225
|
}
|
|
4184
|
-
log(`Published ${allTags.length} image tag(s)`);
|
|
4226
|
+
log$1(`Published ${allTags.length} image tag(s)`);
|
|
4185
4227
|
return {
|
|
4186
4228
|
packages,
|
|
4187
4229
|
tags: allTags
|
|
@@ -4257,11 +4299,251 @@ const dockerBuildCommand = defineCommand({
|
|
|
4257
4299
|
}
|
|
4258
4300
|
});
|
|
4259
4301
|
//#endregion
|
|
4302
|
+
//#region src/docker-check/detect.ts
|
|
4303
|
+
/** Compose file names to scan, in priority order. */
|
|
4304
|
+
const COMPOSE_FILE_CANDIDATES = [
|
|
4305
|
+
"docker-compose.yaml",
|
|
4306
|
+
"docker-compose.yml",
|
|
4307
|
+
"compose.yaml",
|
|
4308
|
+
"compose.yml"
|
|
4309
|
+
];
|
|
4310
|
+
/** Zod schema for the subset of compose YAML we care about. */
|
|
4311
|
+
const ComposePortSchema = z.union([z.string(), z.object({
|
|
4312
|
+
published: z.union([z.string(), z.number()]),
|
|
4313
|
+
target: z.union([z.string(), z.number()]).optional()
|
|
4314
|
+
}).loose()]);
|
|
4315
|
+
const ComposeServiceSchema = z.object({
|
|
4316
|
+
ports: z.array(ComposePortSchema).optional(),
|
|
4317
|
+
healthcheck: z.unknown().optional()
|
|
4318
|
+
}).loose();
|
|
4319
|
+
const ComposeFileSchema = z.object({ services: z.record(z.string(), ComposeServiceSchema).optional() }).loose();
|
|
4320
|
+
/** Directories to scan for compose files, in priority order. */
|
|
4321
|
+
const COMPOSE_DIR_CANDIDATES = [".", "docker"];
|
|
4322
|
+
/** Detect which compose files exist at conventional paths.
|
|
4323
|
+
* Returns the resolved directory and the matching file names. */
|
|
4324
|
+
function detectComposeFiles(cwd) {
|
|
4325
|
+
for (const dir of COMPOSE_DIR_CANDIDATES) {
|
|
4326
|
+
const absDir = path.resolve(cwd, dir);
|
|
4327
|
+
const files = COMPOSE_FILE_CANDIDATES.filter((name) => existsSync(path.join(absDir, name)));
|
|
4328
|
+
if (files.length > 0) return {
|
|
4329
|
+
dir: absDir,
|
|
4330
|
+
files
|
|
4331
|
+
};
|
|
4332
|
+
}
|
|
4333
|
+
return {
|
|
4334
|
+
dir: cwd,
|
|
4335
|
+
files: []
|
|
4336
|
+
};
|
|
4337
|
+
}
|
|
4338
|
+
/** Parse a single port mapping string and extract the host port. */
|
|
4339
|
+
function parsePortString(port) {
|
|
4340
|
+
const withoutProtocol = port.split("/")[0];
|
|
4341
|
+
if (!withoutProtocol) return void 0;
|
|
4342
|
+
const parts = withoutProtocol.split(":");
|
|
4343
|
+
if (parts.length === 2) {
|
|
4344
|
+
const host = parts[0];
|
|
4345
|
+
if (!host) return void 0;
|
|
4346
|
+
const parsed = Number.parseInt(host, 10);
|
|
4347
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
4348
|
+
}
|
|
4349
|
+
if (parts.length === 3) {
|
|
4350
|
+
const host = parts[1];
|
|
4351
|
+
if (!host) return void 0;
|
|
4352
|
+
const parsed = Number.parseInt(host, 10);
|
|
4353
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4356
|
+
/** Extract the host port from a compose port definition. */
|
|
4357
|
+
function extractHostPort(port) {
|
|
4358
|
+
if (typeof port === "string") return parsePortString(port);
|
|
4359
|
+
const published = port.published;
|
|
4360
|
+
if (typeof published === "number") return published;
|
|
4361
|
+
if (typeof published === "string") {
|
|
4362
|
+
const parsed = Number.parseInt(published, 10);
|
|
4363
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
/** Parse compose files and return service info (names, ports, healthcheck presence). */
|
|
4367
|
+
function parseComposeServices(cwd, composeFiles) {
|
|
4368
|
+
const serviceMap = /* @__PURE__ */ new Map();
|
|
4369
|
+
for (const file of composeFiles) {
|
|
4370
|
+
let parsed;
|
|
4371
|
+
try {
|
|
4372
|
+
const content = readFileSync(path.join(cwd, file), "utf-8");
|
|
4373
|
+
parsed = ComposeFileSchema.safeParse(parse$1(content));
|
|
4374
|
+
} catch (_error) {
|
|
4375
|
+
continue;
|
|
4376
|
+
}
|
|
4377
|
+
if (!parsed.success || !parsed.data.services) continue;
|
|
4378
|
+
for (const [name, service] of Object.entries(parsed.data.services)) {
|
|
4379
|
+
const existing = serviceMap.get(name);
|
|
4380
|
+
let hostPort = existing?.hostPort;
|
|
4381
|
+
if (hostPort === void 0 && service.ports) for (const port of service.ports) {
|
|
4382
|
+
const extracted = extractHostPort(port);
|
|
4383
|
+
if (extracted !== void 0) {
|
|
4384
|
+
hostPort = extracted;
|
|
4385
|
+
break;
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
serviceMap.set(name, {
|
|
4389
|
+
name,
|
|
4390
|
+
hostPort,
|
|
4391
|
+
hasHealthcheck: existing?.hasHealthcheck ?? service.healthcheck !== void 0
|
|
4392
|
+
});
|
|
4393
|
+
}
|
|
4394
|
+
}
|
|
4395
|
+
return [...serviceMap.values()];
|
|
4396
|
+
}
|
|
4397
|
+
/** Generate health checks from parsed services: services with exposed ports get HTTP checks, unless they define a compose-level healthcheck. */
|
|
4398
|
+
function deriveHealthChecks(services) {
|
|
4399
|
+
return services.filter((s) => s.hostPort !== void 0 && !s.hasHealthcheck).map((s) => ({
|
|
4400
|
+
name: s.name,
|
|
4401
|
+
url: `http://localhost:${String(s.hostPort)}/`
|
|
4402
|
+
}));
|
|
4403
|
+
}
|
|
4404
|
+
/** Check overlay file name patterns, matched against the base compose file name. */
|
|
4405
|
+
const CHECK_OVERLAY_PATTERNS = [(base) => base.replace(/\.(yaml|yml)$/, `.check.$1`)];
|
|
4406
|
+
/** Detect a user-provided check overlay file alongside the base compose file. */
|
|
4407
|
+
function detectCheckOverlay(dir, baseFile) {
|
|
4408
|
+
for (const pattern of CHECK_OVERLAY_PATTERNS) {
|
|
4409
|
+
const candidate = pattern(baseFile);
|
|
4410
|
+
if (existsSync(path.join(dir, candidate))) return candidate;
|
|
4411
|
+
}
|
|
4412
|
+
}
|
|
4413
|
+
/** Detect a check env file in the compose directory. */
|
|
4414
|
+
function detectCheckEnvFile(dir) {
|
|
4415
|
+
if (existsSync(path.join(dir, "docker-compose.check.env"))) return "docker-compose.check.env";
|
|
4416
|
+
}
|
|
4417
|
+
/** Fast healthcheck intervals for the generated check overlay. */
|
|
4418
|
+
const CHECK_HEALTHCHECK = {
|
|
4419
|
+
interval: "5s",
|
|
4420
|
+
timeout: "5s",
|
|
4421
|
+
retries: 12,
|
|
4422
|
+
start_period: "10s",
|
|
4423
|
+
start_interval: "5s"
|
|
4424
|
+
};
|
|
4425
|
+
/**
|
|
4426
|
+
* Generate a check overlay YAML string from parsed services.
|
|
4427
|
+
* Sets `restart: "no"` on all services, and overrides healthcheck intervals
|
|
4428
|
+
* for services that define a healthcheck in the base compose file.
|
|
4429
|
+
*/
|
|
4430
|
+
function generateCheckOverlay(services) {
|
|
4431
|
+
const serviceOverrides = {};
|
|
4432
|
+
for (const service of services) {
|
|
4433
|
+
const override = { restart: "no" };
|
|
4434
|
+
if (service.hasHealthcheck) override["healthcheck"] = { ...CHECK_HEALTHCHECK };
|
|
4435
|
+
serviceOverrides[service.name] = override;
|
|
4436
|
+
}
|
|
4437
|
+
return stringify({ services: serviceOverrides });
|
|
4438
|
+
}
|
|
4439
|
+
/** Auto-detect compose config from conventional file locations. */
|
|
4440
|
+
function computeCheckDefaults(cwd) {
|
|
4441
|
+
const { dir, files } = detectComposeFiles(cwd);
|
|
4442
|
+
if (files.length === 0) return {};
|
|
4443
|
+
const baseFile = files[0];
|
|
4444
|
+
const checkOverlay = baseFile ? detectCheckOverlay(dir, baseFile) : void 0;
|
|
4445
|
+
const envFile = detectCheckEnvFile(dir);
|
|
4446
|
+
const services = parseComposeServices(dir, files);
|
|
4447
|
+
const healthChecks = deriveHealthChecks(services);
|
|
4448
|
+
return {
|
|
4449
|
+
composeCwd: dir !== cwd ? dir : void 0,
|
|
4450
|
+
composeFiles: files,
|
|
4451
|
+
checkOverlay,
|
|
4452
|
+
envFile,
|
|
4453
|
+
services: services.map((s) => s.name),
|
|
4454
|
+
healthChecks: healthChecks.length > 0 ? healthChecks : void 0
|
|
4455
|
+
};
|
|
4456
|
+
}
|
|
4457
|
+
//#endregion
|
|
4458
|
+
//#region src/commands/docker-check.ts
|
|
4459
|
+
/** Convert declarative health checks to functional ones. */
|
|
4460
|
+
function toHttpHealthChecks(checks) {
|
|
4461
|
+
return checks.map((check) => ({
|
|
4462
|
+
name: check.name,
|
|
4463
|
+
url: check.url,
|
|
4464
|
+
validate: async (res) => check.status ? res.status === check.status : res.ok
|
|
4465
|
+
}));
|
|
4466
|
+
}
|
|
4467
|
+
/** Write the generated check overlay to a temp file. Returns the absolute path. */
|
|
4468
|
+
function writeTempOverlay(content) {
|
|
4469
|
+
const name = `tooling-check-overlay-${process.pid}.yaml`;
|
|
4470
|
+
const filePath = path.join(tmpdir(), name);
|
|
4471
|
+
writeFileSync(filePath, content, "utf-8");
|
|
4472
|
+
return filePath;
|
|
4473
|
+
}
|
|
4474
|
+
function log(message) {
|
|
4475
|
+
console.log(message);
|
|
4476
|
+
}
|
|
4477
|
+
function warn(message) {
|
|
4478
|
+
console.warn(message);
|
|
4479
|
+
}
|
|
4480
|
+
const dockerCheckCommand = defineCommand({
|
|
4481
|
+
meta: {
|
|
4482
|
+
name: "docker:check",
|
|
4483
|
+
description: "Check Docker Compose stack health by auto-detecting services from compose files"
|
|
4484
|
+
},
|
|
4485
|
+
args: {
|
|
4486
|
+
timeout: {
|
|
4487
|
+
type: "string",
|
|
4488
|
+
description: "Maximum time to wait for health checks, in ms (default: 120000)"
|
|
4489
|
+
},
|
|
4490
|
+
"poll-interval": {
|
|
4491
|
+
type: "string",
|
|
4492
|
+
description: "Interval between polling attempts, in ms (default: 5000)"
|
|
4493
|
+
}
|
|
4494
|
+
},
|
|
4495
|
+
async run({ args }) {
|
|
4496
|
+
const cwd = process.cwd();
|
|
4497
|
+
if (loadToolingConfig(cwd)?.dockerCheck === false) {
|
|
4498
|
+
log("Docker check is disabled in .tooling.json");
|
|
4499
|
+
return;
|
|
4500
|
+
}
|
|
4501
|
+
const defaults = computeCheckDefaults(cwd);
|
|
4502
|
+
if (!defaults.composeFiles || defaults.composeFiles.length === 0) throw new FatalError("No compose files found. Expected docker-compose.yaml or compose.yaml in ./ or docker/.");
|
|
4503
|
+
if (!defaults.checkOverlay) {
|
|
4504
|
+
const composeCwd = defaults.composeCwd ?? cwd;
|
|
4505
|
+
const expectedOverlay = (defaults.composeFiles[0] ?? "docker-compose.yaml").replace(/\.(yaml|yml)$/, ".check.$1");
|
|
4506
|
+
warn(`Compose files found but no check overlay. Create ${path.relative(cwd, path.join(composeCwd, expectedOverlay))} to enable docker:check.`);
|
|
4507
|
+
warn("To suppress this warning, set \"dockerCheck\": false in .tooling.json.");
|
|
4508
|
+
return;
|
|
4509
|
+
}
|
|
4510
|
+
if (!defaults.services || defaults.services.length === 0) throw new FatalError("No services found in compose files.");
|
|
4511
|
+
const composeCwd = defaults.composeCwd ?? cwd;
|
|
4512
|
+
const tempOverlayPath = writeTempOverlay(generateCheckOverlay(parseComposeServices(composeCwd, defaults.composeFiles)));
|
|
4513
|
+
const composeFiles = [
|
|
4514
|
+
...defaults.composeFiles,
|
|
4515
|
+
tempOverlayPath,
|
|
4516
|
+
defaults.checkOverlay
|
|
4517
|
+
];
|
|
4518
|
+
try {
|
|
4519
|
+
const config = {
|
|
4520
|
+
compose: {
|
|
4521
|
+
cwd: composeCwd,
|
|
4522
|
+
composeFiles,
|
|
4523
|
+
envFile: defaults.envFile,
|
|
4524
|
+
services: defaults.services
|
|
4525
|
+
},
|
|
4526
|
+
buildCommand: defaults.buildCommand,
|
|
4527
|
+
buildCwd: defaults.buildCwd,
|
|
4528
|
+
healthChecks: defaults.healthChecks ? toHttpHealthChecks(defaults.healthChecks) : [],
|
|
4529
|
+
timeoutMs: args.timeout ? Number.parseInt(args.timeout, 10) : defaults.timeoutMs,
|
|
4530
|
+
pollIntervalMs: args["poll-interval"] ? Number.parseInt(args["poll-interval"], 10) : defaults.pollIntervalMs
|
|
4531
|
+
};
|
|
4532
|
+
const result = await runDockerCheck(createRealExecutor$1(), config);
|
|
4533
|
+
if (!result.success) throw new FatalError(`Check failed (${result.reason}): ${result.message}`);
|
|
4534
|
+
} finally {
|
|
4535
|
+
try {
|
|
4536
|
+
unlinkSync(tempOverlayPath);
|
|
4537
|
+
} catch (_error) {}
|
|
4538
|
+
}
|
|
4539
|
+
}
|
|
4540
|
+
});
|
|
4541
|
+
//#endregion
|
|
4260
4542
|
//#region src/bin.ts
|
|
4261
4543
|
const main = defineCommand({
|
|
4262
4544
|
meta: {
|
|
4263
4545
|
name: "tooling",
|
|
4264
|
-
version: "0.
|
|
4546
|
+
version: "0.19.0",
|
|
4265
4547
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
4266
4548
|
},
|
|
4267
4549
|
subCommands: {
|
|
@@ -4273,10 +4555,11 @@ const main = defineCommand({
|
|
|
4273
4555
|
"changesets:merge": releaseMergeCommand,
|
|
4274
4556
|
"release:simple": releaseSimpleCommand,
|
|
4275
4557
|
"docker:publish": publishDockerCommand,
|
|
4276
|
-
"docker:build": dockerBuildCommand
|
|
4558
|
+
"docker:build": dockerBuildCommand,
|
|
4559
|
+
"docker:check": dockerCheckCommand
|
|
4277
4560
|
}
|
|
4278
4561
|
});
|
|
4279
|
-
console.log(`@bensandee/tooling v0.
|
|
4562
|
+
console.log(`@bensandee/tooling v0.19.0`);
|
|
4280
4563
|
runMain(main);
|
|
4281
4564
|
//#endregion
|
|
4282
4565
|
export {};
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { t as isExecSyncError } from "../exec-CC49vrkM.mjs";
|
|
2
1
|
import { z } from "zod";
|
|
3
2
|
import { execSync } from "node:child_process";
|
|
4
|
-
//#region src/
|
|
3
|
+
//#region src/utils/exec.ts
|
|
4
|
+
/** Type guard for `execSync` errors that carry stdout/stderr/status. */
|
|
5
|
+
function isExecSyncError(err) {
|
|
6
|
+
return err instanceof Error && "stdout" in err && typeof err.stdout === "string" && "stderr" in err && typeof err.stderr === "string" && "status" in err && typeof err.status === "number";
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/docker-check/executor.ts
|
|
5
10
|
/** Create an executor that runs real commands, fetches, and manages process signals. */
|
|
6
11
|
function createRealExecutor() {
|
|
7
12
|
return {
|
|
@@ -61,7 +66,7 @@ function createRealExecutor() {
|
|
|
61
66
|
};
|
|
62
67
|
}
|
|
63
68
|
//#endregion
|
|
64
|
-
//#region src/docker-
|
|
69
|
+
//#region src/docker-check/compose.ts
|
|
65
70
|
/** Zod schema for a single container entry from `docker compose ps --format json`. */
|
|
66
71
|
const ContainerInfoSchema = z.object({
|
|
67
72
|
Service: z.string(),
|
|
@@ -116,7 +121,7 @@ function composePs(executor, config) {
|
|
|
116
121
|
}
|
|
117
122
|
}
|
|
118
123
|
//#endregion
|
|
119
|
-
//#region src/docker-
|
|
124
|
+
//#region src/docker-check/health.ts
|
|
120
125
|
/** Look up the health status of a specific service from container info. */
|
|
121
126
|
function getContainerHealth(containers, serviceName) {
|
|
122
127
|
return containers.find((c) => c.Service === serviceName)?.Health ?? "unknown";
|
|
@@ -131,11 +136,11 @@ async function checkHttpHealth(executor, check) {
|
|
|
131
136
|
}
|
|
132
137
|
}
|
|
133
138
|
//#endregion
|
|
134
|
-
//#region src/docker-
|
|
139
|
+
//#region src/docker-check/check.ts
|
|
135
140
|
const DEFAULT_TIMEOUT_MS = 12e4;
|
|
136
141
|
const DEFAULT_POLL_INTERVAL_MS = 5e3;
|
|
137
|
-
/** Run the full Docker
|
|
138
|
-
async function
|
|
142
|
+
/** Run the full Docker check lifecycle. */
|
|
143
|
+
async function runDockerCheck(executor, config) {
|
|
139
144
|
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
140
145
|
const pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
141
146
|
const { compose } = config;
|
|
@@ -178,7 +183,7 @@ async function runVerification(executor, config) {
|
|
|
178
183
|
}
|
|
179
184
|
}
|
|
180
185
|
if ([...healthStatus.values()].every(Boolean)) {
|
|
181
|
-
executor.log("
|
|
186
|
+
executor.log("Check successful! All systems operational.");
|
|
182
187
|
cleanup();
|
|
183
188
|
return {
|
|
184
189
|
success: true,
|
|
@@ -215,4 +220,4 @@ async function runVerification(executor, config) {
|
|
|
215
220
|
}
|
|
216
221
|
}
|
|
217
222
|
//#endregion
|
|
218
|
-
export {
|
|
223
|
+
export { composeDown as a, composeUp as c, composeCommand as i, createRealExecutor as l, checkHttpHealth as n, composeLogs as o, getContainerHealth as r, composePs as s, runDockerCheck as t, isExecSyncError as u };
|
|
@@ -13,9 +13,9 @@ interface ExecOptions {
|
|
|
13
13
|
env?: Record<string, string>;
|
|
14
14
|
}
|
|
15
15
|
//#endregion
|
|
16
|
-
//#region src/docker-
|
|
16
|
+
//#region src/docker-check/types.d.ts
|
|
17
17
|
/** Abstraction over side effects for testability. */
|
|
18
|
-
interface
|
|
18
|
+
interface DockerCheckExecutor {
|
|
19
19
|
/** Run a shell command and return the result (stdout/stderr captured). */
|
|
20
20
|
exec(command: string, options?: ExecOptions): ExecResult;
|
|
21
21
|
/** Run a shell command, streaming stdout/stderr to the console. */
|
|
@@ -46,15 +46,15 @@ interface HttpHealthCheck {
|
|
|
46
46
|
interface ComposeConfig {
|
|
47
47
|
/** Working directory for docker compose commands. */
|
|
48
48
|
cwd: string;
|
|
49
|
-
/** Compose files to use (e.g. ["docker-compose.yaml", "docker-compose.
|
|
49
|
+
/** Compose files to use (e.g. ["docker-compose.yaml", "docker-compose.check.yaml"]). */
|
|
50
50
|
composeFiles: string[];
|
|
51
51
|
/** Optional env file for compose. */
|
|
52
52
|
envFile?: string;
|
|
53
53
|
/** Service names to monitor for container-level health. */
|
|
54
54
|
services: string[];
|
|
55
55
|
}
|
|
56
|
-
/** Full
|
|
57
|
-
interface
|
|
56
|
+
/** Full docker check configuration. */
|
|
57
|
+
interface CheckConfig {
|
|
58
58
|
/** Docker compose settings. */
|
|
59
59
|
compose: ComposeConfig;
|
|
60
60
|
/** Optional build command to run before starting compose (e.g. "pnpm image:build"). */
|
|
@@ -68,8 +68,8 @@ interface VerifyConfig {
|
|
|
68
68
|
/** Interval between polling attempts, in ms. Default: 5000. */
|
|
69
69
|
pollIntervalMs?: number;
|
|
70
70
|
}
|
|
71
|
-
/** Result of the
|
|
72
|
-
type
|
|
71
|
+
/** Result of the docker check run. */
|
|
72
|
+
type CheckResult = {
|
|
73
73
|
success: true;
|
|
74
74
|
elapsedMs: number;
|
|
75
75
|
} | {
|
|
@@ -79,15 +79,15 @@ type VerifyResult = {
|
|
|
79
79
|
elapsedMs: number;
|
|
80
80
|
};
|
|
81
81
|
//#endregion
|
|
82
|
-
//#region src/docker-
|
|
82
|
+
//#region src/docker-check/executor.d.ts
|
|
83
83
|
/** Create an executor that runs real commands, fetches, and manages process signals. */
|
|
84
|
-
declare function createRealExecutor():
|
|
84
|
+
declare function createRealExecutor(): DockerCheckExecutor;
|
|
85
85
|
//#endregion
|
|
86
|
-
//#region src/docker-
|
|
87
|
-
/** Run the full Docker
|
|
88
|
-
declare function
|
|
86
|
+
//#region src/docker-check/check.d.ts
|
|
87
|
+
/** Run the full Docker check lifecycle. */
|
|
88
|
+
declare function runDockerCheck(executor: DockerCheckExecutor, config: CheckConfig): Promise<CheckResult>;
|
|
89
89
|
//#endregion
|
|
90
|
-
//#region src/docker-
|
|
90
|
+
//#region src/docker-check/compose.d.ts
|
|
91
91
|
/** Zod schema for a single container entry from `docker compose ps --format json`. */
|
|
92
92
|
declare const ContainerInfoSchema: z.ZodObject<{
|
|
93
93
|
Service: z.ZodString;
|
|
@@ -97,21 +97,21 @@ type ContainerInfo = z.infer<typeof ContainerInfoSchema>;
|
|
|
97
97
|
/** Build the `docker compose` base command string from config. */
|
|
98
98
|
declare function composeCommand(config: ComposeConfig): string;
|
|
99
99
|
/** Start the compose stack in detached mode. */
|
|
100
|
-
declare function composeUp(executor:
|
|
100
|
+
declare function composeUp(executor: DockerCheckExecutor, config: ComposeConfig): void;
|
|
101
101
|
/** Tear down the compose stack, removing volumes and orphans. Swallows errors. */
|
|
102
|
-
declare function composeDown(executor:
|
|
102
|
+
declare function composeDown(executor: DockerCheckExecutor, config: ComposeConfig): void;
|
|
103
103
|
/** Show logs for a specific service (or all services if not specified). Swallows errors. */
|
|
104
|
-
declare function composeLogs(executor:
|
|
104
|
+
declare function composeLogs(executor: DockerCheckExecutor, config: ComposeConfig, service?: string): void;
|
|
105
105
|
/**
|
|
106
106
|
* Query container status via `docker compose ps --format json`.
|
|
107
107
|
* Handles both JSON array and newline-delimited JSON (varies by docker compose version).
|
|
108
108
|
*/
|
|
109
|
-
declare function composePs(executor:
|
|
109
|
+
declare function composePs(executor: DockerCheckExecutor, config: ComposeConfig): ContainerInfo[];
|
|
110
110
|
//#endregion
|
|
111
|
-
//#region src/docker-
|
|
111
|
+
//#region src/docker-check/health.d.ts
|
|
112
112
|
/** Look up the health status of a specific service from container info. */
|
|
113
113
|
declare function getContainerHealth(containers: ContainerInfo[], serviceName: string): string;
|
|
114
114
|
/** Run a single HTTP health check, returning true if the validator passes. */
|
|
115
|
-
declare function checkHttpHealth(executor:
|
|
115
|
+
declare function checkHttpHealth(executor: DockerCheckExecutor, check: HttpHealthCheck): Promise<boolean>;
|
|
116
116
|
//#endregion
|
|
117
|
-
export { type
|
|
117
|
+
export { type CheckConfig, type CheckResult, type ComposeConfig, type ContainerInfo, type DockerCheckExecutor, type HttpHealthCheck, checkHttpHealth, composeCommand, composeDown, composeLogs, composePs, composeUp, createRealExecutor, getContainerHealth, runDockerCheck };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as composeDown, c as composeUp, i as composeCommand, l as createRealExecutor, n as checkHttpHealth, o as composeLogs, r as getContainerHealth, s as composePs, t as runDockerCheck } from "../check-VAgrEX2D.mjs";
|
|
2
|
+
export { checkHttpHealth, composeCommand, composeDown, composeLogs, composePs, composeUp, createRealExecutor, getContainerHealth, runDockerCheck };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bensandee/tooling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
|
|
5
5
|
"bin": {
|
|
6
6
|
"tooling": "./dist/bin.mjs"
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"default": "./dist/index.mjs"
|
|
19
19
|
},
|
|
20
20
|
"./bin": "./dist/bin.mjs",
|
|
21
|
-
"./docker-
|
|
22
|
-
"types": "./dist/docker-
|
|
23
|
-
"default": "./dist/docker-
|
|
21
|
+
"./docker-check": {
|
|
22
|
+
"types": "./dist/docker-check/index.d.mts",
|
|
23
|
+
"default": "./dist/docker-check/index.mjs"
|
|
24
24
|
},
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
package/dist/exec-CC49vrkM.mjs
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
//#region src/utils/exec.ts
|
|
2
|
-
/** Type guard for `execSync` errors that carry stdout/stderr/status. */
|
|
3
|
-
function isExecSyncError(err) {
|
|
4
|
-
return err instanceof Error && "stdout" in err && typeof err.stdout === "string" && "stderr" in err && typeof err.stderr === "string" && "status" in err && typeof err.status === "number";
|
|
5
|
-
}
|
|
6
|
-
//#endregion
|
|
7
|
-
export { isExecSyncError as t };
|