@chanom/cli 0.4.0 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +6 -0
  2. package/dist/index.js +84 -25
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -72,6 +72,12 @@ installing anything. `light` sweetness works anywhere in the repo.
72
72
 
73
73
  \* `.mts` instead of `.ts` when your project is CommonJS (no `"type": "module"` in `package.json`).
74
74
 
75
+ oxlint and oxfmt auto-discover `.js`, `.cjs`, `.mjs`, `.ts`, and the JSON rc files, but not
76
+ `.mts` or `.cts`. When the config in play is one of those two, the generated `oxlint`/`oxfmt`
77
+ scripts - and the lint-staged commands - name it with `-c`. This tracks the file that actually
78
+ ends up on disk, so it applies to a config `brew` detected and skipped just as much as one it
79
+ wrote itself.
80
+
75
81
  Config files re-export presets from [`@chanom/dev-config`](../dev-config).
76
82
 
77
83
  The `vscode` topping installs no packages and adds no scripts. It writes editor
package/dist/index.js CHANGED
@@ -257,7 +257,7 @@ const SKILLS = [{
257
257
  },
258
258
  {
259
259
  "path": "SKILL.md",
260
- "contents": "---\nname: coding-standards\ndescription: Coding standards for this Vite + React + TypeScript app. Use when writing or changing components, hooks, utils, or tests, and before committing.\n---\n\n# Coding standards\n\nVite + React 19 + TypeScript, scaffolded with `create-chanom-app`. oxlint lints,\noxfmt formats, vitest tests. Read the reference for what you touch **before**\nwriting, so new code matches the code already here.\n\n## Which reference to load\n\n| You are writing… | Load |\n| -------------------------------------------- | -------------------------- |\n| Any `.ts` / `.tsx` file | `references/typescript.md` |\n| A component, hook, or anything rendering JSX | `references/react.md` |\n| A `*.test.ts` / `*.test.tsx` file | `references/vitest.md` |\n\nThese layer: a new component with a test reads all three. Load every row that\napplies.\n\n## Universal rules\n\n- Filenames are kebab-case: `user-card.tsx`, `use-current-user.ts`, `format-date.ts`.\n- Before adding a file, open two neighbouring files of the same kind and match\n their shape — import order, export style, test naming.\n- Source lives under `src/`. Colocate a test beside the code it covers:\n `src/utils/format-date.ts` → `src/utils/format-date.test.ts`.\n\n## Verify\n\nAfter implementing, from the project root:\n\n```sh\npnpm lint\npnpm test\npnpm build\n```\n\nDone only when all three exit clean. On failure, fix and rerun until they do.\nRun `pnpm format` before committing. Comment- or doc-only changes may skip all\nfour.\n\n## Committing\n\nAsk before committing. Use conventional commits under 100 characters:\n`<type>(<scope>): <subject>`, e.g. `feat(auth): add sign-out button`. Scope is\nthe area you touched, not the repo name.\n"
260
+ "contents": "---\nname: coding-standards\ndescription: Coding standards for this Vite + React + TypeScript app. Use when writing or changing components, hooks, utils, or tests, and before committing.\n---\n\n# Coding standards\n\nVite + React 19 + TypeScript, scaffolded with `create-chanom-app`. oxlint lints,\noxfmt formats, vitest tests. Read the reference for what you touch **before**\nwriting, so new code matches the code already here.\n\n## Which reference to load\n\n| You are writing… | Load |\n| -------------------------------------------- | -------------------------- |\n| Any `.ts` / `.tsx` file | `references/typescript.md` |\n| A component, hook, or anything rendering JSX | `references/react.md` |\n| A `*.test.ts` / `*.test.tsx` file | `references/vitest.md` |\n\nThese layer: a new component with a test reads all three. Load every row that\napplies.\n\n## Universal rules\n\n- Filenames are kebab-case: `user-card.tsx`, `use-current-user.ts`, `format-date.ts`.\n- Before adding a file, open two neighbouring files of the same kind and match\n their shape — import order, export style, test naming.\n- Source lives under `src/`. Colocate a test beside the code it covers:\n `src/utils/format-date.ts` → `src/utils/format-date.test.ts`.\n\n## Verify\n\nAfter implementing, from the project root:\n\n```sh\npnpm lint\npnpm test\npnpm build\npnpm typecheck\n```\n\nDone only when all four exit clean. On failure, fix and rerun until they do.\nRun `pnpm format` before committing. Comment- or doc-only changes may skip all\nfour.\n\n## Committing\n\nAsk before committing. Use conventional commits under 100 characters:\n`<type>(<scope>): <subject>`, e.g. `feat(auth): add sign-out button`. Scope is\nthe area you touched, not the repo name.\n"
261
261
  }
262
262
  ]
263
263
  }];
@@ -424,6 +424,32 @@ const SETUP_FILE_CANDIDATES = {
424
424
  "vite.config.mjs"
425
425
  ]
426
426
  };
427
+ /** Config file name a tool gets scaffolded with, matching the project's module system. */
428
+ function configFileName(tool, esm) {
429
+ return `${tool}.config.${esm ? "ts" : "mts"}`;
430
+ }
431
+ /**
432
+ * Extensions oxlint/oxfmt resolve on their own. `.mts`/`.cts` are absent
433
+ * deliberately: the tools only auto-discover the plain `.ts` variant, so a
434
+ * config using one of those has to be named explicitly on the command line.
435
+ */
436
+ const AUTO_DISCOVERED = /* @__PURE__ */ new Set([
437
+ "js",
438
+ "cjs",
439
+ "mjs",
440
+ "ts",
441
+ "json"
442
+ ]);
443
+ /**
444
+ * `-c <config>` argument naming the config file, or an empty string when the
445
+ * tool finds the file by itself. `configFile` is the config that will actually
446
+ * be on disk - the one chanom is about to write, or an existing one it detected
447
+ * and skipped - so the generated scripts never point at a path that isn't there.
448
+ */
449
+ function configFlag(configFile) {
450
+ const extension = configFile.slice(configFile.lastIndexOf(".") + 1);
451
+ return AUTO_DISCOVERED.has(extension) ? "" : ` -c ${configFile}`;
452
+ }
427
453
  //#endregion
428
454
  //#region src/utils/detect-setup.ts
429
455
  /** Finds an existing config file for the given tool, if any. */
@@ -523,7 +549,7 @@ function getPackages$3(pkg, versions) {
523
549
  }
524
550
  function configFile$2(esm) {
525
551
  return {
526
- fileName: `knip.config.${esm ? "ts" : "mts"}`,
552
+ fileName: configFileName("knip", esm),
527
553
  contents: `export { default } from '@chanom/dev-config/knip/config';\n`
528
554
  };
529
555
  }
@@ -555,22 +581,42 @@ const apply$5 = Effect.fn("add-knip.apply")(function* (cwd, esm, pkg) {
555
581
  function getPackages$2(pkg) {
556
582
  return isPackageInstalled(pkg, "lint-staged") ? [] : ["lint-staged"];
557
583
  }
558
- function buildConfig(linters, formatters) {
584
+ function buildConfig(linters, formatters, configFiles) {
559
585
  const config = {};
560
586
  const jsGlob = "**/*.{js,jsx,ts,tsx,mjs,cjs,mts,cts}";
561
587
  if (linters.includes("oxlint")) {
562
588
  config[jsGlob] ??= [];
563
- config[jsGlob].push("oxlint --fix --no-error-on-unmatched-pattern");
589
+ config[jsGlob].push(`oxlint --fix${configFlag(configFiles.oxlint)} --no-error-on-unmatched-pattern`);
564
590
  }
565
591
  if (formatters.includes("oxfmt")) {
566
592
  config["*"] ??= [];
567
- config["*"].push("oxfmt --no-error-on-unmatched-pattern");
593
+ config["*"].push(`oxfmt${configFlag(configFiles.oxfmt)} --no-error-on-unmatched-pattern`);
568
594
  }
569
595
  return config;
570
596
  }
571
597
  //#endregion
572
598
  //#region src/commands/add-lint-staged/index.ts
573
- const apply$4 = Effect.fn("add-lint-staged.apply")(function* (cwd, linters = [], formatters = [], overwritePreCommit = false) {
599
+ /**
600
+ * Config file each tool will run against: an existing one when `add-oxlint` /
601
+ * `add-oxfmt` skipped writing, otherwise the name they scaffold. lint-staged
602
+ * invokes the same binaries as the package scripts, so it has to resolve the
603
+ * config the same way.
604
+ */
605
+ const resolveConfigFiles = Effect.fn("add-lint-staged.resolveConfigFiles")(function* (cwd, esm) {
606
+ const path = yield* Path.Path;
607
+ const resolve = Effect.fn(function* (tool) {
608
+ const existing = yield* detectSetupFile(tool, cwd);
609
+ return Option.match(existing, {
610
+ onNone: () => configFileName(tool, esm),
611
+ onSome: (file) => path.basename(file)
612
+ });
613
+ });
614
+ return {
615
+ oxlint: yield* resolve("oxlint"),
616
+ oxfmt: yield* resolve("oxfmt")
617
+ };
618
+ });
619
+ const apply$4 = Effect.fn("add-lint-staged.apply")(function* (cwd, esm, linters = [], formatters = [], overwritePreCommit = false) {
574
620
  if (linters.length === 0 && formatters.length === 0) {
575
621
  yield* Effect.logDebug("no linters or formatters selected - skipping lint-staged setup");
576
622
  return;
@@ -580,7 +626,10 @@ const apply$4 = Effect.fn("add-lint-staged.apply")(function* (cwd, linters = [],
580
626
  const prompter = yield* Prompter;
581
627
  const existing = yield* detectSetupFile("lint-staged", cwd);
582
628
  if (Option.isSome(existing)) yield* prompter.warn(`\`${path.basename(existing.value)}\` already exists - skipping lint-staged config`);
583
- else yield* fs.writeFileString(path.join(cwd, ".lintstagedrc.json"), JSON.stringify(buildConfig(linters, formatters), null, 2) + "\n");
629
+ else {
630
+ const configFiles = yield* resolveConfigFiles(cwd, esm);
631
+ yield* fs.writeFileString(path.join(cwd, ".lintstagedrc.json"), JSON.stringify(buildConfig(linters, formatters, configFiles), null, 2) + "\n");
632
+ }
584
633
  const huskyDir = path.join(cwd, ".husky");
585
634
  if (yield* fs.exists(huskyDir)) {
586
635
  const hookPath = path.join(huskyDir, "pre-commit");
@@ -595,14 +644,15 @@ function getPackages$1(pkg, versions) {
595
644
  }
596
645
  function configFile$1(esm) {
597
646
  return {
598
- fileName: `oxfmt.config.${esm ? "ts" : "mts"}`,
647
+ fileName: configFileName("oxfmt", esm),
599
648
  contents: `export { default } from '@chanom/dev-config/oxfmt/config';\n`
600
649
  };
601
650
  }
602
- function getScriptPlan$1(scripts) {
651
+ function getScriptPlan$1(scripts, configFile) {
652
+ const config = configFlag(configFile);
603
653
  return planScripts(scripts, {
604
- format: "oxfmt",
605
- "format:check": "oxfmt --check"
654
+ format: `oxfmt${config}`,
655
+ "format:check": `oxfmt --check${config}`
606
656
  });
607
657
  }
608
658
  //#endregion
@@ -613,12 +663,16 @@ const apply$3 = Effect.fn("add-oxfmt.apply")(function* (cwd, esm, pkg) {
613
663
  const path = yield* Path.Path;
614
664
  const prompter = yield* Prompter;
615
665
  const existing = yield* detectSetupFile("oxfmt", cwd);
616
- if (Option.isSome(existing)) yield* prompter.warn(`\`${path.basename(existing.value)}\` already exists - skipping oxfmt config`);
617
- else {
666
+ let configFileName;
667
+ if (Option.isSome(existing)) {
668
+ configFileName = path.basename(existing.value);
669
+ yield* prompter.warn(`\`${configFileName}\` already exists - skipping oxfmt config`);
670
+ } else {
618
671
  const config = configFile$1(esm);
672
+ configFileName = config.fileName;
619
673
  yield* fs.writeFileString(path.join(cwd, config.fileName), config.contents);
620
674
  }
621
- const plan = getScriptPlan$1(pkg.scripts);
675
+ const plan = getScriptPlan$1(pkg.scripts, configFileName);
622
676
  for (const key of plan.skipped) yield* prompter.warn(`\`${key}\` script already exists in package.json - skipping`);
623
677
  return {
624
678
  ...pkg,
@@ -636,14 +690,15 @@ function getPackages(pkg, versions) {
636
690
  }
637
691
  function configFile(esm) {
638
692
  return {
639
- fileName: `oxlint.config.${esm ? "ts" : "mts"}`,
693
+ fileName: configFileName("oxlint", esm),
640
694
  contents: `export { default } from '@chanom/dev-config/oxlint/config';\n`
641
695
  };
642
696
  }
643
- function getScriptPlan(scripts) {
697
+ function getScriptPlan(scripts, configFile) {
698
+ const config = configFlag(configFile);
644
699
  return planScripts(scripts, {
645
- lint: "oxlint",
646
- "lint:fix": "oxlint --fix"
700
+ lint: `oxlint${config}`,
701
+ "lint:fix": `oxlint --fix${config}`
647
702
  });
648
703
  }
649
704
  //#endregion
@@ -654,12 +709,16 @@ const apply$2 = Effect.fn("add-oxlint.apply")(function* (cwd, esm, pkg) {
654
709
  const path = yield* Path.Path;
655
710
  const prompter = yield* Prompter;
656
711
  const existing = yield* detectSetupFile("oxlint", cwd);
657
- if (Option.isSome(existing)) yield* prompter.warn(`\`${path.basename(existing.value)}\` already exists - skipping oxlint config`);
658
- else {
712
+ let configFileName;
713
+ if (Option.isSome(existing)) {
714
+ configFileName = path.basename(existing.value);
715
+ yield* prompter.warn(`\`${configFileName}\` already exists - skipping oxlint config`);
716
+ } else {
659
717
  const config = configFile(esm);
718
+ configFileName = config.fileName;
660
719
  yield* fs.writeFileString(path.join(cwd, config.fileName), config.contents);
661
720
  }
662
- const plan = getScriptPlan(pkg.scripts);
721
+ const plan = getScriptPlan(pkg.scripts, configFileName);
663
722
  for (const key of plan.skipped) yield* prompter.warn(`\`${key}\` script already exists in package.json - skipping`);
664
723
  return {
665
724
  ...pkg,
@@ -810,7 +869,7 @@ function planPackages(pkg, toppings, sweetness, versions) {
810
869
  * value for a config the user may add later.
811
870
  */
812
871
  function oxfmtConfigPath(esm) {
813
- return `oxfmt.config.${esm ? "ts" : "mts"}`;
872
+ return configFileName("oxfmt", esm);
814
873
  }
815
874
  /** True when at least one topping gives lint-staged something to run. */
816
875
  function wantsLintStaged(toppings) {
@@ -953,14 +1012,14 @@ const ensureRepoRootForHooks = Effect.fn("brew.ensureRepoRootForHooks")(function
953
1012
  root: Option.getOrElse(root, () => cwd)
954
1013
  });
955
1014
  });
956
- const setupCommitGate = Effect.fn("brew.setupCommitGate")(function* (cwd, pm, toppings, sweetness) {
1015
+ const setupCommitGate = Effect.fn("brew.setupCommitGate")(function* (cwd, pm, esm, toppings, sweetness) {
957
1016
  if (sweetness !== "medium") return;
958
1017
  const fs = yield* FileSystem.FileSystem;
959
1018
  const path = yield* Path.Path;
960
1019
  const prompter = yield* Prompter;
961
1020
  const huskyExisted = yield* fs.exists(path.join(cwd, ".husky"));
962
1021
  yield* apply$6(cwd, pm);
963
- if (wantsLintStaged(toppings)) yield* apply$4(cwd, selectedLinters(toppings), selectedFormatters(toppings), !huskyExisted);
1022
+ if (wantsLintStaged(toppings)) yield* apply$4(cwd, esm, selectedLinters(toppings), selectedFormatters(toppings), !huskyExisted);
964
1023
  else {
965
1024
  yield* prompter.warn("No linters or formatters selected - skipping lint-staged");
966
1025
  const seededHook = path.join(cwd, ".husky", "pre-commit");
@@ -1003,7 +1062,7 @@ const brew = (cwd = process.cwd()) => Effect.gen(function* () {
1003
1062
  yield* installPackages(pm, cwd, packages);
1004
1063
  const { pkg: installedPkg } = yield* readPkg(cwd);
1005
1064
  yield* persistScripts(pkgPath, installedPkg, yield* applyToppings(cwd, isEsm(installedPkg), installedPkg, toppings));
1006
- yield* setupCommitGate(cwd, pm, toppings, sweetness);
1065
+ yield* setupCommitGate(cwd, pm, isEsm(installedPkg), toppings, sweetness);
1007
1066
  yield* commitChanges(cwd);
1008
1067
  yield* prompter.outro(pc.green(`Your Chanom with ${toppings.join(", ")} toppings and ${sweetness} sweetness is ready! Enjoy! 🧋`));
1009
1068
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chanom/cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "repository": {
@@ -40,6 +40,7 @@
40
40
  "dev": "tsdown --watch",
41
41
  "test": "vitest run",
42
42
  "test:coverage": "vitest run --coverage",
43
- "test:watch": "vitest"
43
+ "test:watch": "vitest",
44
+ "typecheck": "tsc --noEmit"
44
45
  }
45
46
  }