frontpl 0.4.1 → 0.5.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 CHANGED
@@ -59,6 +59,26 @@ When `pnpm workspace mode` is enabled:
59
59
  - Root contains `pnpm-workspace.yaml` and the workspace `package.json`
60
60
  - `oxlint`/`oxfmt` scripts, dependencies, and config files are generated at the workspace root
61
61
  - App/library package is scaffolded under `packages/<name>/` with its own `package.json`, `src`, and `tsconfig.json`
62
+ - If root `oxlint` is enabled, package `package.json` does not add redundant `typecheck: tsc --noEmit`
63
+
64
+ ### `frontpl add [name]`
65
+
66
+ Add a new package under `packages/<name>/` in an existing `pnpm workspace`.
67
+
68
+ What it does:
69
+
70
+ - Requires workspace root (`pnpm-workspace.yaml`) and `pnpm` package manager
71
+ - Generates package baseline files:
72
+ - `packages/<name>/package.json`
73
+ - `packages/<name>/README.md`
74
+ - `packages/<name>/src/index.ts`
75
+ - `packages/<name>/tsconfig.json`
76
+ - Optionally adds `vitest` (`src/index.test.ts`) and `tsdown` (`tsdown.config.ts`)
77
+ - Reuses root toolchain strategy:
78
+ - package does not scaffold `oxlint`/`oxfmt` scripts
79
+ - if root `oxlint` exists, package does not scaffold `typecheck: tsc --noEmit`
80
+
81
+ Use `--yes` (or `-y`) to skip confirmations and use defaults inferred from root scripts.
62
82
 
63
83
  ### `frontpl ci`
64
84
 
@@ -109,8 +129,6 @@ What it does:
109
129
  - Ensures `package.json` scripts use:
110
130
  - `format`: `oxfmt`
111
131
  - `format:check`: `oxfmt --check`
112
- - `fmt`: `oxfmt`
113
- - `fmt:check`: `oxfmt --check`
114
132
  - Ensures `devDependencies.oxfmt` exists (defaults to `latest` when missing)
115
133
  - Creates or updates `.oxfmtrc.json`
116
134
  - Optionally removes `prettier` / `prettier-plugin-*` / `@prettier/plugin-*` dependencies, `package.json#prettier`, and Prettier config files (`.prettierrc*`, `prettier.config.*`)
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as runCi, n as runOxlint, r as runInit, t as runOxfmt } from "./oxfmt-DlwbhnpJ.mjs";
2
+ import { a as runInit, i as runAdd, n as runOxlint, r as runCi, t as runOxfmt } from "./oxfmt-DQBJ0JMs.mjs";
3
3
  import bin from "tiny-bin";
4
4
 
5
5
  //#region src/cli.ts
@@ -10,6 +10,11 @@ async function main() {
10
10
  await runInit({ nameArg: args[0] });
11
11
  }).command("ci", "Add CI/release workflows to an existing project").action(async () => {
12
12
  await runCi();
13
+ }).command("add", "Add a new package to an existing pnpm workspace").argument("[name]", "Package name (directory name under packages/)").option("--yes, -y", "Skip confirmations and use defaults").action(async (options, args) => {
14
+ await runAdd({
15
+ nameArg: args[0],
16
+ yes: options.yes === true
17
+ });
13
18
  }).command("oxlint", "Add/migrate linter to oxlint in current project").option("--yes, -y", "Skip confirmations and use defaults").action(async (options) => {
14
19
  await runOxlint({ yes: options.yes === true });
15
20
  }).command("oxfmt", "Add/migrate formatter to oxfmt in current project").option("--yes, -y", "Skip confirmations and use defaults").action(async (options) => {
package/dist/index.d.mts CHANGED
@@ -1,3 +1,13 @@
1
+ //#region src/commands/add.d.ts
2
+ type CommandOptions$2 = {
3
+ nameArg?: string;
4
+ yes?: boolean;
5
+ };
6
+ declare function runAdd({
7
+ nameArg,
8
+ yes
9
+ }?: CommandOptions$2): Promise<void>;
10
+ //#endregion
1
11
  //#region src/commands/ci.d.ts
2
12
  declare function runCi(): Promise<undefined>;
3
13
  //#endregion
@@ -36,6 +46,7 @@ declare function packageJsonTemplate(opts: {
36
46
  packageManager: string;
37
47
  typescriptVersion: string;
38
48
  useOxlint: boolean;
49
+ includeTypecheckWithoutOxlint?: boolean;
39
50
  oxlintVersion?: string;
40
51
  oxlintTsgolintVersion?: string;
41
52
  kingswordLintConfigVersion?: string;
@@ -77,4 +88,4 @@ declare function githubDependabotTemplate(opts: {
77
88
  workingDirectory: string;
78
89
  }): string;
79
90
  //#endregion
80
- export { githubCliCiWorkflowTemplate, githubDependabotTemplate, oxlintConfigTemplate, packageJsonTemplate, runCi, runInit, runOxfmt, runOxlint, validateProjectName, workspaceRootPackageJsonTemplate };
91
+ export { githubCliCiWorkflowTemplate, githubDependabotTemplate, oxlintConfigTemplate, packageJsonTemplate, runAdd, runCi, runInit, runOxfmt, runOxlint, validateProjectName, workspaceRootPackageJsonTemplate };
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { a as runCi, c as oxlintConfigTemplate, i as validateProjectName, l as packageJsonTemplate, n as runOxlint, o as githubCliCiWorkflowTemplate, r as runInit, s as githubDependabotTemplate, t as runOxfmt, u as workspaceRootPackageJsonTemplate } from "./oxfmt-DlwbhnpJ.mjs";
1
+ import { a as runInit, c as githubDependabotTemplate, d as workspaceRootPackageJsonTemplate, i as runAdd, l as oxlintConfigTemplate, n as runOxlint, o as validateProjectName, r as runCi, s as githubCliCiWorkflowTemplate, t as runOxfmt, u as packageJsonTemplate } from "./oxfmt-DQBJ0JMs.mjs";
2
2
 
3
- export { githubCliCiWorkflowTemplate, githubDependabotTemplate, oxlintConfigTemplate, packageJsonTemplate, runCi, runInit, runOxfmt, runOxlint, validateProjectName, workspaceRootPackageJsonTemplate };
3
+ export { githubCliCiWorkflowTemplate, githubDependabotTemplate, oxlintConfigTemplate, packageJsonTemplate, runAdd, runCi, runInit, runOxfmt, runOxlint, validateProjectName, workspaceRootPackageJsonTemplate };
@@ -5,52 +5,49 @@ import process from "node:process";
5
5
  import { spawn } from "node:child_process";
6
6
  import os from "node:os";
7
7
 
8
- //#region src/lib/fs.ts
9
- async function writeText(filePath, contents) {
10
- await mkdir(path.dirname(filePath), { recursive: true });
11
- await writeFile(filePath, contents, "utf8");
12
- }
13
-
14
- //#endregion
15
- //#region src/lib/utils.ts
16
- async function pathExists(pathname) {
17
- try {
18
- await access(pathname);
19
- return true;
20
- } catch {
21
- return false;
22
- }
23
- }
24
-
25
- //#endregion
26
- //#region src/lib/project.ts
27
- async function readPackageJson(filePath) {
28
- try {
29
- return JSON.parse(await readFile(filePath, "utf8"));
30
- } catch {
31
- return;
8
+ //#region src/lib/versions.ts
9
+ async function detectPackageManagerVersion(pm) {
10
+ switch (pm) {
11
+ case "npm": return (await execCapture("npm", ["--version"])).stdout.trim() || void 0;
12
+ case "pnpm": return (await execCapture("pnpm", ["--version"])).stdout.trim() || void 0;
13
+ case "yarn": return (await execCapture("yarn", ["--version"])).stdout.trim() || void 0;
14
+ case "bun": return (await execCapture("bun", ["--version"])).stdout.trim() || void 0;
15
+ case "deno": return ((await execCapture("deno", ["--version"])).stdout.trim().split("\n")[0] ?? "").match(/deno\\s+([0-9]+\\.[0-9]+\\.[0-9]+)/)?.[1];
32
16
  }
33
17
  }
34
- async function writePackageJson(filePath, value) {
35
- await writeText(filePath, JSON.stringify(value, null, 2) + "\n");
36
- }
37
- async function detectPackageManager(rootDir) {
38
- const pmField = (await readPackageJson(path.join(rootDir, "package.json")))?.packageManager;
39
- if (pmField) {
40
- const pm = pmField.split("@")[0] ?? "";
41
- if (isPackageManager(pm)) return pm;
42
- }
43
- const candidates = [];
44
- if (await pathExists(path.join(rootDir, "pnpm-lock.yaml"))) candidates.push("pnpm");
45
- if (await pathExists(path.join(rootDir, "yarn.lock"))) candidates.push("yarn");
46
- if (await pathExists(path.join(rootDir, "package-lock.json"))) candidates.push("npm");
47
- if (await pathExists(path.join(rootDir, "bun.lockb"))) candidates.push("bun");
48
- if (await pathExists(path.join(rootDir, "bun.lock"))) candidates.push("bun");
49
- if (await pathExists(path.join(rootDir, "deno.json")) || await pathExists(path.join(rootDir, "deno.jsonc"))) candidates.push("deno");
50
- return candidates.length === 1 ? candidates[0] : void 0;
18
+ async function execCapture(command, args) {
19
+ const resolved = resolveCommand$1(command);
20
+ return new Promise((resolve) => {
21
+ const child = spawn(resolved, args, {
22
+ cwd: os.tmpdir(),
23
+ stdio: [
24
+ "ignore",
25
+ "pipe",
26
+ "ignore"
27
+ ],
28
+ shell: false,
29
+ env: process.env
30
+ });
31
+ const chunks = [];
32
+ child.stdout.on("data", (d) => chunks.push(Buffer.from(d)));
33
+ child.on("close", (code) => {
34
+ resolve({
35
+ ok: code === 0,
36
+ stdout: Buffer.concat(chunks).toString("utf8")
37
+ });
38
+ });
39
+ child.on("error", () => resolve({
40
+ ok: false,
41
+ stdout: ""
42
+ }));
43
+ });
51
44
  }
52
- function isPackageManager(value) {
53
- return value === "npm" || value === "pnpm" || value === "yarn" || value === "bun" || value === "deno";
45
+ function resolveCommand$1(command) {
46
+ if (process.platform !== "win32") return command;
47
+ if (command === "npm") return "npm.cmd";
48
+ if (command === "pnpm") return "pnpm.cmd";
49
+ if (command === "yarn") return "yarn.cmd";
50
+ return command;
54
51
  }
55
52
 
56
53
  //#endregion
@@ -161,8 +158,6 @@ function applyLintAndFormatScripts(scripts, opts) {
161
158
  if (opts.useOxfmt) {
162
159
  scripts.format = "oxfmt";
163
160
  scripts["format:check"] = "oxfmt --check";
164
- scripts.fmt = "oxfmt";
165
- scripts["fmt:check"] = "oxfmt --check";
166
161
  }
167
162
  }
168
163
  function applyLintAndFormatDependencies(devDependencies, opts) {
@@ -175,7 +170,7 @@ function applyLintAndFormatDependencies(devDependencies, opts) {
175
170
  }
176
171
  function packageJsonTemplate(opts) {
177
172
  const scripts = {};
178
- if (!opts.useOxlint) scripts.typecheck = "tsc --noEmit";
173
+ if (!opts.useOxlint && opts.includeTypecheckWithoutOxlint !== false) scripts.typecheck = "tsc --noEmit";
179
174
  applyLintAndFormatScripts(scripts, opts);
180
175
  if (opts.useVitest) scripts.test = "vitest";
181
176
  if (opts.useTsdown) scripts.build = "tsdown";
@@ -399,267 +394,58 @@ function yamlString(value) {
399
394
  }
400
395
 
401
396
  //#endregion
402
- //#region src/commands/ci.ts
403
- async function runCi() {
397
+ //#region src/lib/fs.ts
398
+ async function writeText(filePath, contents) {
399
+ await mkdir(path.dirname(filePath), { recursive: true });
400
+ await writeFile(filePath, contents, "utf8");
401
+ }
402
+
403
+ //#endregion
404
+ //#region src/lib/utils.ts
405
+ async function pathExists(pathname) {
404
406
  try {
405
- intro("frontpl (ci)");
406
- const rootDir = process.cwd();
407
- const detectedPackageManager = await detectPackageManager(rootDir);
408
- const packageManager = await select({
409
- message: detectedPackageManager ? `Package manager (detected: ${detectedPackageManager})` : "Package manager",
410
- initialValue: detectedPackageManager ?? "pnpm",
411
- options: [
412
- {
413
- value: "npm",
414
- label: "npm"
415
- },
416
- {
417
- value: "yarn",
418
- label: "yarn"
419
- },
420
- {
421
- value: "pnpm",
422
- label: "pnpm"
423
- },
424
- {
425
- value: "bun",
426
- label: "bun"
427
- },
428
- {
429
- value: "deno",
430
- label: "deno"
431
- }
432
- ]
433
- });
434
- if (isCancel(packageManager)) return abort$2();
435
- const candidates = await listPackageCandidates(rootDir, packageManager);
436
- if (candidates.length === 0) {
437
- cancel("No package found. Run this command in a project root (with package.json or deno.json).");
438
- process.exitCode = 1;
439
- return;
440
- }
441
- const initialWorkingDirectory = await detectWorkingDirectory(rootDir, candidates);
442
- const workingDirectory = candidates.length === 1 ? candidates[0] : await select({
443
- message: "Working directory (package folder)",
444
- initialValue: initialWorkingDirectory,
445
- options: candidates.map((c) => ({
446
- value: c,
447
- label: c
448
- }))
449
- });
450
- if (isCancel(workingDirectory)) return abort$2();
451
- const nodeVersionDefault = await detectNodeMajorVersion(rootDir) ?? 22;
452
- const nodeVersionText = await text({
453
- message: "Node.js major version (for GitHub Actions)",
454
- initialValue: String(nodeVersionDefault),
455
- validate: (value = "") => {
456
- const major = Number.parseInt(value.trim(), 10);
457
- if (!Number.isFinite(major) || major <= 0) return "Enter a valid major version (e.g. 22)";
458
- }
459
- });
460
- if (isCancel(nodeVersionText)) return abort$2();
461
- const nodeVersion = Number.parseInt(String(nodeVersionText).trim(), 10);
462
- const { runLint, runFormatCheck, runTests, lintCommand, formatCheckCommand, testCommand } = await resolveCiCommands(rootDir, workingDirectory, packageManager);
463
- const addRelease = await confirm({
464
- message: "Add release workflow too?",
465
- initialValue: true
466
- });
467
- if (isCancel(addRelease)) return abort$2();
468
- const releaseMode = addRelease ? await select({
469
- message: "Release workflows",
470
- initialValue: "tag",
471
- options: [
472
- {
473
- value: "tag",
474
- label: "Tag push (vX.Y.Z) — recommended"
475
- },
476
- {
477
- value: "commit",
478
- label: "Release commit (chore(release): vX.Y.Z) — legacy"
479
- },
480
- {
481
- value: "both",
482
- label: "Both (tag + commit)"
483
- }
484
- ]
485
- }) : void 0;
486
- if (isCancel(releaseMode)) return abort$2();
487
- const trustedPublishing = addRelease && packageManager !== "deno" ? await confirm({
488
- message: "Release: npm trusted publishing (OIDC)?",
489
- initialValue: true
490
- }) : void 0;
491
- if (isCancel(trustedPublishing)) return abort$2();
492
- const addDependabot = await pathExists(path.join(rootDir, ".git")) ? await confirm({
493
- message: "Add/update Dependabot config (.github/dependabot.yml)?",
494
- initialValue: true
495
- }) : false;
496
- if (isCancel(addDependabot)) return abort$2();
497
- const ciWorkflowPath = path.join(rootDir, ".github/workflows/ci.yml");
498
- const releaseWorkflowPath = path.join(rootDir, ".github/workflows/release.yml");
499
- const dependabotPath = path.join(rootDir, ".github/dependabot.yml");
500
- if (!await confirmOverwriteIfExists(ciWorkflowPath, ".github/workflows/ci.yml")) {
501
- cancel("Skipped CI workflow");
502
- process.exitCode = 0;
503
- return;
504
- }
505
- await writeText(ciWorkflowPath, githubCliCiWorkflowTemplate({
506
- packageManager,
507
- nodeVersion,
508
- workingDirectory,
509
- runLint,
510
- runFormatCheck,
511
- runTests,
512
- lintCommand,
513
- formatCheckCommand,
514
- testCommand
515
- }));
516
- if (addRelease) {
517
- if (await confirmOverwriteIfExists(releaseWorkflowPath, ".github/workflows/release.yml")) await writeText(releaseWorkflowPath, (releaseMode === "both" ? githubCliReleaseBothWorkflowTemplate : releaseMode === "commit" ? githubCliReleaseWorkflowTemplate : githubCliReleaseTagWorkflowTemplate)({
518
- packageManager,
519
- nodeVersion,
520
- workingDirectory,
521
- trustedPublishing
522
- }));
523
- }
524
- if (addDependabot) {
525
- if (await confirmOverwriteIfExists(dependabotPath, ".github/dependabot.yml")) await writeText(dependabotPath, githubDependabotTemplate({
526
- packageManager,
527
- workingDirectory
528
- }));
529
- }
530
- outro(addRelease ? "Done. Generated CI + release workflows (and optional Dependabot)." : "Done. Generated CI workflow (and optional Dependabot).");
531
- } catch (err) {
532
- if (err instanceof CancelledError$2) return;
533
- throw err;
407
+ await access(pathname);
408
+ return true;
409
+ } catch {
410
+ return false;
534
411
  }
535
412
  }
536
- var CancelledError$2 = class extends Error {
537
- constructor() {
538
- super("Cancelled");
413
+
414
+ //#endregion
415
+ //#region src/lib/project.ts
416
+ async function readPackageJson(filePath) {
417
+ try {
418
+ return JSON.parse(await readFile(filePath, "utf8"));
419
+ } catch {
420
+ return;
539
421
  }
540
- };
541
- function abort$2(opts = {}) {
542
- cancel(opts.message ?? "Cancelled");
543
- process.exitCode = opts.exitCode ?? 0;
544
- throw new CancelledError$2();
545
422
  }
546
- async function confirmOverwriteIfExists(absPath, label) {
547
- if (!await pathExists(absPath)) return true;
548
- const overwrite = await confirm({
549
- message: `Overwrite existing ${label}?`,
550
- initialValue: true
551
- });
552
- if (isCancel(overwrite)) return abort$2();
553
- return overwrite;
423
+ async function writePackageJson(filePath, value) {
424
+ await writeText(filePath, JSON.stringify(value, null, 2) + "\n");
554
425
  }
555
- async function listPackageCandidates(rootDir, packageManager) {
556
- const candidates = /* @__PURE__ */ new Set();
557
- if (await pathExists(path.join(rootDir, "package.json"))) candidates.add(".");
558
- if (packageManager === "deno" && (await pathExists(path.join(rootDir, "deno.json")) || await pathExists(path.join(rootDir, "deno.jsonc")))) candidates.add(".");
559
- for (const base of ["packages", "apps"]) {
560
- const baseDir = path.join(rootDir, base);
561
- if (!await pathExists(baseDir)) continue;
562
- const entries = await readdir(baseDir, { withFileTypes: true });
563
- for (const entry of entries) {
564
- if (!entry.isDirectory()) continue;
565
- if (await pathExists(path.join(baseDir, entry.name, "package.json"))) candidates.add(path.posix.join(base, entry.name));
566
- }
567
- }
568
- return [...candidates];
569
- }
570
- async function detectWorkingDirectory(rootDir, candidates) {
571
- if (candidates.length === 1) return candidates[0];
572
- const rootScripts = (await readPackageJson(path.join(rootDir, "package.json")))?.scripts ?? {};
573
- const rootHasScripts = Object.keys(rootScripts).length > 0;
574
- const nonRoot = candidates.filter((c) => c !== ".");
575
- if (!rootHasScripts && nonRoot.length === 1) return nonRoot[0];
576
- return ".";
577
- }
578
- async function detectNodeMajorVersion(rootDir) {
579
- for (const file of [".nvmrc", ".node-version"]) {
580
- const filePath = path.join(rootDir, file);
581
- if (!await pathExists(filePath)) continue;
582
- const major = parseMajorVersion((await readFile(filePath, "utf8")).split("\n")[0]?.trim() ?? "");
583
- if (major) return major;
426
+ async function detectPackageManager(rootDir) {
427
+ const pmField = (await readPackageJson(path.join(rootDir, "package.json")))?.packageManager;
428
+ if (pmField) {
429
+ const pm = pmField.split("@")[0] ?? "";
430
+ if (isPackageManager(pm)) return pm;
584
431
  }
585
- const engine = (await readPackageJson(path.join(rootDir, "package.json")))?.engines?.node;
586
- if (!engine) return;
587
- const match = engine.match(/([0-9]{2,})/);
588
- if (!match) return;
589
- return Number.parseInt(match[1], 10);
590
- }
591
- function parseMajorVersion(input) {
592
- const trimmed = input.trim().replace(/^v/, "");
593
- const major = Number.parseInt(trimmed.split(".")[0] ?? "", 10);
594
- if (!Number.isFinite(major) || major <= 0) return;
595
- return major;
596
- }
597
- async function resolveCiCommands(rootDir, workingDirectory, packageManager) {
598
- if (packageManager === "deno") return {
599
- runLint: true,
600
- runFormatCheck: true,
601
- runTests: true
602
- };
603
- const pkg = await readPackageJson(path.join(rootDir, workingDirectory, "package.json"));
604
- if (!pkg) return abort$2({
605
- message: `Missing package.json in ${workingDirectory}`,
606
- exitCode: 1
607
- });
608
- const scripts = pkg.scripts ?? {};
609
- const hasLint = typeof scripts.lint === "string";
610
- const hasTest = typeof scripts.test === "string";
611
- const hasFormatCheck = typeof scripts["format:check"] === "string";
612
- const hasFmtCheck = typeof scripts["fmt:check"] === "string";
613
- const runLintDefault = hasLint;
614
- const runFormatCheckDefault = hasFormatCheck || hasFmtCheck;
615
- const runTestsDefault = hasTest;
616
- const runLint = await confirm({
617
- message: `CI: run lint${hasLint ? "" : " (no lint script detected)"}`,
618
- initialValue: runLintDefault
619
- });
620
- if (isCancel(runLint)) return abort$2();
621
- const runFormatCheck = await confirm({
622
- message: `CI: run format check${runFormatCheckDefault ? "" : " (no format check script detected)"}`,
623
- initialValue: runFormatCheckDefault
624
- });
625
- if (isCancel(runFormatCheck)) return abort$2();
626
- const runTests = await confirm({
627
- message: `CI: run tests${hasTest ? "" : " (no test script detected)"}`,
628
- initialValue: runTestsDefault
629
- });
630
- if (isCancel(runTests)) return abort$2();
631
- return {
632
- runLint,
633
- runFormatCheck,
634
- runTests,
635
- lintCommand: runLint && hasLint ? pmRun$1(packageManager, "lint") : runLint ? await promptCommand("Lint command", pmRun$1(packageManager, "lint")) : void 0,
636
- formatCheckCommand: runFormatCheck && hasFormatCheck ? pmRun$1(packageManager, "format:check") : runFormatCheck && hasFmtCheck ? pmRun$1(packageManager, "fmt:check") : runFormatCheck ? await promptCommand("Format check command", pmRun$1(packageManager, "format:check")) : void 0,
637
- testCommand: runTests && hasTest ? pmRun$1(packageManager, "test") : runTests ? await promptCommand("Test command", pmRun$1(packageManager, "test")) : void 0
638
- };
639
- }
640
- async function promptCommand(message, initialValue) {
641
- const value = await text({
642
- message,
643
- initialValue,
644
- validate: (v = "") => !v.trim() ? "Command is required" : void 0
645
- });
646
- if (isCancel(value)) return abort$2();
647
- return String(value).trim();
432
+ const candidates = [];
433
+ if (await pathExists(path.join(rootDir, "pnpm-lock.yaml"))) candidates.push("pnpm");
434
+ if (await pathExists(path.join(rootDir, "yarn.lock"))) candidates.push("yarn");
435
+ if (await pathExists(path.join(rootDir, "package-lock.json"))) candidates.push("npm");
436
+ if (await pathExists(path.join(rootDir, "bun.lockb"))) candidates.push("bun");
437
+ if (await pathExists(path.join(rootDir, "bun.lock"))) candidates.push("bun");
438
+ if (await pathExists(path.join(rootDir, "deno.json")) || await pathExists(path.join(rootDir, "deno.jsonc"))) candidates.push("deno");
439
+ return candidates.length === 1 ? candidates[0] : void 0;
648
440
  }
649
- function pmRun$1(pm, script) {
650
- switch (pm) {
651
- case "npm": return `npm run ${script}`;
652
- case "pnpm": return `pnpm run ${script}`;
653
- case "yarn": return `yarn ${script}`;
654
- case "bun": return `bun run ${script}`;
655
- case "deno": return script;
656
- }
441
+ function isPackageManager(value) {
442
+ return value === "npm" || value === "pnpm" || value === "yarn" || value === "bun" || value === "deno";
657
443
  }
658
444
 
659
445
  //#endregion
660
446
  //#region src/lib/exec.ts
661
447
  async function exec(command, args, opts = {}) {
662
- const resolved = resolveCommand$1(command);
448
+ const resolved = resolveCommand(command);
663
449
  return new Promise((resolve) => {
664
450
  const child = spawn(resolved, args, {
665
451
  cwd: opts.cwd,
@@ -671,52 +457,6 @@ async function exec(command, args, opts = {}) {
671
457
  child.on("error", () => resolve({ ok: false }));
672
458
  });
673
459
  }
674
- function resolveCommand$1(command) {
675
- if (process.platform !== "win32") return command;
676
- if (command === "npm") return "npm.cmd";
677
- if (command === "pnpm") return "pnpm.cmd";
678
- if (command === "yarn") return "yarn.cmd";
679
- return command;
680
- }
681
-
682
- //#endregion
683
- //#region src/lib/versions.ts
684
- async function detectPackageManagerVersion(pm) {
685
- switch (pm) {
686
- case "npm": return (await execCapture("npm", ["--version"])).stdout.trim() || void 0;
687
- case "pnpm": return (await execCapture("pnpm", ["--version"])).stdout.trim() || void 0;
688
- case "yarn": return (await execCapture("yarn", ["--version"])).stdout.trim() || void 0;
689
- case "bun": return (await execCapture("bun", ["--version"])).stdout.trim() || void 0;
690
- case "deno": return ((await execCapture("deno", ["--version"])).stdout.trim().split("\n")[0] ?? "").match(/deno\\s+([0-9]+\\.[0-9]+\\.[0-9]+)/)?.[1];
691
- }
692
- }
693
- async function execCapture(command, args) {
694
- const resolved = resolveCommand(command);
695
- return new Promise((resolve) => {
696
- const child = spawn(resolved, args, {
697
- cwd: os.tmpdir(),
698
- stdio: [
699
- "ignore",
700
- "pipe",
701
- "ignore"
702
- ],
703
- shell: false,
704
- env: process.env
705
- });
706
- const chunks = [];
707
- child.stdout.on("data", (d) => chunks.push(Buffer.from(d)));
708
- child.on("close", (code) => {
709
- resolve({
710
- ok: code === 0,
711
- stdout: Buffer.concat(chunks).toString("utf8")
712
- });
713
- });
714
- child.on("error", () => resolve({
715
- ok: false,
716
- stdout: ""
717
- }));
718
- });
719
- }
720
460
  function resolveCommand(command) {
721
461
  if (process.platform !== "win32") return command;
722
462
  if (command === "npm") return "npm.cmd";
@@ -727,7 +467,7 @@ function resolveCommand(command) {
727
467
 
728
468
  //#endregion
729
469
  //#region src/commands/init.ts
730
- function pmRun(pm, script) {
470
+ function pmRun$1(pm, script) {
731
471
  switch (pm) {
732
472
  case "npm": return `npm run ${script}`;
733
473
  case "pnpm": return `pnpm run ${script}`;
@@ -743,7 +483,7 @@ async function runInit({ nameArg }) {
743
483
  initialValue: nameArg ?? "my-frontend",
744
484
  validate: validateProjectName
745
485
  });
746
- if (isCancel(projectName)) return onCancel();
486
+ if (isCancel(projectName)) return onCancel$1();
747
487
  const packageManager = await select({
748
488
  message: "Package manager",
749
489
  initialValue: "pnpm",
@@ -770,37 +510,37 @@ async function runInit({ nameArg }) {
770
510
  }
771
511
  ]
772
512
  });
773
- if (isCancel(packageManager)) return onCancel();
513
+ if (isCancel(packageManager)) return onCancel$1();
774
514
  const pnpmWorkspace = packageManager === "pnpm" ? await confirm({
775
515
  message: "pnpm workspace mode (monorepo skeleton)?",
776
516
  initialValue: false
777
517
  }) : false;
778
- if (isCancel(pnpmWorkspace)) return onCancel();
518
+ if (isCancel(pnpmWorkspace)) return onCancel$1();
779
519
  const useOxlint = await confirm({
780
520
  message: "Enable oxlint (@kingsword/lint-config preset)?",
781
521
  initialValue: true
782
522
  });
783
- if (isCancel(useOxlint)) return onCancel();
523
+ if (isCancel(useOxlint)) return onCancel$1();
784
524
  const useOxfmt = await confirm({
785
525
  message: "Enable oxfmt (code formatting)?",
786
526
  initialValue: true
787
527
  });
788
- if (isCancel(useOxfmt)) return onCancel();
528
+ if (isCancel(useOxfmt)) return onCancel$1();
789
529
  const useVitest = await confirm({
790
530
  message: "Add Vitest?",
791
531
  initialValue: false
792
532
  });
793
- if (isCancel(useVitest)) return onCancel();
533
+ if (isCancel(useVitest)) return onCancel$1();
794
534
  const useTsdown = await confirm({
795
535
  message: "Add tsdown build?",
796
536
  initialValue: true
797
537
  });
798
- if (isCancel(useTsdown)) return onCancel();
538
+ if (isCancel(useTsdown)) return onCancel$1();
799
539
  const initGit = await confirm({
800
540
  message: "Initialize a git repository?",
801
541
  initialValue: true
802
542
  });
803
- if (isCancel(initGit)) return onCancel();
543
+ if (isCancel(initGit)) return onCancel$1();
804
544
  const githubActions = await select({
805
545
  message: "GitHub Actions workflows",
806
546
  initialValue: "ci",
@@ -819,7 +559,7 @@ async function runInit({ nameArg }) {
819
559
  }
820
560
  ]
821
561
  });
822
- if (isCancel(githubActions)) return onCancel();
562
+ if (isCancel(githubActions)) return onCancel$1();
823
563
  const releaseMode = githubActions === "ci+release" ? await select({
824
564
  message: "Release workflows",
825
565
  initialValue: "tag",
@@ -838,17 +578,17 @@ async function runInit({ nameArg }) {
838
578
  }
839
579
  ]
840
580
  }) : void 0;
841
- if (isCancel(releaseMode)) return onCancel();
581
+ if (isCancel(releaseMode)) return onCancel$1();
842
582
  const addDependabot = initGit && githubActions !== "none" ? await confirm({
843
583
  message: "Add Dependabot config (.github/dependabot.yml)?",
844
584
  initialValue: true
845
585
  }) : false;
846
- if (isCancel(addDependabot)) return onCancel();
586
+ if (isCancel(addDependabot)) return onCancel$1();
847
587
  const trustedPublishing = githubActions === "ci+release" && packageManager !== "deno" ? await confirm({
848
588
  message: "Release: npm trusted publishing (OIDC)?",
849
589
  initialValue: true
850
590
  }) : void 0;
851
- if (isCancel(trustedPublishing)) return onCancel();
591
+ if (isCancel(trustedPublishing)) return onCancel$1();
852
592
  const rootDir = path.resolve(process.cwd(), projectName);
853
593
  if (await pathExists(rootDir)) {
854
594
  cancel(`Directory already exists: ${rootDir}`);
@@ -859,6 +599,7 @@ async function runInit({ nameArg }) {
859
599
  const toolingDir = pnpmWorkspace ? rootDir : pkgDir;
860
600
  const packageUseOxlint = pnpmWorkspace ? false : useOxlint;
861
601
  const packageUseOxfmt = pnpmWorkspace ? false : useOxfmt;
602
+ const packageIncludeTypecheckWithoutOxlint = !(pnpmWorkspace && useOxlint);
862
603
  const pmVersion = await detectPackageManagerVersion(packageManager);
863
604
  const packageManagerField = pmVersion ? `${packageManager}@${pmVersion}` : `${packageManager}@latest`;
864
605
  await mkdir(path.join(pkgDir, "src"), { recursive: true });
@@ -895,6 +636,7 @@ async function runInit({ nameArg }) {
895
636
  packageManager: packageManagerField,
896
637
  typescriptVersion: "latest",
897
638
  useOxlint: packageUseOxlint,
639
+ includeTypecheckWithoutOxlint: packageIncludeTypecheckWithoutOxlint,
898
640
  oxlintVersion: "latest",
899
641
  oxlintTsgolintVersion: "latest",
900
642
  kingswordLintConfigVersion: "latest",
@@ -913,64 +655,416 @@ async function runInit({ nameArg }) {
913
655
  if (packageManager === "deno") await writeText(path.join(rootDir, "deno.json"), JSON.stringify({ nodeModulesDir: "auto" }, null, 2) + "\n");
914
656
  if (githubActions !== "none") {
915
657
  const workingDirectory = ".";
916
- const lintCommand = useOxlint && packageManager !== "deno" ? pmRun(packageManager, "lint") : void 0;
917
- const formatCheckCommand = useOxfmt && packageManager !== "deno" ? pmRun(packageManager, "format:check") : void 0;
918
- const testCommand = useVitest && packageManager !== "deno" ? pmRun(packageManager, "test") : void 0;
658
+ const lintCommand = useOxlint && packageManager !== "deno" ? pmRun$1(packageManager, "lint") : void 0;
659
+ const formatCheckCommand = useOxfmt && packageManager !== "deno" ? pmRun$1(packageManager, "format:check") : void 0;
660
+ const testCommand = useVitest && packageManager !== "deno" ? pmRun$1(packageManager, "test") : void 0;
919
661
  await writeText(path.join(rootDir, ".github/workflows/ci.yml"), githubCliCiWorkflowTemplate({
920
662
  packageManager,
921
- nodeVersion: 22,
663
+ nodeVersion: 22,
664
+ workingDirectory,
665
+ runLint: useOxlint,
666
+ runFormatCheck: useOxfmt,
667
+ runTests: useVitest,
668
+ lintCommand,
669
+ formatCheckCommand,
670
+ testCommand
671
+ }));
672
+ if (addDependabot) await writeText(path.join(rootDir, ".github/dependabot.yml"), githubDependabotTemplate({
673
+ packageManager,
674
+ workingDirectory
675
+ }));
676
+ }
677
+ if (githubActions === "ci+release") {
678
+ const workingDirectory = pnpmWorkspace ? path.posix.join("packages", projectName) : ".";
679
+ await writeText(path.join(rootDir, ".github/workflows/release.yml"), (releaseMode === "both" ? githubCliReleaseBothWorkflowTemplate : releaseMode === "commit" ? githubCliReleaseWorkflowTemplate : githubCliReleaseTagWorkflowTemplate)({
680
+ packageManager,
681
+ nodeVersion: 22,
682
+ workingDirectory,
683
+ trustedPublishing
684
+ }));
685
+ }
686
+ const canInstall = Boolean(pmVersion);
687
+ let installOk = false;
688
+ if (canInstall) {
689
+ const installSpinner = spinner();
690
+ installSpinner.start(`Installing dependencies with ${packageManager}`);
691
+ installOk = (await exec(packageManager, ["install"], { cwd: rootDir })).ok;
692
+ installSpinner.stop(installOk ? "Dependencies installed" : "Install failed (skipped)");
693
+ }
694
+ if (initGit) await exec("git", ["init"], { cwd: rootDir });
695
+ outro(`Done. Next:\n cd ${projectName}${!canInstall ? `\n (${packageManager} not found, run install manually)` : !installOk ? `\n (${packageManager} install failed, run install manually)` : ""}\n ${nextStepHint(packageManager)}`);
696
+ }
697
+ function validateProjectName(value) {
698
+ const name = (value ?? "").trim();
699
+ if (!name) return "Project name is required";
700
+ if (name.length > 214) return "Project name is too long";
701
+ if (name.startsWith(".")) return "Project name cannot start with '.'";
702
+ if (name.startsWith("_")) return "Project name cannot start with '_'";
703
+ if (!/^[A-Za-z0-9._-]+$/.test(name)) return "Use letters, numbers, '.', '_' or '-'";
704
+ }
705
+ function onCancel$1() {
706
+ cancel("Cancelled");
707
+ process.exitCode = 0;
708
+ }
709
+ function nextStepHint(pm) {
710
+ switch (pm) {
711
+ case "npm": return "npm run lint";
712
+ case "pnpm": return "pnpm run lint";
713
+ case "yarn": return "yarn lint";
714
+ case "bun": return "bun run lint";
715
+ case "deno": return "deno task lint # (or run the package.json scripts with your preferred runner)";
716
+ }
717
+ }
718
+
719
+ //#endregion
720
+ //#region src/commands/add.ts
721
+ async function runAdd({ nameArg, yes = false } = {}) {
722
+ intro("frontpl (add)");
723
+ const rootDir = process.cwd();
724
+ if (!await pathExists(path.join(rootDir, "pnpm-workspace.yaml"))) {
725
+ cancel("Missing pnpm-workspace.yaml. This command only supports pnpm workspace roots.");
726
+ process.exitCode = 1;
727
+ return;
728
+ }
729
+ const rootPkg = await readPackageJson(path.join(rootDir, "package.json"));
730
+ const detectedPm = await detectPackageManager(rootDir);
731
+ const packageManagerField = rootPkg?.packageManager?.trim();
732
+ if (!(packageManagerField?.startsWith("pnpm@") || detectedPm === "pnpm")) {
733
+ cancel("Only pnpm workspace projects are supported for `frontpl add`.");
734
+ process.exitCode = 1;
735
+ return;
736
+ }
737
+ const packageName = typeof nameArg === "string" && nameArg.trim().length > 0 ? resolvePackageNameFromArg(nameArg) : await text({
738
+ message: "Package name",
739
+ initialValue: "my-package",
740
+ validate: validateProjectName
741
+ });
742
+ if (!packageName) return;
743
+ if (isCancel(packageName)) return onCancel();
744
+ const packageDir = path.join(rootDir, "packages", packageName);
745
+ if (await pathExists(packageDir)) {
746
+ cancel(`Package already exists: ${packageDir}`);
747
+ process.exitCode = 1;
748
+ return;
749
+ }
750
+ const useVitestDefault = typeof rootPkg?.scripts?.test === "string";
751
+ const useTsdownDefault = typeof rootPkg?.scripts?.build === "string";
752
+ const useVitest = yes ? useVitestDefault : await confirm({
753
+ message: "Add Vitest?",
754
+ initialValue: useVitestDefault
755
+ });
756
+ if (isCancel(useVitest)) return onCancel();
757
+ const useTsdown = yes ? useTsdownDefault : await confirm({
758
+ message: "Add tsdown build?",
759
+ initialValue: useTsdownDefault
760
+ });
761
+ if (isCancel(useTsdown)) return onCancel();
762
+ const resolvedPackageManagerField = await resolvePnpmPackageManagerField(packageManagerField);
763
+ const rootHasOxlint = Boolean(rootPkg?.devDependencies?.oxlint) || Boolean(rootPkg?.devDependencies?.["oxlint-tsgolint"]) || await pathExists(path.join(rootDir, "oxlint.config.ts"));
764
+ const typescriptVersion = rootPkg?.devDependencies?.typescript ?? "latest";
765
+ const vitestVersion = rootPkg?.devDependencies?.vitest ?? "latest";
766
+ const tsdownVersion = rootPkg?.devDependencies?.tsdown ?? "latest";
767
+ await mkdir(path.join(packageDir, "src"), { recursive: true });
768
+ await Promise.all([
769
+ writeText(path.join(packageDir, "README.md"), readmeTemplate(packageName)),
770
+ writeText(path.join(packageDir, "src/index.ts"), srcIndexTemplate()),
771
+ writeText(path.join(packageDir, "tsconfig.json"), tsconfigTemplate()),
772
+ writeText(path.join(packageDir, "package.json"), packageJsonTemplate({
773
+ name: packageName,
774
+ packageManager: resolvedPackageManagerField,
775
+ typescriptVersion,
776
+ useOxlint: false,
777
+ includeTypecheckWithoutOxlint: !rootHasOxlint,
778
+ useOxfmt: false,
779
+ useVitest,
780
+ vitestVersion,
781
+ useTsdown,
782
+ tsdownVersion
783
+ }))
784
+ ]);
785
+ if (useVitest) await writeText(path.join(packageDir, "src/index.test.ts"), srcVitestTemplate());
786
+ if (useTsdown) await writeText(path.join(packageDir, "tsdown.config.ts"), tsdownConfigTemplate());
787
+ outro([
788
+ "Done. Added workspace package.",
789
+ `- path: packages/${packageName}`,
790
+ `- vitest: ${useVitest ? "enabled" : "disabled"}`,
791
+ `- tsdown: ${useTsdown ? "enabled" : "disabled"}`
792
+ ].join("\n"));
793
+ }
794
+ async function resolvePnpmPackageManagerField(existing) {
795
+ if (existing?.startsWith("pnpm@")) return existing;
796
+ const pnpmVersion = await detectPackageManagerVersion("pnpm");
797
+ return pnpmVersion ? `pnpm@${pnpmVersion}` : "pnpm@latest";
798
+ }
799
+ function resolvePackageNameFromArg(nameArg) {
800
+ const value = nameArg.trim();
801
+ const invalidReason = validateProjectName(value);
802
+ if (invalidReason) {
803
+ cancel(invalidReason);
804
+ process.exitCode = 1;
805
+ return;
806
+ }
807
+ return value;
808
+ }
809
+ function onCancel() {
810
+ cancel("Cancelled");
811
+ process.exitCode = 0;
812
+ }
813
+
814
+ //#endregion
815
+ //#region src/commands/ci.ts
816
+ async function runCi() {
817
+ try {
818
+ intro("frontpl (ci)");
819
+ const rootDir = process.cwd();
820
+ const detectedPackageManager = await detectPackageManager(rootDir);
821
+ const packageManager = await select({
822
+ message: detectedPackageManager ? `Package manager (detected: ${detectedPackageManager})` : "Package manager",
823
+ initialValue: detectedPackageManager ?? "pnpm",
824
+ options: [
825
+ {
826
+ value: "npm",
827
+ label: "npm"
828
+ },
829
+ {
830
+ value: "yarn",
831
+ label: "yarn"
832
+ },
833
+ {
834
+ value: "pnpm",
835
+ label: "pnpm"
836
+ },
837
+ {
838
+ value: "bun",
839
+ label: "bun"
840
+ },
841
+ {
842
+ value: "deno",
843
+ label: "deno"
844
+ }
845
+ ]
846
+ });
847
+ if (isCancel(packageManager)) return abort$2();
848
+ const candidates = await listPackageCandidates(rootDir, packageManager);
849
+ if (candidates.length === 0) {
850
+ cancel("No package found. Run this command in a project root (with package.json or deno.json).");
851
+ process.exitCode = 1;
852
+ return;
853
+ }
854
+ const initialWorkingDirectory = await detectWorkingDirectory(rootDir, candidates);
855
+ const workingDirectory = candidates.length === 1 ? candidates[0] : await select({
856
+ message: "Working directory (package folder)",
857
+ initialValue: initialWorkingDirectory,
858
+ options: candidates.map((c) => ({
859
+ value: c,
860
+ label: c
861
+ }))
862
+ });
863
+ if (isCancel(workingDirectory)) return abort$2();
864
+ const nodeVersionDefault = await detectNodeMajorVersion(rootDir) ?? 22;
865
+ const nodeVersionText = await text({
866
+ message: "Node.js major version (for GitHub Actions)",
867
+ initialValue: String(nodeVersionDefault),
868
+ validate: (value = "") => {
869
+ const major = Number.parseInt(value.trim(), 10);
870
+ if (!Number.isFinite(major) || major <= 0) return "Enter a valid major version (e.g. 22)";
871
+ }
872
+ });
873
+ if (isCancel(nodeVersionText)) return abort$2();
874
+ const nodeVersion = Number.parseInt(String(nodeVersionText).trim(), 10);
875
+ const { runLint, runFormatCheck, runTests, lintCommand, formatCheckCommand, testCommand } = await resolveCiCommands(rootDir, workingDirectory, packageManager);
876
+ const addRelease = await confirm({
877
+ message: "Add release workflow too?",
878
+ initialValue: true
879
+ });
880
+ if (isCancel(addRelease)) return abort$2();
881
+ const releaseMode = addRelease ? await select({
882
+ message: "Release workflows",
883
+ initialValue: "tag",
884
+ options: [
885
+ {
886
+ value: "tag",
887
+ label: "Tag push (vX.Y.Z) — recommended"
888
+ },
889
+ {
890
+ value: "commit",
891
+ label: "Release commit (chore(release): vX.Y.Z) — legacy"
892
+ },
893
+ {
894
+ value: "both",
895
+ label: "Both (tag + commit)"
896
+ }
897
+ ]
898
+ }) : void 0;
899
+ if (isCancel(releaseMode)) return abort$2();
900
+ const trustedPublishing = addRelease && packageManager !== "deno" ? await confirm({
901
+ message: "Release: npm trusted publishing (OIDC)?",
902
+ initialValue: true
903
+ }) : void 0;
904
+ if (isCancel(trustedPublishing)) return abort$2();
905
+ const addDependabot = await pathExists(path.join(rootDir, ".git")) ? await confirm({
906
+ message: "Add/update Dependabot config (.github/dependabot.yml)?",
907
+ initialValue: true
908
+ }) : false;
909
+ if (isCancel(addDependabot)) return abort$2();
910
+ const ciWorkflowPath = path.join(rootDir, ".github/workflows/ci.yml");
911
+ const releaseWorkflowPath = path.join(rootDir, ".github/workflows/release.yml");
912
+ const dependabotPath = path.join(rootDir, ".github/dependabot.yml");
913
+ if (!await confirmOverwriteIfExists(ciWorkflowPath, ".github/workflows/ci.yml")) {
914
+ cancel("Skipped CI workflow");
915
+ process.exitCode = 0;
916
+ return;
917
+ }
918
+ await writeText(ciWorkflowPath, githubCliCiWorkflowTemplate({
919
+ packageManager,
920
+ nodeVersion,
922
921
  workingDirectory,
923
- runLint: useOxlint,
924
- runFormatCheck: useOxfmt,
925
- runTests: useVitest,
922
+ runLint,
923
+ runFormatCheck,
924
+ runTests,
926
925
  lintCommand,
927
926
  formatCheckCommand,
928
927
  testCommand
929
928
  }));
930
- if (addDependabot) await writeText(path.join(rootDir, ".github/dependabot.yml"), githubDependabotTemplate({
931
- packageManager,
932
- workingDirectory
933
- }));
929
+ if (addRelease) {
930
+ if (await confirmOverwriteIfExists(releaseWorkflowPath, ".github/workflows/release.yml")) await writeText(releaseWorkflowPath, (releaseMode === "both" ? githubCliReleaseBothWorkflowTemplate : releaseMode === "commit" ? githubCliReleaseWorkflowTemplate : githubCliReleaseTagWorkflowTemplate)({
931
+ packageManager,
932
+ nodeVersion,
933
+ workingDirectory,
934
+ trustedPublishing
935
+ }));
936
+ }
937
+ if (addDependabot) {
938
+ if (await confirmOverwriteIfExists(dependabotPath, ".github/dependabot.yml")) await writeText(dependabotPath, githubDependabotTemplate({
939
+ packageManager,
940
+ workingDirectory
941
+ }));
942
+ }
943
+ outro(addRelease ? "Done. Generated CI + release workflows (and optional Dependabot)." : "Done. Generated CI workflow (and optional Dependabot).");
944
+ } catch (err) {
945
+ if (err instanceof CancelledError$2) return;
946
+ throw err;
934
947
  }
935
- if (githubActions === "ci+release") {
936
- const workingDirectory = pnpmWorkspace ? path.posix.join("packages", projectName) : ".";
937
- await writeText(path.join(rootDir, ".github/workflows/release.yml"), (releaseMode === "both" ? githubCliReleaseBothWorkflowTemplate : releaseMode === "commit" ? githubCliReleaseWorkflowTemplate : githubCliReleaseTagWorkflowTemplate)({
938
- packageManager,
939
- nodeVersion: 22,
940
- workingDirectory,
941
- trustedPublishing
942
- }));
948
+ }
949
+ var CancelledError$2 = class extends Error {
950
+ constructor() {
951
+ super("Cancelled");
943
952
  }
944
- const canInstall = Boolean(pmVersion);
945
- let installOk = false;
946
- if (canInstall) {
947
- const installSpinner = spinner();
948
- installSpinner.start(`Installing dependencies with ${packageManager}`);
949
- installOk = (await exec(packageManager, ["install"], { cwd: rootDir })).ok;
950
- installSpinner.stop(installOk ? "Dependencies installed" : "Install failed (skipped)");
953
+ };
954
+ function abort$2(opts = {}) {
955
+ cancel(opts.message ?? "Cancelled");
956
+ process.exitCode = opts.exitCode ?? 0;
957
+ throw new CancelledError$2();
958
+ }
959
+ async function confirmOverwriteIfExists(absPath, label) {
960
+ if (!await pathExists(absPath)) return true;
961
+ const overwrite = await confirm({
962
+ message: `Overwrite existing ${label}?`,
963
+ initialValue: true
964
+ });
965
+ if (isCancel(overwrite)) return abort$2();
966
+ return overwrite;
967
+ }
968
+ async function listPackageCandidates(rootDir, packageManager) {
969
+ const candidates = /* @__PURE__ */ new Set();
970
+ if (await pathExists(path.join(rootDir, "package.json"))) candidates.add(".");
971
+ if (packageManager === "deno" && (await pathExists(path.join(rootDir, "deno.json")) || await pathExists(path.join(rootDir, "deno.jsonc")))) candidates.add(".");
972
+ for (const base of ["packages", "apps"]) {
973
+ const baseDir = path.join(rootDir, base);
974
+ if (!await pathExists(baseDir)) continue;
975
+ const entries = await readdir(baseDir, { withFileTypes: true });
976
+ for (const entry of entries) {
977
+ if (!entry.isDirectory()) continue;
978
+ if (await pathExists(path.join(baseDir, entry.name, "package.json"))) candidates.add(path.posix.join(base, entry.name));
979
+ }
951
980
  }
952
- if (initGit) await exec("git", ["init"], { cwd: rootDir });
953
- outro(`Done. Next:\n cd ${projectName}${!canInstall ? `\n (${packageManager} not found, run install manually)` : !installOk ? `\n (${packageManager} install failed, run install manually)` : ""}\n ${nextStepHint(packageManager)}`);
981
+ return [...candidates];
954
982
  }
955
- function validateProjectName(value) {
956
- const name = (value ?? "").trim();
957
- if (!name) return "Project name is required";
958
- if (name.length > 214) return "Project name is too long";
959
- if (name.startsWith(".")) return "Project name cannot start with '.'";
960
- if (name.startsWith("_")) return "Project name cannot start with '_'";
961
- if (!/^[A-Za-z0-9._-]+$/.test(name)) return "Use letters, numbers, '.', '_' or '-'";
983
+ async function detectWorkingDirectory(rootDir, candidates) {
984
+ if (candidates.length === 1) return candidates[0];
985
+ const rootScripts = (await readPackageJson(path.join(rootDir, "package.json")))?.scripts ?? {};
986
+ const rootHasScripts = Object.keys(rootScripts).length > 0;
987
+ const nonRoot = candidates.filter((c) => c !== ".");
988
+ if (!rootHasScripts && nonRoot.length === 1) return nonRoot[0];
989
+ return ".";
962
990
  }
963
- function onCancel() {
964
- cancel("Cancelled");
965
- process.exitCode = 0;
991
+ async function detectNodeMajorVersion(rootDir) {
992
+ for (const file of [".nvmrc", ".node-version"]) {
993
+ const filePath = path.join(rootDir, file);
994
+ if (!await pathExists(filePath)) continue;
995
+ const major = parseMajorVersion((await readFile(filePath, "utf8")).split("\n")[0]?.trim() ?? "");
996
+ if (major) return major;
997
+ }
998
+ const engine = (await readPackageJson(path.join(rootDir, "package.json")))?.engines?.node;
999
+ if (!engine) return;
1000
+ const match = engine.match(/([0-9]{2,})/);
1001
+ if (!match) return;
1002
+ return Number.parseInt(match[1], 10);
966
1003
  }
967
- function nextStepHint(pm) {
1004
+ function parseMajorVersion(input) {
1005
+ const trimmed = input.trim().replace(/^v/, "");
1006
+ const major = Number.parseInt(trimmed.split(".")[0] ?? "", 10);
1007
+ if (!Number.isFinite(major) || major <= 0) return;
1008
+ return major;
1009
+ }
1010
+ async function resolveCiCommands(rootDir, workingDirectory, packageManager) {
1011
+ if (packageManager === "deno") return {
1012
+ runLint: true,
1013
+ runFormatCheck: true,
1014
+ runTests: true
1015
+ };
1016
+ const pkg = await readPackageJson(path.join(rootDir, workingDirectory, "package.json"));
1017
+ if (!pkg) return abort$2({
1018
+ message: `Missing package.json in ${workingDirectory}`,
1019
+ exitCode: 1
1020
+ });
1021
+ const scripts = pkg.scripts ?? {};
1022
+ const hasLint = typeof scripts.lint === "string";
1023
+ const hasTest = typeof scripts.test === "string";
1024
+ const hasFormatCheck = typeof scripts["format:check"] === "string";
1025
+ const runLintDefault = hasLint;
1026
+ const runFormatCheckDefault = hasFormatCheck;
1027
+ const runTestsDefault = hasTest;
1028
+ const runLint = await confirm({
1029
+ message: `CI: run lint${hasLint ? "" : " (no lint script detected)"}`,
1030
+ initialValue: runLintDefault
1031
+ });
1032
+ if (isCancel(runLint)) return abort$2();
1033
+ const runFormatCheck = await confirm({
1034
+ message: `CI: run format check${runFormatCheckDefault ? "" : " (no format check script detected)"}`,
1035
+ initialValue: runFormatCheckDefault
1036
+ });
1037
+ if (isCancel(runFormatCheck)) return abort$2();
1038
+ const runTests = await confirm({
1039
+ message: `CI: run tests${hasTest ? "" : " (no test script detected)"}`,
1040
+ initialValue: runTestsDefault
1041
+ });
1042
+ if (isCancel(runTests)) return abort$2();
1043
+ return {
1044
+ runLint,
1045
+ runFormatCheck,
1046
+ runTests,
1047
+ lintCommand: runLint && hasLint ? pmRun(packageManager, "lint") : runLint ? await promptCommand("Lint command", pmRun(packageManager, "lint")) : void 0,
1048
+ formatCheckCommand: runFormatCheck && hasFormatCheck ? pmRun(packageManager, "format:check") : runFormatCheck ? await promptCommand("Format check command", pmRun(packageManager, "format:check")) : void 0,
1049
+ testCommand: runTests && hasTest ? pmRun(packageManager, "test") : runTests ? await promptCommand("Test command", pmRun(packageManager, "test")) : void 0
1050
+ };
1051
+ }
1052
+ async function promptCommand(message, initialValue) {
1053
+ const value = await text({
1054
+ message,
1055
+ initialValue,
1056
+ validate: (v = "") => !v.trim() ? "Command is required" : void 0
1057
+ });
1058
+ if (isCancel(value)) return abort$2();
1059
+ return String(value).trim();
1060
+ }
1061
+ function pmRun(pm, script) {
968
1062
  switch (pm) {
969
- case "npm": return "npm run lint";
970
- case "pnpm": return "pnpm run lint";
971
- case "yarn": return "yarn lint";
972
- case "bun": return "bun run lint";
973
- case "deno": return "deno task lint # (or run the package.json scripts with your preferred runner)";
1063
+ case "npm": return `npm run ${script}`;
1064
+ case "pnpm": return `pnpm run ${script}`;
1065
+ case "yarn": return `yarn ${script}`;
1066
+ case "bun": return `bun run ${script}`;
1067
+ case "deno": return script;
974
1068
  }
975
1069
  }
976
1070
 
@@ -1223,7 +1317,9 @@ function abort$1(opts = {}) {
1223
1317
  //#region src/commands/oxfmt.ts
1224
1318
  const OXFMT_SCRIPTS = {
1225
1319
  format: "oxfmt",
1226
- "format:check": "oxfmt --check",
1320
+ "format:check": "oxfmt --check"
1321
+ };
1322
+ const OXFMT_LEGACY_SCRIPTS = {
1227
1323
  fmt: "oxfmt",
1228
1324
  "fmt:check": "oxfmt --check"
1229
1325
  };
@@ -1278,6 +1374,7 @@ async function runOxfmt({ yes = false } = {}) {
1278
1374
  rootDir
1279
1375
  });
1280
1376
  const scriptSummary = stats.scriptsUpdated.length > 0 ? `updated scripts: ${stats.scriptsUpdated.join(", ")}` : stats.scriptsKept.length > 0 ? `kept existing scripts: ${stats.scriptsKept.join(", ")}` : "scripts already aligned";
1377
+ const legacyScriptSummary = stats.removedLegacyScripts.length > 0 ? `removed legacy scripts: ${stats.removedLegacyScripts.join(", ")}` : "no legacy scripts removed";
1281
1378
  const depSummary = stats.addedOxfmtDependency ? "added devDependency: oxfmt" : "devDependency oxfmt already present";
1282
1379
  const removedDepsSummary = stats.removedDependencies.length > 0 ? `removed prettier deps: ${stats.removedDependencies.join(", ")}` : "no prettier deps removed";
1283
1380
  const removedPackageJsonPrettierSummary = stats.removedPackageJsonPrettierConfig ? "removed package.json#prettier" : "no package.json#prettier removed";
@@ -1287,6 +1384,7 @@ async function runOxfmt({ yes = false } = {}) {
1287
1384
  outro([
1288
1385
  "Done. Applied oxfmt migration.",
1289
1386
  `- ${scriptSummary}`,
1387
+ `- ${legacyScriptSummary}`,
1290
1388
  `- ${depSummary}`,
1291
1389
  `- ${removedDepsSummary}`,
1292
1390
  `- ${removedPackageJsonPrettierSummary}`,
@@ -1319,6 +1417,12 @@ async function migrateToOxfmt(opts) {
1319
1417
  scripts[name] = command;
1320
1418
  scriptsUpdated.push(name);
1321
1419
  }
1420
+ const removedLegacyScripts = [];
1421
+ for (const [name, command] of Object.entries(OXFMT_LEGACY_SCRIPTS)) {
1422
+ if (scripts[name] !== command) continue;
1423
+ delete scripts[name];
1424
+ removedLegacyScripts.push(name);
1425
+ }
1322
1426
  pkg.scripts = scripts;
1323
1427
  const devDependencies = { ...pkg.devDependencies };
1324
1428
  let addedOxfmtDependency = false;
@@ -1356,6 +1460,7 @@ async function migrateToOxfmt(opts) {
1356
1460
  return {
1357
1461
  scriptsUpdated,
1358
1462
  scriptsKept,
1463
+ removedLegacyScripts,
1359
1464
  addedOxfmtDependency,
1360
1465
  removedPackageJsonPrettierConfig,
1361
1466
  removedDependencies,
@@ -1487,4 +1592,4 @@ function abort(opts = {}) {
1487
1592
  }
1488
1593
 
1489
1594
  //#endregion
1490
- export { runCi as a, oxlintConfigTemplate as c, validateProjectName as i, packageJsonTemplate as l, runOxlint as n, githubCliCiWorkflowTemplate as o, runInit as r, githubDependabotTemplate as s, runOxfmt as t, workspaceRootPackageJsonTemplate as u };
1595
+ export { runInit as a, githubDependabotTemplate as c, workspaceRootPackageJsonTemplate as d, runAdd as i, oxlintConfigTemplate as l, runOxlint as n, validateProjectName as o, runCi as r, githubCliCiWorkflowTemplate as s, runOxfmt as t, packageJsonTemplate as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frontpl",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Interactive CLI to scaffold standardized frontend project templates.",
5
5
  "keywords": [
6
6
  "cli",
@@ -44,8 +44,6 @@
44
44
  "lint:fix": "oxlint --type-aware --type-check --fix",
45
45
  "format": "oxfmt",
46
46
  "format:check": "oxfmt --check",
47
- "fmt": "pnpm run format",
48
- "fmt:check": "pnpm run format:check",
49
47
  "prepublishOnly": "pnpm run build"
50
48
  },
51
49
  "dependencies": {