@chanom/cli 0.0.2 → 0.0.3

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 +62 -0
  2. package/dist/index.js +129 -33
  3. package/package.json +2 -2
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # @chanom/cli
2
+
3
+ Interactive CLI that brews dev tooling config into your project - oxlint, oxfmt, knip, and commit hooks, picked with a couple of prompts.
4
+
5
+ ## Install
6
+
7
+ Run it directly, no install needed:
8
+
9
+ ```sh
10
+ npx @chanom/cli brew
11
+ # or
12
+ pnpm dlx @chanom/cli brew
13
+ ```
14
+
15
+ or if you want to install it within your project:
16
+
17
+ ```sh
18
+ pnpm add -D @chanom/cli
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```sh
24
+ chanom brew
25
+ ```
26
+
27
+ `brew` walks you through two prompts:
28
+
29
+ 1. **Toppings** - which tools to add, any combination of:
30
+ - `oxlint` - fast Rust-based linter
31
+ - `oxfmt` - fast Rust-based formatter
32
+ - `knip` - dead code remover
33
+ 2. **Sweetness** - how strict to be:
34
+ - `light` - warn only, no blocking
35
+ - `medium` - block on commit via husky + lint-staged + commitlint
36
+
37
+ Based on your answers, `brew` will:
38
+
39
+ - Initialize a git repo if one doesn't exist yet (with a `.gitignore`)
40
+ - Detect your package manager (`pnpm`, `npm`, `yarn`, or `bun`) from `package.json`/`packageManager` or the invoking user agent
41
+ - Install the packages needed for your selected toppings, skipping any already installed
42
+ - Write config files (`oxlint.config.ts`, `oxfmt.config.ts`, `knip.config.ts`, `.commitlintrc.json`, `.lintstagedrc.json`) and wire up `package.json` scripts
43
+ - If `medium` sweetness is selected, run `husky init` and add `pre-commit` / `commit-msg` hooks
44
+ - Stage and commit the changes it made
45
+
46
+ ## What gets added
47
+
48
+ | Topping | Config file | Scripts added |
49
+ | -------- | ------------------ | ------------------------ |
50
+ | `oxlint` | `oxlint.config.ts` | `lint`, `lint:fix` |
51
+ | `oxfmt` | `oxfmt.config.ts` | `format`, `format:check` |
52
+ | `knip` | `knip.config.ts` | `knip` |
53
+
54
+ Config files re-export presets from [`@chanom/dev-config`](../dev-config).
55
+
56
+ `medium` sweetness additionally adds:
57
+
58
+ - `husky` - installed via `husky init`
59
+ - `lint-staged` - `.lintstagedrc.json` running the selected linter/formatter on staged files
60
+ - `commitlint` - `.commitlintrc.json` extending `@commitlint/config-conventional`, enforced in the `commit-msg` hook
61
+
62
+ Existing scripts and config are never overwritten - if a script name already exists in `package.json`, `brew` logs a warning and leaves it alone.
package/dist/index.js CHANGED
@@ -1,10 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import { intro, log, multiselect, outro, select, spinner } from "@clack/prompts";
3
- import pc from "picocolors";
4
3
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
5
- import { join } from "node:path";
4
+ import { basename, join } from "node:path";
5
+ import pc from "picocolors";
6
6
  import { spawnSync } from "node:child_process";
7
7
  //#region src/utils/detect-pm.ts
8
+ /** How to run a locally installed binary with each package manager. */
9
+ const PM_EXEC = {
10
+ pnpm: ["pnpm", "exec"],
11
+ npm: [
12
+ "npx",
13
+ "--no",
14
+ "--"
15
+ ],
16
+ yarn: ["yarn"],
17
+ bun: ["bunx"]
18
+ };
8
19
  const KNOWN_PMS = [
9
20
  "pnpm",
10
21
  "yarn",
@@ -92,14 +103,14 @@ function writeGitignore(cwd, contents = "node_modules\n") {
92
103
  //#endregion
93
104
  //#region src/utils/install.ts
94
105
  function installDev(pm, cwd, ...packages) {
95
- spawnSync(pm, [
106
+ return spawnSync(pm, [
96
107
  "add",
97
108
  "-D",
98
109
  ...packages
99
110
  ], {
100
111
  stdio: "inherit",
101
112
  cwd
102
- });
113
+ }).status === 0;
103
114
  }
104
115
  //#endregion
105
116
  //#region src/utils/pkg.ts
@@ -108,6 +119,7 @@ function isEsm(pkg) {
108
119
  }
109
120
  function readPkg(cwd) {
110
121
  const pkgPath = join(cwd, "package.json");
122
+ if (!existsSync(pkgPath)) throw new Error(`No package.json found in ${cwd}`);
111
123
  return {
112
124
  pkg: JSON.parse(readFileSync(pkgPath, "utf-8")),
113
125
  pkgPath
@@ -117,6 +129,58 @@ function isPackageInstalled(pkg, name) {
117
129
  return name in (pkg.dependencies ?? {}) || name in (pkg.devDependencies ?? {});
118
130
  }
119
131
  //#endregion
132
+ //#region src/utils/detect-setup.ts
133
+ const CONFIG_FILES = {
134
+ oxlint: [
135
+ "oxlint.config.js",
136
+ "oxlint.config.cjs",
137
+ "oxlint.config.mjs",
138
+ "oxlint.config.ts",
139
+ "oxlint.config.cts",
140
+ "oxlint.config.mts",
141
+ ".oxlintrc.json"
142
+ ],
143
+ oxfmt: [
144
+ "oxfmt.config.js",
145
+ "oxfmt.config.cjs",
146
+ "oxfmt.config.mjs",
147
+ "oxfmt.config.ts",
148
+ "oxfmt.config.cts",
149
+ "oxfmt.config.mts",
150
+ ".oxfmtrc.json"
151
+ ],
152
+ knip: [
153
+ "knip.json",
154
+ "knip.ts",
155
+ "knip.config.ts",
156
+ "knip.config.mts",
157
+ ".knip.json"
158
+ ],
159
+ husky: [".husky"],
160
+ "lint-staged": [
161
+ ".lintstagedrc.json",
162
+ ".lintstagedrc.js",
163
+ "lint-staged.config.js"
164
+ ],
165
+ commitlint: [
166
+ ".commitlintrc.json",
167
+ ".commitlintrc.js",
168
+ "commitlint.config.js"
169
+ ],
170
+ vite: [
171
+ "vite.config.ts",
172
+ "vite.config.js",
173
+ "vite.config.mjs"
174
+ ]
175
+ };
176
+ function detectSetupFile(type, cwd = process.cwd()) {
177
+ for (const filename of CONFIG_FILES[type]) {
178
+ const filepath = join(cwd, filename);
179
+ if (existsSync(filepath)) return filepath;
180
+ }
181
+ return null;
182
+ }
183
+ //#endregion
120
184
  //#region src/commands/add-commitlint/index.ts
121
185
  function getPackages$5(pkg) {
122
186
  const missing = [];
@@ -124,22 +188,31 @@ function getPackages$5(pkg) {
124
188
  if (!isPackageInstalled(pkg, "@commitlint/config-conventional")) missing.push("@commitlint/config-conventional");
125
189
  return missing;
126
190
  }
127
- function apply$5(cwd) {
128
- writeFileSync(join(cwd, ".commitlintrc.json"), JSON.stringify({ extends: ["@commitlint/config-conventional"] }, null, 2) + "\n");
129
- if (existsSync(join(cwd, ".husky"))) writeFileSync(join(cwd, ".husky", "commit-msg"), "npx --no -- commitlint --edit $1\n");
191
+ function apply$5(cwd, pm) {
192
+ const existing = detectSetupFile("commitlint", cwd);
193
+ if (existing) log.warn(`\`${basename(existing)}\` already exists - skipping commitlint config`);
194
+ else writeFileSync(join(cwd, ".commitlintrc.json"), JSON.stringify({ extends: ["@commitlint/config-conventional"] }, null, 2) + "\n");
195
+ if (existsSync(join(cwd, ".husky"))) {
196
+ const hookPath = join(cwd, ".husky", "commit-msg");
197
+ if (existsSync(hookPath)) log.warn("`.husky/commit-msg` already exists - skipping");
198
+ else writeFileSync(hookPath, [
199
+ ...PM_EXEC[pm],
200
+ "commitlint",
201
+ "--edit",
202
+ "$1"
203
+ ].join(" ") + "\n");
204
+ }
130
205
  }
131
206
  //#endregion
132
207
  //#region src/commands/add-husky/index.ts
133
- const PM_EXEC = {
134
- pnpm: ["pnpm", "exec"],
135
- npm: ["npx"],
136
- yarn: ["yarn"],
137
- bun: ["bunx"]
138
- };
139
208
  function getPackages$4(pkg) {
140
209
  return isPackageInstalled(pkg, "husky") ? [] : ["husky"];
141
210
  }
142
211
  function apply$4(cwd, pm) {
212
+ if (existsSync(join(cwd, ".husky"))) {
213
+ log.warn("`.husky` already exists - skipping husky init");
214
+ return;
215
+ }
143
216
  const [cmd, ...args] = PM_EXEC[pm];
144
217
  spawnSync(cmd, [
145
218
  ...args,
@@ -160,10 +233,12 @@ function getMissingScripts$2(scripts) {
160
233
  //#endregion
161
234
  //#region src/commands/add-knip/index.ts
162
235
  function getPackages$3(pkg) {
163
- return isPackageInstalled(pkg, "knip") ? [] : [`knip@6.23.0`, "@chanom/dev-config"];
236
+ return isPackageInstalled(pkg, "knip") ? [] : [`knip@6.23.0`, `@chanom/dev-config@0.0.3`];
164
237
  }
165
238
  function apply$3(cwd, esm) {
166
- writeFileSync(join(cwd, `knip.config.${esm ? "ts" : "mts"}`), `export { default } from '@chanom/dev-config/knip/config';\n`);
239
+ const existing = detectSetupFile("knip", cwd);
240
+ if (existing) log.warn(`\`${basename(existing)}\` already exists - skipping knip config`);
241
+ else writeFileSync(join(cwd, `knip.config.${esm ? "ts" : "mts"}`), `export { default } from '@chanom/dev-config/knip/config';\n`);
167
242
  const { pkg, pkgPath } = readPkg(cwd);
168
243
  pkg.scripts ??= {};
169
244
  const missing = getMissingScripts$2(pkg.scripts);
@@ -177,19 +252,27 @@ function apply$3(cwd, esm) {
177
252
  function getPackages$2(pkg) {
178
253
  return isPackageInstalled(pkg, "lint-staged") ? [] : ["lint-staged"];
179
254
  }
180
- function apply$2(cwd, linters = [], formatters = []) {
181
- const config = {};
182
- const jsGlob = "**/*.{js,jsx,ts,tsx,mjs,cjs,mts,cts}";
183
- if (linters.includes("oxlint")) {
184
- config[jsGlob] ??= [];
185
- config[jsGlob].push("oxlint --fix");
255
+ function apply$2(cwd, linters = [], formatters = [], overwritePreCommit = false) {
256
+ const existing = detectSetupFile("lint-staged", cwd);
257
+ if (existing) log.warn(`\`${basename(existing)}\` already exists - skipping lint-staged config`);
258
+ else {
259
+ const config = {};
260
+ const jsGlob = "**/*.{js,jsx,ts,tsx,mjs,cjs,mts,cts}";
261
+ if (linters.includes("oxlint")) {
262
+ config[jsGlob] ??= [];
263
+ config[jsGlob].push("oxlint --fix");
264
+ }
265
+ if (formatters.includes("oxfmt")) {
266
+ config["*"] ??= [];
267
+ config["*"].push("oxfmt");
268
+ }
269
+ writeFileSync(join(cwd, ".lintstagedrc.json"), JSON.stringify(config, null, 2) + "\n");
186
270
  }
187
- if (formatters.includes("oxfmt")) {
188
- config["*"] ??= [];
189
- config["*"].push("oxfmt");
271
+ if (existsSync(join(cwd, ".husky"))) {
272
+ const hookPath = join(cwd, ".husky", "pre-commit");
273
+ if (existsSync(hookPath) && !overwritePreCommit) log.warn("`.husky/pre-commit` already exists - skipping");
274
+ else writeFileSync(hookPath, "lint-staged\n");
190
275
  }
191
- writeFileSync(join(cwd, ".lintstagedrc.json"), JSON.stringify(config, null, 2) + "\n");
192
- if (existsSync(join(cwd, ".husky"))) writeFileSync(join(cwd, ".husky", "pre-commit"), "lint-staged\n");
193
276
  }
194
277
  //#endregion
195
278
  //#region src/commands/add-oxfmt/logic.ts
@@ -202,10 +285,12 @@ function getMissingScripts$1(scripts) {
202
285
  //#endregion
203
286
  //#region src/commands/add-oxfmt/index.ts
204
287
  function getPackages$1(pkg) {
205
- return isPackageInstalled(pkg, "oxfmt") ? [] : [`oxfmt@0.56.0`, "@chanom/dev-config"];
288
+ return isPackageInstalled(pkg, "oxfmt") ? [] : [`oxfmt@0.56.0`, `@chanom/dev-config@0.0.3`];
206
289
  }
207
290
  function apply$1(cwd, esm) {
208
- writeFileSync(join(cwd, `oxfmt.config.${esm ? "ts" : "mts"}`), `export { default } from '@chanom/dev-config/oxfmt/config';\n`);
291
+ const existing = detectSetupFile("oxfmt", cwd);
292
+ if (existing) log.warn(`\`${basename(existing)}\` already exists - skipping oxfmt config`);
293
+ else writeFileSync(join(cwd, `oxfmt.config.${esm ? "ts" : "mts"}`), `export { default } from '@chanom/dev-config/oxfmt/config';\n`);
209
294
  const { pkg, pkgPath } = readPkg(cwd);
210
295
  pkg.scripts ??= {};
211
296
  const missing = getMissingScripts$1(pkg.scripts);
@@ -225,10 +310,12 @@ function getMissingScripts(scripts) {
225
310
  //#endregion
226
311
  //#region src/commands/add-oxlint/index.ts
227
312
  function getPackages(pkg) {
228
- return isPackageInstalled(pkg, "oxlint") ? [] : [`oxlint@1.71.0`, "@chanom/dev-config"];
313
+ return isPackageInstalled(pkg, "oxlint") ? [] : [`oxlint@1.71.0`, `@chanom/dev-config@0.0.3`];
229
314
  }
230
315
  function apply(cwd, esm) {
231
- writeFileSync(join(cwd, `oxlint.config.${esm ? "ts" : "mts"}`), `export { default } from '@chanom/dev-config/oxlint/config';\n`);
316
+ const existing = detectSetupFile("oxlint", cwd);
317
+ if (existing) log.warn(`\`${basename(existing)}\` already exists - skipping oxlint config`);
318
+ else writeFileSync(join(cwd, `oxlint.config.${esm ? "ts" : "mts"}`), `export { default } from '@chanom/dev-config/oxlint/config';\n`);
232
319
  const { pkg, pkgPath } = readPkg(cwd);
233
320
  pkg.scripts ??= {};
234
321
  const missing = getMissingScripts(pkg.scripts);
@@ -249,6 +336,10 @@ async function brew(cwd = process.cwd()) {
249
336
  s.stop("Git initialized");
250
337
  }
251
338
  const pm = detectPm(cwd);
339
+ if (!existsSync(join(cwd, "package.json"))) {
340
+ log.error(`No package.json found. Run \`${pm} init\` first, then re-run \`chanom brew\`.`);
341
+ process.exit(1);
342
+ }
252
343
  const toppings = await multiselect({
253
344
  message: "Which toppings would you like?",
254
345
  options: [
@@ -295,16 +386,21 @@ async function brew(cwd = process.cwd()) {
295
386
  const s = spinner();
296
387
  const uniquePackages = Array.from(new Set(packages));
297
388
  s.start(`Installing ${uniquePackages.join(", ")}...`);
298
- installDev(pm, cwd, ...uniquePackages);
389
+ if (!installDev(pm, cwd, ...uniquePackages)) {
390
+ s.stop(pc.red("Package installation failed"));
391
+ log.error(`\`${pm} add -D\` failed. Fix the error above and re-run \`chanom brew\`.`);
392
+ process.exit(1);
393
+ }
299
394
  s.stop("Packages installed");
300
395
  }
301
396
  if (toppings.includes("oxlint")) apply(cwd, esm);
302
397
  if (toppings.includes("oxfmt")) apply$1(cwd, esm);
303
398
  if (toppings.includes("knip")) apply$3(cwd, esm);
304
399
  if (sweetness === "medium") {
400
+ const huskyExisted = existsSync(join(cwd, ".husky"));
305
401
  apply$4(cwd, pm);
306
- apply$2(cwd, toppings.filter((t) => ["oxlint"].includes(t)), toppings.filter((t) => ["oxfmt"].includes(t)));
307
- apply$5(cwd);
402
+ apply$2(cwd, toppings.filter((t) => ["oxlint"].includes(t)), toppings.filter((t) => ["oxfmt"].includes(t)), !huskyExisted);
403
+ apply$5(cwd, pm);
308
404
  }
309
405
  const s2 = spinner();
310
406
  s2.start("Committing changes...");
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@chanom/cli",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "",
5
5
  "keywords": [],
6
- "type": "module",
7
6
  "bin": {
8
7
  "chanom": "./dist/index.js"
9
8
  },
10
9
  "files": [
11
10
  "dist"
12
11
  ],
12
+ "type": "module",
13
13
  "dependencies": {
14
14
  "@clack/prompts": "1.6.0",
15
15
  "picocolors": "1.1.1"