@bensandee/tooling 0.19.0 → 0.21.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/dist/bin.mjs +533 -420
- package/package.json +1 -1
package/dist/bin.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, unlinkSync, w
|
|
|
7
7
|
import JSON5 from "json5";
|
|
8
8
|
import { parse } from "jsonc-parser";
|
|
9
9
|
import { z } from "zod";
|
|
10
|
-
import { isMap, isSeq, parse as parse$1, parseDocument, stringify } from "yaml";
|
|
10
|
+
import { isMap, isScalar, 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
13
|
import { tmpdir } from "node:os";
|
|
@@ -206,6 +206,32 @@ function computeDefaults(targetDir) {
|
|
|
206
206
|
detectPackageTypes: true
|
|
207
207
|
};
|
|
208
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* List packages that would be published to npm (non-private, have a name).
|
|
211
|
+
* For monorepos, scans packages/ subdirectories. For single repos, checks root package.json.
|
|
212
|
+
* An optional pre-parsed root package.json can be passed to avoid re-reading from disk.
|
|
213
|
+
*/
|
|
214
|
+
function getPublishablePackages(targetDir, structure, rootPackageJson) {
|
|
215
|
+
if (structure === "monorepo") {
|
|
216
|
+
const packages = getMonorepoPackages(targetDir);
|
|
217
|
+
const results = [];
|
|
218
|
+
for (const pkg of packages) {
|
|
219
|
+
const pkgJson = readPackageJson(pkg.dir);
|
|
220
|
+
if (!pkgJson || pkgJson.private || !pkgJson.name) continue;
|
|
221
|
+
results.push({
|
|
222
|
+
name: pkgJson.name,
|
|
223
|
+
dir: pkg.dir
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return results;
|
|
227
|
+
}
|
|
228
|
+
const pkg = rootPackageJson ?? readPackageJson(targetDir);
|
|
229
|
+
if (!pkg || pkg.private || !pkg.name) return [];
|
|
230
|
+
return [{
|
|
231
|
+
name: pkg.name,
|
|
232
|
+
dir: targetDir
|
|
233
|
+
}];
|
|
234
|
+
}
|
|
209
235
|
/** List packages in a monorepo's packages/ directory. */
|
|
210
236
|
function getMonorepoPackages(targetDir) {
|
|
211
237
|
const packagesDir = path.join(targetDir, "packages");
|
|
@@ -569,6 +595,334 @@ function mergeWithSavedConfig(detected, saved) {
|
|
|
569
595
|
};
|
|
570
596
|
}
|
|
571
597
|
//#endregion
|
|
598
|
+
//#region src/utils/yaml-merge.ts
|
|
599
|
+
const IGNORE_PATTERN = "@bensandee/tooling:ignore";
|
|
600
|
+
const FORGEJO_SCHEMA_COMMENT = "# yaml-language-server: $schema=../../.vscode/forgejo-workflow.schema.json\n";
|
|
601
|
+
/** Returns a yaml-language-server schema comment for Forgejo workflows, empty string otherwise. */
|
|
602
|
+
function workflowSchemaComment(ci) {
|
|
603
|
+
return ci === "forgejo" ? FORGEJO_SCHEMA_COMMENT : "";
|
|
604
|
+
}
|
|
605
|
+
/** Prepend the Forgejo schema comment if it's not already present. No-op for GitHub. */
|
|
606
|
+
function ensureSchemaComment(content, ci) {
|
|
607
|
+
if (ci !== "forgejo") return content;
|
|
608
|
+
if (content.includes("yaml-language-server")) return content;
|
|
609
|
+
return FORGEJO_SCHEMA_COMMENT + content;
|
|
610
|
+
}
|
|
611
|
+
/** Check if a YAML file has an opt-out comment in the first 10 lines. */
|
|
612
|
+
function isToolingIgnored(content) {
|
|
613
|
+
return content.split("\n", 10).some((line) => line.includes(IGNORE_PATTERN));
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Ensure required commands exist under `pre-commit.commands` in a lefthook config.
|
|
617
|
+
* Only adds missing commands — never modifies existing ones.
|
|
618
|
+
* Returns unchanged content if the file has an opt-out comment or can't be parsed.
|
|
619
|
+
*/
|
|
620
|
+
function mergeLefthookCommands(existing, requiredCommands) {
|
|
621
|
+
if (isToolingIgnored(existing)) return {
|
|
622
|
+
content: existing,
|
|
623
|
+
changed: false
|
|
624
|
+
};
|
|
625
|
+
try {
|
|
626
|
+
const doc = parseDocument(existing);
|
|
627
|
+
let changed = false;
|
|
628
|
+
if (!doc.hasIn(["pre-commit", "commands"])) {
|
|
629
|
+
doc.setIn(["pre-commit", "commands"], requiredCommands);
|
|
630
|
+
return {
|
|
631
|
+
content: doc.toString(),
|
|
632
|
+
changed: true
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
const commands = doc.getIn(["pre-commit", "commands"]);
|
|
636
|
+
if (!isMap(commands)) return {
|
|
637
|
+
content: existing,
|
|
638
|
+
changed: false
|
|
639
|
+
};
|
|
640
|
+
for (const [name, config] of Object.entries(requiredCommands)) if (!commands.has(name)) {
|
|
641
|
+
commands.set(name, config);
|
|
642
|
+
changed = true;
|
|
643
|
+
}
|
|
644
|
+
return {
|
|
645
|
+
content: changed ? doc.toString() : existing,
|
|
646
|
+
changed
|
|
647
|
+
};
|
|
648
|
+
} catch {
|
|
649
|
+
return {
|
|
650
|
+
content: existing,
|
|
651
|
+
changed: false
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Ensure required steps exist in a workflow job's steps array.
|
|
657
|
+
* Only adds missing steps at the end — never modifies existing ones.
|
|
658
|
+
* Returns unchanged content if the file has an opt-out comment or can't be parsed.
|
|
659
|
+
*/
|
|
660
|
+
function mergeWorkflowSteps(existing, jobName, requiredSteps) {
|
|
661
|
+
if (isToolingIgnored(existing)) return {
|
|
662
|
+
content: existing,
|
|
663
|
+
changed: false
|
|
664
|
+
};
|
|
665
|
+
try {
|
|
666
|
+
const doc = parseDocument(existing);
|
|
667
|
+
const steps = doc.getIn([
|
|
668
|
+
"jobs",
|
|
669
|
+
jobName,
|
|
670
|
+
"steps"
|
|
671
|
+
]);
|
|
672
|
+
if (!isSeq(steps)) return {
|
|
673
|
+
content: existing,
|
|
674
|
+
changed: false
|
|
675
|
+
};
|
|
676
|
+
let changed = false;
|
|
677
|
+
for (const { match, step } of requiredSteps) if (!steps.items.some((item) => {
|
|
678
|
+
if (!isMap(item)) return false;
|
|
679
|
+
if (match.run) {
|
|
680
|
+
const run = item.get("run");
|
|
681
|
+
return typeof run === "string" && run.includes(match.run);
|
|
682
|
+
}
|
|
683
|
+
if (match.uses) {
|
|
684
|
+
const uses = item.get("uses");
|
|
685
|
+
return typeof uses === "string" && uses.startsWith(match.uses);
|
|
686
|
+
}
|
|
687
|
+
return false;
|
|
688
|
+
})) {
|
|
689
|
+
steps.add(doc.createNode(step));
|
|
690
|
+
changed = true;
|
|
691
|
+
}
|
|
692
|
+
return {
|
|
693
|
+
content: changed ? doc.toString() : existing,
|
|
694
|
+
changed
|
|
695
|
+
};
|
|
696
|
+
} catch {
|
|
697
|
+
return {
|
|
698
|
+
content: existing,
|
|
699
|
+
changed: false
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Add a job to an existing workflow YAML if it doesn't already exist.
|
|
705
|
+
* Returns unchanged content if the job already exists, the file has an opt-out comment,
|
|
706
|
+
* or the document can't be parsed.
|
|
707
|
+
*/
|
|
708
|
+
/**
|
|
709
|
+
* Ensure a `concurrency` block exists at the workflow top level.
|
|
710
|
+
* Adds it if missing — never modifies an existing one.
|
|
711
|
+
* Returns unchanged content if the file has an opt-out comment or can't be parsed.
|
|
712
|
+
*/
|
|
713
|
+
function ensureWorkflowConcurrency(existing, concurrency) {
|
|
714
|
+
if (isToolingIgnored(existing)) return {
|
|
715
|
+
content: existing,
|
|
716
|
+
changed: false
|
|
717
|
+
};
|
|
718
|
+
try {
|
|
719
|
+
const doc = parseDocument(existing);
|
|
720
|
+
if (doc.has("concurrency")) return {
|
|
721
|
+
content: existing,
|
|
722
|
+
changed: false
|
|
723
|
+
};
|
|
724
|
+
doc.set("concurrency", concurrency);
|
|
725
|
+
const contents = doc.contents;
|
|
726
|
+
if (isMap(contents)) {
|
|
727
|
+
const items = contents.items;
|
|
728
|
+
const nameIdx = items.findIndex((p) => isScalar(p.key) && p.key.value === "name");
|
|
729
|
+
const concPair = items.pop();
|
|
730
|
+
if (concPair) items.splice(nameIdx + 1, 0, concPair);
|
|
731
|
+
}
|
|
732
|
+
return {
|
|
733
|
+
content: doc.toString(),
|
|
734
|
+
changed: true
|
|
735
|
+
};
|
|
736
|
+
} catch {
|
|
737
|
+
return {
|
|
738
|
+
content: existing,
|
|
739
|
+
changed: false
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function addWorkflowJob(existing, jobName, jobConfig) {
|
|
744
|
+
if (isToolingIgnored(existing)) return {
|
|
745
|
+
content: existing,
|
|
746
|
+
changed: false
|
|
747
|
+
};
|
|
748
|
+
try {
|
|
749
|
+
const doc = parseDocument(existing);
|
|
750
|
+
const jobs = doc.getIn(["jobs"]);
|
|
751
|
+
if (!isMap(jobs)) return {
|
|
752
|
+
content: existing,
|
|
753
|
+
changed: false
|
|
754
|
+
};
|
|
755
|
+
if (jobs.has(jobName)) return {
|
|
756
|
+
content: existing,
|
|
757
|
+
changed: false
|
|
758
|
+
};
|
|
759
|
+
jobs.set(jobName, doc.createNode(jobConfig));
|
|
760
|
+
return {
|
|
761
|
+
content: doc.toString(),
|
|
762
|
+
changed: true
|
|
763
|
+
};
|
|
764
|
+
} catch {
|
|
765
|
+
return {
|
|
766
|
+
content: existing,
|
|
767
|
+
changed: false
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
//#endregion
|
|
772
|
+
//#region src/generators/deploy-ci.ts
|
|
773
|
+
/** Build a GitHub Actions expression like `${{ expr }}` without triggering no-template-curly-in-string. */
|
|
774
|
+
function actionsExpr$2(expr) {
|
|
775
|
+
return `\${{ ${expr} }}`;
|
|
776
|
+
}
|
|
777
|
+
function hasEnginesNode$2(ctx) {
|
|
778
|
+
return typeof ctx.packageJson?.["engines"]?.["node"] === "string";
|
|
779
|
+
}
|
|
780
|
+
function deployWorkflow(ci, nodeVersionYaml) {
|
|
781
|
+
return `${workflowSchemaComment(ci)}name: Deploy
|
|
782
|
+
on:
|
|
783
|
+
push:
|
|
784
|
+
tags:
|
|
785
|
+
- "v[0-9]+.[0-9]+.[0-9]+"
|
|
786
|
+
|
|
787
|
+
jobs:
|
|
788
|
+
deploy:
|
|
789
|
+
runs-on: ubuntu-latest
|
|
790
|
+
steps:
|
|
791
|
+
- uses: actions/checkout@v4
|
|
792
|
+
- uses: pnpm/action-setup@v4
|
|
793
|
+
- uses: actions/setup-node@v4
|
|
794
|
+
with:
|
|
795
|
+
${nodeVersionYaml}
|
|
796
|
+
- run: pnpm install --frozen-lockfile
|
|
797
|
+
- name: Publish Docker images
|
|
798
|
+
env:
|
|
799
|
+
DOCKER_REGISTRY_HOST: ${actionsExpr$2("vars.DOCKER_REGISTRY_HOST")}
|
|
800
|
+
DOCKER_REGISTRY_NAMESPACE: ${actionsExpr$2("vars.DOCKER_REGISTRY_NAMESPACE")}
|
|
801
|
+
DOCKER_USERNAME: ${actionsExpr$2("secrets.DOCKER_USERNAME")}
|
|
802
|
+
DOCKER_PASSWORD: ${actionsExpr$2("secrets.DOCKER_PASSWORD")}
|
|
803
|
+
run: pnpm exec tooling docker:publish
|
|
804
|
+
`;
|
|
805
|
+
}
|
|
806
|
+
function requiredDeploySteps() {
|
|
807
|
+
return [
|
|
808
|
+
{
|
|
809
|
+
match: { uses: "actions/checkout" },
|
|
810
|
+
step: { uses: "actions/checkout@v4" }
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
match: { uses: "pnpm/action-setup" },
|
|
814
|
+
step: { uses: "pnpm/action-setup@v4" }
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
match: { uses: "actions/setup-node" },
|
|
818
|
+
step: { uses: "actions/setup-node@v4" }
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
match: { run: "pnpm install" },
|
|
822
|
+
step: { run: "pnpm install --frozen-lockfile" }
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
match: { run: "docker:publish" },
|
|
826
|
+
step: { run: "pnpm exec tooling docker:publish" }
|
|
827
|
+
}
|
|
828
|
+
];
|
|
829
|
+
}
|
|
830
|
+
/** Convention paths to check for Dockerfiles. */
|
|
831
|
+
const CONVENTION_DOCKERFILE_PATHS$1 = ["Dockerfile", "docker/Dockerfile"];
|
|
832
|
+
const DockerMapSchema = z.object({ docker: z.record(z.string(), z.unknown()).optional() });
|
|
833
|
+
/** Get names of packages that have Docker builds (by convention or .tooling.json config). */
|
|
834
|
+
function getDockerPackageNames(ctx) {
|
|
835
|
+
const names = [];
|
|
836
|
+
const configRaw = ctx.read(".tooling.json");
|
|
837
|
+
if (configRaw) {
|
|
838
|
+
const result = DockerMapSchema.safeParse(JSON.parse(configRaw));
|
|
839
|
+
if (result.success && result.data.docker) names.push(...Object.keys(result.data.docker));
|
|
840
|
+
}
|
|
841
|
+
if (ctx.config.structure === "monorepo") {
|
|
842
|
+
const packages = getMonorepoPackages(ctx.targetDir);
|
|
843
|
+
for (const pkg of packages) {
|
|
844
|
+
const dirName = pkg.name.split("/").pop() ?? pkg.name;
|
|
845
|
+
if (names.includes(dirName)) continue;
|
|
846
|
+
for (const rel of CONVENTION_DOCKERFILE_PATHS$1) if (ctx.exists(`packages/${dirName}/${rel}`)) {
|
|
847
|
+
names.push(dirName);
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
} else for (const rel of CONVENTION_DOCKERFILE_PATHS$1) if (ctx.exists(rel)) {
|
|
852
|
+
if (!names.includes(ctx.config.name)) names.push(ctx.config.name);
|
|
853
|
+
break;
|
|
854
|
+
}
|
|
855
|
+
return names;
|
|
856
|
+
}
|
|
857
|
+
/** Check whether any Docker packages exist by convention or .tooling.json config. */
|
|
858
|
+
function hasDockerPackages(ctx) {
|
|
859
|
+
return getDockerPackageNames(ctx).length > 0;
|
|
860
|
+
}
|
|
861
|
+
async function generateDeployCi(ctx) {
|
|
862
|
+
const filePath = "deploy-ci";
|
|
863
|
+
if (!hasDockerPackages(ctx) || ctx.config.ci === "none") return {
|
|
864
|
+
filePath,
|
|
865
|
+
action: "skipped",
|
|
866
|
+
description: "Deploy CI workflow not applicable"
|
|
867
|
+
};
|
|
868
|
+
const isGitHub = ctx.config.ci === "github";
|
|
869
|
+
const workflowPath = isGitHub ? ".github/workflows/publish.yml" : ".forgejo/workflows/publish.yml";
|
|
870
|
+
const nodeVersionYaml = hasEnginesNode$2(ctx) ? "node-version-file: package.json" : "node-version: \"24\"";
|
|
871
|
+
const content = deployWorkflow(ctx.config.ci, nodeVersionYaml);
|
|
872
|
+
if (ctx.exists(workflowPath)) {
|
|
873
|
+
const existing = ctx.read(workflowPath);
|
|
874
|
+
if (existing) {
|
|
875
|
+
if (existing === content || ensureSchemaComment(existing, ctx.config.ci) === content) return {
|
|
876
|
+
filePath: workflowPath,
|
|
877
|
+
action: "skipped",
|
|
878
|
+
description: "Deploy workflow already up to date"
|
|
879
|
+
};
|
|
880
|
+
const merged = mergeWorkflowSteps(existing, "deploy", requiredDeploySteps());
|
|
881
|
+
const withComment = ensureSchemaComment(merged.content, ctx.config.ci);
|
|
882
|
+
if (withComment === content) {
|
|
883
|
+
ctx.write(workflowPath, content);
|
|
884
|
+
return {
|
|
885
|
+
filePath: workflowPath,
|
|
886
|
+
action: "updated",
|
|
887
|
+
description: "Added missing steps to deploy workflow"
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
if (await ctx.confirmOverwrite(workflowPath) === "skip") {
|
|
891
|
+
if (merged.changed || withComment !== merged.content) {
|
|
892
|
+
ctx.write(workflowPath, withComment);
|
|
893
|
+
return {
|
|
894
|
+
filePath: workflowPath,
|
|
895
|
+
action: "updated",
|
|
896
|
+
description: "Added missing steps to deploy workflow"
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
return {
|
|
900
|
+
filePath: workflowPath,
|
|
901
|
+
action: "skipped",
|
|
902
|
+
description: "Existing deploy workflow preserved"
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
ctx.write(workflowPath, content);
|
|
906
|
+
return {
|
|
907
|
+
filePath: workflowPath,
|
|
908
|
+
action: "updated",
|
|
909
|
+
description: "Replaced deploy workflow with updated template"
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
return {
|
|
913
|
+
filePath: workflowPath,
|
|
914
|
+
action: "skipped",
|
|
915
|
+
description: "Deploy workflow already up to date"
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
ctx.write(workflowPath, content);
|
|
919
|
+
return {
|
|
920
|
+
filePath: workflowPath,
|
|
921
|
+
action: "created",
|
|
922
|
+
description: `Generated ${isGitHub ? "GitHub" : "Forgejo"} Actions deploy workflow`
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
//#endregion
|
|
572
926
|
//#region src/generators/package-json.ts
|
|
573
927
|
const STANDARD_SCRIPTS_SINGLE = {
|
|
574
928
|
build: "tsdown",
|
|
@@ -598,7 +952,9 @@ const MANAGED_SCRIPTS = {
|
|
|
598
952
|
check: "checks:run",
|
|
599
953
|
"ci:check": "pnpm check",
|
|
600
954
|
"tooling:check": "repo:sync --check",
|
|
601
|
-
"tooling:sync": "repo:sync"
|
|
955
|
+
"tooling:sync": "repo:sync",
|
|
956
|
+
"docker:build": "docker:build",
|
|
957
|
+
"docker:check": "docker:check"
|
|
602
958
|
};
|
|
603
959
|
/** Deprecated scripts to remove during migration. */
|
|
604
960
|
const DEPRECATED_SCRIPTS = ["tooling:init", "tooling:update"];
|
|
@@ -644,9 +1000,7 @@ function addReleaseDeps(deps, config) {
|
|
|
644
1000
|
deps["release-it"] = "18.1.2";
|
|
645
1001
|
if (config.structure === "monorepo") deps["@release-it/bumper"] = "7.0.2";
|
|
646
1002
|
break;
|
|
647
|
-
case "simple":
|
|
648
|
-
deps["commit-and-tag-version"] = "12.5.0";
|
|
649
|
-
break;
|
|
1003
|
+
case "simple": break;
|
|
650
1004
|
case "changesets":
|
|
651
1005
|
deps["@changesets/cli"] = "2.29.4";
|
|
652
1006
|
break;
|
|
@@ -657,7 +1011,7 @@ function getAddedDevDepNames(config) {
|
|
|
657
1011
|
const deps = { ...ROOT_DEV_DEPS };
|
|
658
1012
|
if (config.structure !== "monorepo") Object.assign(deps, PER_PACKAGE_DEV_DEPS);
|
|
659
1013
|
deps["@bensandee/config"] = "0.8.2";
|
|
660
|
-
deps["@bensandee/tooling"] = "0.
|
|
1014
|
+
deps["@bensandee/tooling"] = "0.21.0";
|
|
661
1015
|
if (config.formatter === "oxfmt") deps["oxfmt"] = "0.35.0";
|
|
662
1016
|
if (config.formatter === "prettier") deps["prettier"] = "3.8.1";
|
|
663
1017
|
addReleaseDeps(deps, config);
|
|
@@ -675,10 +1029,14 @@ async function generatePackageJson(ctx) {
|
|
|
675
1029
|
};
|
|
676
1030
|
if (ctx.config.releaseStrategy === "changesets") allScripts["changeset"] = "changeset";
|
|
677
1031
|
if (ctx.config.releaseStrategy !== "none" && ctx.config.releaseStrategy !== "changesets") allScripts["trigger-release"] = "pnpm exec tooling release:trigger";
|
|
1032
|
+
if (hasDockerPackages(ctx)) {
|
|
1033
|
+
allScripts["docker:build"] = "pnpm exec tooling docker:build";
|
|
1034
|
+
allScripts["docker:check"] = "pnpm exec tooling docker:check";
|
|
1035
|
+
}
|
|
678
1036
|
const devDeps = { ...ROOT_DEV_DEPS };
|
|
679
1037
|
if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
|
|
680
1038
|
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.8.2";
|
|
681
|
-
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.
|
|
1039
|
+
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.21.0";
|
|
682
1040
|
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.9.2";
|
|
683
1041
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = "0.35.0";
|
|
684
1042
|
if (ctx.config.formatter === "prettier") devDeps["prettier"] = "3.8.1";
|
|
@@ -1121,219 +1479,52 @@ const ALL_ENTRIES = [...REQUIRED_ENTRIES, ...OPTIONAL_ENTRIES];
|
|
|
1121
1479
|
function normalizeEntry(entry) {
|
|
1122
1480
|
let s = entry.trim();
|
|
1123
1481
|
if (s.startsWith("/")) s = s.slice(1);
|
|
1124
|
-
if (s.endsWith("/")) s = s.slice(0, -1);
|
|
1125
|
-
return s;
|
|
1126
|
-
}
|
|
1127
|
-
async function generateGitignore(ctx) {
|
|
1128
|
-
const filePath = ".gitignore";
|
|
1129
|
-
const existing = ctx.read(filePath);
|
|
1130
|
-
if (existing) {
|
|
1131
|
-
const existingNormalized = new Set(existing.split("\n").map(normalizeEntry).filter((line) => line.length > 0));
|
|
1132
|
-
const missing = ALL_ENTRIES.filter((entry) => !existingNormalized.has(normalizeEntry(entry)));
|
|
1133
|
-
if (missing.length === 0) return {
|
|
1134
|
-
filePath,
|
|
1135
|
-
action: "skipped",
|
|
1136
|
-
description: "Already has all standard entries"
|
|
1137
|
-
};
|
|
1138
|
-
const missingRequired = REQUIRED_ENTRIES.filter((entry) => !existingNormalized.has(normalizeEntry(entry)));
|
|
1139
|
-
const updated = existing.trimEnd() + "\n\n# Added by @bensandee/tooling\n" + missing.join("\n") + "\n";
|
|
1140
|
-
ctx.write(filePath, updated);
|
|
1141
|
-
if (missingRequired.length === 0) return {
|
|
1142
|
-
filePath,
|
|
1143
|
-
action: "skipped",
|
|
1144
|
-
description: "Only optional entries missing"
|
|
1145
|
-
};
|
|
1146
|
-
return {
|
|
1147
|
-
filePath,
|
|
1148
|
-
action: "updated",
|
|
1149
|
-
description: `Appended ${String(missing.length)} missing entries`
|
|
1150
|
-
};
|
|
1151
|
-
}
|
|
1152
|
-
ctx.write(filePath, ALL_ENTRIES.join("\n") + "\n");
|
|
1153
|
-
return {
|
|
1154
|
-
filePath,
|
|
1155
|
-
action: "created",
|
|
1156
|
-
description: "Generated .gitignore"
|
|
1157
|
-
};
|
|
1158
|
-
}
|
|
1159
|
-
//#endregion
|
|
1160
|
-
//#region src/utils/yaml-merge.ts
|
|
1161
|
-
const IGNORE_PATTERN = "@bensandee/tooling:ignore";
|
|
1162
|
-
const FORGEJO_SCHEMA_COMMENT = "# yaml-language-server: $schema=../../.vscode/forgejo-workflow.schema.json\n";
|
|
1163
|
-
/** Returns a yaml-language-server schema comment for Forgejo workflows, empty string otherwise. */
|
|
1164
|
-
function workflowSchemaComment(ci) {
|
|
1165
|
-
return ci === "forgejo" ? FORGEJO_SCHEMA_COMMENT : "";
|
|
1166
|
-
}
|
|
1167
|
-
/** Prepend the Forgejo schema comment if it's not already present. No-op for GitHub. */
|
|
1168
|
-
function ensureSchemaComment(content, ci) {
|
|
1169
|
-
if (ci !== "forgejo") return content;
|
|
1170
|
-
if (content.includes("yaml-language-server")) return content;
|
|
1171
|
-
return FORGEJO_SCHEMA_COMMENT + content;
|
|
1172
|
-
}
|
|
1173
|
-
/** Check if a YAML file has an opt-out comment in the first 10 lines. */
|
|
1174
|
-
function isToolingIgnored(content) {
|
|
1175
|
-
return content.split("\n", 10).some((line) => line.includes(IGNORE_PATTERN));
|
|
1176
|
-
}
|
|
1177
|
-
/**
|
|
1178
|
-
* Ensure required commands exist under `pre-commit.commands` in a lefthook config.
|
|
1179
|
-
* Only adds missing commands — never modifies existing ones.
|
|
1180
|
-
* Returns unchanged content if the file has an opt-out comment or can't be parsed.
|
|
1181
|
-
*/
|
|
1182
|
-
function mergeLefthookCommands(existing, requiredCommands) {
|
|
1183
|
-
if (isToolingIgnored(existing)) return {
|
|
1184
|
-
content: existing,
|
|
1185
|
-
changed: false
|
|
1186
|
-
};
|
|
1187
|
-
try {
|
|
1188
|
-
const doc = parseDocument(existing);
|
|
1189
|
-
let changed = false;
|
|
1190
|
-
if (!doc.hasIn(["pre-commit", "commands"])) {
|
|
1191
|
-
doc.setIn(["pre-commit", "commands"], requiredCommands);
|
|
1192
|
-
return {
|
|
1193
|
-
content: doc.toString(),
|
|
1194
|
-
changed: true
|
|
1195
|
-
};
|
|
1196
|
-
}
|
|
1197
|
-
const commands = doc.getIn(["pre-commit", "commands"]);
|
|
1198
|
-
if (!isMap(commands)) return {
|
|
1199
|
-
content: existing,
|
|
1200
|
-
changed: false
|
|
1201
|
-
};
|
|
1202
|
-
for (const [name, config] of Object.entries(requiredCommands)) if (!commands.has(name)) {
|
|
1203
|
-
commands.set(name, config);
|
|
1204
|
-
changed = true;
|
|
1205
|
-
}
|
|
1206
|
-
return {
|
|
1207
|
-
content: changed ? doc.toString() : existing,
|
|
1208
|
-
changed
|
|
1209
|
-
};
|
|
1210
|
-
} catch {
|
|
1211
|
-
return {
|
|
1212
|
-
content: existing,
|
|
1213
|
-
changed: false
|
|
1214
|
-
};
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
/**
|
|
1218
|
-
* Ensure required steps exist in a workflow job's steps array.
|
|
1219
|
-
* Only adds missing steps at the end — never modifies existing ones.
|
|
1220
|
-
* Returns unchanged content if the file has an opt-out comment or can't be parsed.
|
|
1221
|
-
*/
|
|
1222
|
-
function mergeWorkflowSteps(existing, jobName, requiredSteps) {
|
|
1223
|
-
if (isToolingIgnored(existing)) return {
|
|
1224
|
-
content: existing,
|
|
1225
|
-
changed: false
|
|
1226
|
-
};
|
|
1227
|
-
try {
|
|
1228
|
-
const doc = parseDocument(existing);
|
|
1229
|
-
const steps = doc.getIn([
|
|
1230
|
-
"jobs",
|
|
1231
|
-
jobName,
|
|
1232
|
-
"steps"
|
|
1233
|
-
]);
|
|
1234
|
-
if (!isSeq(steps)) return {
|
|
1235
|
-
content: existing,
|
|
1236
|
-
changed: false
|
|
1237
|
-
};
|
|
1238
|
-
let changed = false;
|
|
1239
|
-
for (const { match, step } of requiredSteps) if (!steps.items.some((item) => {
|
|
1240
|
-
if (!isMap(item)) return false;
|
|
1241
|
-
if (match.run) {
|
|
1242
|
-
const run = item.get("run");
|
|
1243
|
-
return typeof run === "string" && run.includes(match.run);
|
|
1244
|
-
}
|
|
1245
|
-
if (match.uses) {
|
|
1246
|
-
const uses = item.get("uses");
|
|
1247
|
-
return typeof uses === "string" && uses.startsWith(match.uses);
|
|
1248
|
-
}
|
|
1249
|
-
return false;
|
|
1250
|
-
})) {
|
|
1251
|
-
steps.add(doc.createNode(step));
|
|
1252
|
-
changed = true;
|
|
1253
|
-
}
|
|
1254
|
-
return {
|
|
1255
|
-
content: changed ? doc.toString() : existing,
|
|
1256
|
-
changed
|
|
1257
|
-
};
|
|
1258
|
-
} catch {
|
|
1259
|
-
return {
|
|
1260
|
-
content: existing,
|
|
1261
|
-
changed: false
|
|
1262
|
-
};
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
/**
|
|
1266
|
-
* Add a job to an existing workflow YAML if it doesn't already exist.
|
|
1267
|
-
* Returns unchanged content if the job already exists, the file has an opt-out comment,
|
|
1268
|
-
* or the document can't be parsed.
|
|
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
|
-
}
|
|
1482
|
+
if (s.endsWith("/")) s = s.slice(0, -1);
|
|
1483
|
+
return s;
|
|
1297
1484
|
}
|
|
1298
|
-
function
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
changed: false
|
|
1309
|
-
};
|
|
1310
|
-
if (jobs.has(jobName)) return {
|
|
1311
|
-
content: existing,
|
|
1312
|
-
changed: false
|
|
1485
|
+
async function generateGitignore(ctx) {
|
|
1486
|
+
const filePath = ".gitignore";
|
|
1487
|
+
const existing = ctx.read(filePath);
|
|
1488
|
+
if (existing) {
|
|
1489
|
+
const existingNormalized = new Set(existing.split("\n").map(normalizeEntry).filter((line) => line.length > 0));
|
|
1490
|
+
const missing = ALL_ENTRIES.filter((entry) => !existingNormalized.has(normalizeEntry(entry)));
|
|
1491
|
+
if (missing.length === 0) return {
|
|
1492
|
+
filePath,
|
|
1493
|
+
action: "skipped",
|
|
1494
|
+
description: "Already has all standard entries"
|
|
1313
1495
|
};
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1496
|
+
const missingRequired = REQUIRED_ENTRIES.filter((entry) => !existingNormalized.has(normalizeEntry(entry)));
|
|
1497
|
+
const updated = existing.trimEnd() + "\n\n# Added by @bensandee/tooling\n" + missing.join("\n") + "\n";
|
|
1498
|
+
ctx.write(filePath, updated);
|
|
1499
|
+
if (missingRequired.length === 0) return {
|
|
1500
|
+
filePath,
|
|
1501
|
+
action: "skipped",
|
|
1502
|
+
description: "Only optional entries missing"
|
|
1318
1503
|
};
|
|
1319
|
-
} catch {
|
|
1320
1504
|
return {
|
|
1321
|
-
|
|
1322
|
-
|
|
1505
|
+
filePath,
|
|
1506
|
+
action: "updated",
|
|
1507
|
+
description: `Appended ${String(missing.length)} missing entries`
|
|
1323
1508
|
};
|
|
1324
1509
|
}
|
|
1510
|
+
ctx.write(filePath, ALL_ENTRIES.join("\n") + "\n");
|
|
1511
|
+
return {
|
|
1512
|
+
filePath,
|
|
1513
|
+
action: "created",
|
|
1514
|
+
description: "Generated .gitignore"
|
|
1515
|
+
};
|
|
1325
1516
|
}
|
|
1326
1517
|
//#endregion
|
|
1327
1518
|
//#region src/generators/ci.ts
|
|
1328
1519
|
/** Build a GitHub Actions expression like `${{ expr }}` without triggering no-template-curly-in-string. */
|
|
1329
|
-
function actionsExpr$
|
|
1520
|
+
function actionsExpr$1(expr) {
|
|
1330
1521
|
return `\${{ ${expr} }}`;
|
|
1331
1522
|
}
|
|
1332
1523
|
const CI_CONCURRENCY = {
|
|
1333
|
-
group: `ci-${actionsExpr$
|
|
1334
|
-
"cancel-in-progress":
|
|
1524
|
+
group: `ci-${actionsExpr$1("github.ref")}`,
|
|
1525
|
+
"cancel-in-progress": true
|
|
1335
1526
|
};
|
|
1336
|
-
function hasEnginesNode$
|
|
1527
|
+
function hasEnginesNode$1(ctx) {
|
|
1337
1528
|
const raw = ctx.read("package.json");
|
|
1338
1529
|
if (!raw) return false;
|
|
1339
1530
|
return typeof parsePackageJson(raw)?.engines?.["node"] === "string";
|
|
@@ -1347,8 +1538,8 @@ ${emailNotifications}on:
|
|
|
1347
1538
|
pull_request:
|
|
1348
1539
|
|
|
1349
1540
|
concurrency:
|
|
1350
|
-
group: ci-${actionsExpr$
|
|
1351
|
-
cancel-in-progress:
|
|
1541
|
+
group: ci-${actionsExpr$1("github.ref")}
|
|
1542
|
+
cancel-in-progress: true
|
|
1352
1543
|
|
|
1353
1544
|
jobs:
|
|
1354
1545
|
check:
|
|
@@ -1405,7 +1596,7 @@ async function generateCi(ctx) {
|
|
|
1405
1596
|
description: "CI workflow not requested"
|
|
1406
1597
|
};
|
|
1407
1598
|
const isGitHub = ctx.config.ci === "github";
|
|
1408
|
-
const nodeVersionYaml = hasEnginesNode$
|
|
1599
|
+
const nodeVersionYaml = hasEnginesNode$1(ctx) ? "node-version-file: package.json" : "node-version: \"24\"";
|
|
1409
1600
|
const filePath = isGitHub ? ".github/workflows/check.yml" : ".forgejo/workflows/check.yml";
|
|
1410
1601
|
const content = ciWorkflow(nodeVersionYaml, !isGitHub);
|
|
1411
1602
|
if (ctx.exists(filePath)) {
|
|
@@ -1883,13 +2074,13 @@ async function generateChangesets(ctx) {
|
|
|
1883
2074
|
//#endregion
|
|
1884
2075
|
//#region src/generators/release-ci.ts
|
|
1885
2076
|
/** Build a GitHub Actions expression like `${{ expr }}` without triggering no-template-curly-in-string. */
|
|
1886
|
-
function actionsExpr
|
|
2077
|
+
function actionsExpr(expr) {
|
|
1887
2078
|
return `\${{ ${expr} }}`;
|
|
1888
2079
|
}
|
|
1889
|
-
function hasEnginesNode
|
|
2080
|
+
function hasEnginesNode(ctx) {
|
|
1890
2081
|
return typeof ctx.packageJson?.["engines"]?.["node"] === "string";
|
|
1891
2082
|
}
|
|
1892
|
-
function commonSteps(nodeVersionYaml) {
|
|
2083
|
+
function commonSteps(nodeVersionYaml, publishesNpm) {
|
|
1893
2084
|
return ` - uses: actions/checkout@v4
|
|
1894
2085
|
with:
|
|
1895
2086
|
fetch-depth: 0
|
|
@@ -1897,18 +2088,18 @@ function commonSteps(nodeVersionYaml) {
|
|
|
1897
2088
|
- uses: actions/setup-node@v4
|
|
1898
2089
|
with:
|
|
1899
2090
|
${nodeVersionYaml}
|
|
1900
|
-
cache: pnpm
|
|
1901
|
-
registry-url: "https://registry.npmjs.org"
|
|
2091
|
+
cache: pnpm${publishesNpm ? `\n registry-url: "https://registry.npmjs.org"` : ""}
|
|
1902
2092
|
- run: pnpm install --frozen-lockfile
|
|
1903
2093
|
- run: pnpm build`;
|
|
1904
2094
|
}
|
|
1905
|
-
function releaseItWorkflow(ci, nodeVersionYaml) {
|
|
2095
|
+
function releaseItWorkflow(ci, nodeVersionYaml, publishesNpm) {
|
|
1906
2096
|
const isGitHub = ci === "github";
|
|
1907
2097
|
const permissions = isGitHub ? `
|
|
1908
2098
|
permissions:
|
|
1909
2099
|
contents: write
|
|
1910
2100
|
` : "";
|
|
1911
2101
|
const tokenEnv = isGitHub ? `GITHUB_TOKEN: \${{ github.token }}` : `FORGEJO_TOKEN: \${{ secrets.FORGEJO_TOKEN }}`;
|
|
2102
|
+
const npmEnv = publishesNpm ? `\n NODE_AUTH_TOKEN: \${{ secrets.NPM_TOKEN }}` : "";
|
|
1912
2103
|
return `${workflowSchemaComment(ci)}name: Release
|
|
1913
2104
|
on:
|
|
1914
2105
|
workflow_dispatch:
|
|
@@ -1917,14 +2108,13 @@ jobs:
|
|
|
1917
2108
|
release:
|
|
1918
2109
|
runs-on: ubuntu-latest
|
|
1919
2110
|
steps:
|
|
1920
|
-
${commonSteps(nodeVersionYaml)}
|
|
2111
|
+
${commonSteps(nodeVersionYaml, publishesNpm)}
|
|
1921
2112
|
- run: pnpm release-it --ci
|
|
1922
2113
|
env:
|
|
1923
|
-
${tokenEnv}
|
|
1924
|
-
NODE_AUTH_TOKEN: \${{ secrets.NPM_TOKEN }}
|
|
2114
|
+
${tokenEnv}${npmEnv}
|
|
1925
2115
|
`;
|
|
1926
2116
|
}
|
|
1927
|
-
function commitAndTagVersionWorkflow(ci, nodeVersionYaml) {
|
|
2117
|
+
function commitAndTagVersionWorkflow(ci, nodeVersionYaml, publishesNpm) {
|
|
1928
2118
|
const isGitHub = ci === "github";
|
|
1929
2119
|
const permissions = isGitHub ? `
|
|
1930
2120
|
permissions:
|
|
@@ -1954,52 +2144,69 @@ jobs:
|
|
|
1954
2144
|
release:
|
|
1955
2145
|
runs-on: ubuntu-latest
|
|
1956
2146
|
steps:
|
|
1957
|
-
${commonSteps(nodeVersionYaml)}${gitConfigStep}${releaseStep}
|
|
2147
|
+
${commonSteps(nodeVersionYaml, publishesNpm)}${gitConfigStep}${releaseStep}
|
|
1958
2148
|
`;
|
|
1959
2149
|
}
|
|
1960
|
-
function changesetsReleaseJobConfig(ci, nodeVersionYaml) {
|
|
2150
|
+
function changesetsReleaseJobConfig(ci, nodeVersionYaml, publishesNpm) {
|
|
1961
2151
|
const isGitHub = ci === "github";
|
|
1962
2152
|
const nodeWith = {
|
|
1963
2153
|
...nodeVersionYaml.startsWith("node-version-file") ? { "node-version-file": "package.json" } : { "node-version": "24" },
|
|
1964
2154
|
cache: "pnpm",
|
|
1965
|
-
"registry-url": "https://registry.npmjs.org"
|
|
2155
|
+
...publishesNpm && { "registry-url": "https://registry.npmjs.org" }
|
|
1966
2156
|
};
|
|
1967
|
-
if (isGitHub)
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
with: { "fetch-depth": 0 }
|
|
2157
|
+
if (isGitHub) {
|
|
2158
|
+
const changesetsEnv = {
|
|
2159
|
+
GITHUB_TOKEN: actionsExpr("github.token"),
|
|
2160
|
+
...publishesNpm && { NPM_TOKEN: actionsExpr("secrets.NPM_TOKEN") }
|
|
2161
|
+
};
|
|
2162
|
+
return {
|
|
2163
|
+
needs: "check",
|
|
2164
|
+
if: "github.ref == 'refs/heads/main'",
|
|
2165
|
+
concurrency: {
|
|
2166
|
+
group: "release",
|
|
2167
|
+
"cancel-in-progress": false
|
|
1979
2168
|
},
|
|
1980
|
-
|
|
1981
|
-
{
|
|
1982
|
-
|
|
1983
|
-
|
|
2169
|
+
"runs-on": "ubuntu-latest",
|
|
2170
|
+
permissions: {
|
|
2171
|
+
contents: "write",
|
|
2172
|
+
"pull-requests": "write"
|
|
1984
2173
|
},
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
2174
|
+
steps: [
|
|
2175
|
+
{
|
|
2176
|
+
uses: "actions/checkout@v4",
|
|
2177
|
+
with: { "fetch-depth": 0 }
|
|
2178
|
+
},
|
|
2179
|
+
{ uses: "pnpm/action-setup@v4" },
|
|
2180
|
+
{
|
|
2181
|
+
uses: "actions/setup-node@v4",
|
|
2182
|
+
with: nodeWith
|
|
1992
2183
|
},
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
2184
|
+
{ run: "pnpm install --frozen-lockfile" },
|
|
2185
|
+
{ run: "pnpm build" },
|
|
2186
|
+
{
|
|
2187
|
+
uses: "changesets/action@v1",
|
|
2188
|
+
with: {
|
|
2189
|
+
publish: "pnpm changeset publish",
|
|
2190
|
+
version: "pnpm changeset version"
|
|
2191
|
+
},
|
|
2192
|
+
env: changesetsEnv
|
|
1996
2193
|
}
|
|
1997
|
-
|
|
1998
|
-
|
|
2194
|
+
]
|
|
2195
|
+
};
|
|
2196
|
+
}
|
|
2197
|
+
const releaseEnv = {
|
|
2198
|
+
FORGEJO_SERVER_URL: actionsExpr("github.server_url"),
|
|
2199
|
+
FORGEJO_REPOSITORY: actionsExpr("github.repository"),
|
|
2200
|
+
FORGEJO_TOKEN: actionsExpr("secrets.FORGEJO_TOKEN"),
|
|
2201
|
+
...publishesNpm && { NODE_AUTH_TOKEN: actionsExpr("secrets.NPM_TOKEN") }
|
|
1999
2202
|
};
|
|
2000
2203
|
return {
|
|
2001
2204
|
needs: "check",
|
|
2002
2205
|
if: "github.ref == 'refs/heads/main'",
|
|
2206
|
+
concurrency: {
|
|
2207
|
+
group: "release",
|
|
2208
|
+
"cancel-in-progress": false
|
|
2209
|
+
},
|
|
2003
2210
|
"runs-on": "ubuntu-latest",
|
|
2004
2211
|
steps: [
|
|
2005
2212
|
{
|
|
@@ -2019,18 +2226,13 @@ function changesetsReleaseJobConfig(ci, nodeVersionYaml) {
|
|
|
2019
2226
|
},
|
|
2020
2227
|
{
|
|
2021
2228
|
name: "Release",
|
|
2022
|
-
env:
|
|
2023
|
-
FORGEJO_SERVER_URL: actionsExpr$1("github.server_url"),
|
|
2024
|
-
FORGEJO_REPOSITORY: actionsExpr$1("github.repository"),
|
|
2025
|
-
FORGEJO_TOKEN: actionsExpr$1("secrets.FORGEJO_TOKEN"),
|
|
2026
|
-
NODE_AUTH_TOKEN: actionsExpr$1("secrets.NPM_TOKEN")
|
|
2027
|
-
},
|
|
2229
|
+
env: releaseEnv,
|
|
2028
2230
|
run: "pnpm exec tooling release:changesets"
|
|
2029
2231
|
}
|
|
2030
2232
|
]
|
|
2031
2233
|
};
|
|
2032
2234
|
}
|
|
2033
|
-
function requiredReleaseSteps(strategy, nodeVersionYaml) {
|
|
2235
|
+
function requiredReleaseSteps(strategy, nodeVersionYaml, publishesNpm) {
|
|
2034
2236
|
const isNodeVersionFile = nodeVersionYaml.startsWith("node-version-file");
|
|
2035
2237
|
const steps = [
|
|
2036
2238
|
{
|
|
@@ -2051,7 +2253,7 @@ function requiredReleaseSteps(strategy, nodeVersionYaml) {
|
|
|
2051
2253
|
with: {
|
|
2052
2254
|
...isNodeVersionFile ? { "node-version-file": "package.json" } : { "node-version": "24" },
|
|
2053
2255
|
cache: "pnpm",
|
|
2054
|
-
"registry-url": "https://registry.npmjs.org"
|
|
2256
|
+
...publishesNpm && { "registry-url": "https://registry.npmjs.org" }
|
|
2055
2257
|
}
|
|
2056
2258
|
}
|
|
2057
2259
|
},
|
|
@@ -2086,23 +2288,23 @@ function requiredReleaseSteps(strategy, nodeVersionYaml) {
|
|
|
2086
2288
|
}
|
|
2087
2289
|
return steps;
|
|
2088
2290
|
}
|
|
2089
|
-
function buildWorkflow(strategy, ci, nodeVersionYaml) {
|
|
2291
|
+
function buildWorkflow(strategy, ci, nodeVersionYaml, publishesNpm) {
|
|
2090
2292
|
switch (strategy) {
|
|
2091
|
-
case "release-it": return releaseItWorkflow(ci, nodeVersionYaml);
|
|
2092
|
-
case "simple": return commitAndTagVersionWorkflow(ci, nodeVersionYaml);
|
|
2293
|
+
case "release-it": return releaseItWorkflow(ci, nodeVersionYaml, publishesNpm);
|
|
2294
|
+
case "simple": return commitAndTagVersionWorkflow(ci, nodeVersionYaml, publishesNpm);
|
|
2093
2295
|
default: return null;
|
|
2094
2296
|
}
|
|
2095
2297
|
}
|
|
2096
|
-
function generateChangesetsReleaseCi(ctx) {
|
|
2298
|
+
function generateChangesetsReleaseCi(ctx, publishesNpm) {
|
|
2097
2299
|
const checkPath = ctx.config.ci === "github" ? ".github/workflows/check.yml" : ".forgejo/workflows/check.yml";
|
|
2098
|
-
const nodeVersionYaml = hasEnginesNode
|
|
2300
|
+
const nodeVersionYaml = hasEnginesNode(ctx) ? "node-version-file: package.json" : "node-version: \"24\"";
|
|
2099
2301
|
const existing = ctx.read(checkPath);
|
|
2100
2302
|
if (!existing) return {
|
|
2101
2303
|
filePath: checkPath,
|
|
2102
2304
|
action: "skipped",
|
|
2103
2305
|
description: "CI workflow not found — run check generator first"
|
|
2104
2306
|
};
|
|
2105
|
-
const addResult = addWorkflowJob(existing, "release", changesetsReleaseJobConfig(ctx.config.ci, nodeVersionYaml));
|
|
2307
|
+
const addResult = addWorkflowJob(existing, "release", changesetsReleaseJobConfig(ctx.config.ci, nodeVersionYaml, publishesNpm));
|
|
2106
2308
|
if (addResult.changed) {
|
|
2107
2309
|
const withComment = ensureSchemaComment(addResult.content, ctx.config.ci);
|
|
2108
2310
|
ctx.write(checkPath, withComment);
|
|
@@ -2112,7 +2314,7 @@ function generateChangesetsReleaseCi(ctx) {
|
|
|
2112
2314
|
description: "Added release job to CI workflow"
|
|
2113
2315
|
};
|
|
2114
2316
|
}
|
|
2115
|
-
const merged = mergeWorkflowSteps(existing, "release", requiredReleaseSteps("changesets", nodeVersionYaml));
|
|
2317
|
+
const merged = mergeWorkflowSteps(existing, "release", requiredReleaseSteps("changesets", nodeVersionYaml, publishesNpm));
|
|
2116
2318
|
if (!merged.changed) return {
|
|
2117
2319
|
filePath: checkPath,
|
|
2118
2320
|
action: "skipped",
|
|
@@ -2133,11 +2335,12 @@ async function generateReleaseCi(ctx) {
|
|
|
2133
2335
|
action: "skipped",
|
|
2134
2336
|
description: "Release CI workflow not applicable"
|
|
2135
2337
|
};
|
|
2136
|
-
|
|
2338
|
+
const publishesNpm = getPublishablePackages(ctx.targetDir, ctx.config.structure, ctx.packageJson).length > 0;
|
|
2339
|
+
if (ctx.config.releaseStrategy === "changesets") return generateChangesetsReleaseCi(ctx, publishesNpm);
|
|
2137
2340
|
const isGitHub = ctx.config.ci === "github";
|
|
2138
2341
|
const workflowPath = isGitHub ? ".github/workflows/release.yml" : ".forgejo/workflows/release.yml";
|
|
2139
|
-
const nodeVersionYaml = hasEnginesNode
|
|
2140
|
-
const content = buildWorkflow(ctx.config.releaseStrategy, ctx.config.ci, nodeVersionYaml);
|
|
2342
|
+
const nodeVersionYaml = hasEnginesNode(ctx) ? "node-version-file: package.json" : "node-version: \"24\"";
|
|
2343
|
+
const content = buildWorkflow(ctx.config.releaseStrategy, ctx.config.ci, nodeVersionYaml, publishesNpm);
|
|
2141
2344
|
if (!content) return {
|
|
2142
2345
|
filePath,
|
|
2143
2346
|
action: "skipped",
|
|
@@ -2151,7 +2354,7 @@ async function generateReleaseCi(ctx) {
|
|
|
2151
2354
|
action: "skipped",
|
|
2152
2355
|
description: "Release workflow already up to date"
|
|
2153
2356
|
};
|
|
2154
|
-
const merged = mergeWorkflowSteps(existing, "release", requiredReleaseSteps(ctx.config.releaseStrategy, nodeVersionYaml));
|
|
2357
|
+
const merged = mergeWorkflowSteps(existing, "release", requiredReleaseSteps(ctx.config.releaseStrategy, nodeVersionYaml, publishesNpm));
|
|
2155
2358
|
const withComment = ensureSchemaComment(merged.content, ctx.config.ci);
|
|
2156
2359
|
if (withComment === content) {
|
|
2157
2360
|
ctx.write(workflowPath, content);
|
|
@@ -2452,148 +2655,6 @@ async function generateVscodeSettings(ctx) {
|
|
|
2452
2655
|
return results;
|
|
2453
2656
|
}
|
|
2454
2657
|
//#endregion
|
|
2455
|
-
//#region src/generators/deploy-ci.ts
|
|
2456
|
-
/** Build a GitHub Actions expression like `${{ expr }}` without triggering no-template-curly-in-string. */
|
|
2457
|
-
function actionsExpr(expr) {
|
|
2458
|
-
return `\${{ ${expr} }}`;
|
|
2459
|
-
}
|
|
2460
|
-
function hasEnginesNode(ctx) {
|
|
2461
|
-
return typeof ctx.packageJson?.["engines"]?.["node"] === "string";
|
|
2462
|
-
}
|
|
2463
|
-
function deployWorkflow(ci, nodeVersionYaml) {
|
|
2464
|
-
return `${workflowSchemaComment(ci)}name: Deploy
|
|
2465
|
-
on:
|
|
2466
|
-
push:
|
|
2467
|
-
tags:
|
|
2468
|
-
- "v[0-9]+.[0-9]+.[0-9]+"
|
|
2469
|
-
|
|
2470
|
-
jobs:
|
|
2471
|
-
deploy:
|
|
2472
|
-
runs-on: ubuntu-latest
|
|
2473
|
-
steps:
|
|
2474
|
-
- uses: actions/checkout@v4
|
|
2475
|
-
- uses: pnpm/action-setup@v4
|
|
2476
|
-
- uses: actions/setup-node@v4
|
|
2477
|
-
with:
|
|
2478
|
-
${nodeVersionYaml}
|
|
2479
|
-
- run: pnpm install --frozen-lockfile
|
|
2480
|
-
- name: Publish Docker images
|
|
2481
|
-
env:
|
|
2482
|
-
DOCKER_REGISTRY_HOST: ${actionsExpr("vars.DOCKER_REGISTRY_HOST")}
|
|
2483
|
-
DOCKER_REGISTRY_NAMESPACE: ${actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE")}
|
|
2484
|
-
DOCKER_USERNAME: ${actionsExpr("secrets.DOCKER_USERNAME")}
|
|
2485
|
-
DOCKER_PASSWORD: ${actionsExpr("secrets.DOCKER_PASSWORD")}
|
|
2486
|
-
run: pnpm exec tooling docker:publish
|
|
2487
|
-
`;
|
|
2488
|
-
}
|
|
2489
|
-
function requiredDeploySteps() {
|
|
2490
|
-
return [
|
|
2491
|
-
{
|
|
2492
|
-
match: { uses: "actions/checkout" },
|
|
2493
|
-
step: { uses: "actions/checkout@v4" }
|
|
2494
|
-
},
|
|
2495
|
-
{
|
|
2496
|
-
match: { uses: "pnpm/action-setup" },
|
|
2497
|
-
step: { uses: "pnpm/action-setup@v4" }
|
|
2498
|
-
},
|
|
2499
|
-
{
|
|
2500
|
-
match: { uses: "actions/setup-node" },
|
|
2501
|
-
step: { uses: "actions/setup-node@v4" }
|
|
2502
|
-
},
|
|
2503
|
-
{
|
|
2504
|
-
match: { run: "pnpm install" },
|
|
2505
|
-
step: { run: "pnpm install --frozen-lockfile" }
|
|
2506
|
-
},
|
|
2507
|
-
{
|
|
2508
|
-
match: { run: "docker:publish" },
|
|
2509
|
-
step: { run: "pnpm exec tooling docker:publish" }
|
|
2510
|
-
}
|
|
2511
|
-
];
|
|
2512
|
-
}
|
|
2513
|
-
/** Convention paths to check for Dockerfiles. */
|
|
2514
|
-
const CONVENTION_DOCKERFILE_PATHS$1 = ["Dockerfile", "docker/Dockerfile"];
|
|
2515
|
-
const DockerMapSchema = z.object({ docker: z.record(z.string(), z.unknown()).optional() });
|
|
2516
|
-
/** Check whether any Docker packages exist by convention or .tooling.json config. */
|
|
2517
|
-
function hasDockerPackages(ctx) {
|
|
2518
|
-
const configRaw = ctx.read(".tooling.json");
|
|
2519
|
-
if (configRaw) {
|
|
2520
|
-
const result = DockerMapSchema.safeParse(JSON.parse(configRaw));
|
|
2521
|
-
if (result.success && result.data.docker && Object.keys(result.data.docker).length > 0) return true;
|
|
2522
|
-
}
|
|
2523
|
-
if (ctx.config.structure === "monorepo") {
|
|
2524
|
-
const packages = getMonorepoPackages(ctx.targetDir);
|
|
2525
|
-
for (const pkg of packages) {
|
|
2526
|
-
const dirName = pkg.name.split("/").pop() ?? pkg.name;
|
|
2527
|
-
for (const rel of CONVENTION_DOCKERFILE_PATHS$1) if (ctx.exists(`packages/${dirName}/${rel}`)) return true;
|
|
2528
|
-
}
|
|
2529
|
-
} else for (const rel of CONVENTION_DOCKERFILE_PATHS$1) if (ctx.exists(rel)) return true;
|
|
2530
|
-
return false;
|
|
2531
|
-
}
|
|
2532
|
-
async function generateDeployCi(ctx) {
|
|
2533
|
-
const filePath = "deploy-ci";
|
|
2534
|
-
if (!hasDockerPackages(ctx) || ctx.config.ci === "none") return {
|
|
2535
|
-
filePath,
|
|
2536
|
-
action: "skipped",
|
|
2537
|
-
description: "Deploy CI workflow not applicable"
|
|
2538
|
-
};
|
|
2539
|
-
const isGitHub = ctx.config.ci === "github";
|
|
2540
|
-
const workflowPath = isGitHub ? ".github/workflows/publish.yml" : ".forgejo/workflows/publish.yml";
|
|
2541
|
-
const nodeVersionYaml = hasEnginesNode(ctx) ? "node-version-file: package.json" : "node-version: \"24\"";
|
|
2542
|
-
const content = deployWorkflow(ctx.config.ci, nodeVersionYaml);
|
|
2543
|
-
if (ctx.exists(workflowPath)) {
|
|
2544
|
-
const existing = ctx.read(workflowPath);
|
|
2545
|
-
if (existing) {
|
|
2546
|
-
if (existing === content || ensureSchemaComment(existing, ctx.config.ci) === content) return {
|
|
2547
|
-
filePath: workflowPath,
|
|
2548
|
-
action: "skipped",
|
|
2549
|
-
description: "Deploy workflow already up to date"
|
|
2550
|
-
};
|
|
2551
|
-
const merged = mergeWorkflowSteps(existing, "deploy", requiredDeploySteps());
|
|
2552
|
-
const withComment = ensureSchemaComment(merged.content, ctx.config.ci);
|
|
2553
|
-
if (withComment === content) {
|
|
2554
|
-
ctx.write(workflowPath, content);
|
|
2555
|
-
return {
|
|
2556
|
-
filePath: workflowPath,
|
|
2557
|
-
action: "updated",
|
|
2558
|
-
description: "Added missing steps to deploy workflow"
|
|
2559
|
-
};
|
|
2560
|
-
}
|
|
2561
|
-
if (await ctx.confirmOverwrite(workflowPath) === "skip") {
|
|
2562
|
-
if (merged.changed || withComment !== merged.content) {
|
|
2563
|
-
ctx.write(workflowPath, withComment);
|
|
2564
|
-
return {
|
|
2565
|
-
filePath: workflowPath,
|
|
2566
|
-
action: "updated",
|
|
2567
|
-
description: "Added missing steps to deploy workflow"
|
|
2568
|
-
};
|
|
2569
|
-
}
|
|
2570
|
-
return {
|
|
2571
|
-
filePath: workflowPath,
|
|
2572
|
-
action: "skipped",
|
|
2573
|
-
description: "Existing deploy workflow preserved"
|
|
2574
|
-
};
|
|
2575
|
-
}
|
|
2576
|
-
ctx.write(workflowPath, content);
|
|
2577
|
-
return {
|
|
2578
|
-
filePath: workflowPath,
|
|
2579
|
-
action: "updated",
|
|
2580
|
-
description: "Replaced deploy workflow with updated template"
|
|
2581
|
-
};
|
|
2582
|
-
}
|
|
2583
|
-
return {
|
|
2584
|
-
filePath: workflowPath,
|
|
2585
|
-
action: "skipped",
|
|
2586
|
-
description: "Deploy workflow already up to date"
|
|
2587
|
-
};
|
|
2588
|
-
}
|
|
2589
|
-
ctx.write(workflowPath, content);
|
|
2590
|
-
return {
|
|
2591
|
-
filePath: workflowPath,
|
|
2592
|
-
action: "created",
|
|
2593
|
-
description: `Generated ${isGitHub ? "GitHub" : "Forgejo"} Actions deploy workflow`
|
|
2594
|
-
};
|
|
2595
|
-
}
|
|
2596
|
-
//#endregion
|
|
2597
2658
|
//#region src/generators/pipeline.ts
|
|
2598
2659
|
/** Run all generators sequentially and return their results. */
|
|
2599
2660
|
async function runGenerators(ctx) {
|
|
@@ -2790,6 +2851,16 @@ function generateMigratePrompt(results, config, detected) {
|
|
|
2790
2851
|
}
|
|
2791
2852
|
//#endregion
|
|
2792
2853
|
//#region src/commands/repo-init.ts
|
|
2854
|
+
/** Log what was detected so the user understands generator decisions. */
|
|
2855
|
+
function logDetectionSummary(ctx) {
|
|
2856
|
+
const dockerNames = getDockerPackageNames(ctx);
|
|
2857
|
+
if (dockerNames.length > 0) p.log.info(`Detected Docker packages: ${dockerNames.join(", ")}`);
|
|
2858
|
+
if (ctx.config.releaseStrategy !== "none") {
|
|
2859
|
+
const publishable = getPublishablePackages(ctx.targetDir, ctx.config.structure, ctx.packageJson);
|
|
2860
|
+
if (publishable.length > 0) p.log.info(`Will publish npm packages: ${publishable.map((pkg) => pkg.name).join(", ")}`);
|
|
2861
|
+
else p.log.info("No publishable npm packages — npm registry setup will be skipped");
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2793
2864
|
async function runInit(config, options = {}) {
|
|
2794
2865
|
const detected = detectProject(config.targetDir);
|
|
2795
2866
|
const s = p.spinner();
|
|
@@ -2809,6 +2880,7 @@ async function runInit(config, options = {}) {
|
|
|
2809
2880
|
if (p.isCancel(result)) return "skip";
|
|
2810
2881
|
return result;
|
|
2811
2882
|
}));
|
|
2883
|
+
logDetectionSummary(ctx);
|
|
2812
2884
|
s.start("Generating configuration files...");
|
|
2813
2885
|
const results = await runGenerators(ctx);
|
|
2814
2886
|
const alreadyArchived = new Set(results.filter((r) => r.action === "archived").map((r) => r.filePath));
|
|
@@ -2819,8 +2891,7 @@ async function runInit(config, options = {}) {
|
|
|
2819
2891
|
});
|
|
2820
2892
|
const created = results.filter((r) => r.action === "created");
|
|
2821
2893
|
const updated = results.filter((r) => r.action === "updated");
|
|
2822
|
-
|
|
2823
|
-
if (!(created.length > 0 || updated.length > 0 || archived.length > 0) && options.noPrompt) {
|
|
2894
|
+
if (!(created.length > 0 || updated.length > 0 || archivedFiles.length > 0) && options.noPrompt) {
|
|
2824
2895
|
s.stop("Repository is up to date.");
|
|
2825
2896
|
return results;
|
|
2826
2897
|
}
|
|
@@ -2834,7 +2905,6 @@ async function runInit(config, options = {}) {
|
|
|
2834
2905
|
const summaryLines = [];
|
|
2835
2906
|
if (created.length > 0) summaryLines.push(`Created: ${created.map((r) => r.filePath).join(", ")}`);
|
|
2836
2907
|
if (updated.length > 0) summaryLines.push(`Updated: ${updated.map((r) => r.filePath).join(", ")}`);
|
|
2837
|
-
if (archived.length > 0) summaryLines.push(`Archived: ${archived.map((r) => r.filePath).join(", ")}`);
|
|
2838
2908
|
p.note(summaryLines.join("\n"), "Summary");
|
|
2839
2909
|
if (!options.noPrompt) {
|
|
2840
2910
|
const prompt = generateMigratePrompt(results, config, detected);
|
|
@@ -2929,6 +2999,7 @@ async function runCheck(targetDir) {
|
|
|
2929
2999
|
const saved = loadToolingConfig(targetDir);
|
|
2930
3000
|
const detected = buildDefaultConfig(targetDir, {});
|
|
2931
3001
|
const { ctx, pendingWrites } = createDryRunContext(saved ? mergeWithSavedConfig(detected, saved) : detected);
|
|
3002
|
+
logDetectionSummary(ctx);
|
|
2932
3003
|
const actionable = (await runGenerators(ctx)).filter((r) => {
|
|
2933
3004
|
if (r.action !== "created" && r.action !== "updated") return false;
|
|
2934
3005
|
const newContent = pendingWrites.get(r.filePath);
|
|
@@ -3845,7 +3916,7 @@ const CHECKS = [
|
|
|
3845
3916
|
},
|
|
3846
3917
|
{ name: "knip" },
|
|
3847
3918
|
{ name: "tooling:check" },
|
|
3848
|
-
{ name: "
|
|
3919
|
+
{ name: "docker:check" }
|
|
3849
3920
|
];
|
|
3850
3921
|
function defaultGetScripts(targetDir) {
|
|
3851
3922
|
try {
|
|
@@ -3912,7 +3983,7 @@ function runRunChecks(targetDir, options = {}) {
|
|
|
3912
3983
|
const runChecksCommand = defineCommand({
|
|
3913
3984
|
meta: {
|
|
3914
3985
|
name: "checks:run",
|
|
3915
|
-
description: "Run all standard checks (build, typecheck, lint, test, format, knip, tooling:check,
|
|
3986
|
+
description: "Run all standard checks (build, typecheck, lint, test, format, knip, tooling:check, docker:check)"
|
|
3916
3987
|
},
|
|
3917
3988
|
args: {
|
|
3918
3989
|
dir: {
|
|
@@ -3922,7 +3993,7 @@ const runChecksCommand = defineCommand({
|
|
|
3922
3993
|
},
|
|
3923
3994
|
skip: {
|
|
3924
3995
|
type: "string",
|
|
3925
|
-
description: "Comma-separated list of checks to skip (build, typecheck, lint, test, format, knip, tooling:check,
|
|
3996
|
+
description: "Comma-separated list of checks to skip (build, typecheck, lint, test, format, knip, tooling:check, docker:check)",
|
|
3926
3997
|
required: false
|
|
3927
3998
|
},
|
|
3928
3999
|
add: {
|
|
@@ -4313,6 +4384,7 @@ const ComposePortSchema = z.union([z.string(), z.object({
|
|
|
4313
4384
|
target: z.union([z.string(), z.number()]).optional()
|
|
4314
4385
|
}).loose()]);
|
|
4315
4386
|
const ComposeServiceSchema = z.object({
|
|
4387
|
+
image: z.string().optional(),
|
|
4316
4388
|
ports: z.array(ComposePortSchema).optional(),
|
|
4317
4389
|
healthcheck: z.unknown().optional()
|
|
4318
4390
|
}).loose();
|
|
@@ -4387,6 +4459,7 @@ function parseComposeServices(cwd, composeFiles) {
|
|
|
4387
4459
|
}
|
|
4388
4460
|
serviceMap.set(name, {
|
|
4389
4461
|
name,
|
|
4462
|
+
image: existing?.image ?? service.image,
|
|
4390
4463
|
hostPort,
|
|
4391
4464
|
hasHealthcheck: existing?.hasHealthcheck ?? service.healthcheck !== void 0
|
|
4392
4465
|
});
|
|
@@ -4394,6 +4467,15 @@ function parseComposeServices(cwd, composeFiles) {
|
|
|
4394
4467
|
}
|
|
4395
4468
|
return [...serviceMap.values()];
|
|
4396
4469
|
}
|
|
4470
|
+
/** Extract deduplicated bare image names (without tags) from parsed services. */
|
|
4471
|
+
function extractComposeImageNames(services) {
|
|
4472
|
+
const names = /* @__PURE__ */ new Set();
|
|
4473
|
+
for (const service of services) if (service.image) {
|
|
4474
|
+
const bare = service.image.split(":")[0];
|
|
4475
|
+
if (bare) names.add(bare);
|
|
4476
|
+
}
|
|
4477
|
+
return [...names];
|
|
4478
|
+
}
|
|
4397
4479
|
/** Generate health checks from parsed services: services with exposed ports get HTTP checks, unless they define a compose-level healthcheck. */
|
|
4398
4480
|
function deriveHealthChecks(services) {
|
|
4399
4481
|
return services.filter((s) => s.hostPort !== void 0 && !s.hasHealthcheck).map((s) => ({
|
|
@@ -4454,6 +4536,26 @@ function computeCheckDefaults(cwd) {
|
|
|
4454
4536
|
healthChecks: healthChecks.length > 0 ? healthChecks : void 0
|
|
4455
4537
|
};
|
|
4456
4538
|
}
|
|
4539
|
+
/** Create a DockerFileReader backed by the real filesystem. */
|
|
4540
|
+
function createFileReader() {
|
|
4541
|
+
return {
|
|
4542
|
+
listPackageDirs(cwd) {
|
|
4543
|
+
const packagesDir = path.join(cwd, "packages");
|
|
4544
|
+
try {
|
|
4545
|
+
return readdirSync(packagesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
4546
|
+
} catch {
|
|
4547
|
+
return [];
|
|
4548
|
+
}
|
|
4549
|
+
},
|
|
4550
|
+
readFile(filePath) {
|
|
4551
|
+
try {
|
|
4552
|
+
return readFileSync(filePath, "utf-8");
|
|
4553
|
+
} catch {
|
|
4554
|
+
return null;
|
|
4555
|
+
}
|
|
4556
|
+
}
|
|
4557
|
+
};
|
|
4558
|
+
}
|
|
4457
4559
|
//#endregion
|
|
4458
4560
|
//#region src/commands/docker-check.ts
|
|
4459
4561
|
/** Convert declarative health checks to functional ones. */
|
|
@@ -4509,7 +4611,18 @@ const dockerCheckCommand = defineCommand({
|
|
|
4509
4611
|
}
|
|
4510
4612
|
if (!defaults.services || defaults.services.length === 0) throw new FatalError("No services found in compose files.");
|
|
4511
4613
|
const composeCwd = defaults.composeCwd ?? cwd;
|
|
4512
|
-
const
|
|
4614
|
+
const services = parseComposeServices(composeCwd, defaults.composeFiles);
|
|
4615
|
+
const fileReader = createFileReader();
|
|
4616
|
+
const rootPkgRaw = fileReader.readFile(path.join(cwd, "package.json"));
|
|
4617
|
+
if (rootPkgRaw) {
|
|
4618
|
+
const rootPkg = parsePackageJson(rootPkgRaw);
|
|
4619
|
+
if (rootPkg?.name) {
|
|
4620
|
+
const dockerPackages = detectDockerPackages(fileReader, cwd, rootPkg.name);
|
|
4621
|
+
const composeImages = extractComposeImageNames(services);
|
|
4622
|
+
for (const pkg of dockerPackages) if (!composeImages.some((img) => img === pkg.imageName || img.endsWith(`/${pkg.imageName}`))) warn(`Docker package "${pkg.dir}" (image: ${pkg.imageName}) is not referenced in any compose service.`);
|
|
4623
|
+
}
|
|
4624
|
+
}
|
|
4625
|
+
const tempOverlayPath = writeTempOverlay(generateCheckOverlay(services));
|
|
4513
4626
|
const composeFiles = [
|
|
4514
4627
|
...defaults.composeFiles,
|
|
4515
4628
|
tempOverlayPath,
|
|
@@ -4543,7 +4656,7 @@ const dockerCheckCommand = defineCommand({
|
|
|
4543
4656
|
const main = defineCommand({
|
|
4544
4657
|
meta: {
|
|
4545
4658
|
name: "tooling",
|
|
4546
|
-
version: "0.
|
|
4659
|
+
version: "0.21.0",
|
|
4547
4660
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
4548
4661
|
},
|
|
4549
4662
|
subCommands: {
|
|
@@ -4559,7 +4672,7 @@ const main = defineCommand({
|
|
|
4559
4672
|
"docker:check": dockerCheckCommand
|
|
4560
4673
|
}
|
|
4561
4674
|
});
|
|
4562
|
-
console.log(`@bensandee/tooling v0.
|
|
4675
|
+
console.log(`@bensandee/tooling v0.21.0`);
|
|
4563
4676
|
runMain(main);
|
|
4564
4677
|
//#endregion
|
|
4565
4678
|
export {};
|