@chanom/cli 0.1.1 → 0.2.0
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 +1 -1
- package/dist/index.js +95 -67
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ Based on your answers, `brew` will:
|
|
|
46
46
|
|
|
47
47
|
- Initialize a git repo if one doesn't exist yet (with a `.gitignore`)
|
|
48
48
|
- Detect your package manager (`pnpm`, `npm`, `yarn`, or `bun`) - the `packageManager` field in `package.json` wins over the invoking user agent, falling back to `pnpm`
|
|
49
|
-
- Install the packages needed for your selected toppings as dev dependencies
|
|
49
|
+
- Install the packages needed for your selected toppings as dev dependencies - when run at a workspace root (a `pnpm-workspace.yaml` or a `workspaces` field in `package.json`), the install gets the flag your package manager needs: `-w` for pnpm, `-W` for yarn classic, none for npm, bun, and yarn berry
|
|
50
50
|
- Write config files and wire up `package.json` scripts
|
|
51
51
|
- If `medium` sweetness is selected, run `husky init` and add `pre-commit` / `commit-msg` hooks
|
|
52
52
|
- Stage and commit the changes it made
|
package/dist/index.js
CHANGED
|
@@ -107,16 +107,77 @@ var Git = class extends Effect.Service()("cli/Git", { effect: Effect.gen(functio
|
|
|
107
107
|
};
|
|
108
108
|
}) }) {};
|
|
109
109
|
//#endregion
|
|
110
|
+
//#region src/domain/package-manager.ts
|
|
111
|
+
/** How to run a locally installed binary with each package manager. */
|
|
112
|
+
const PM_EXEC = {
|
|
113
|
+
pnpm: ["pnpm", "exec"],
|
|
114
|
+
npm: [
|
|
115
|
+
"npx",
|
|
116
|
+
"--no",
|
|
117
|
+
"--"
|
|
118
|
+
],
|
|
119
|
+
yarn: ["yarn"],
|
|
120
|
+
bun: ["bunx"]
|
|
121
|
+
};
|
|
122
|
+
const KNOWN_PMS = [
|
|
123
|
+
"pnpm",
|
|
124
|
+
"yarn",
|
|
125
|
+
"bun",
|
|
126
|
+
"npm"
|
|
127
|
+
];
|
|
128
|
+
/** Parses the `packageManager` field of package.json, e.g. `pnpm@11.9.0`. */
|
|
129
|
+
function parsePmField(field) {
|
|
130
|
+
if (!field) return void 0;
|
|
131
|
+
const name = field.split("@")[0];
|
|
132
|
+
return KNOWN_PMS.find((pm) => pm === name);
|
|
133
|
+
}
|
|
134
|
+
/** Parses the `npm_config_user_agent` environment variable, e.g. `pnpm/11.9.0 npm/? node/v22`. */
|
|
135
|
+
function parsePmUserAgent(agent) {
|
|
136
|
+
if (!agent) return void 0;
|
|
137
|
+
return KNOWN_PMS.find((pm) => agent.startsWith(pm));
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Extra `add` flags needed when installing at a workspace root: pnpm and yarn
|
|
141
|
+
* classic refuse root installs without `-w` / `-W`. npm, bun, and yarn berry
|
|
142
|
+
* install to the root with no flag (npm's `-w` targets a child workspace instead).
|
|
143
|
+
*/
|
|
144
|
+
function workspaceRootFlags(pm, hints) {
|
|
145
|
+
switch (pm) {
|
|
146
|
+
case "pnpm": return hints.hasPnpmWorkspaceFile ? ["-w"] : [];
|
|
147
|
+
case "yarn": return hints.hasWorkspacesField && !hints.hasYarnBerryConfig ? ["-W"] : [];
|
|
148
|
+
case "npm":
|
|
149
|
+
case "bun": return [];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** The `packageManager` field wins over the user agent, which wins over the fallback. */
|
|
153
|
+
function resolvePm(hints) {
|
|
154
|
+
return parsePmField(hints.packageManagerField) ?? parsePmUserAgent(hints.userAgent) ?? hints.fallback ?? "pnpm";
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
110
157
|
//#region src/services/package-installer.ts
|
|
111
158
|
var InstallFailed = class extends Data.TaggedError("InstallFailed") {};
|
|
112
|
-
/**
|
|
159
|
+
/**
|
|
160
|
+
* Installs npm packages with the detected package manager, adding the
|
|
161
|
+
* workspace-root flag (pnpm `-w`, yarn classic `-W`) when the target
|
|
162
|
+
* directory is a workspace root.
|
|
163
|
+
*/
|
|
113
164
|
var PackageInstaller = class extends Effect.Service()("cli/PackageInstaller", { effect: Effect.gen(function* () {
|
|
114
165
|
const runner = yield* CommandRunner;
|
|
115
|
-
|
|
166
|
+
const fs = yield* FileSystem.FileSystem;
|
|
167
|
+
const path = yield* Path.Path;
|
|
168
|
+
const has = (cwd, file) => fs.exists(path.join(cwd, file)).pipe(Effect.orElseSucceed(() => false));
|
|
169
|
+
const hasWorkspacesField = (cwd) => fs.readFileString(path.join(cwd, "package.json")).pipe(Effect.flatMap((contents) => Effect.try(() => JSON.parse(contents).workspaces !== void 0)), Effect.orElseSucceed(() => false));
|
|
170
|
+
const workspaceHints = (cwd) => Effect.all({
|
|
171
|
+
hasPnpmWorkspaceFile: has(cwd, "pnpm-workspace.yaml"),
|
|
172
|
+
hasWorkspacesField: hasWorkspacesField(cwd),
|
|
173
|
+
hasYarnBerryConfig: has(cwd, ".yarnrc.yml")
|
|
174
|
+
}, { concurrency: 3 });
|
|
175
|
+
return { installDev: (pm, cwd, packages) => workspaceHints(cwd).pipe(Effect.flatMap((hints) => runner.execInherit(pm, [
|
|
116
176
|
"add",
|
|
117
177
|
"-D",
|
|
178
|
+
...workspaceRootFlags(pm, hints),
|
|
118
179
|
...packages
|
|
119
|
-
], cwd)
|
|
180
|
+
], cwd)), Effect.filterOrFail((code) => code === 0, () => new InstallFailed({ pm })), Effect.asVoid) };
|
|
120
181
|
}) }) {};
|
|
121
182
|
//#endregion
|
|
122
183
|
//#region src/services/prompter.ts
|
|
@@ -148,40 +209,6 @@ var Prompter = class extends Effect.Service()("cli/Prompter", { succeed: {
|
|
|
148
209
|
})
|
|
149
210
|
} }) {};
|
|
150
211
|
//#endregion
|
|
151
|
-
//#region src/domain/package-manager.ts
|
|
152
|
-
/** How to run a locally installed binary with each package manager. */
|
|
153
|
-
const PM_EXEC = {
|
|
154
|
-
pnpm: ["pnpm", "exec"],
|
|
155
|
-
npm: [
|
|
156
|
-
"npx",
|
|
157
|
-
"--no",
|
|
158
|
-
"--"
|
|
159
|
-
],
|
|
160
|
-
yarn: ["yarn"],
|
|
161
|
-
bun: ["bunx"]
|
|
162
|
-
};
|
|
163
|
-
const KNOWN_PMS = [
|
|
164
|
-
"pnpm",
|
|
165
|
-
"yarn",
|
|
166
|
-
"bun",
|
|
167
|
-
"npm"
|
|
168
|
-
];
|
|
169
|
-
/** Parses the `packageManager` field of package.json, e.g. `pnpm@11.9.0`. */
|
|
170
|
-
function parsePmField(field) {
|
|
171
|
-
if (!field) return void 0;
|
|
172
|
-
const name = field.split("@")[0];
|
|
173
|
-
return KNOWN_PMS.find((pm) => pm === name);
|
|
174
|
-
}
|
|
175
|
-
/** Parses the `npm_config_user_agent` environment variable, e.g. `pnpm/11.9.0 npm/? node/v22`. */
|
|
176
|
-
function parsePmUserAgent(agent) {
|
|
177
|
-
if (!agent) return void 0;
|
|
178
|
-
return KNOWN_PMS.find((pm) => agent.startsWith(pm));
|
|
179
|
-
}
|
|
180
|
-
/** The `packageManager` field wins over the user agent, which wins over the fallback. */
|
|
181
|
-
function resolvePm(hints) {
|
|
182
|
-
return parsePmField(hints.packageManagerField) ?? parsePmUserAgent(hints.userAgent) ?? hints.fallback ?? "pnpm";
|
|
183
|
-
}
|
|
184
|
-
//#endregion
|
|
185
212
|
//#region src/utils/detect-pm.ts
|
|
186
213
|
const detectPm = (cwd, userAgent = process.env["npm_config_user_agent"], fallback = "pnpm") => Effect.gen(function* () {
|
|
187
214
|
const fs = yield* FileSystem.FileSystem;
|
|
@@ -286,7 +313,7 @@ function commitMsgHook(pm) {
|
|
|
286
313
|
}
|
|
287
314
|
//#endregion
|
|
288
315
|
//#region src/commands/add-commitlint/index.ts
|
|
289
|
-
const apply$5 =
|
|
316
|
+
const apply$5 = Effect.fn("add-commitlint.apply")(function* (cwd, pm) {
|
|
290
317
|
const fs = yield* FileSystem.FileSystem;
|
|
291
318
|
const path = yield* Path.Path;
|
|
292
319
|
const prompter = yield* Prompter;
|
|
@@ -299,7 +326,7 @@ const apply$5 = (cwd, pm) => Effect.gen(function* () {
|
|
|
299
326
|
if (yield* fs.exists(hookPath)) yield* prompter.warn("`.husky/commit-msg` already exists - skipping");
|
|
300
327
|
else yield* fs.writeFileString(hookPath, commitMsgHook(pm));
|
|
301
328
|
}
|
|
302
|
-
})
|
|
329
|
+
});
|
|
303
330
|
//#endregion
|
|
304
331
|
//#region src/commands/add-husky/logic.ts
|
|
305
332
|
function getPackages$4(pkg) {
|
|
@@ -308,7 +335,7 @@ function getPackages$4(pkg) {
|
|
|
308
335
|
//#endregion
|
|
309
336
|
//#region src/commands/add-husky/index.ts
|
|
310
337
|
var HuskyInitFailed = class extends Data.TaggedError("HuskyInitFailed") {};
|
|
311
|
-
const apply$4 =
|
|
338
|
+
const apply$4 = Effect.fn("add-husky.apply")(function* (cwd, pm) {
|
|
312
339
|
const fs = yield* FileSystem.FileSystem;
|
|
313
340
|
const path = yield* Path.Path;
|
|
314
341
|
const prompter = yield* Prompter;
|
|
@@ -325,7 +352,7 @@ const apply$4 = (cwd, pm) => Effect.gen(function* () {
|
|
|
325
352
|
"init"
|
|
326
353
|
], cwd);
|
|
327
354
|
if (exitCode !== 0) return yield* new HuskyInitFailed({ exitCode });
|
|
328
|
-
})
|
|
355
|
+
});
|
|
329
356
|
//#endregion
|
|
330
357
|
//#region src/domain/scripts.ts
|
|
331
358
|
/**
|
|
@@ -364,7 +391,7 @@ function getScriptPlan$2(scripts) {
|
|
|
364
391
|
//#endregion
|
|
365
392
|
//#region src/commands/add-knip/index.ts
|
|
366
393
|
/** Returns a copy of `pkg` with missing scripts added; the caller owns writing package.json. */
|
|
367
|
-
const apply$3 = (cwd, esm, pkg)
|
|
394
|
+
const apply$3 = Effect.fn("add-knip.apply")(function* (cwd, esm, pkg) {
|
|
368
395
|
const fs = yield* FileSystem.FileSystem;
|
|
369
396
|
const path = yield* Path.Path;
|
|
370
397
|
const prompter = yield* Prompter;
|
|
@@ -380,7 +407,7 @@ const apply$3 = (cwd, esm, pkg) => Effect.gen(function* () {
|
|
|
380
407
|
...pkg,
|
|
381
408
|
scripts: plan.scripts
|
|
382
409
|
};
|
|
383
|
-
})
|
|
410
|
+
});
|
|
384
411
|
//#endregion
|
|
385
412
|
//#region src/commands/add-lint-staged/logic.ts
|
|
386
413
|
function getPackages$2(pkg) {
|
|
@@ -401,7 +428,7 @@ function buildConfig(linters, formatters) {
|
|
|
401
428
|
}
|
|
402
429
|
//#endregion
|
|
403
430
|
//#region src/commands/add-lint-staged/index.ts
|
|
404
|
-
const apply$2 = (cwd, linters = [], formatters = [], overwritePreCommit = false)
|
|
431
|
+
const apply$2 = Effect.fn("add-lint-staged.apply")(function* (cwd, linters = [], formatters = [], overwritePreCommit = false) {
|
|
405
432
|
const fs = yield* FileSystem.FileSystem;
|
|
406
433
|
const path = yield* Path.Path;
|
|
407
434
|
const prompter = yield* Prompter;
|
|
@@ -414,7 +441,7 @@ const apply$2 = (cwd, linters = [], formatters = [], overwritePreCommit = false)
|
|
|
414
441
|
if ((yield* fs.exists(hookPath)) && !overwritePreCommit) yield* prompter.warn("`.husky/pre-commit` already exists - skipping");
|
|
415
442
|
else yield* fs.writeFileString(hookPath, "lint-staged\n");
|
|
416
443
|
}
|
|
417
|
-
})
|
|
444
|
+
});
|
|
418
445
|
//#endregion
|
|
419
446
|
//#region src/commands/add-oxfmt/logic.ts
|
|
420
447
|
function getPackages$1(pkg, versions) {
|
|
@@ -435,7 +462,7 @@ function getScriptPlan$1(scripts) {
|
|
|
435
462
|
//#endregion
|
|
436
463
|
//#region src/commands/add-oxfmt/index.ts
|
|
437
464
|
/** Returns a copy of `pkg` with missing scripts added; the caller owns writing package.json. */
|
|
438
|
-
const apply$1 = (cwd, esm, pkg)
|
|
465
|
+
const apply$1 = Effect.fn("add-oxfmt.apply")(function* (cwd, esm, pkg) {
|
|
439
466
|
const fs = yield* FileSystem.FileSystem;
|
|
440
467
|
const path = yield* Path.Path;
|
|
441
468
|
const prompter = yield* Prompter;
|
|
@@ -451,7 +478,7 @@ const apply$1 = (cwd, esm, pkg) => Effect.gen(function* () {
|
|
|
451
478
|
...pkg,
|
|
452
479
|
scripts: plan.scripts
|
|
453
480
|
};
|
|
454
|
-
})
|
|
481
|
+
});
|
|
455
482
|
//#endregion
|
|
456
483
|
//#region src/commands/add-oxlint/logic.ts
|
|
457
484
|
function getPackages(pkg, versions) {
|
|
@@ -472,7 +499,7 @@ function getScriptPlan(scripts) {
|
|
|
472
499
|
//#endregion
|
|
473
500
|
//#region src/commands/add-oxlint/index.ts
|
|
474
501
|
/** Returns a copy of `pkg` with missing scripts added; the caller owns writing package.json. */
|
|
475
|
-
const apply = (cwd, esm, pkg)
|
|
502
|
+
const apply = Effect.fn("add-oxlint.apply")(function* (cwd, esm, pkg) {
|
|
476
503
|
const fs = yield* FileSystem.FileSystem;
|
|
477
504
|
const path = yield* Path.Path;
|
|
478
505
|
const prompter = yield* Prompter;
|
|
@@ -488,7 +515,7 @@ const apply = (cwd, esm, pkg) => Effect.gen(function* () {
|
|
|
488
515
|
...pkg,
|
|
489
516
|
scripts: plan.scripts
|
|
490
517
|
};
|
|
491
|
-
})
|
|
518
|
+
});
|
|
492
519
|
//#endregion
|
|
493
520
|
//#region src/commands/brew/logic.ts
|
|
494
521
|
/** Deduplicated list of packages to install for the chosen toppings and sweetness. */
|
|
@@ -523,7 +550,7 @@ function selectedFormatters(toppings) {
|
|
|
523
550
|
}
|
|
524
551
|
//#endregion
|
|
525
552
|
//#region src/commands/brew/index.ts
|
|
526
|
-
const ensureGitRepo =
|
|
553
|
+
const ensureGitRepo = Effect.fn("brew.ensureGitRepo")(function* (cwd) {
|
|
527
554
|
const git = yield* Git;
|
|
528
555
|
if (yield* git.isRepo(cwd)) {
|
|
529
556
|
yield* Effect.logDebug("git repository already present");
|
|
@@ -532,8 +559,8 @@ const ensureGitRepo = (cwd) => Effect.gen(function* () {
|
|
|
532
559
|
const s = yield* (yield* Prompter).spinner("Git not found, initializing repository...");
|
|
533
560
|
yield* git.init(cwd);
|
|
534
561
|
yield* s.stop("Git initialized");
|
|
535
|
-
})
|
|
536
|
-
const ensureGitIgnore =
|
|
562
|
+
});
|
|
563
|
+
const ensureGitIgnore = Effect.fn("brew.ensureGitIgnore")(function* (cwd) {
|
|
537
564
|
const git = yield* Git;
|
|
538
565
|
const prompter = yield* Prompter;
|
|
539
566
|
const existing = yield* git.readGitignore(cwd).pipe(Effect.catchTag("SystemError", (e) => e.reason === "NotFound" ? Effect.succeed(void 0) : Effect.fail(e)));
|
|
@@ -544,8 +571,8 @@ const ensureGitIgnore = (cwd) => Effect.gen(function* () {
|
|
|
544
571
|
const s = yield* prompter.spinner(existing === void 0 ? "Creating .gitignore..." : "Adding node_modules to .gitignore...");
|
|
545
572
|
yield* git.writeGitignore(cwd, appendGitignoreEntry(existing ?? "", "node_modules")).pipe(Effect.tapError(() => s.stop(pc.red("Could not update .gitignore"))));
|
|
546
573
|
yield* s.stop(existing === void 0 ? ".gitignore created" : "node_modules added to .gitignore");
|
|
547
|
-
})
|
|
548
|
-
const askRecipe =
|
|
574
|
+
});
|
|
575
|
+
const askRecipe = Effect.fn("brew.askRecipe")(function* () {
|
|
549
576
|
const prompter = yield* Prompter;
|
|
550
577
|
return {
|
|
551
578
|
toppings: yield* prompter.multiselect({
|
|
@@ -582,8 +609,8 @@ const askRecipe = () => Effect.gen(function* () {
|
|
|
582
609
|
}]
|
|
583
610
|
})
|
|
584
611
|
};
|
|
585
|
-
})
|
|
586
|
-
const installPackages = (pm, cwd, packages)
|
|
612
|
+
});
|
|
613
|
+
const installPackages = Effect.fn("brew.installPackages")(function* (pm, cwd, packages) {
|
|
587
614
|
if (packages.length === 0) {
|
|
588
615
|
yield* Effect.logDebug("no packages to install");
|
|
589
616
|
return;
|
|
@@ -593,21 +620,22 @@ const installPackages = (pm, cwd, packages) => Effect.gen(function* () {
|
|
|
593
620
|
const s = yield* prompter.spinner(`Installing ${packages.join(", ")}...`);
|
|
594
621
|
yield* installer.installDev(pm, cwd, packages).pipe(Effect.tapError(() => s.stop(pc.red("Package installation failed"))));
|
|
595
622
|
yield* s.stop("Packages installed");
|
|
596
|
-
})
|
|
597
|
-
const applyToppings = (cwd, esm, pkg, toppings)
|
|
623
|
+
});
|
|
624
|
+
const applyToppings = Effect.fn("brew.applyToppings")(function* (cwd, esm, pkg, toppings) {
|
|
598
625
|
let updated = pkg;
|
|
599
626
|
if (toppings.includes("oxlint")) updated = yield* apply(cwd, esm, updated);
|
|
600
627
|
if (toppings.includes("oxfmt")) updated = yield* apply$1(cwd, esm, updated);
|
|
601
628
|
if (toppings.includes("knip")) updated = yield* apply$3(cwd, esm, updated);
|
|
602
629
|
return updated;
|
|
603
|
-
})
|
|
604
|
-
const persistScripts = (pkgPath, pkg, updated)
|
|
605
|
-
if (JSON.stringify(updated.scripts ?? {})
|
|
630
|
+
});
|
|
631
|
+
const persistScripts = Effect.fn("brew.persistScripts")(function* (pkgPath, pkg, updated) {
|
|
632
|
+
if (JSON.stringify(updated.scripts ?? {}) === JSON.stringify(pkg.scripts ?? {})) yield* Effect.logDebug("scripts unchanged, skipping package.json write");
|
|
633
|
+
else {
|
|
606
634
|
yield* Effect.logDebug(`writing updated scripts to ${pkgPath}`);
|
|
607
635
|
yield* writePkg(pkgPath, updated);
|
|
608
|
-
}
|
|
609
|
-
})
|
|
610
|
-
const setupCommitGate = (cwd, pm, toppings, sweetness)
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
const setupCommitGate = Effect.fn("brew.setupCommitGate")(function* (cwd, pm, toppings, sweetness) {
|
|
611
639
|
if (sweetness !== "medium") return;
|
|
612
640
|
const fs = yield* FileSystem.FileSystem;
|
|
613
641
|
const path = yield* Path.Path;
|
|
@@ -615,8 +643,8 @@ const setupCommitGate = (cwd, pm, toppings, sweetness) => Effect.gen(function* (
|
|
|
615
643
|
yield* apply$4(cwd, pm);
|
|
616
644
|
yield* apply$2(cwd, selectedLinters(toppings), selectedFormatters(toppings), !huskyExisted);
|
|
617
645
|
yield* apply$5(cwd, pm);
|
|
618
|
-
})
|
|
619
|
-
const commitChanges =
|
|
646
|
+
});
|
|
647
|
+
const commitChanges = Effect.fn("brew.commitChanges")(function* (cwd) {
|
|
620
648
|
const git = yield* Git;
|
|
621
649
|
const prompter = yield* Prompter;
|
|
622
650
|
const s = yield* prompter.spinner("Committing changes...");
|
|
@@ -634,7 +662,7 @@ const commitChanges = (cwd) => Effect.gen(function* () {
|
|
|
634
662
|
yield* s.stop(pc.yellow("Could not commit changes automatically"));
|
|
635
663
|
const detail = result.stderr || result.stdout;
|
|
636
664
|
yield* prompter.warn(`${detail ? detail + "\n" : ""}Your files are staged - commit them manually when ready.`);
|
|
637
|
-
})
|
|
665
|
+
});
|
|
638
666
|
const brew = (cwd = process.cwd()) => Effect.gen(function* () {
|
|
639
667
|
const prompter = yield* Prompter;
|
|
640
668
|
yield* prompter.intro(pc.magenta("🧋 Chanom - Let's brew your project!"));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chanom/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"repository": {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"@tsconfig/node-lts": "24.0.0",
|
|
29
29
|
"@tsconfig/node-ts": "23.6.4",
|
|
30
30
|
"@types/node": "26.0.1",
|
|
31
|
+
"@vitest/coverage-v8": "3.2.6",
|
|
31
32
|
"tsdown": "0.22.3",
|
|
32
33
|
"typescript": "6.0.3",
|
|
33
34
|
"vitest": "3.2.6",
|
|
@@ -37,6 +38,7 @@
|
|
|
37
38
|
"build": "tsdown",
|
|
38
39
|
"dev": "tsdown --watch",
|
|
39
40
|
"test": "vitest run",
|
|
41
|
+
"test:coverage": "vitest run --coverage",
|
|
40
42
|
"test:watch": "vitest"
|
|
41
43
|
}
|
|
42
44
|
}
|