@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.
- package/README.md +6 -0
- package/dist/index.js +93 -30
- 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
|
|
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.
|
|
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.
|
|
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((
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
605
|
-
"format: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
|
-
|
|
617
|
-
|
|
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:
|
|
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:
|
|
646
|
-
"lint: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
|
-
|
|
658
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
}
|