@chanom/cli 0.0.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 (2) hide show
  1. package/dist/index.js +383 -0
  2. package/package.json +29 -0
package/dist/index.js ADDED
@@ -0,0 +1,383 @@
1
+ #!/usr/bin/env node
2
+ import { intro, log, multiselect, outro, select, spinner } from "@clack/prompts";
3
+ import pc from "picocolors";
4
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { spawnSync } from "node:child_process";
7
+ //#region src/utils/detect-pm.ts
8
+ const KNOWN_PMS = [
9
+ "pnpm",
10
+ "yarn",
11
+ "bun",
12
+ "npm"
13
+ ];
14
+ function fromPkgJson(cwd) {
15
+ const pkgPath = join(cwd, "package.json");
16
+ if (!existsSync(pkgPath)) return void 0;
17
+ try {
18
+ const { packageManager } = JSON.parse(readFileSync(pkgPath, "utf-8"));
19
+ if (!packageManager) return void 0;
20
+ const name = packageManager.split("@")[0];
21
+ return KNOWN_PMS.includes(name) ? name : void 0;
22
+ } catch {
23
+ return;
24
+ }
25
+ }
26
+ function fromUserAgent() {
27
+ const agent = process.env["npm_config_user_agent"];
28
+ if (!agent) return void 0;
29
+ return KNOWN_PMS.find((pm) => agent.startsWith(pm));
30
+ }
31
+ function detectPm(cwd = process.cwd(), defaultPm = "pnpm") {
32
+ return fromPkgJson(cwd) ?? fromUserAgent() ?? defaultPm;
33
+ }
34
+ //#endregion
35
+ //#region src/utils/git.ts
36
+ function run(args, cwd) {
37
+ const { status, stdout, stderr } = spawnSync("git", args, {
38
+ stdio: "pipe",
39
+ cwd,
40
+ encoding: "utf-8"
41
+ });
42
+ return {
43
+ ok: status === 0,
44
+ stdout: (stdout ?? "").trim(),
45
+ stderr: (stderr ?? "").trim()
46
+ };
47
+ }
48
+ function isGitRepo(cwd = process.cwd()) {
49
+ return existsSync(join(cwd, ".git"));
50
+ }
51
+ function init(cwd) {
52
+ return run(["init"], cwd);
53
+ }
54
+ /** True if git has a usable author identity (committer.name + committer.email). */
55
+ function hasIdentity(cwd) {
56
+ return run(["var", "GIT_COMMITTER_IDENT"], cwd).ok;
57
+ }
58
+ /** Set a repo-local identity so commits succeed on a fresh repo without global config. */
59
+ function setLocalIdentity(cwd, name, email) {
60
+ run([
61
+ "config",
62
+ "user.name",
63
+ name
64
+ ], cwd);
65
+ run([
66
+ "config",
67
+ "user.email",
68
+ email
69
+ ], cwd);
70
+ }
71
+ function stageAll(cwd) {
72
+ return run(["add", "."], cwd);
73
+ }
74
+ /** True if there is anything staged to commit. */
75
+ function hasStagedChanges(cwd) {
76
+ return !run([
77
+ "diff",
78
+ "--cached",
79
+ "--quiet"
80
+ ], cwd).ok;
81
+ }
82
+ function commit(cwd, message) {
83
+ return run([
84
+ "commit",
85
+ "-m",
86
+ message
87
+ ], cwd);
88
+ }
89
+ function writeGitignore(cwd, contents = "node_modules\n") {
90
+ writeFileSync(join(cwd, ".gitignore"), contents, "utf-8");
91
+ }
92
+ //#endregion
93
+ //#region src/utils/install.ts
94
+ function installDev(pm, cwd, ...packages) {
95
+ spawnSync(pm, [
96
+ "add",
97
+ "-D",
98
+ ...packages
99
+ ], {
100
+ stdio: "inherit",
101
+ cwd
102
+ });
103
+ }
104
+ //#endregion
105
+ //#region src/utils/pkg.ts
106
+ function isEsm(pkg) {
107
+ return pkg.type === "module";
108
+ }
109
+ function readPkg(cwd) {
110
+ const pkgPath = join(cwd, "package.json");
111
+ return {
112
+ pkg: JSON.parse(readFileSync(pkgPath, "utf-8")),
113
+ pkgPath
114
+ };
115
+ }
116
+ function isPackageInstalled(pkg, name) {
117
+ return name in (pkg.dependencies ?? {}) || name in (pkg.devDependencies ?? {});
118
+ }
119
+ //#endregion
120
+ //#region src/commands/add-commitlint/index.ts
121
+ function getPackages$7(pkg) {
122
+ const missing = [];
123
+ if (!isPackageInstalled(pkg, "@commitlint/cli")) missing.push("@commitlint/cli");
124
+ if (!isPackageInstalled(pkg, "@commitlint/config-conventional")) missing.push("@commitlint/config-conventional");
125
+ return missing;
126
+ }
127
+ function apply$7(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");
130
+ }
131
+ //#endregion
132
+ //#region src/commands/add-eslint/index.ts
133
+ function getPackages$6(pkg) {
134
+ return isPackageInstalled(pkg, "eslint") ? [] : ["eslint"];
135
+ }
136
+ function apply$6(cwd, esm) {
137
+ writeFileSync(join(cwd, `eslint.config.${esm ? "ts" : "mts"}`), `export { default } from '@chanom/dev-config/eslint/config';\n`);
138
+ const { pkg, pkgPath } = readPkg(cwd);
139
+ pkg.scripts ??= {};
140
+ const missing = {};
141
+ if (!pkg.scripts["lint"]) missing["lint"] = "eslint .";
142
+ if (!pkg.scripts["lint:fix"]) missing["lint:fix"] = "eslint . --fix";
143
+ for (const [key, value] of Object.entries(missing)) pkg.scripts[key] = value;
144
+ const skipped = ["lint", "lint:fix"].filter((k) => !(k in missing));
145
+ for (const key of skipped) log.warn(`\`${key}\` script already exists in package.json - skipping`);
146
+ if (Object.keys(missing).length > 0) writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
147
+ }
148
+ //#endregion
149
+ //#region src/commands/add-husky/index.ts
150
+ const PM_EXEC = {
151
+ pnpm: ["pnpm", "exec"],
152
+ npm: ["npx"],
153
+ yarn: ["yarn"],
154
+ bun: ["bunx"]
155
+ };
156
+ function getPackages$5(pkg) {
157
+ return isPackageInstalled(pkg, "husky") ? [] : ["husky"];
158
+ }
159
+ function apply$5(cwd, pm) {
160
+ const [cmd, ...args] = PM_EXEC[pm];
161
+ spawnSync(cmd, [
162
+ ...args,
163
+ "husky",
164
+ "init"
165
+ ], {
166
+ stdio: "inherit",
167
+ cwd
168
+ });
169
+ }
170
+ //#endregion
171
+ //#region src/commands/add-knip/index.ts
172
+ function getPackages$4(pkg) {
173
+ return isPackageInstalled(pkg, "knip") ? [] : ["knip"];
174
+ }
175
+ function apply$4(cwd) {
176
+ writeFileSync(join(cwd, "knip.json"), JSON.stringify({ entry: ["src/index.ts"] }, null, 2) + "\n");
177
+ const { pkg, pkgPath } = readPkg(cwd);
178
+ pkg.scripts ??= {};
179
+ if (pkg.scripts["knip"]) log.warn("`knip` script already exists in package.json - skipping");
180
+ else {
181
+ pkg.scripts["knip"] = "knip";
182
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
183
+ }
184
+ }
185
+ //#endregion
186
+ //#region src/commands/add-lint-staged/index.ts
187
+ function getPackages$3(pkg) {
188
+ return isPackageInstalled(pkg, "lint-staged") ? [] : ["lint-staged"];
189
+ }
190
+ function apply$3(cwd, linters = [], formatters = []) {
191
+ const config = {};
192
+ const jsGlob = "**/*.{js,jsx,ts,tsx,mjs,cjs,mts,cts}";
193
+ if (linters.includes("oxlint")) {
194
+ config[jsGlob] ??= [];
195
+ config[jsGlob].push("oxlint --fix");
196
+ }
197
+ if (linters.includes("eslint")) {
198
+ config[jsGlob] ??= [];
199
+ config[jsGlob].push("eslint --fix");
200
+ }
201
+ if (formatters.includes("oxfmt")) {
202
+ config["*"] ??= [];
203
+ config["*"].push("oxfmt");
204
+ }
205
+ if (formatters.includes("prettier")) {
206
+ config["*"] ??= [];
207
+ config["*"].push("prettier --ignore-unknown --write");
208
+ }
209
+ writeFileSync(join(cwd, ".lintstagedrc.json"), JSON.stringify(config, null, 2) + "\n");
210
+ if (existsSync(join(cwd, ".husky"))) writeFileSync(join(cwd, ".husky", "pre-commit"), "lint-staged\n");
211
+ }
212
+ //#endregion
213
+ //#region src/commands/add-oxfmt/logic.ts
214
+ function getMissingScripts$1(scripts) {
215
+ const missing = {};
216
+ if (!scripts["format"]) missing["format"] = "oxfmt";
217
+ if (!scripts["format:check"]) missing["format:check"] = "oxfmt --check";
218
+ return missing;
219
+ }
220
+ //#endregion
221
+ //#region src/commands/add-oxfmt/index.ts
222
+ function getPackages$2(pkg) {
223
+ return isPackageInstalled(pkg, "oxfmt") ? [] : [`oxfmt@0.56.0`, "@chanom/dev-config"];
224
+ }
225
+ function apply$2(cwd, esm) {
226
+ writeFileSync(join(cwd, `oxfmt.config.${esm ? "ts" : "mts"}`), `export { default } from '@chanom/dev-config/oxfmt/config';\n`);
227
+ const { pkg, pkgPath } = readPkg(cwd);
228
+ pkg.scripts ??= {};
229
+ const missing = getMissingScripts$1(pkg.scripts);
230
+ for (const [key, value] of Object.entries(missing)) pkg.scripts[key] = value;
231
+ const skipped = ["format", "format:check"].filter((k) => !(k in missing));
232
+ for (const key of skipped) log.warn(`\`${key}\` script already exists in package.json - skipping`);
233
+ if (Object.keys(missing).length > 0) writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
234
+ }
235
+ //#endregion
236
+ //#region src/commands/add-oxlint/logic.ts
237
+ function getMissingScripts(scripts) {
238
+ const missing = {};
239
+ if (!scripts["lint"]) missing["lint"] = "oxlint";
240
+ if (!scripts["lint:fix"]) missing["lint:fix"] = "oxlint --fix";
241
+ return missing;
242
+ }
243
+ //#endregion
244
+ //#region src/commands/add-oxlint/index.ts
245
+ function getPackages$1(pkg) {
246
+ return isPackageInstalled(pkg, "oxlint") ? [] : [`oxlint@1.71.0`, "@chanom/dev-config"];
247
+ }
248
+ function apply$1(cwd, esm) {
249
+ writeFileSync(join(cwd, `oxlint.config.${esm ? "ts" : "mts"}`), `export { default } from '@chanom/dev-config/oxlint/config';\n`);
250
+ const { pkg, pkgPath } = readPkg(cwd);
251
+ pkg.scripts ??= {};
252
+ const missing = getMissingScripts(pkg.scripts);
253
+ for (const [key, value] of Object.entries(missing)) pkg.scripts[key] = value;
254
+ const skipped = ["lint", "lint:fix"].filter((k) => !(k in missing));
255
+ for (const key of skipped) log.warn(`\`${key}\` script already exists in package.json - skipping`);
256
+ if (Object.keys(missing).length > 0) writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
257
+ }
258
+ //#endregion
259
+ //#region src/commands/add-prettier/index.ts
260
+ function getPackages(pkg) {
261
+ return isPackageInstalled(pkg, "prettier") ? [] : ["prettier"];
262
+ }
263
+ function apply(cwd) {
264
+ writeFileSync(join(cwd, ".prettierrc"), JSON.stringify({}, null, 2) + "\n");
265
+ const { pkg, pkgPath } = readPkg(cwd);
266
+ pkg.scripts ??= {};
267
+ const missing = {};
268
+ if (!pkg.scripts["format"]) missing["format"] = "prettier --write .";
269
+ if (!pkg.scripts["format:check"]) missing["format:check"] = "prettier --check .";
270
+ for (const [key, value] of Object.entries(missing)) pkg.scripts[key] = value;
271
+ const skipped = ["format", "format:check"].filter((k) => !(k in missing));
272
+ for (const key of skipped) log.warn(`\`${key}\` script already exists in package.json - skipping`);
273
+ if (Object.keys(missing).length > 0) writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
274
+ }
275
+ //#endregion
276
+ //#region src/commands/brew/index.ts
277
+ async function brew(cwd = process.cwd()) {
278
+ intro(pc.magenta("🧋 Chanom - Let's brew your project!"));
279
+ if (!isGitRepo(cwd)) {
280
+ const s = spinner();
281
+ s.start("Git not found, initializing repository...");
282
+ init(cwd);
283
+ writeGitignore(cwd);
284
+ s.stop("Git initialized");
285
+ }
286
+ const pm = detectPm(cwd);
287
+ const toppings = await multiselect({
288
+ message: "Which toppings would you like?",
289
+ options: [
290
+ {
291
+ value: "oxlint",
292
+ label: "oxlint",
293
+ hint: "fast Rust-based linter"
294
+ },
295
+ {
296
+ value: "oxfmt",
297
+ label: "oxfmt",
298
+ hint: "fast Rust-based formatter"
299
+ },
300
+ {
301
+ value: "eslint",
302
+ label: "eslint",
303
+ hint: "stub config"
304
+ },
305
+ {
306
+ value: "prettier",
307
+ label: "prettier",
308
+ hint: "stub config"
309
+ },
310
+ {
311
+ value: "knip",
312
+ label: "knip",
313
+ hint: "stub config"
314
+ }
315
+ ],
316
+ required: false
317
+ });
318
+ if (typeof toppings === "symbol") process.exit(0);
319
+ const sweetness = await select({
320
+ message: "How sweet would you like it?",
321
+ options: [{
322
+ value: "light",
323
+ label: "light",
324
+ hint: "warn only, no blocking"
325
+ }, {
326
+ value: "medium",
327
+ label: "medium",
328
+ hint: "block on commit (husky + lint-staged + commitlint)"
329
+ }]
330
+ });
331
+ if (typeof sweetness === "symbol") process.exit(0);
332
+ const { pkg } = readPkg(cwd);
333
+ const esm = isEsm(pkg);
334
+ const packages = [];
335
+ if (toppings.includes("oxlint")) packages.push(...getPackages$1(pkg));
336
+ if (toppings.includes("oxfmt")) packages.push(...getPackages$2(pkg));
337
+ if (toppings.includes("eslint")) packages.push(...getPackages$6(pkg));
338
+ if (toppings.includes("prettier")) packages.push(...getPackages(pkg));
339
+ if (toppings.includes("knip")) packages.push(...getPackages$4(pkg));
340
+ if (sweetness === "medium") packages.push(...getPackages$5(pkg), ...getPackages$3(pkg), ...getPackages$7(pkg));
341
+ if (packages.length > 0) {
342
+ const s = spinner();
343
+ const uniquePackages = Array.from(new Set(packages));
344
+ s.start(`Installing ${uniquePackages.join(", ")}...`);
345
+ installDev(pm, cwd, ...uniquePackages);
346
+ s.stop("Packages installed");
347
+ }
348
+ if (toppings.includes("oxlint")) apply$1(cwd, esm);
349
+ if (toppings.includes("oxfmt")) apply$2(cwd, esm);
350
+ if (toppings.includes("eslint")) apply$6(cwd, esm);
351
+ if (toppings.includes("prettier")) apply(cwd);
352
+ if (toppings.includes("knip")) apply$4(cwd);
353
+ if (sweetness === "medium") {
354
+ apply$5(cwd, pm);
355
+ apply$3(cwd, toppings.filter((t) => ["oxlint", "eslint"].includes(t)), toppings.filter((t) => ["oxfmt", "prettier"].includes(t)));
356
+ apply$7(cwd);
357
+ }
358
+ const s2 = spinner();
359
+ s2.start("Committing changes...");
360
+ if (!hasIdentity(cwd)) setLocalIdentity(cwd, "Chanom", "chanom@local");
361
+ stageAll(cwd);
362
+ if (hasStagedChanges(cwd)) {
363
+ const result = commit(cwd, "chore: setup chanom configuration");
364
+ if (result.ok) s2.stop("Changes committed");
365
+ else {
366
+ s2.stop(pc.yellow("Could not commit changes automatically"));
367
+ const detail = result.stderr || result.stdout;
368
+ log.warn(`${detail ? detail + "\n" : ""}Your files are staged - commit them manually when ready.`);
369
+ }
370
+ } else s2.stop("Nothing to commit, skipping");
371
+ outro(pc.green(`Your Chanom with ${toppings.join(", ")} toppings and ${sweetness} sweetness is ready! Enjoy! 🧋`));
372
+ }
373
+ //#endregion
374
+ //#region src/index.ts
375
+ const command = process.argv[2];
376
+ if (command === "brew") await brew();
377
+ else {
378
+ console.error(`Unknown command: ${command ?? "(none)"}`);
379
+ console.error("Usage: chanom brew");
380
+ process.exit(1);
381
+ }
382
+ //#endregion
383
+ export {};
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@chanom/cli",
3
+ "version": "0.0.1",
4
+ "description": "",
5
+ "keywords": [],
6
+ "type": "module",
7
+ "bin": {
8
+ "chanom": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "dependencies": {
14
+ "@clack/prompts": "1.6.0",
15
+ "picocolors": "1.1.1"
16
+ },
17
+ "devDependencies": {
18
+ "@tsconfig/node-lts": "24.0.0",
19
+ "@tsconfig/node-ts": "23.6.4",
20
+ "@types/node": "26.0.1",
21
+ "tsdown": "0.22.3",
22
+ "typescript": "6.0.3",
23
+ "@chanom/dev-config": "0.0.2"
24
+ },
25
+ "scripts": {
26
+ "build": "tsdown",
27
+ "dev": "tsdown --watch"
28
+ }
29
+ }