@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 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.71.0",
10
- oxfmt: "0.56.0",
11
- knip: "6.23.0",
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
- isRepo: (cwd) => fs.exists(path.join(cwd, ".git")),
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 [getMismatchedPackage(pkg, "oxlint", versions.oxlint), getMismatchedPackage(pkg, "@chanom/dev-config", versions.devConfig)].filter((p) => p !== void 0);
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") packages.push(...getPackages$4(pkg), ...getPackages$2(pkg), ...getPackages$5(pkg));
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
- yield* persistScripts(pkgPath, pkg, yield* applyToppings(cwd, isEsm(pkg), pkg, toppings));
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chanom/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "repository": {