@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.
- package/README.md +62 -0
- package/dist/index.js +129 -33
- 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
|
-
|
|
129
|
-
if (
|
|
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`,
|
|
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
|
-
|
|
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
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
config
|
|
185
|
-
|
|
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 (
|
|
188
|
-
|
|
189
|
-
|
|
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`,
|
|
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
|
-
|
|
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`,
|
|
313
|
+
return isPackageInstalled(pkg, "oxlint") ? [] : [`oxlint@1.71.0`, `@chanom/dev-config@0.0.3`];
|
|
229
314
|
}
|
|
230
315
|
function apply(cwd, esm) {
|
|
231
|
-
|
|
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.
|
|
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"
|