@chanom/cli 0.4.0 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +6 -0
  2. package/dist/index.js +93 -30
  3. package/package.json +4 -3
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
  }];
@@ -290,7 +290,7 @@ const bundledVersions = {
290
290
  oxlintTsgolint: "0.24.0",
291
291
  oxfmt: "0.58.0",
292
292
  knip: "6.26.0",
293
- devConfig: "0.0.3"
293
+ devConfig: "0.0.4"
294
294
  };
295
295
  //#endregion
296
296
  //#region src/domain/pkg.ts
@@ -330,7 +330,8 @@ var InstallFailed = class extends Data.TaggedError("InstallFailed") {};
330
330
  /**
331
331
  * Installs npm packages with the detected package manager, adding the
332
332
  * workspace-root flag (pnpm `-w`, yarn classic `-W`) when the target
333
- * directory is a workspace root.
333
+ * directory is a workspace root. Output is captured rather than inherited so
334
+ * the caller's spinner is not redrawn over the package manager's own progress.
334
335
  */
335
336
  var PackageInstaller = class extends Effect.Service()("cli/PackageInstaller", { effect: Effect.gen(function* () {
336
337
  const runner = yield* CommandRunner;
@@ -343,12 +344,15 @@ var PackageInstaller = class extends Effect.Service()("cli/PackageInstaller", {
343
344
  hasWorkspacesField: hasWorkspacesField(cwd),
344
345
  hasYarnBerryConfig: has(cwd, ".yarnrc.yml")
345
346
  }, { concurrency: 3 });
346
- return { installDev: (pm, cwd, packages) => workspaceHints(cwd).pipe(Effect.flatMap((hints) => runner.execInherit(pm, [
347
+ return { installDev: (pm, cwd, packages) => workspaceHints(cwd).pipe(Effect.flatMap((hints) => runner.capture(pm, [
347
348
  "add",
348
349
  "-D",
349
350
  ...workspaceRootFlags(pm, hints),
350
351
  ...packages
351
- ], cwd)), Effect.filterOrFail((code) => code === 0, () => new InstallFailed({ pm })), Effect.asVoid) };
352
+ ], cwd)), Effect.filterOrFail((result) => result.exitCode === 0, (result) => new InstallFailed({
353
+ pm,
354
+ output: [result.stdout, result.stderr].filter(Boolean).join("\n")
355
+ })), Effect.asVoid) };
352
356
  }) }) {};
353
357
  //#endregion
354
358
  //#region src/utils/detect-pm.ts
@@ -424,6 +428,32 @@ const SETUP_FILE_CANDIDATES = {
424
428
  "vite.config.mjs"
425
429
  ]
426
430
  };
431
+ /** Config file name a tool gets scaffolded with, matching the project's module system. */
432
+ function configFileName(tool, esm) {
433
+ return `${tool}.config.${esm ? "ts" : "mts"}`;
434
+ }
435
+ /**
436
+ * Extensions oxlint/oxfmt resolve on their own. `.mts`/`.cts` are absent
437
+ * deliberately: the tools only auto-discover the plain `.ts` variant, so a
438
+ * config using one of those has to be named explicitly on the command line.
439
+ */
440
+ const AUTO_DISCOVERED = /* @__PURE__ */ new Set([
441
+ "js",
442
+ "cjs",
443
+ "mjs",
444
+ "ts",
445
+ "json"
446
+ ]);
447
+ /**
448
+ * `-c <config>` argument naming the config file, or an empty string when the
449
+ * tool finds the file by itself. `configFile` is the config that will actually
450
+ * be on disk - the one chanom is about to write, or an existing one it detected
451
+ * and skipped - so the generated scripts never point at a path that isn't there.
452
+ */
453
+ function configFlag(configFile) {
454
+ const extension = configFile.slice(configFile.lastIndexOf(".") + 1);
455
+ return AUTO_DISCOVERED.has(extension) ? "" : ` -c ${configFile}`;
456
+ }
427
457
  //#endregion
428
458
  //#region src/utils/detect-setup.ts
429
459
  /** Finds an existing config file for the given tool, if any. */
@@ -523,7 +553,7 @@ function getPackages$3(pkg, versions) {
523
553
  }
524
554
  function configFile$2(esm) {
525
555
  return {
526
- fileName: `knip.config.${esm ? "ts" : "mts"}`,
556
+ fileName: configFileName("knip", esm),
527
557
  contents: `export { default } from '@chanom/dev-config/knip/config';\n`
528
558
  };
529
559
  }
@@ -555,22 +585,42 @@ const apply$5 = Effect.fn("add-knip.apply")(function* (cwd, esm, pkg) {
555
585
  function getPackages$2(pkg) {
556
586
  return isPackageInstalled(pkg, "lint-staged") ? [] : ["lint-staged"];
557
587
  }
558
- function buildConfig(linters, formatters) {
588
+ function buildConfig(linters, formatters, configFiles) {
559
589
  const config = {};
560
590
  const jsGlob = "**/*.{js,jsx,ts,tsx,mjs,cjs,mts,cts}";
561
591
  if (linters.includes("oxlint")) {
562
592
  config[jsGlob] ??= [];
563
- config[jsGlob].push("oxlint --fix --no-error-on-unmatched-pattern");
593
+ config[jsGlob].push(`oxlint --fix${configFlag(configFiles.oxlint)} --no-error-on-unmatched-pattern`);
564
594
  }
565
595
  if (formatters.includes("oxfmt")) {
566
596
  config["*"] ??= [];
567
- config["*"].push("oxfmt --no-error-on-unmatched-pattern");
597
+ config["*"].push(`oxfmt${configFlag(configFiles.oxfmt)} --no-error-on-unmatched-pattern`);
568
598
  }
569
599
  return config;
570
600
  }
571
601
  //#endregion
572
602
  //#region src/commands/add-lint-staged/index.ts
573
- const apply$4 = Effect.fn("add-lint-staged.apply")(function* (cwd, linters = [], formatters = [], overwritePreCommit = false) {
603
+ /**
604
+ * Config file each tool will run against: an existing one when `add-oxlint` /
605
+ * `add-oxfmt` skipped writing, otherwise the name they scaffold. lint-staged
606
+ * invokes the same binaries as the package scripts, so it has to resolve the
607
+ * config the same way.
608
+ */
609
+ const resolveConfigFiles = Effect.fn("add-lint-staged.resolveConfigFiles")(function* (cwd, esm) {
610
+ const path = yield* Path.Path;
611
+ const resolve = Effect.fn(function* (tool) {
612
+ const existing = yield* detectSetupFile(tool, cwd);
613
+ return Option.match(existing, {
614
+ onNone: () => configFileName(tool, esm),
615
+ onSome: (file) => path.basename(file)
616
+ });
617
+ });
618
+ return {
619
+ oxlint: yield* resolve("oxlint"),
620
+ oxfmt: yield* resolve("oxfmt")
621
+ };
622
+ });
623
+ const apply$4 = Effect.fn("add-lint-staged.apply")(function* (cwd, esm, linters = [], formatters = [], overwritePreCommit = false) {
574
624
  if (linters.length === 0 && formatters.length === 0) {
575
625
  yield* Effect.logDebug("no linters or formatters selected - skipping lint-staged setup");
576
626
  return;
@@ -580,7 +630,10 @@ const apply$4 = Effect.fn("add-lint-staged.apply")(function* (cwd, linters = [],
580
630
  const prompter = yield* Prompter;
581
631
  const existing = yield* detectSetupFile("lint-staged", cwd);
582
632
  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");
633
+ else {
634
+ const configFiles = yield* resolveConfigFiles(cwd, esm);
635
+ yield* fs.writeFileString(path.join(cwd, ".lintstagedrc.json"), JSON.stringify(buildConfig(linters, formatters, configFiles), null, 2) + "\n");
636
+ }
584
637
  const huskyDir = path.join(cwd, ".husky");
585
638
  if (yield* fs.exists(huskyDir)) {
586
639
  const hookPath = path.join(huskyDir, "pre-commit");
@@ -595,14 +648,15 @@ function getPackages$1(pkg, versions) {
595
648
  }
596
649
  function configFile$1(esm) {
597
650
  return {
598
- fileName: `oxfmt.config.${esm ? "ts" : "mts"}`,
651
+ fileName: configFileName("oxfmt", esm),
599
652
  contents: `export { default } from '@chanom/dev-config/oxfmt/config';\n`
600
653
  };
601
654
  }
602
- function getScriptPlan$1(scripts) {
655
+ function getScriptPlan$1(scripts, configFile) {
656
+ const config = configFlag(configFile);
603
657
  return planScripts(scripts, {
604
- format: "oxfmt",
605
- "format:check": "oxfmt --check"
658
+ format: `oxfmt${config}`,
659
+ "format:check": `oxfmt --check${config}`
606
660
  });
607
661
  }
608
662
  //#endregion
@@ -613,12 +667,16 @@ const apply$3 = Effect.fn("add-oxfmt.apply")(function* (cwd, esm, pkg) {
613
667
  const path = yield* Path.Path;
614
668
  const prompter = yield* Prompter;
615
669
  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 {
670
+ let configFileName;
671
+ if (Option.isSome(existing)) {
672
+ configFileName = path.basename(existing.value);
673
+ yield* prompter.warn(`\`${configFileName}\` already exists - skipping oxfmt config`);
674
+ } else {
618
675
  const config = configFile$1(esm);
676
+ configFileName = config.fileName;
619
677
  yield* fs.writeFileString(path.join(cwd, config.fileName), config.contents);
620
678
  }
621
- const plan = getScriptPlan$1(pkg.scripts);
679
+ const plan = getScriptPlan$1(pkg.scripts, configFileName);
622
680
  for (const key of plan.skipped) yield* prompter.warn(`\`${key}\` script already exists in package.json - skipping`);
623
681
  return {
624
682
  ...pkg,
@@ -636,14 +694,15 @@ function getPackages(pkg, versions) {
636
694
  }
637
695
  function configFile(esm) {
638
696
  return {
639
- fileName: `oxlint.config.${esm ? "ts" : "mts"}`,
697
+ fileName: configFileName("oxlint", esm),
640
698
  contents: `export { default } from '@chanom/dev-config/oxlint/config';\n`
641
699
  };
642
700
  }
643
- function getScriptPlan(scripts) {
701
+ function getScriptPlan(scripts, configFile) {
702
+ const config = configFlag(configFile);
644
703
  return planScripts(scripts, {
645
- lint: "oxlint",
646
- "lint:fix": "oxlint --fix"
704
+ lint: `oxlint${config}`,
705
+ "lint:fix": `oxlint --fix${config}`
647
706
  });
648
707
  }
649
708
  //#endregion
@@ -654,12 +713,16 @@ const apply$2 = Effect.fn("add-oxlint.apply")(function* (cwd, esm, pkg) {
654
713
  const path = yield* Path.Path;
655
714
  const prompter = yield* Prompter;
656
715
  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 {
716
+ let configFileName;
717
+ if (Option.isSome(existing)) {
718
+ configFileName = path.basename(existing.value);
719
+ yield* prompter.warn(`\`${configFileName}\` already exists - skipping oxlint config`);
720
+ } else {
659
721
  const config = configFile(esm);
722
+ configFileName = config.fileName;
660
723
  yield* fs.writeFileString(path.join(cwd, config.fileName), config.contents);
661
724
  }
662
- const plan = getScriptPlan(pkg.scripts);
725
+ const plan = getScriptPlan(pkg.scripts, configFileName);
663
726
  for (const key of plan.skipped) yield* prompter.warn(`\`${key}\` script already exists in package.json - skipping`);
664
727
  return {
665
728
  ...pkg,
@@ -810,7 +873,7 @@ function planPackages(pkg, toppings, sweetness, versions) {
810
873
  * value for a config the user may add later.
811
874
  */
812
875
  function oxfmtConfigPath(esm) {
813
- return `oxfmt.config.${esm ? "ts" : "mts"}`;
876
+ return configFileName("oxfmt", esm);
814
877
  }
815
878
  /** True when at least one topping gives lint-staged something to run. */
816
879
  function wantsLintStaged(toppings) {
@@ -953,14 +1016,14 @@ const ensureRepoRootForHooks = Effect.fn("brew.ensureRepoRootForHooks")(function
953
1016
  root: Option.getOrElse(root, () => cwd)
954
1017
  });
955
1018
  });
956
- const setupCommitGate = Effect.fn("brew.setupCommitGate")(function* (cwd, pm, toppings, sweetness) {
1019
+ const setupCommitGate = Effect.fn("brew.setupCommitGate")(function* (cwd, pm, esm, toppings, sweetness) {
957
1020
  if (sweetness !== "medium") return;
958
1021
  const fs = yield* FileSystem.FileSystem;
959
1022
  const path = yield* Path.Path;
960
1023
  const prompter = yield* Prompter;
961
1024
  const huskyExisted = yield* fs.exists(path.join(cwd, ".husky"));
962
1025
  yield* apply$6(cwd, pm);
963
- if (wantsLintStaged(toppings)) yield* apply$4(cwd, selectedLinters(toppings), selectedFormatters(toppings), !huskyExisted);
1026
+ if (wantsLintStaged(toppings)) yield* apply$4(cwd, esm, selectedLinters(toppings), selectedFormatters(toppings), !huskyExisted);
964
1027
  else {
965
1028
  yield* prompter.warn("No linters or formatters selected - skipping lint-staged");
966
1029
  const seededHook = path.join(cwd, ".husky", "pre-commit");
@@ -1003,7 +1066,7 @@ const brew = (cwd = process.cwd()) => Effect.gen(function* () {
1003
1066
  yield* installPackages(pm, cwd, packages);
1004
1067
  const { pkg: installedPkg } = yield* readPkg(cwd);
1005
1068
  yield* persistScripts(pkgPath, installedPkg, yield* applyToppings(cwd, isEsm(installedPkg), installedPkg, toppings));
1006
- yield* setupCommitGate(cwd, pm, toppings, sweetness);
1069
+ yield* setupCommitGate(cwd, pm, isEsm(installedPkg), toppings, sweetness);
1007
1070
  yield* commitChanges(cwd);
1008
1071
  yield* prompter.outro(pc.green(`Your Chanom with ${toppings.join(", ")} toppings and ${sweetness} sweetness is ready! Enjoy! 🧋`));
1009
1072
  });
@@ -1042,7 +1105,7 @@ const run = (name, cwd = process.cwd(), options = {}) => {
1042
1105
  UnknownCommand: (e) => reportError(`Unknown command: ${e.command ?? "(none)"}\nAvailable commands: ${Object.keys(commands).join(", ")}`),
1043
1106
  PkgNotFound: (e) => reportError(`No package.json found in ${e.cwd}. Run this inside a project.`),
1044
1107
  PkgInvalid: (e) => reportError(`Could not parse ${e.pkgPath}. Is it valid JSON?`),
1045
- InstallFailed: (e) => reportError(`Package installation with ${e.pm} failed.`),
1108
+ InstallFailed: (e) => reportError([`Package installation with ${e.pm} failed.`, e.output].filter(Boolean).join("\n")),
1046
1109
  HuskyInitFailed: (e) => reportError(`\`husky init\` failed (exit code ${e.exitCode}).`),
1047
1110
  NotAtRepoRoot: (e) => reportError(`Git hooks must be set up at the repository root: ${e.root}\nRun \`chanom brew\` there, or choose light sweetness.`)
1048
1111
  }))).pipe(Logger.withMinimumLogLevel(debug ? LogLevel.Debug : LogLevel.Info));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chanom/cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "repository": {
@@ -32,7 +32,7 @@
32
32
  "tsdown": "0.22.3",
33
33
  "typescript": "6.0.3",
34
34
  "vitest": "3.2.6",
35
- "@chanom/dev-config": "0.0.3",
35
+ "@chanom/dev-config": "0.0.4",
36
36
  "@chanom/internal": "0.0.0"
37
37
  },
38
38
  "scripts": {
@@ -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
  }