@bensandee/tooling 0.58.0 → 0.59.1
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/dist/bin.mjs +223 -194
- package/dist/docker-check/index.d.mts +121 -1
- package/dist/docker-check/index.mjs +2 -2
- package/dist/index.d.mts +1 -118
- package/dist/{check-B5DFYJ3x.mjs → project-name-CAVRoqNy.mjs} +172 -2
- package/dist/types-DtwkblrK.d.mts +119 -0
- package/package.json +2 -2
|
@@ -53,6 +53,12 @@ interface ComposeConfig {
|
|
|
53
53
|
cwd: string;
|
|
54
54
|
/** Compose files to use (e.g. ["docker-compose.yaml", "docker-compose.check.yaml"]). */
|
|
55
55
|
composeFiles: string[];
|
|
56
|
+
/**
|
|
57
|
+
* Project namespace passed to `docker compose --project-name`. Required so a
|
|
58
|
+
* check run cannot share a project (and thus its named volumes) with another
|
|
59
|
+
* stack on the same docker daemon. See `deriveProjectName()`.
|
|
60
|
+
*/
|
|
61
|
+
projectName: string;
|
|
56
62
|
/** Optional env file for compose. */
|
|
57
63
|
envFile?: string;
|
|
58
64
|
/** Service names to monitor for container-level health. */
|
|
@@ -125,4 +131,118 @@ declare function getContainerHealth(containers: ContainerInfo[], serviceName: st
|
|
|
125
131
|
/** Run a single HTTP health check, returning true if the validator passes. */
|
|
126
132
|
declare function checkHttpHealth(executor: DockerCheckExecutor, check: HttpHealthCheck): Promise<boolean>;
|
|
127
133
|
//#endregion
|
|
128
|
-
|
|
134
|
+
//#region src/docker-check/project-name.d.ts
|
|
135
|
+
/**
|
|
136
|
+
* Derive the `docker compose --project-name` value for a check run.
|
|
137
|
+
*
|
|
138
|
+
* Always returns a unique, non-colliding namespace so a stray `compose down -v`
|
|
139
|
+
* during the check cannot target volumes belonging to a long-running stack
|
|
140
|
+
* sharing the same docker daemon (the failure mode that wiped prod volumes
|
|
141
|
+
* when a CI runner bind-mounted /var/run/docker.sock and the check overlay
|
|
142
|
+
* inherited the base file's `name:`).
|
|
143
|
+
*
|
|
144
|
+
* Format: `<sanitized-repo>-check-<suffix>` where suffix prefers a CI run id
|
|
145
|
+
* for traceability and falls back to `process.pid` locally so two concurrent
|
|
146
|
+
* `bst docker:check` invocations on the same machine don't collide.
|
|
147
|
+
*/
|
|
148
|
+
declare function deriveProjectName(repoName: string, env?: NodeJS.ProcessEnv): string;
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region src/utils/tooling-config.d.ts
|
|
151
|
+
declare const ToolingConfigSchema: z.ZodObject<{
|
|
152
|
+
$schema: z.ZodOptional<z.ZodString>;
|
|
153
|
+
structure: z.ZodOptional<z.ZodEnum<{
|
|
154
|
+
single: "single";
|
|
155
|
+
monorepo: "monorepo";
|
|
156
|
+
}>>;
|
|
157
|
+
useEslintPlugin: z.ZodOptional<z.ZodBoolean>;
|
|
158
|
+
formatter: z.ZodOptional<z.ZodEnum<{
|
|
159
|
+
prettier: "prettier";
|
|
160
|
+
oxfmt: "oxfmt";
|
|
161
|
+
}>>;
|
|
162
|
+
setupVitest: z.ZodOptional<z.ZodBoolean>;
|
|
163
|
+
ci: z.ZodOptional<z.ZodEnum<{
|
|
164
|
+
github: "github";
|
|
165
|
+
forgejo: "forgejo";
|
|
166
|
+
none: "none";
|
|
167
|
+
}>>;
|
|
168
|
+
setupRenovate: z.ZodOptional<z.ZodBoolean>;
|
|
169
|
+
releaseStrategy: z.ZodOptional<z.ZodEnum<{
|
|
170
|
+
none: "none";
|
|
171
|
+
"release-it": "release-it";
|
|
172
|
+
simple: "simple";
|
|
173
|
+
changesets: "changesets";
|
|
174
|
+
}>>;
|
|
175
|
+
publishNpm: z.ZodOptional<z.ZodBoolean>;
|
|
176
|
+
publishDocker: z.ZodOptional<z.ZodBoolean>;
|
|
177
|
+
projectType: z.ZodOptional<z.ZodEnum<{
|
|
178
|
+
default: "default";
|
|
179
|
+
node: "node";
|
|
180
|
+
react: "react";
|
|
181
|
+
library: "library";
|
|
182
|
+
}>>;
|
|
183
|
+
detectPackageTypes: z.ZodOptional<z.ZodBoolean>;
|
|
184
|
+
docker: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
185
|
+
dockerfile: z.ZodOptional<z.ZodString>;
|
|
186
|
+
context: z.ZodOptional<z.ZodString>;
|
|
187
|
+
buildArgs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
188
|
+
}, z.core.$strip>>>;
|
|
189
|
+
dockerCheck: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<false>, z.ZodObject<{
|
|
190
|
+
composeFiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
191
|
+
envFile: z.ZodOptional<z.ZodString>;
|
|
192
|
+
services: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
193
|
+
healthChecks: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
194
|
+
name: z.ZodString;
|
|
195
|
+
url: z.ZodString;
|
|
196
|
+
status: z.ZodOptional<z.ZodNumber>;
|
|
197
|
+
}, z.core.$strip>>>;
|
|
198
|
+
buildCommand: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
199
|
+
buildCwd: z.ZodOptional<z.ZodString>;
|
|
200
|
+
smoketest: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
201
|
+
smoketestCwd: z.ZodOptional<z.ZodString>;
|
|
202
|
+
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
203
|
+
pollIntervalMs: z.ZodOptional<z.ZodNumber>;
|
|
204
|
+
}, z.core.$strip>]>>;
|
|
205
|
+
registryHost: z.ZodOptional<z.ZodString>;
|
|
206
|
+
registryNamespace: z.ZodOptional<z.ZodString>;
|
|
207
|
+
releaseAssets: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
208
|
+
command: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
209
|
+
file: z.ZodString;
|
|
210
|
+
name: z.ZodOptional<z.ZodString>;
|
|
211
|
+
}, z.core.$strict>>>;
|
|
212
|
+
postReleaseHook: z.ZodOptional<z.ZodString>;
|
|
213
|
+
postReleaseHookSecrets: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
214
|
+
coverage: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<false>, z.ZodObject<{
|
|
215
|
+
history: z.ZodOptional<z.ZodEnum<{
|
|
216
|
+
none: "none";
|
|
217
|
+
"orphan-branch": "orphan-branch";
|
|
218
|
+
}>>;
|
|
219
|
+
historyBranch: z.ZodOptional<z.ZodString>;
|
|
220
|
+
tolerancePp: z.ZodOptional<z.ZodNumber>;
|
|
221
|
+
gateOnPullRequests: z.ZodOptional<z.ZodBoolean>;
|
|
222
|
+
}, z.core.$strict>]>>;
|
|
223
|
+
pnpmWorkspace: z.ZodOptional<z.ZodLiteral<false>>;
|
|
224
|
+
}, z.core.$strict>;
|
|
225
|
+
type ToolingConfig = z.infer<typeof ToolingConfigSchema>;
|
|
226
|
+
//#endregion
|
|
227
|
+
//#region src/docker-check/compose-lint.d.ts
|
|
228
|
+
interface ComposeLintIssue {
|
|
229
|
+
kind: "missing-name" | "duplicate-name";
|
|
230
|
+
/** Repo-relative paths involved in this issue (1 for missing-name, 2+ for duplicate-name). */
|
|
231
|
+
files: string[];
|
|
232
|
+
/** Pre-rendered human-readable message ready for `log.error`. */
|
|
233
|
+
message: string;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Walk compose files in `targetDir` and report lint violations:
|
|
237
|
+
*
|
|
238
|
+
* - Rule A (`missing-name`): every override compose file must declare a
|
|
239
|
+
* top-level `name:` directive. Base files (docker-compose.yml etc.) are
|
|
240
|
+
* exempt — their default of dir-basename is fine — but overrides without
|
|
241
|
+
* `name:` silently inherit the base's project namespace, which is how a
|
|
242
|
+
* `compose down -v` against the check overlay wiped the prod-volume project.
|
|
243
|
+
* - Rule B (`duplicate-name`): no two compose files in a single dir may
|
|
244
|
+
* declare the same `name:`. Base files participate in collision detection.
|
|
245
|
+
*/
|
|
246
|
+
declare function lintComposeFiles(targetDir: string, config: ToolingConfig | undefined): ComposeLintIssue[];
|
|
247
|
+
//#endregion
|
|
248
|
+
export { type CheckConfig, type CheckResult, type ComposeConfig, type ComposeLintIssue, type ContainerInfo, type DockerCheckExecutor, type HttpHealthCheck, checkHttpHealth, composeArgs, composeDown, composeLogs, composePs, composeUp, createRealExecutor, deriveProjectName, getContainerHealth, lintComposeFiles, runDockerCheck };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { checkHttpHealth, composeArgs, composeDown, composeLogs, composePs, composeUp, createRealExecutor, getContainerHealth, runDockerCheck };
|
|
1
|
+
import { a as composeArgs, c as composePs, f as lintComposeFiles, i as getContainerHealth, l as composeUp, n as runDockerCheck, o as composeDown, r as checkHttpHealth, s as composeLogs, t as deriveProjectName, u as createRealExecutor } from "../project-name-CAVRoqNy.mjs";
|
|
2
|
+
export { checkHttpHealth, composeArgs, composeDown, composeLogs, composePs, composeUp, createRealExecutor, deriveProjectName, getContainerHealth, lintComposeFiles, runDockerCheck };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,119 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
//#region src/utils/json.d.ts
|
|
4
|
-
declare const PackageJsonSchema: z.ZodObject<{
|
|
5
|
-
name: z.ZodOptional<z.ZodString>;
|
|
6
|
-
version: z.ZodOptional<z.ZodString>;
|
|
7
|
-
private: z.ZodOptional<z.ZodBoolean>;
|
|
8
|
-
type: z.ZodOptional<z.ZodString>;
|
|
9
|
-
scripts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
10
|
-
dependencies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
11
|
-
devDependencies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
12
|
-
bin: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
13
|
-
exports: z.ZodOptional<z.ZodUnknown>;
|
|
14
|
-
main: z.ZodOptional<z.ZodString>;
|
|
15
|
-
types: z.ZodOptional<z.ZodString>;
|
|
16
|
-
typings: z.ZodOptional<z.ZodString>;
|
|
17
|
-
engines: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
18
|
-
repository: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
19
|
-
url: z.ZodString;
|
|
20
|
-
}, z.core.$loose>]>>;
|
|
21
|
-
packageManager: z.ZodOptional<z.ZodString>;
|
|
22
|
-
}, z.core.$loose>;
|
|
23
|
-
type PackageJson = z.infer<typeof PackageJsonSchema>;
|
|
24
|
-
//#endregion
|
|
25
|
-
//#region src/types.d.ts
|
|
26
|
-
type CiPlatform = "github" | "forgejo" | "none";
|
|
27
|
-
type ReleaseStrategy = "release-it" | "simple" | "changesets" | "none";
|
|
28
|
-
type Formatter = "oxfmt" | "prettier";
|
|
29
|
-
/** User's answers from the interactive prompt or CLI flags. */
|
|
30
|
-
interface ProjectConfig {
|
|
31
|
-
/** Project name (from package.json name or user input) */
|
|
32
|
-
name: string;
|
|
33
|
-
/** Project structure */
|
|
34
|
-
structure: "single" | "monorepo";
|
|
35
|
-
/** Include @bensandee/eslint-plugin oxlint plugin */
|
|
36
|
-
useEslintPlugin: boolean;
|
|
37
|
-
/** Formatter choice */
|
|
38
|
-
formatter: Formatter;
|
|
39
|
-
/** Set up vitest with a starter test */
|
|
40
|
-
setupVitest: boolean;
|
|
41
|
-
/** CI platform choice */
|
|
42
|
-
ci: CiPlatform;
|
|
43
|
-
/** Set up Renovate for automated dependency updates */
|
|
44
|
-
setupRenovate: boolean;
|
|
45
|
-
/** Release management strategy */
|
|
46
|
-
releaseStrategy: ReleaseStrategy;
|
|
47
|
-
/** Opt-in: publish packages to npm registry */
|
|
48
|
-
publishNpm: boolean;
|
|
49
|
-
/** Opt-in: publish Docker images to a registry */
|
|
50
|
-
publishDocker: boolean;
|
|
51
|
-
/** Project type determines tsconfig base configuration */
|
|
52
|
-
projectType: "default" | "node" | "react" | "library";
|
|
53
|
-
/** Auto-detect and configure tsconfig bases for monorepo packages */
|
|
54
|
-
detectPackageTypes: boolean;
|
|
55
|
-
/** Target directory (default: cwd) */
|
|
56
|
-
targetDir: string;
|
|
57
|
-
}
|
|
58
|
-
/** Result from a single generator: what file was written and how. */
|
|
59
|
-
interface GeneratorResult {
|
|
60
|
-
filePath: string;
|
|
61
|
-
action: "created" | "updated" | "skipped" | "archived" | "removed";
|
|
62
|
-
/** Human-readable description of what changed */
|
|
63
|
-
description: string;
|
|
64
|
-
}
|
|
65
|
-
/** Context passed to each generator function. */
|
|
66
|
-
interface GeneratorContext {
|
|
67
|
-
config: ProjectConfig;
|
|
68
|
-
/** Absolute path to target directory */
|
|
69
|
-
targetDir: string;
|
|
70
|
-
/** Pre-parsed package.json from the target directory, or undefined if missing/invalid */
|
|
71
|
-
packageJson: PackageJson | undefined;
|
|
72
|
-
/** Check whether a file exists in the target directory */
|
|
73
|
-
exists: (relativePath: string) => boolean;
|
|
74
|
-
/** Read an existing file from the target directory, returns undefined if not found */
|
|
75
|
-
read: (relativePath: string) => string | undefined;
|
|
76
|
-
/** Write a file to the target directory (creating directories as needed) */
|
|
77
|
-
write: (relativePath: string, content: string) => void;
|
|
78
|
-
/** Remove a file from the target directory (no-op if not found) */
|
|
79
|
-
remove: (relativePath: string) => void;
|
|
80
|
-
/** Prompt user for conflict resolution on non-mergeable files */
|
|
81
|
-
confirmOverwrite: (relativePath: string) => Promise<"overwrite" | "skip">;
|
|
82
|
-
}
|
|
83
|
-
/** Generator function signature. */
|
|
84
|
-
type Generator = (ctx: GeneratorContext) => Promise<GeneratorResult>;
|
|
85
|
-
/** State detected from an existing project directory. */
|
|
86
|
-
interface DetectedProjectState {
|
|
87
|
-
hasPackageJson: boolean;
|
|
88
|
-
hasTsconfig: boolean;
|
|
89
|
-
hasOxlintConfig: boolean;
|
|
90
|
-
/** Existing oxlint.config.ts already imports @bensandee/config/oxlint/recommended */
|
|
91
|
-
oxlintExtendsRecommended: boolean;
|
|
92
|
-
/** Legacy .oxlintrc.json found (should be migrated to oxlint.config.ts) */
|
|
93
|
-
hasLegacyOxlintJson: boolean;
|
|
94
|
-
hasGitignore: boolean;
|
|
95
|
-
hasVitestConfig: boolean;
|
|
96
|
-
hasTsdownConfig: boolean;
|
|
97
|
-
hasPnpmWorkspace: boolean;
|
|
98
|
-
hasKnipConfig: boolean;
|
|
99
|
-
hasRenovateConfig: boolean;
|
|
100
|
-
hasReleaseItConfig: boolean;
|
|
101
|
-
hasSimpleReleaseConfig: boolean;
|
|
102
|
-
hasChangesetsConfig: boolean;
|
|
103
|
-
/** Whether package.json has a repository field (needed for release workflows) */
|
|
104
|
-
hasRepositoryField: boolean;
|
|
105
|
-
/** Whether commit-and-tag-version is in root devDependencies (required for simple release strategy) */
|
|
106
|
-
hasCommitAndTagVersion: boolean;
|
|
107
|
-
/** Git remote origin URL (for inferring repository field) */
|
|
108
|
-
gitRemoteUrl: string | null;
|
|
109
|
-
/** Legacy tooling configs found */
|
|
110
|
-
legacyConfigs: LegacyConfig[];
|
|
111
|
-
}
|
|
112
|
-
declare const LEGACY_TOOLS: readonly ["eslint", "prettier", "jest", "webpack", "rollup"];
|
|
113
|
-
type LegacyTool = (typeof LEGACY_TOOLS)[number];
|
|
114
|
-
interface LegacyConfig {
|
|
115
|
-
tool: LegacyTool;
|
|
116
|
-
files: string[];
|
|
117
|
-
}
|
|
118
|
-
//#endregion
|
|
1
|
+
import { a as LegacyConfig, i as GeneratorResult, n as Generator, o as ProjectConfig, r as GeneratorContext, t as DetectedProjectState } from "./types-DtwkblrK.mjs";
|
|
119
2
|
export { type DetectedProjectState, type Generator, type GeneratorContext, type GeneratorResult, type LegacyConfig, type ProjectConfig };
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import * as clack from "@clack/prompts";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
4
|
import { execFileSync } from "node:child_process";
|
|
3
5
|
import { z } from "zod";
|
|
6
|
+
import { parse } from "yaml";
|
|
4
7
|
//#region src/utils/log.ts
|
|
5
8
|
const out = (msg) => console.log(msg);
|
|
6
9
|
const isCI = Boolean(process.env["CI"]);
|
|
@@ -42,6 +45,148 @@ function debugExec(config, label, result) {
|
|
|
42
45
|
log.info(lines.join("\n"));
|
|
43
46
|
}
|
|
44
47
|
//#endregion
|
|
48
|
+
//#region src/docker-check/compose-lint.ts
|
|
49
|
+
const ComposeFileShape = z.looseObject({ name: z.unknown().optional() });
|
|
50
|
+
/**
|
|
51
|
+
* Compose files live by convention in `./` or `docker/`. The lint walks those
|
|
52
|
+
* by default; if `.tooling.json#dockerCheck.composeFiles` overrides the
|
|
53
|
+
* location, each listed file (and its parent dir) is also linted.
|
|
54
|
+
*/
|
|
55
|
+
const COMPOSE_DIR_CANDIDATES = [".", "docker"];
|
|
56
|
+
/** Base compose file names whose project namespace defaults to dir basename. */
|
|
57
|
+
const BASE_COMPOSE_FILES = new Set([
|
|
58
|
+
"docker-compose.yml",
|
|
59
|
+
"docker-compose.yaml",
|
|
60
|
+
"compose.yml",
|
|
61
|
+
"compose.yaml"
|
|
62
|
+
]);
|
|
63
|
+
const COMPOSE_FILENAME_PATTERN = /^(?:docker-compose|compose).*\.(?:yml|yaml)$/;
|
|
64
|
+
/**
|
|
65
|
+
* Walk compose files in `targetDir` and report lint violations:
|
|
66
|
+
*
|
|
67
|
+
* - Rule A (`missing-name`): every override compose file must declare a
|
|
68
|
+
* top-level `name:` directive. Base files (docker-compose.yml etc.) are
|
|
69
|
+
* exempt — their default of dir-basename is fine — but overrides without
|
|
70
|
+
* `name:` silently inherit the base's project namespace, which is how a
|
|
71
|
+
* `compose down -v` against the check overlay wiped the prod-volume project.
|
|
72
|
+
* - Rule B (`duplicate-name`): no two compose files in a single dir may
|
|
73
|
+
* declare the same `name:`. Base files participate in collision detection.
|
|
74
|
+
*/
|
|
75
|
+
function lintComposeFiles(targetDir, config) {
|
|
76
|
+
const issues = [];
|
|
77
|
+
const visitedDirs = /* @__PURE__ */ new Set();
|
|
78
|
+
const standaloneFiles = [];
|
|
79
|
+
const dockerCheck = config?.dockerCheck;
|
|
80
|
+
const isObjectConfig = dockerCheck !== void 0 && dockerCheck !== false;
|
|
81
|
+
for (const candidate of COMPOSE_DIR_CANDIDATES) {
|
|
82
|
+
const absDir = path.resolve(targetDir, candidate);
|
|
83
|
+
if (existsSync(absDir)) visitDir(targetDir, absDir, visitedDirs, issues);
|
|
84
|
+
}
|
|
85
|
+
if (isObjectConfig && dockerCheck.composeFiles) for (const rel of dockerCheck.composeFiles) {
|
|
86
|
+
const absFile = path.resolve(targetDir, rel);
|
|
87
|
+
if (!existsSync(absFile)) continue;
|
|
88
|
+
const dir = path.dirname(absFile);
|
|
89
|
+
if (visitedDirs.has(dir)) continue;
|
|
90
|
+
const basename = path.basename(absFile);
|
|
91
|
+
standaloneFiles.push({
|
|
92
|
+
absPath: absFile,
|
|
93
|
+
relPath: path.relative(targetDir, absFile),
|
|
94
|
+
basename
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const standaloneByDir = /* @__PURE__ */ new Map();
|
|
98
|
+
for (const file of standaloneFiles) {
|
|
99
|
+
const dir = path.dirname(file.absPath);
|
|
100
|
+
const list = standaloneByDir.get(dir) ?? [];
|
|
101
|
+
list.push(file);
|
|
102
|
+
standaloneByDir.set(dir, list);
|
|
103
|
+
}
|
|
104
|
+
for (const files of standaloneByDir.values()) {
|
|
105
|
+
files.sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
106
|
+
lintDirFiles(files, issues);
|
|
107
|
+
}
|
|
108
|
+
return issues;
|
|
109
|
+
}
|
|
110
|
+
function visitDir(targetDir, absDir, visitedDirs, issues) {
|
|
111
|
+
if (visitedDirs.has(absDir)) return;
|
|
112
|
+
visitedDirs.add(absDir);
|
|
113
|
+
let entries;
|
|
114
|
+
try {
|
|
115
|
+
entries = readdirSync(absDir);
|
|
116
|
+
} catch (_error) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const files = entries.filter((name) => COMPOSE_FILENAME_PATTERN.test(name)).toSorted().map((name) => {
|
|
120
|
+
const absPath = path.join(absDir, name);
|
|
121
|
+
return {
|
|
122
|
+
absPath,
|
|
123
|
+
relPath: path.relative(targetDir, absPath),
|
|
124
|
+
basename: name
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
if (files.length === 0) return;
|
|
128
|
+
lintDirFiles(files, issues);
|
|
129
|
+
}
|
|
130
|
+
function lintDirFiles(files, issues) {
|
|
131
|
+
const namesByFile = /* @__PURE__ */ new Map();
|
|
132
|
+
for (const file of files) {
|
|
133
|
+
const declaredName = readTopLevelName(file.absPath);
|
|
134
|
+
namesByFile.set(file.relPath, declaredName);
|
|
135
|
+
if (!BASE_COMPOSE_FILES.has(file.basename) && (declaredName === void 0 || declaredName === "")) {
|
|
136
|
+
const suggested = suggestProjectName(file);
|
|
137
|
+
issues.push({
|
|
138
|
+
kind: "missing-name",
|
|
139
|
+
files: [file.relPath],
|
|
140
|
+
message: `${file.relPath}: override compose file is missing required top-level 'name:' directive.\n Add: name: ${suggested}\n Why: without an explicit name, this override inherits the base file's project namespace.\n A 'docker compose down -v' against this override would target the base project's volumes\n — which has caused production volume loss in the past.`
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const filesByName = /* @__PURE__ */ new Map();
|
|
145
|
+
for (const [relPath, declaredName] of namesByFile) {
|
|
146
|
+
if (declaredName === void 0 || declaredName === "") continue;
|
|
147
|
+
const list = filesByName.get(declaredName) ?? [];
|
|
148
|
+
list.push(relPath);
|
|
149
|
+
filesByName.set(declaredName, list);
|
|
150
|
+
}
|
|
151
|
+
for (const [declaredName, dupFiles] of filesByName) {
|
|
152
|
+
if (dupFiles.length < 2) continue;
|
|
153
|
+
dupFiles.sort();
|
|
154
|
+
const [first, second, ...rest] = dupFiles;
|
|
155
|
+
const ext = rest.length > 0 ? `, ${rest.join(", ")}` : "";
|
|
156
|
+
issues.push({
|
|
157
|
+
kind: "duplicate-name",
|
|
158
|
+
files: dupFiles,
|
|
159
|
+
message: `${String(first)} and ${String(second)}${ext} both declare 'name: ${declaredName}'.\n Each compose file in the same directory must declare a unique 'name:'.\n Suggested fix: rename one (e.g. 'name: ${declaredName}-test' in one of them).`
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Suggest a default name for an override file by stripping the variant from the filename. */
|
|
164
|
+
function suggestProjectName(file) {
|
|
165
|
+
const parts = file.basename.replace(/\.(yml|yaml)$/, "").split(".");
|
|
166
|
+
const variant = parts[parts.length - 1];
|
|
167
|
+
if (parts.length > 1 && variant) return `<project>-${variant}`;
|
|
168
|
+
return "<project>-check";
|
|
169
|
+
}
|
|
170
|
+
function readTopLevelName(absPath) {
|
|
171
|
+
let raw;
|
|
172
|
+
try {
|
|
173
|
+
raw = readFileSync(absPath, "utf-8");
|
|
174
|
+
} catch (_error) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
let yamlValue;
|
|
178
|
+
try {
|
|
179
|
+
yamlValue = parse(raw);
|
|
180
|
+
} catch (_error) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const parsed = ComposeFileShape.safeParse(yamlValue);
|
|
184
|
+
if (!parsed.success) return void 0;
|
|
185
|
+
const value = parsed.data.name;
|
|
186
|
+
if (typeof value !== "string") return void 0;
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
45
190
|
//#region src/utils/exec.ts
|
|
46
191
|
/** Type guard for `execSync` errors that carry stdout/stderr/status. */
|
|
47
192
|
function isExecSyncError(err) {
|
|
@@ -116,7 +261,11 @@ const ContainerInfoSchema = z.object({
|
|
|
116
261
|
});
|
|
117
262
|
/** Build the `docker compose` base args from config. */
|
|
118
263
|
function composeArgs(config) {
|
|
119
|
-
const args = [
|
|
264
|
+
const args = [
|
|
265
|
+
"compose",
|
|
266
|
+
"--project-name",
|
|
267
|
+
config.projectName
|
|
268
|
+
];
|
|
120
269
|
for (const f of config.composeFiles) args.push("-f", f);
|
|
121
270
|
if (config.envFile) args.push("--env-file", config.envFile);
|
|
122
271
|
return args;
|
|
@@ -340,4 +489,25 @@ async function runDockerCheck(executor, config) {
|
|
|
340
489
|
}
|
|
341
490
|
}
|
|
342
491
|
//#endregion
|
|
343
|
-
|
|
492
|
+
//#region src/docker-check/project-name.ts
|
|
493
|
+
/**
|
|
494
|
+
* Derive the `docker compose --project-name` value for a check run.
|
|
495
|
+
*
|
|
496
|
+
* Always returns a unique, non-colliding namespace so a stray `compose down -v`
|
|
497
|
+
* during the check cannot target volumes belonging to a long-running stack
|
|
498
|
+
* sharing the same docker daemon (the failure mode that wiped prod volumes
|
|
499
|
+
* when a CI runner bind-mounted /var/run/docker.sock and the check overlay
|
|
500
|
+
* inherited the base file's `name:`).
|
|
501
|
+
*
|
|
502
|
+
* Format: `<sanitized-repo>-check-<suffix>` where suffix prefers a CI run id
|
|
503
|
+
* for traceability and falls back to `process.pid` locally so two concurrent
|
|
504
|
+
* `bst docker:check` invocations on the same machine don't collide.
|
|
505
|
+
*/
|
|
506
|
+
function deriveProjectName(repoName, env = process.env) {
|
|
507
|
+
return `${sanitize(repoName)}-check-${sanitize(env["GITHUB_RUN_ID"] ?? env["CI_RUN_ID"] ?? String(process.pid))}`;
|
|
508
|
+
}
|
|
509
|
+
function sanitize(name) {
|
|
510
|
+
return name.replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
511
|
+
}
|
|
512
|
+
//#endregion
|
|
513
|
+
export { log as _, composeArgs as a, composePs as c, isExecSyncError as d, lintComposeFiles as f, redact as g, isEnvVerbose as h, getContainerHealth as i, composeUp as l, debugExec as m, runDockerCheck as n, composeDown as o, debug as p, checkHttpHealth as r, composeLogs as s, deriveProjectName as t, createRealExecutor as u, note as v };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
//#region src/utils/json.d.ts
|
|
4
|
+
declare const PackageJsonSchema: z.ZodObject<{
|
|
5
|
+
name: z.ZodOptional<z.ZodString>;
|
|
6
|
+
version: z.ZodOptional<z.ZodString>;
|
|
7
|
+
private: z.ZodOptional<z.ZodBoolean>;
|
|
8
|
+
type: z.ZodOptional<z.ZodString>;
|
|
9
|
+
scripts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
10
|
+
dependencies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
11
|
+
devDependencies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
12
|
+
bin: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
13
|
+
exports: z.ZodOptional<z.ZodUnknown>;
|
|
14
|
+
main: z.ZodOptional<z.ZodString>;
|
|
15
|
+
types: z.ZodOptional<z.ZodString>;
|
|
16
|
+
typings: z.ZodOptional<z.ZodString>;
|
|
17
|
+
engines: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
18
|
+
repository: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
19
|
+
url: z.ZodString;
|
|
20
|
+
}, z.core.$loose>]>>;
|
|
21
|
+
packageManager: z.ZodOptional<z.ZodString>;
|
|
22
|
+
}, z.core.$loose>;
|
|
23
|
+
type PackageJson = z.infer<typeof PackageJsonSchema>;
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/types.d.ts
|
|
26
|
+
type CiPlatform = "github" | "forgejo" | "none";
|
|
27
|
+
type ReleaseStrategy = "release-it" | "simple" | "changesets" | "none";
|
|
28
|
+
type Formatter = "oxfmt" | "prettier";
|
|
29
|
+
/** User's answers from the interactive prompt or CLI flags. */
|
|
30
|
+
interface ProjectConfig {
|
|
31
|
+
/** Project name (from package.json name or user input) */
|
|
32
|
+
name: string;
|
|
33
|
+
/** Project structure */
|
|
34
|
+
structure: "single" | "monorepo";
|
|
35
|
+
/** Include @bensandee/eslint-plugin oxlint plugin */
|
|
36
|
+
useEslintPlugin: boolean;
|
|
37
|
+
/** Formatter choice */
|
|
38
|
+
formatter: Formatter;
|
|
39
|
+
/** Set up vitest with a starter test */
|
|
40
|
+
setupVitest: boolean;
|
|
41
|
+
/** CI platform choice */
|
|
42
|
+
ci: CiPlatform;
|
|
43
|
+
/** Set up Renovate for automated dependency updates */
|
|
44
|
+
setupRenovate: boolean;
|
|
45
|
+
/** Release management strategy */
|
|
46
|
+
releaseStrategy: ReleaseStrategy;
|
|
47
|
+
/** Opt-in: publish packages to npm registry */
|
|
48
|
+
publishNpm: boolean;
|
|
49
|
+
/** Opt-in: publish Docker images to a registry */
|
|
50
|
+
publishDocker: boolean;
|
|
51
|
+
/** Project type determines tsconfig base configuration */
|
|
52
|
+
projectType: "default" | "node" | "react" | "library";
|
|
53
|
+
/** Auto-detect and configure tsconfig bases for monorepo packages */
|
|
54
|
+
detectPackageTypes: boolean;
|
|
55
|
+
/** Target directory (default: cwd) */
|
|
56
|
+
targetDir: string;
|
|
57
|
+
}
|
|
58
|
+
/** Result from a single generator: what file was written and how. */
|
|
59
|
+
interface GeneratorResult {
|
|
60
|
+
filePath: string;
|
|
61
|
+
action: "created" | "updated" | "skipped" | "archived" | "removed";
|
|
62
|
+
/** Human-readable description of what changed */
|
|
63
|
+
description: string;
|
|
64
|
+
}
|
|
65
|
+
/** Context passed to each generator function. */
|
|
66
|
+
interface GeneratorContext {
|
|
67
|
+
config: ProjectConfig;
|
|
68
|
+
/** Absolute path to target directory */
|
|
69
|
+
targetDir: string;
|
|
70
|
+
/** Pre-parsed package.json from the target directory, or undefined if missing/invalid */
|
|
71
|
+
packageJson: PackageJson | undefined;
|
|
72
|
+
/** Check whether a file exists in the target directory */
|
|
73
|
+
exists: (relativePath: string) => boolean;
|
|
74
|
+
/** Read an existing file from the target directory, returns undefined if not found */
|
|
75
|
+
read: (relativePath: string) => string | undefined;
|
|
76
|
+
/** Write a file to the target directory (creating directories as needed) */
|
|
77
|
+
write: (relativePath: string, content: string) => void;
|
|
78
|
+
/** Remove a file from the target directory (no-op if not found) */
|
|
79
|
+
remove: (relativePath: string) => void;
|
|
80
|
+
/** Prompt user for conflict resolution on non-mergeable files */
|
|
81
|
+
confirmOverwrite: (relativePath: string) => Promise<"overwrite" | "skip">;
|
|
82
|
+
}
|
|
83
|
+
/** Generator function signature. */
|
|
84
|
+
type Generator = (ctx: GeneratorContext) => Promise<GeneratorResult>;
|
|
85
|
+
/** State detected from an existing project directory. */
|
|
86
|
+
interface DetectedProjectState {
|
|
87
|
+
hasPackageJson: boolean;
|
|
88
|
+
hasTsconfig: boolean;
|
|
89
|
+
hasOxlintConfig: boolean;
|
|
90
|
+
/** Existing oxlint.config.ts already imports @bensandee/config/oxlint/recommended */
|
|
91
|
+
oxlintExtendsRecommended: boolean;
|
|
92
|
+
/** Legacy .oxlintrc.json found (should be migrated to oxlint.config.ts) */
|
|
93
|
+
hasLegacyOxlintJson: boolean;
|
|
94
|
+
hasGitignore: boolean;
|
|
95
|
+
hasVitestConfig: boolean;
|
|
96
|
+
hasTsdownConfig: boolean;
|
|
97
|
+
hasPnpmWorkspace: boolean;
|
|
98
|
+
hasKnipConfig: boolean;
|
|
99
|
+
hasRenovateConfig: boolean;
|
|
100
|
+
hasReleaseItConfig: boolean;
|
|
101
|
+
hasSimpleReleaseConfig: boolean;
|
|
102
|
+
hasChangesetsConfig: boolean;
|
|
103
|
+
/** Whether package.json has a repository field (needed for release workflows) */
|
|
104
|
+
hasRepositoryField: boolean;
|
|
105
|
+
/** Whether commit-and-tag-version is in root devDependencies (required for simple release strategy) */
|
|
106
|
+
hasCommitAndTagVersion: boolean;
|
|
107
|
+
/** Git remote origin URL (for inferring repository field) */
|
|
108
|
+
gitRemoteUrl: string | null;
|
|
109
|
+
/** Legacy tooling configs found */
|
|
110
|
+
legacyConfigs: LegacyConfig[];
|
|
111
|
+
}
|
|
112
|
+
declare const LEGACY_TOOLS: readonly ["eslint", "prettier", "jest", "webpack", "rollup"];
|
|
113
|
+
type LegacyTool = (typeof LEGACY_TOOLS)[number];
|
|
114
|
+
interface LegacyConfig {
|
|
115
|
+
tool: LegacyTool;
|
|
116
|
+
files: string[];
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
export { LegacyConfig as a, GeneratorResult as i, Generator as n, ProjectConfig as o, GeneratorContext as r, DetectedProjectState as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bensandee/tooling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.59.1",
|
|
4
4
|
"description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"release-it": "20.0.1",
|
|
56
56
|
"tsdown": "0.22.0",
|
|
57
57
|
"@bensandee/claude-hooks": "0.7.4",
|
|
58
|
-
"@bensandee/config": "0.11.
|
|
58
|
+
"@bensandee/config": "0.11.2"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"build": "tsdown",
|