@chanom/cli 0.2.0 → 0.2.2
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 +9 -0
- package/dist/index.js +61 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,6 +53,11 @@ Based on your answers, `brew` will:
|
|
|
53
53
|
|
|
54
54
|
Cancelling a prompt (Ctrl+C) exits cleanly without touching anything further.
|
|
55
55
|
|
|
56
|
+
`medium` sweetness must be brewed at the git repository root - husky can only
|
|
57
|
+
install hooks from the directory that holds `.git`. Run inside a subdirectory
|
|
58
|
+
(e.g. a workspace package), `brew` stops with an error naming the root before
|
|
59
|
+
installing anything. `light` sweetness works anywhere in the repo.
|
|
60
|
+
|
|
56
61
|
## What gets added
|
|
57
62
|
|
|
58
63
|
| Topping | Config file | Scripts added |
|
|
@@ -71,6 +76,10 @@ Config files re-export presets from [`@chanom/dev-config`](../dev-config).
|
|
|
71
76
|
- `lint-staged` - `.lintstagedrc.json` running the selected linter/formatter on staged files, wired into the `pre-commit` hook
|
|
72
77
|
- `commitlint` - `.commitlintrc.json` extending `@commitlint/config-conventional`, enforced in the `commit-msg` hook
|
|
73
78
|
|
|
79
|
+
When neither `oxlint` nor `oxfmt` is selected, lint-staged is skipped entirely
|
|
80
|
+
(an empty config would fail every commit) and the `npm test` pre-commit hook
|
|
81
|
+
seeded by `husky init` is removed - only the `commit-msg` gate remains.
|
|
82
|
+
|
|
74
83
|
## Version pinning
|
|
75
84
|
|
|
76
85
|
The CLI bundles the tool versions its generated configs are known to work with (baked in at build time). `oxlint`, `oxfmt`, and `@chanom/dev-config` are (re)installed whenever the installed version doesn't match the pinned one exactly - in either direction, since the generated config must match. `knip`, `husky`, `lint-staged`, and the commitlint packages are only installed when missing.
|
package/dist/index.js
CHANGED
|
@@ -6,9 +6,10 @@ import pc from "picocolors";
|
|
|
6
6
|
import * as clack from "@clack/prompts";
|
|
7
7
|
//#region src/bundled-versions.ts
|
|
8
8
|
const bundledVersions = {
|
|
9
|
-
oxlint: "1.
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
oxlint: "1.73.0",
|
|
10
|
+
oxlintTsgolint: "0.24.0",
|
|
11
|
+
oxfmt: "0.58.0",
|
|
12
|
+
knip: "6.26.0",
|
|
12
13
|
devConfig: "0.0.3"
|
|
13
14
|
};
|
|
14
15
|
//#endregion
|
|
@@ -101,7 +102,16 @@ var Git = class extends Effect.Service()("cli/Git", { effect: Effect.gen(functio
|
|
|
101
102
|
"--cached",
|
|
102
103
|
"--quiet"
|
|
103
104
|
], cwd).pipe(Effect.map((clean) => !clean)),
|
|
104
|
-
|
|
105
|
+
/**
|
|
106
|
+
* True when `cwd` is anywhere inside a git work tree — a `.git` folder
|
|
107
|
+
* check would miss subdirectories (e.g. workspace packages) and lead to
|
|
108
|
+
* nested `git init` calls.
|
|
109
|
+
*/
|
|
110
|
+
isRepo: (cwd) => exitOk(["rev-parse", "--is-inside-work-tree"], cwd),
|
|
111
|
+
/** Path of `cwd` relative to the work-tree root: empty at the root, `None` outside a repository. */
|
|
112
|
+
prefix: (cwd) => run(["rev-parse", "--show-prefix"], cwd).pipe(Effect.map((result) => result.ok ? Option.some(result.stdout) : Option.none())),
|
|
113
|
+
/** Absolute path of the work-tree root, or `None` outside a repository. */
|
|
114
|
+
root: (cwd) => run(["rev-parse", "--show-toplevel"], cwd).pipe(Effect.map((result) => result.ok ? Option.some(result.stdout) : Option.none())),
|
|
105
115
|
writeGitignore: (cwd, contents = "node_modules\n") => fs.writeFileString(path.join(cwd, ".gitignore"), contents),
|
|
106
116
|
readGitignore: (cwd) => fs.readFileString(path.join(cwd, ".gitignore"))
|
|
107
117
|
};
|
|
@@ -429,6 +439,10 @@ function buildConfig(linters, formatters) {
|
|
|
429
439
|
//#endregion
|
|
430
440
|
//#region src/commands/add-lint-staged/index.ts
|
|
431
441
|
const apply$2 = Effect.fn("add-lint-staged.apply")(function* (cwd, linters = [], formatters = [], overwritePreCommit = false) {
|
|
442
|
+
if (linters.length === 0 && formatters.length === 0) {
|
|
443
|
+
yield* Effect.logDebug("no linters or formatters selected - skipping lint-staged setup");
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
432
446
|
const fs = yield* FileSystem.FileSystem;
|
|
433
447
|
const path = yield* Path.Path;
|
|
434
448
|
const prompter = yield* Prompter;
|
|
@@ -482,7 +496,11 @@ const apply$1 = Effect.fn("add-oxfmt.apply")(function* (cwd, esm, pkg) {
|
|
|
482
496
|
//#endregion
|
|
483
497
|
//#region src/commands/add-oxlint/logic.ts
|
|
484
498
|
function getPackages(pkg, versions) {
|
|
485
|
-
return [
|
|
499
|
+
return [
|
|
500
|
+
getMismatchedPackage(pkg, "oxlint", versions.oxlint),
|
|
501
|
+
getMismatchedPackage(pkg, "oxlint-tsgolint", versions.oxlintTsgolint),
|
|
502
|
+
getMismatchedPackage(pkg, "@chanom/dev-config", versions.devConfig)
|
|
503
|
+
].filter((p) => p !== void 0);
|
|
486
504
|
}
|
|
487
505
|
function configFile(esm) {
|
|
488
506
|
return {
|
|
@@ -524,9 +542,17 @@ function planPackages(pkg, toppings, sweetness, versions) {
|
|
|
524
542
|
if (toppings.includes("oxlint")) packages.push(...getPackages(pkg, versions));
|
|
525
543
|
if (toppings.includes("oxfmt")) packages.push(...getPackages$1(pkg, versions));
|
|
526
544
|
if (toppings.includes("knip")) packages.push(...getPackages$3(pkg, versions));
|
|
527
|
-
if (sweetness === "medium")
|
|
545
|
+
if (sweetness === "medium") {
|
|
546
|
+
packages.push(...getPackages$4(pkg));
|
|
547
|
+
if (wantsLintStaged(toppings)) packages.push(...getPackages$2(pkg));
|
|
548
|
+
packages.push(...getPackages$5(pkg));
|
|
549
|
+
}
|
|
528
550
|
return [...new Set(packages)];
|
|
529
551
|
}
|
|
552
|
+
/** True when at least one topping gives lint-staged something to run. */
|
|
553
|
+
function wantsLintStaged(toppings) {
|
|
554
|
+
return selectedLinters(toppings).length > 0 || selectedFormatters(toppings).length > 0;
|
|
555
|
+
}
|
|
530
556
|
/** True when the .gitignore content already has the entry as its own line. */
|
|
531
557
|
function hasGitignoreEntry(content, entry) {
|
|
532
558
|
const forms = /* @__PURE__ */ new Set([
|
|
@@ -550,6 +576,7 @@ function selectedFormatters(toppings) {
|
|
|
550
576
|
}
|
|
551
577
|
//#endregion
|
|
552
578
|
//#region src/commands/brew/index.ts
|
|
579
|
+
var NotAtRepoRoot = class extends Data.TaggedError("NotAtRepoRoot") {};
|
|
553
580
|
const ensureGitRepo = Effect.fn("brew.ensureGitRepo")(function* (cwd) {
|
|
554
581
|
const git = yield* Git;
|
|
555
582
|
if (yield* git.isRepo(cwd)) {
|
|
@@ -635,13 +662,35 @@ const persistScripts = Effect.fn("brew.persistScripts")(function* (pkgPath, pkg,
|
|
|
635
662
|
yield* writePkg(pkgPath, updated);
|
|
636
663
|
}
|
|
637
664
|
});
|
|
665
|
+
/**
|
|
666
|
+
* Fails when medium sweetness is chosen away from the git work-tree root:
|
|
667
|
+
* husky can only install hooks from the directory that holds `.git`, so
|
|
668
|
+
* running it in e.g. a workspace package would leave hooks that never fire.
|
|
669
|
+
*/
|
|
670
|
+
const ensureRepoRootForHooks = Effect.fn("brew.ensureRepoRootForHooks")(function* (cwd, sweetness) {
|
|
671
|
+
if (sweetness !== "medium") return;
|
|
672
|
+
const git = yield* Git;
|
|
673
|
+
const prefix = yield* git.prefix(cwd);
|
|
674
|
+
if (Option.isNone(prefix) || prefix.value === "") return;
|
|
675
|
+
const root = yield* git.root(cwd);
|
|
676
|
+
return yield* new NotAtRepoRoot({
|
|
677
|
+
cwd,
|
|
678
|
+
root: Option.getOrElse(root, () => cwd)
|
|
679
|
+
});
|
|
680
|
+
});
|
|
638
681
|
const setupCommitGate = Effect.fn("brew.setupCommitGate")(function* (cwd, pm, toppings, sweetness) {
|
|
639
682
|
if (sweetness !== "medium") return;
|
|
640
683
|
const fs = yield* FileSystem.FileSystem;
|
|
641
684
|
const path = yield* Path.Path;
|
|
685
|
+
const prompter = yield* Prompter;
|
|
642
686
|
const huskyExisted = yield* fs.exists(path.join(cwd, ".husky"));
|
|
643
687
|
yield* apply$4(cwd, pm);
|
|
644
|
-
yield* apply$2(cwd, selectedLinters(toppings), selectedFormatters(toppings), !huskyExisted);
|
|
688
|
+
if (wantsLintStaged(toppings)) yield* apply$2(cwd, selectedLinters(toppings), selectedFormatters(toppings), !huskyExisted);
|
|
689
|
+
else {
|
|
690
|
+
yield* prompter.warn("No linters or formatters selected - skipping lint-staged");
|
|
691
|
+
const seededHook = path.join(cwd, ".husky", "pre-commit");
|
|
692
|
+
if (!huskyExisted && (yield* fs.exists(seededHook))) yield* fs.remove(seededHook);
|
|
693
|
+
}
|
|
645
694
|
yield* apply$5(cwd, pm);
|
|
646
695
|
});
|
|
647
696
|
const commitChanges = Effect.fn("brew.commitChanges")(function* (cwd) {
|
|
@@ -673,10 +722,12 @@ const brew = (cwd = process.cwd()) => Effect.gen(function* () {
|
|
|
673
722
|
const { pkg, pkgPath } = yield* readPkg(cwd);
|
|
674
723
|
const { toppings, sweetness } = yield* askRecipe();
|
|
675
724
|
yield* Effect.logDebug(`recipe: toppings=[${toppings.join(", ")}] sweetness=${sweetness} esm=${isEsm(pkg)}`);
|
|
725
|
+
yield* ensureRepoRootForHooks(cwd, sweetness);
|
|
676
726
|
const packages = planPackages(pkg, toppings, sweetness, bundledVersions);
|
|
677
727
|
yield* Effect.logDebug(`planned packages: ${packages.length > 0 ? packages.join(", ") : "(none)"}`);
|
|
678
728
|
yield* installPackages(pm, cwd, packages);
|
|
679
|
-
|
|
729
|
+
const { pkg: installedPkg } = yield* readPkg(cwd);
|
|
730
|
+
yield* persistScripts(pkgPath, installedPkg, yield* applyToppings(cwd, isEsm(installedPkg), installedPkg, toppings));
|
|
680
731
|
yield* setupCommitGate(cwd, pm, toppings, sweetness);
|
|
681
732
|
yield* commitChanges(cwd);
|
|
682
733
|
yield* prompter.outro(pc.green(`Your Chanom with ${toppings.join(", ")} toppings and ${sweetness} sweetness is ready! Enjoy! 🧋`));
|
|
@@ -708,7 +759,8 @@ const run = (name, cwd = process.cwd(), options = {}) => {
|
|
|
708
759
|
PkgNotFound: (e) => reportError(`No package.json found in ${e.cwd}. Run this inside a project.`),
|
|
709
760
|
PkgInvalid: (e) => reportError(`Could not parse ${e.pkgPath}. Is it valid JSON?`),
|
|
710
761
|
InstallFailed: (e) => reportError(`Package installation with ${e.pm} failed.`),
|
|
711
|
-
HuskyInitFailed: (e) => reportError(`\`husky init\` failed (exit code ${e.exitCode}).`)
|
|
762
|
+
HuskyInitFailed: (e) => reportError(`\`husky init\` failed (exit code ${e.exitCode}).`),
|
|
763
|
+
NotAtRepoRoot: (e) => reportError(`Git hooks must be set up at the repository root: ${e.root}\nRun \`chanom brew\` there, or choose light sweetness.`)
|
|
712
764
|
}), Effect.catchAll((e) => reportError(`Unexpected error: ${e.message}`)), Logger.withMinimumLogLevel(debug ? LogLevel.Debug : LogLevel.Info));
|
|
713
765
|
};
|
|
714
766
|
//#endregion
|