frontpl 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,6 +27,7 @@ frontpl init my-frontend
27
27
  Follow the prompts to choose:
28
28
 
29
29
  - Package manager (`npm`/`pnpm`/`yarn`/`bun`/`deno`)
30
+ - Optional `pnpm workspace` mode (monorepo skeleton)
30
31
  - Optional tooling: `oxlint`, `oxfmt`, `vitest`, `tsdown`
31
32
 
32
33
  When `oxlint` is enabled, generated projects use `@kingsword/lint-config` via `oxlint.config.ts`.
@@ -42,15 +43,23 @@ Generated lint-related dependencies (`oxlint`, `oxlint-tsgolint`, `oxfmt`, `@kin
42
43
  ### `frontpl [name]` / `frontpl init [name]`
43
44
 
44
45
  Scaffold a new project into `./<name>` (or prompt for a name when omitted).
46
+ Project names support letters (including uppercase/camel case), numbers, `.`, `_`, `-` (cannot start with `.` or `_`).
45
47
 
46
48
  Generated output includes (based on options):
47
49
 
48
50
  - `.editorconfig`, `.gitignore`, `.gitattributes`
49
51
  - `package.json` (+ scripts like optional `lint`, `format:check`, `test`, `build`)
50
52
  - `tsconfig.json`, `src/index.ts`
53
+ - Relative TypeScript imports use explicit `.ts` extensions (e.g. generated `src/index.test.ts`)
51
54
  - Optional configs: `oxlint.config.ts`, `.oxfmtrc.json`, `tsdown.config.ts`
52
55
  - Optional GitHub Actions workflows in `.github/workflows/`
53
56
 
57
+ When `pnpm workspace mode` is enabled:
58
+
59
+ - Root contains `pnpm-workspace.yaml` and the workspace `package.json`
60
+ - `oxlint`/`oxfmt` scripts, dependencies, and config files are generated at the workspace root
61
+ - App/library package is scaffolded under `packages/<name>/` with its own `package.json`, `src`, and `tsconfig.json`
62
+
54
63
  ### `frontpl ci`
55
64
 
56
65
  Add or update CI/Release workflows for an existing project (run it in your repo root).
@@ -64,6 +73,52 @@ What it does:
64
73
  - Optionally generates `.github/workflows/release.yml` (tag/commit/both)
65
74
  - Optionally generates `.github/dependabot.yml` with grouped updates (`dependencies`, `github-actions`)
66
75
 
76
+ ### `frontpl oxlint`
77
+
78
+ Add/migrate linting in the current project to `oxlint`.
79
+
80
+ What it does:
81
+
82
+ - Asks strategy interactively:
83
+ - Migrate gradually (keep existing ESLint assets)
84
+ - Replace ESLint directly (current mode)
85
+ - Ensures `package.json` scripts use:
86
+ - `lint`: `oxlint --type-aware --type-check`
87
+ - `lint:fix`: `oxlint --type-aware --type-check --fix`
88
+ - Removes `typecheck: tsc --noEmit` when confirmed (or by default with `--yes`)
89
+ - Ensures devDependencies exist:
90
+ - `oxlint`
91
+ - `oxlint-tsgolint`
92
+ - `@kingsword/lint-config`
93
+ - Creates or updates `oxlint.config.ts` using `@kingsword/lint-config`
94
+ - In replace mode, removes ESLint deps/configs (`eslint*`, `@eslint/*`, `@typescript-eslint/*`, `eslintConfig`, `.eslintrc*`, `eslint.config.*`)
95
+ - Optionally installs dependencies with detected package manager
96
+
97
+ Use `--yes` (or `-y`) to skip confirmations and apply default choices.
98
+ With `--yes`, strategy defaults to `replace`.
99
+
100
+ ### `frontpl oxfmt`
101
+
102
+ Add/migrate formatting in the current project to `oxfmt`.
103
+
104
+ What it does:
105
+
106
+ - Asks config strategy interactively:
107
+ - Migrate from Prettier (`oxfmt --migrate=prettier`)
108
+ - Rebuild `.oxfmtrc.json` (current mode)
109
+ - Ensures `package.json` scripts use:
110
+ - `format`: `oxfmt`
111
+ - `format:check`: `oxfmt --check`
112
+ - `fmt`: `oxfmt`
113
+ - `fmt:check`: `oxfmt --check`
114
+ - Ensures `devDependencies.oxfmt` exists (defaults to `latest` when missing)
115
+ - Creates or updates `.oxfmtrc.json`
116
+ - Optionally removes `prettier` / `prettier-plugin-*` / `@prettier/plugin-*` dependencies, `package.json#prettier`, and Prettier config files (`.prettierrc*`, `prettier.config.*`)
117
+ - Optionally installs dependencies with detected package manager
118
+
119
+ Use `--yes` (or `-y`) to skip confirmations and apply default choices.
120
+ With `--yes`, config strategy defaults to rebuild `.oxfmtrc.json`.
121
+
67
122
  ## GitHub Actions (CI + Release)
68
123
 
69
124
  frontpl generates workflows that call reusable workflows from `kingsword09/workflows` (pinned to commit SHA + `# vX.Y.Z` comment by default):
@@ -90,6 +145,7 @@ When CI workflows are enabled, frontpl can also generate `.github/dependabot.yml
90
145
  - Keeps `github-actions` updates enabled
91
146
  - Adds grouped dependencies updates (`groups.dependencies`)
92
147
  - Uses the selected `workingDirectory` (`.` -> `/`, monorepo package -> `/packages/<name>`)
148
+ - In `frontpl init` + `pnpm workspace mode`, default `workingDirectory` is workspace root (`/`)
93
149
  - Maps JavaScript package managers (`npm`/`pnpm`/`yarn`/`bun`) to Dependabot `package-ecosystem: "npm"`
94
150
 
95
151
  ## Development
@@ -100,6 +156,8 @@ pnpm run lint
100
156
  pnpm run build
101
157
  node dist/cli.mjs --help
102
158
  node dist/cli.mjs ci
159
+ node dist/cli.mjs oxlint --help
160
+ node dist/cli.mjs oxfmt --help
103
161
  ```
104
162
 
105
163
  ## Lint preset
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as runCi, t as runInit } from "./init-oJwslpqP.mjs";
2
+ import { a as runCi, n as runOxlint, r as runInit, t as runOxfmt } from "./oxfmt-DlwbhnpJ.mjs";
3
3
  import bin from "tiny-bin";
4
4
 
5
5
  //#region src/cli.ts
@@ -10,6 +10,10 @@ 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("oxlint", "Add/migrate linter to oxlint in current project").option("--yes, -y", "Skip confirmations and use defaults").action(async (options) => {
14
+ await runOxlint({ yes: options.yes === true });
15
+ }).command("oxfmt", "Add/migrate formatter to oxfmt in current project").option("--yes, -y", "Skip confirmations and use defaults").action(async (options) => {
16
+ await runOxfmt({ yes: options.yes === true });
13
17
  }).run();
14
18
  }
15
19
  main();
package/dist/index.d.mts CHANGED
@@ -7,6 +7,23 @@ declare function runInit({
7
7
  }: {
8
8
  nameArg?: string;
9
9
  }): Promise<void>;
10
+ declare function validateProjectName(value: string | undefined): "Project name is required" | "Project name is too long" | "Project name cannot start with '.'" | "Project name cannot start with '_'" | "Use letters, numbers, '.', '_' or '-'" | undefined;
11
+ //#endregion
12
+ //#region src/commands/oxlint.d.ts
13
+ type CommandOptions$1 = {
14
+ yes?: boolean;
15
+ };
16
+ declare function runOxlint({
17
+ yes
18
+ }?: CommandOptions$1): Promise<void>;
19
+ //#endregion
20
+ //#region src/commands/oxfmt.d.ts
21
+ type CommandOptions = {
22
+ yes?: boolean;
23
+ };
24
+ declare function runOxfmt({
25
+ yes
26
+ }?: CommandOptions): Promise<void>;
10
27
  //#endregion
11
28
  //#region src/lib/templates.d.ts
12
29
  declare function oxlintConfigTemplate({
@@ -29,6 +46,18 @@ declare function packageJsonTemplate(opts: {
29
46
  useTsdown: boolean;
30
47
  tsdownVersion?: string;
31
48
  }): string;
49
+ declare function workspaceRootPackageJsonTemplate(opts: {
50
+ name: string;
51
+ packageManager: string;
52
+ useOxlint: boolean;
53
+ oxlintVersion?: string;
54
+ oxlintTsgolintVersion?: string;
55
+ kingswordLintConfigVersion?: string;
56
+ useOxfmt: boolean;
57
+ oxfmtVersion?: string;
58
+ useVitest: boolean;
59
+ useTsdown: boolean;
60
+ }): string;
32
61
  declare function githubCliCiWorkflowTemplate(opts: {
33
62
  packageManager: "npm" | "pnpm" | "yarn" | "bun" | "deno";
34
63
  nodeVersion: number;
@@ -48,4 +77,4 @@ declare function githubDependabotTemplate(opts: {
48
77
  workingDirectory: string;
49
78
  }): string;
50
79
  //#endregion
51
- export { githubCliCiWorkflowTemplate, githubDependabotTemplate, oxlintConfigTemplate, packageJsonTemplate, runCi, runInit };
80
+ export { githubCliCiWorkflowTemplate, githubDependabotTemplate, oxlintConfigTemplate, packageJsonTemplate, runCi, runInit, runOxfmt, runOxlint, validateProjectName, workspaceRootPackageJsonTemplate };
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { a as oxlintConfigTemplate, i as githubDependabotTemplate, n as runCi, o as packageJsonTemplate, r as githubCliCiWorkflowTemplate, t as runInit } from "./init-oJwslpqP.mjs";
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";
2
2
 
3
- export { githubCliCiWorkflowTemplate, githubDependabotTemplate, oxlintConfigTemplate, packageJsonTemplate, runCi, runInit };
3
+ export { githubCliCiWorkflowTemplate, githubDependabotTemplate, oxlintConfigTemplate, packageJsonTemplate, runCi, runInit, runOxfmt, runOxlint, validateProjectName, workspaceRootPackageJsonTemplate };
@@ -1,5 +1,5 @@
1
1
  import { cancel, confirm, intro, isCancel, outro, select, spinner, text } from "@clack/prompts";
2
- import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
2
+ import { access, mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import process from "node:process";
5
5
  import { spawn } from "node:child_process";
@@ -11,6 +11,48 @@ async function writeText(filePath, contents) {
11
11
  await writeFile(filePath, contents, "utf8");
12
12
  }
13
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;
32
+ }
33
+ }
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;
51
+ }
52
+ function isPackageManager(value) {
53
+ return value === "npm" || value === "pnpm" || value === "yarn" || value === "bun" || value === "deno";
54
+ }
55
+
14
56
  //#endregion
15
57
  //#region src/lib/templates.ts
16
58
  function editorconfigTemplate() {
@@ -69,7 +111,7 @@ function srcIndexTemplate() {
69
111
  function srcVitestTemplate() {
70
112
  return [
71
113
  "import { describe, expect, it } from \"vitest\";",
72
- "import { hello } from \"./index.js\";",
114
+ "import { hello } from \"./index.ts\";",
73
115
  "",
74
116
  "describe(\"hello\", () => {",
75
117
  " it(\"greets\", () => {",
@@ -110,28 +152,35 @@ function tsdownConfigTemplate() {
110
152
  ""
111
153
  ].join("\n");
112
154
  }
113
- function packageJsonTemplate(opts) {
114
- const scripts = {};
155
+ function applyLintAndFormatScripts(scripts, opts) {
115
156
  if (opts.useOxlint) {
116
157
  const oxlintCmd = "oxlint --type-aware --type-check";
117
158
  scripts.lint = oxlintCmd;
118
159
  scripts["lint:fix"] = `${oxlintCmd} --fix`;
119
- } else scripts.typecheck = "tsc --noEmit";
160
+ }
120
161
  if (opts.useOxfmt) {
121
162
  scripts.format = "oxfmt";
122
163
  scripts["format:check"] = "oxfmt --check";
123
164
  scripts.fmt = "oxfmt";
124
165
  scripts["fmt:check"] = "oxfmt --check";
125
166
  }
126
- if (opts.useVitest) scripts.test = "vitest";
127
- if (opts.useTsdown) scripts.build = "tsdown";
128
- const devDependencies = { typescript: opts.typescriptVersion };
167
+ }
168
+ function applyLintAndFormatDependencies(devDependencies, opts) {
129
169
  if (opts.useOxlint) {
130
170
  if (opts.oxlintVersion) devDependencies.oxlint = opts.oxlintVersion;
131
171
  if (opts.oxlintTsgolintVersion) devDependencies["oxlint-tsgolint"] = opts.oxlintTsgolintVersion;
132
172
  if (opts.kingswordLintConfigVersion) devDependencies["@kingsword/lint-config"] = opts.kingswordLintConfigVersion;
133
173
  }
134
174
  if (opts.useOxfmt && opts.oxfmtVersion) devDependencies.oxfmt = opts.oxfmtVersion;
175
+ }
176
+ function packageJsonTemplate(opts) {
177
+ const scripts = {};
178
+ if (!opts.useOxlint) scripts.typecheck = "tsc --noEmit";
179
+ applyLintAndFormatScripts(scripts, opts);
180
+ if (opts.useVitest) scripts.test = "vitest";
181
+ if (opts.useTsdown) scripts.build = "tsdown";
182
+ const devDependencies = { typescript: opts.typescriptVersion };
183
+ applyLintAndFormatDependencies(devDependencies, opts);
135
184
  if (opts.useVitest && opts.vitestVersion) devDependencies.vitest = opts.vitestVersion;
136
185
  if (opts.useTsdown && opts.tsdownVersion) devDependencies.tsdown = opts.tsdownVersion;
137
186
  return JSON.stringify({
@@ -144,6 +193,22 @@ function packageJsonTemplate(opts) {
144
193
  packageManager: opts.packageManager
145
194
  }, null, 2) + "\n";
146
195
  }
196
+ function workspaceRootPackageJsonTemplate(opts) {
197
+ const scripts = {};
198
+ applyLintAndFormatScripts(scripts, opts);
199
+ if (opts.useVitest) scripts.test = "pnpm -r --if-present run test";
200
+ if (opts.useTsdown) scripts.build = "pnpm -r --if-present run build";
201
+ const devDependencies = {};
202
+ applyLintAndFormatDependencies(devDependencies, opts);
203
+ const manifest = {
204
+ name: opts.name,
205
+ private: true,
206
+ packageManager: opts.packageManager
207
+ };
208
+ if (Object.keys(scripts).length > 0) manifest.scripts = scripts;
209
+ if (Object.keys(devDependencies).length > 0) manifest.devDependencies = devDependencies;
210
+ return JSON.stringify(manifest, null, 2) + "\n";
211
+ }
147
212
  const DEFAULT_WORKFLOWS_REF = "7320d30bcd47cee17cc2d8d28250ba1ab1f742b8";
148
213
  const DEFAULT_WORKFLOWS_VERSION = "v1.0.3";
149
214
  function resolveWorkflowsPin(opts) {
@@ -333,17 +398,6 @@ function yamlString(value) {
333
398
  return JSON.stringify(value);
334
399
  }
335
400
 
336
- //#endregion
337
- //#region src/lib/utils.ts
338
- async function pathExists(pathname) {
339
- try {
340
- await access(pathname);
341
- return true;
342
- } catch {
343
- return false;
344
- }
345
- }
346
-
347
401
  //#endregion
348
402
  //#region src/commands/ci.ts
349
403
  async function runCi() {
@@ -377,7 +431,7 @@ async function runCi() {
377
431
  }
378
432
  ]
379
433
  });
380
- if (isCancel(packageManager)) return abort();
434
+ if (isCancel(packageManager)) return abort$2();
381
435
  const candidates = await listPackageCandidates(rootDir, packageManager);
382
436
  if (candidates.length === 0) {
383
437
  cancel("No package found. Run this command in a project root (with package.json or deno.json).");
@@ -393,7 +447,7 @@ async function runCi() {
393
447
  label: c
394
448
  }))
395
449
  });
396
- if (isCancel(workingDirectory)) return abort();
450
+ if (isCancel(workingDirectory)) return abort$2();
397
451
  const nodeVersionDefault = await detectNodeMajorVersion(rootDir) ?? 22;
398
452
  const nodeVersionText = await text({
399
453
  message: "Node.js major version (for GitHub Actions)",
@@ -403,14 +457,14 @@ async function runCi() {
403
457
  if (!Number.isFinite(major) || major <= 0) return "Enter a valid major version (e.g. 22)";
404
458
  }
405
459
  });
406
- if (isCancel(nodeVersionText)) return abort();
460
+ if (isCancel(nodeVersionText)) return abort$2();
407
461
  const nodeVersion = Number.parseInt(String(nodeVersionText).trim(), 10);
408
462
  const { runLint, runFormatCheck, runTests, lintCommand, formatCheckCommand, testCommand } = await resolveCiCommands(rootDir, workingDirectory, packageManager);
409
463
  const addRelease = await confirm({
410
464
  message: "Add release workflow too?",
411
465
  initialValue: true
412
466
  });
413
- if (isCancel(addRelease)) return abort();
467
+ if (isCancel(addRelease)) return abort$2();
414
468
  const releaseMode = addRelease ? await select({
415
469
  message: "Release workflows",
416
470
  initialValue: "tag",
@@ -429,17 +483,17 @@ async function runCi() {
429
483
  }
430
484
  ]
431
485
  }) : void 0;
432
- if (isCancel(releaseMode)) return abort();
486
+ if (isCancel(releaseMode)) return abort$2();
433
487
  const trustedPublishing = addRelease && packageManager !== "deno" ? await confirm({
434
488
  message: "Release: npm trusted publishing (OIDC)?",
435
489
  initialValue: true
436
490
  }) : void 0;
437
- if (isCancel(trustedPublishing)) return abort();
491
+ if (isCancel(trustedPublishing)) return abort$2();
438
492
  const addDependabot = await pathExists(path.join(rootDir, ".git")) ? await confirm({
439
493
  message: "Add/update Dependabot config (.github/dependabot.yml)?",
440
494
  initialValue: true
441
495
  }) : false;
442
- if (isCancel(addDependabot)) return abort();
496
+ if (isCancel(addDependabot)) return abort$2();
443
497
  const ciWorkflowPath = path.join(rootDir, ".github/workflows/ci.yml");
444
498
  const releaseWorkflowPath = path.join(rootDir, ".github/workflows/release.yml");
445
499
  const dependabotPath = path.join(rootDir, ".github/dependabot.yml");
@@ -475,19 +529,19 @@ async function runCi() {
475
529
  }
476
530
  outro(addRelease ? "Done. Generated CI + release workflows (and optional Dependabot)." : "Done. Generated CI workflow (and optional Dependabot).");
477
531
  } catch (err) {
478
- if (err instanceof CancelledError) return;
532
+ if (err instanceof CancelledError$2) return;
479
533
  throw err;
480
534
  }
481
535
  }
482
- var CancelledError = class extends Error {
536
+ var CancelledError$2 = class extends Error {
483
537
  constructor() {
484
538
  super("Cancelled");
485
539
  }
486
540
  };
487
- function abort(opts = {}) {
541
+ function abort$2(opts = {}) {
488
542
  cancel(opts.message ?? "Cancelled");
489
543
  process.exitCode = opts.exitCode ?? 0;
490
- throw new CancelledError();
544
+ throw new CancelledError$2();
491
545
  }
492
546
  async function confirmOverwriteIfExists(absPath, label) {
493
547
  if (!await pathExists(absPath)) return true;
@@ -495,26 +549,9 @@ async function confirmOverwriteIfExists(absPath, label) {
495
549
  message: `Overwrite existing ${label}?`,
496
550
  initialValue: true
497
551
  });
498
- if (isCancel(overwrite)) return abort();
552
+ if (isCancel(overwrite)) return abort$2();
499
553
  return overwrite;
500
554
  }
501
- function isPackageManager(value) {
502
- return value === "npm" || value === "pnpm" || value === "yarn" || value === "bun" || value === "deno";
503
- }
504
- async function detectPackageManager(rootDir) {
505
- const pmField = (await readPackageJson(path.join(rootDir, "package.json")))?.packageManager;
506
- if (pmField) {
507
- const pm = pmField.split("@")[0] ?? "";
508
- if (isPackageManager(pm)) return pm;
509
- }
510
- const candidates = [];
511
- if (await pathExists(path.join(rootDir, "pnpm-lock.yaml"))) candidates.push("pnpm");
512
- if (await pathExists(path.join(rootDir, "yarn.lock"))) candidates.push("yarn");
513
- if (await pathExists(path.join(rootDir, "package-lock.json"))) candidates.push("npm");
514
- if (await pathExists(path.join(rootDir, "bun.lockb"))) candidates.push("bun");
515
- if (await pathExists(path.join(rootDir, "deno.json")) || await pathExists(path.join(rootDir, "deno.jsonc"))) candidates.push("deno");
516
- return candidates.length === 1 ? candidates[0] : void 0;
517
- }
518
555
  async function listPackageCandidates(rootDir, packageManager) {
519
556
  const candidates = /* @__PURE__ */ new Set();
520
557
  if (await pathExists(path.join(rootDir, "package.json"))) candidates.add(".");
@@ -564,7 +601,7 @@ async function resolveCiCommands(rootDir, workingDirectory, packageManager) {
564
601
  runTests: true
565
602
  };
566
603
  const pkg = await readPackageJson(path.join(rootDir, workingDirectory, "package.json"));
567
- if (!pkg) return abort({
604
+ if (!pkg) return abort$2({
568
605
  message: `Missing package.json in ${workingDirectory}`,
569
606
  exitCode: 1
570
607
  });
@@ -580,17 +617,17 @@ async function resolveCiCommands(rootDir, workingDirectory, packageManager) {
580
617
  message: `CI: run lint${hasLint ? "" : " (no lint script detected)"}`,
581
618
  initialValue: runLintDefault
582
619
  });
583
- if (isCancel(runLint)) return abort();
620
+ if (isCancel(runLint)) return abort$2();
584
621
  const runFormatCheck = await confirm({
585
622
  message: `CI: run format check${runFormatCheckDefault ? "" : " (no format check script detected)"}`,
586
623
  initialValue: runFormatCheckDefault
587
624
  });
588
- if (isCancel(runFormatCheck)) return abort();
625
+ if (isCancel(runFormatCheck)) return abort$2();
589
626
  const runTests = await confirm({
590
627
  message: `CI: run tests${hasTest ? "" : " (no test script detected)"}`,
591
628
  initialValue: runTestsDefault
592
629
  });
593
- if (isCancel(runTests)) return abort();
630
+ if (isCancel(runTests)) return abort$2();
594
631
  return {
595
632
  runLint,
596
633
  runFormatCheck,
@@ -606,7 +643,7 @@ async function promptCommand(message, initialValue) {
606
643
  initialValue,
607
644
  validate: (v = "") => !v.trim() ? "Command is required" : void 0
608
645
  });
609
- if (isCancel(value)) return abort();
646
+ if (isCancel(value)) return abort$2();
610
647
  return String(value).trim();
611
648
  }
612
649
  function pmRun$1(pm, script) {
@@ -618,13 +655,6 @@ function pmRun$1(pm, script) {
618
655
  case "deno": return script;
619
656
  }
620
657
  }
621
- async function readPackageJson(filePath) {
622
- try {
623
- return JSON.parse(await readFile(filePath, "utf8"));
624
- } catch {
625
- return;
626
- }
627
- }
628
658
 
629
659
  //#endregion
630
660
  //#region src/lib/exec.ts
@@ -826,6 +856,9 @@ async function runInit({ nameArg }) {
826
856
  return;
827
857
  }
828
858
  const pkgDir = pnpmWorkspace ? path.join(rootDir, "packages", projectName) : rootDir;
859
+ const toolingDir = pnpmWorkspace ? rootDir : pkgDir;
860
+ const packageUseOxlint = pnpmWorkspace ? false : useOxlint;
861
+ const packageUseOxfmt = pnpmWorkspace ? false : useOxfmt;
829
862
  const pmVersion = await detectPackageManagerVersion(packageManager);
830
863
  const packageManagerField = pmVersion ? `${packageManager}@${pmVersion}` : `${packageManager}@latest`;
831
864
  await mkdir(path.join(pkgDir, "src"), { recursive: true });
@@ -840,11 +873,18 @@ async function runInit({ nameArg }) {
840
873
  " - \"packages/*\"",
841
874
  ""
842
875
  ].join("\n"));
843
- await writeText(path.join(rootDir, "package.json"), JSON.stringify({
876
+ await writeText(path.join(rootDir, "package.json"), workspaceRootPackageJsonTemplate({
844
877
  name: projectName,
845
- private: true,
846
- packageManager: packageManagerField
847
- }, null, 2) + "\n");
878
+ packageManager: packageManagerField,
879
+ useOxlint,
880
+ oxlintVersion: "latest",
881
+ oxlintTsgolintVersion: "latest",
882
+ kingswordLintConfigVersion: "latest",
883
+ useOxfmt,
884
+ oxfmtVersion: "latest",
885
+ useVitest,
886
+ useTsdown
887
+ }));
848
888
  }
849
889
  await Promise.all([
850
890
  writeText(path.join(pkgDir, "README.md"), readmeTemplate(projectName)),
@@ -854,11 +894,11 @@ async function runInit({ nameArg }) {
854
894
  name: projectName,
855
895
  packageManager: packageManagerField,
856
896
  typescriptVersion: "latest",
857
- useOxlint,
897
+ useOxlint: packageUseOxlint,
858
898
  oxlintVersion: "latest",
859
899
  oxlintTsgolintVersion: "latest",
860
900
  kingswordLintConfigVersion: "latest",
861
- useOxfmt,
901
+ useOxfmt: packageUseOxfmt,
862
902
  oxfmtVersion: "latest",
863
903
  useVitest,
864
904
  vitestVersion: "latest",
@@ -866,13 +906,13 @@ async function runInit({ nameArg }) {
866
906
  tsdownVersion: "latest"
867
907
  }))
868
908
  ]);
869
- if (useOxlint) await writeText(path.join(pkgDir, "oxlint.config.ts"), oxlintConfigTemplate({ useVitest }));
870
- if (useOxfmt) await writeText(path.join(pkgDir, ".oxfmtrc.json"), oxfmtConfigTemplate());
909
+ if (useOxlint) await writeText(path.join(toolingDir, "oxlint.config.ts"), oxlintConfigTemplate({ useVitest }));
910
+ if (useOxfmt) await writeText(path.join(toolingDir, ".oxfmtrc.json"), oxfmtConfigTemplate());
871
911
  if (useVitest) await writeText(path.join(pkgDir, "src/index.test.ts"), srcVitestTemplate());
872
912
  if (useTsdown) await writeText(path.join(pkgDir, "tsdown.config.ts"), tsdownConfigTemplate());
873
913
  if (packageManager === "deno") await writeText(path.join(rootDir, "deno.json"), JSON.stringify({ nodeModulesDir: "auto" }, null, 2) + "\n");
874
914
  if (githubActions !== "none") {
875
- const workingDirectory = pnpmWorkspace ? path.posix.join("packages", projectName) : ".";
915
+ const workingDirectory = ".";
876
916
  const lintCommand = useOxlint && packageManager !== "deno" ? pmRun(packageManager, "lint") : void 0;
877
917
  const formatCheckCommand = useOxfmt && packageManager !== "deno" ? pmRun(packageManager, "format:check") : void 0;
878
918
  const testCommand = useVitest && packageManager !== "deno" ? pmRun(packageManager, "test") : void 0;
@@ -918,8 +958,7 @@ function validateProjectName(value) {
918
958
  if (name.length > 214) return "Project name is too long";
919
959
  if (name.startsWith(".")) return "Project name cannot start with '.'";
920
960
  if (name.startsWith("_")) return "Project name cannot start with '_'";
921
- if (/[A-Z]/.test(name)) return "Use lowercase letters only";
922
- if (!/^[a-z0-9._-]+$/.test(name)) return "Use letters, numbers, '.', '_' or '-'";
961
+ if (!/^[A-Za-z0-9._-]+$/.test(name)) return "Use letters, numbers, '.', '_' or '-'";
923
962
  }
924
963
  function onCancel() {
925
964
  cancel("Cancelled");
@@ -936,4 +975,516 @@ function nextStepHint(pm) {
936
975
  }
937
976
 
938
977
  //#endregion
939
- export { oxlintConfigTemplate as a, githubDependabotTemplate as i, runCi as n, packageJsonTemplate as o, githubCliCiWorkflowTemplate as r, runInit as t };
978
+ //#region src/commands/oxlint.ts
979
+ const OXLINT_COMMAND = "oxlint --type-aware --type-check";
980
+ const OXLINT_FIX_COMMAND = `${OXLINT_COMMAND} --fix`;
981
+ const OXLINT_SCRIPTS = {
982
+ lint: OXLINT_COMMAND,
983
+ "lint:fix": OXLINT_FIX_COMMAND
984
+ };
985
+ const ESLINT_CONFIG_FILES = [
986
+ ".eslintrc",
987
+ ".eslintrc.js",
988
+ ".eslintrc.cjs",
989
+ ".eslintrc.mjs",
990
+ ".eslintrc.json",
991
+ ".eslintrc.yaml",
992
+ ".eslintrc.yml",
993
+ ".eslintrc.ts",
994
+ ".eslintrc.cts",
995
+ ".eslintrc.mts",
996
+ "eslint.config.js",
997
+ "eslint.config.cjs",
998
+ "eslint.config.mjs",
999
+ "eslint.config.ts",
1000
+ "eslint.config.cts",
1001
+ "eslint.config.mts"
1002
+ ];
1003
+ const OXLINT_DEPENDENCIES = [
1004
+ "oxlint",
1005
+ "oxlint-tsgolint",
1006
+ "@kingsword/lint-config"
1007
+ ];
1008
+ async function runOxlint({ yes = false } = {}) {
1009
+ try {
1010
+ intro("frontpl (oxlint)");
1011
+ const rootDir = process.cwd();
1012
+ const packageJsonPath = path.join(rootDir, "package.json");
1013
+ const oxlintConfigPath = path.join(rootDir, "oxlint.config.ts");
1014
+ const pkg = await readPackageJson(packageJsonPath);
1015
+ if (!pkg) {
1016
+ cancel("Missing package.json. Run this command in a Node project root.");
1017
+ process.exitCode = 1;
1018
+ return;
1019
+ }
1020
+ const packageManager = await detectPackageManager(rootDir) ?? "pnpm";
1021
+ const stats = await migrateToOxlint({
1022
+ pkg,
1023
+ rootDir,
1024
+ oxlintConfigPath,
1025
+ strategy: yes ? "replace" : await askMigrationStrategy({
1026
+ rootDir,
1027
+ pkg
1028
+ }),
1029
+ yes
1030
+ });
1031
+ await writePackageJson(packageJsonPath, pkg);
1032
+ const installOk = await maybeInstallDependencies$1({
1033
+ yes,
1034
+ packageManager,
1035
+ rootDir
1036
+ });
1037
+ const scriptSummary = stats.scriptsUpdated.length > 0 ? `updated scripts: ${stats.scriptsUpdated.join(", ")}` : stats.scriptsKept.length > 0 ? `kept existing scripts: ${stats.scriptsKept.join(", ")}` : "scripts already aligned";
1038
+ const dependencySummary = stats.addedDevDependencies.length > 0 ? `added devDependencies: ${stats.addedDevDependencies.join(", ")}` : "required oxlint devDependencies already present";
1039
+ const typecheckSummary = stats.removedTypecheckScript ? "removed redundant typecheck script (tsc --noEmit)" : "kept typecheck script";
1040
+ const eslintDependencySummary = stats.removedDependencies.length > 0 ? `removed eslint deps: ${stats.removedDependencies.join(", ")}` : "no eslint deps removed";
1041
+ const eslintConfigSummary = stats.removedPackageJsonEslintConfig ? "removed package.json#eslintConfig" : "no package.json#eslintConfig removed";
1042
+ const eslintFileSummary = stats.removedConfigFiles.length > 0 ? `removed eslint config files: ${stats.removedConfigFiles.join(", ")}` : "no eslint config files removed";
1043
+ const oxlintConfigSummary = stats.oxlintConfigAction === "written" ? "wrote oxlint.config.ts" : "kept existing oxlint.config.ts";
1044
+ const installSummary = packageManager === "deno" ? "skipped dependency install (deno project)" : installOk === true ? `installed dependencies with ${packageManager}` : installOk === false ? `dependency install failed with ${packageManager}` : "skipped dependency install";
1045
+ outro([
1046
+ "Done. Applied oxlint migration.",
1047
+ `- strategy: ${stats.strategy === "migrate" ? "migrate (keep ESLint assets)" : "replace ESLint assets"}`,
1048
+ `- ${scriptSummary}`,
1049
+ `- ${typecheckSummary}`,
1050
+ `- ${dependencySummary}`,
1051
+ `- ${eslintDependencySummary}`,
1052
+ `- ${eslintConfigSummary}`,
1053
+ `- ${eslintFileSummary}`,
1054
+ `- ${oxlintConfigSummary}`,
1055
+ `- ${installSummary}`
1056
+ ].join("\n"));
1057
+ } catch (error) {
1058
+ if (error instanceof CancelledError$1) return;
1059
+ throw error;
1060
+ }
1061
+ }
1062
+ async function migrateToOxlint(opts) {
1063
+ const { pkg, rootDir, oxlintConfigPath, strategy, yes } = opts;
1064
+ const scripts = { ...pkg.scripts };
1065
+ const conflictingScripts = Object.entries(OXLINT_SCRIPTS).filter(([name, command]) => typeof scripts[name] === "string" && scripts[name] !== command).map(([name]) => name);
1066
+ const shouldOverwriteConflicts = conflictingScripts.length === 0 ? true : yes ? true : await askConfirm$1({
1067
+ message: `Overwrite conflicting scripts (${conflictingScripts.join(", ")}) with oxlint?`,
1068
+ initialValue: true
1069
+ });
1070
+ const scriptsUpdated = [];
1071
+ const scriptsKept = [];
1072
+ for (const [name, command] of Object.entries(OXLINT_SCRIPTS)) {
1073
+ const current = scripts[name];
1074
+ if (current === command) continue;
1075
+ if (current && !shouldOverwriteConflicts) {
1076
+ scriptsKept.push(name);
1077
+ continue;
1078
+ }
1079
+ scripts[name] = command;
1080
+ scriptsUpdated.push(name);
1081
+ }
1082
+ let removedTypecheckScript = false;
1083
+ if (scripts.typecheck === "tsc --noEmit") {
1084
+ if (yes || await askConfirm$1({
1085
+ message: "Remove redundant typecheck script (tsc --noEmit)?",
1086
+ initialValue: true
1087
+ })) {
1088
+ delete scripts.typecheck;
1089
+ removedTypecheckScript = true;
1090
+ }
1091
+ }
1092
+ pkg.scripts = scripts;
1093
+ const devDependencies = { ...pkg.devDependencies };
1094
+ const addedDevDependencies = [];
1095
+ for (const dependency of OXLINT_DEPENDENCIES) {
1096
+ if (devDependencies[dependency]) continue;
1097
+ devDependencies[dependency] = "latest";
1098
+ addedDevDependencies.push(dependency);
1099
+ }
1100
+ pkg.devDependencies = devDependencies;
1101
+ const oxlintConfigAction = await applyOxlintConfig({
1102
+ pkg,
1103
+ oxlintConfigPath,
1104
+ yes
1105
+ });
1106
+ let removedDependencies = [];
1107
+ let removedPackageJsonEslintConfig = false;
1108
+ const removedConfigFiles = [];
1109
+ if (strategy === "replace") {
1110
+ removedDependencies = [...removeEslintDependencies(pkg, "dependencies"), ...removeEslintDependencies(pkg, "devDependencies")];
1111
+ removedPackageJsonEslintConfig = removeEslintConfigFromPackageJson(pkg);
1112
+ cleanupEmptyDependencyBuckets$1(pkg);
1113
+ for (const file of ESLINT_CONFIG_FILES) {
1114
+ const filePath = path.join(rootDir, file);
1115
+ if (!await pathExists(filePath)) continue;
1116
+ await unlink(filePath);
1117
+ removedConfigFiles.push(file);
1118
+ }
1119
+ }
1120
+ return {
1121
+ strategy,
1122
+ scriptsUpdated,
1123
+ scriptsKept,
1124
+ removedTypecheckScript,
1125
+ addedDevDependencies,
1126
+ removedDependencies,
1127
+ removedPackageJsonEslintConfig,
1128
+ removedConfigFiles,
1129
+ oxlintConfigAction
1130
+ };
1131
+ }
1132
+ async function maybeInstallDependencies$1(opts) {
1133
+ const { yes, packageManager, rootDir } = opts;
1134
+ if (packageManager === "deno") return void 0;
1135
+ if (!(yes || await askConfirm$1({
1136
+ message: `Install dependencies now with ${packageManager}?`,
1137
+ initialValue: true
1138
+ }))) return void 0;
1139
+ const installSpinner = spinner();
1140
+ installSpinner.start(`Installing dependencies with ${packageManager}`);
1141
+ const result = await exec(packageManager, ["install"], { cwd: rootDir });
1142
+ installSpinner.stop(result.ok ? "Dependencies installed" : "Dependency install failed");
1143
+ return result.ok;
1144
+ }
1145
+ async function askConfirm$1(opts) {
1146
+ const answer = await confirm({
1147
+ message: opts.message,
1148
+ initialValue: opts.initialValue
1149
+ });
1150
+ if (isCancel(answer)) return abort$1();
1151
+ return answer;
1152
+ }
1153
+ async function askMigrationStrategy(opts) {
1154
+ const strategy = await select({
1155
+ message: "ESLint strategy",
1156
+ initialValue: await detectEslintAssets(opts.rootDir, opts.pkg) ? "migrate" : "replace",
1157
+ options: [{
1158
+ value: "migrate",
1159
+ label: "Migrate gradually (keep ESLint assets)"
1160
+ }, {
1161
+ value: "replace",
1162
+ label: "Replace ESLint directly (current mode)"
1163
+ }]
1164
+ });
1165
+ if (isCancel(strategy)) return abort$1();
1166
+ return strategy;
1167
+ }
1168
+ async function detectEslintAssets(rootDir, pkg) {
1169
+ if (Object.prototype.hasOwnProperty.call(pkg, "eslintConfig")) return true;
1170
+ const dependencies = pkg.dependencies ?? {};
1171
+ const devDependencies = pkg.devDependencies ?? {};
1172
+ if (Object.keys(dependencies).some(isEslintDependency) || Object.keys(devDependencies).some(isEslintDependency)) return true;
1173
+ for (const file of ESLINT_CONFIG_FILES) if (await pathExists(path.join(rootDir, file))) return true;
1174
+ return false;
1175
+ }
1176
+ async function applyOxlintConfig(opts) {
1177
+ const { pkg, oxlintConfigPath, yes } = opts;
1178
+ if (!(!await pathExists(oxlintConfigPath) || yes || await askConfirm$1({
1179
+ message: "Overwrite existing oxlint.config.ts?",
1180
+ initialValue: true
1181
+ }))) return "kept-existing";
1182
+ await writeText(oxlintConfigPath, oxlintConfigTemplate({ useVitest: detectUseVitest(pkg.scripts) }));
1183
+ return "written";
1184
+ }
1185
+ function detectUseVitest(scripts) {
1186
+ return typeof scripts?.test === "string" && scripts.test.includes("vitest");
1187
+ }
1188
+ function removeEslintDependencies(pkg, key) {
1189
+ const bucket = pkg[key];
1190
+ if (!bucket) return [];
1191
+ const removed = [];
1192
+ for (const name of Object.keys(bucket)) {
1193
+ if (!isEslintDependency(name)) continue;
1194
+ delete bucket[name];
1195
+ removed.push(name);
1196
+ }
1197
+ return removed;
1198
+ }
1199
+ function isEslintDependency(name) {
1200
+ return name === "eslint" || name === "typescript-eslint" || name.startsWith("@eslint/") || name.startsWith("@typescript-eslint/") || name.startsWith("eslint-") || /(^|\/)eslint-(plugin|config|import-resolver)-/.test(name);
1201
+ }
1202
+ function removeEslintConfigFromPackageJson(pkg) {
1203
+ if (!Object.prototype.hasOwnProperty.call(pkg, "eslintConfig")) return false;
1204
+ delete pkg.eslintConfig;
1205
+ return true;
1206
+ }
1207
+ function cleanupEmptyDependencyBuckets$1(pkg) {
1208
+ if (pkg.dependencies && Object.keys(pkg.dependencies).length === 0) delete pkg.dependencies;
1209
+ if (pkg.devDependencies && Object.keys(pkg.devDependencies).length === 0) delete pkg.devDependencies;
1210
+ }
1211
+ var CancelledError$1 = class extends Error {
1212
+ constructor() {
1213
+ super("Cancelled");
1214
+ }
1215
+ };
1216
+ function abort$1(opts = {}) {
1217
+ cancel(opts.message ?? "Cancelled");
1218
+ process.exitCode = opts.exitCode ?? 0;
1219
+ throw new CancelledError$1();
1220
+ }
1221
+
1222
+ //#endregion
1223
+ //#region src/commands/oxfmt.ts
1224
+ const OXFMT_SCRIPTS = {
1225
+ format: "oxfmt",
1226
+ "format:check": "oxfmt --check",
1227
+ fmt: "oxfmt",
1228
+ "fmt:check": "oxfmt --check"
1229
+ };
1230
+ const PRETTIER_CONFIG_FILES = [
1231
+ ".prettierrc",
1232
+ ".prettierrc.json",
1233
+ ".prettierrc.json5",
1234
+ ".prettierrc.yaml",
1235
+ ".prettierrc.yml",
1236
+ ".prettierrc.toml",
1237
+ ".prettierrc.js",
1238
+ ".prettierrc.cjs",
1239
+ ".prettierrc.mjs",
1240
+ ".prettierrc.ts",
1241
+ ".prettierrc.cts",
1242
+ ".prettierrc.mts",
1243
+ "prettier.config.js",
1244
+ "prettier.config.cjs",
1245
+ "prettier.config.mjs",
1246
+ "prettier.config.ts",
1247
+ "prettier.config.cts",
1248
+ "prettier.config.mts"
1249
+ ];
1250
+ async function runOxfmt({ yes = false } = {}) {
1251
+ try {
1252
+ intro("frontpl (oxfmt)");
1253
+ const rootDir = process.cwd();
1254
+ const packageJsonPath = path.join(rootDir, "package.json");
1255
+ const oxfmtConfigPath = path.join(rootDir, ".oxfmtrc.json");
1256
+ const pkg = await readPackageJson(packageJsonPath);
1257
+ if (!pkg) {
1258
+ cancel("Missing package.json. Run this command in a Node project root.");
1259
+ process.exitCode = 1;
1260
+ return;
1261
+ }
1262
+ const packageManager = await detectPackageManager(rootDir) ?? "pnpm";
1263
+ const stats = await migrateToOxfmt({
1264
+ pkg,
1265
+ rootDir,
1266
+ oxfmtConfigPath,
1267
+ packageManager,
1268
+ configMode: yes ? "rebuild" : await askConfigMode({
1269
+ rootDir,
1270
+ pkg
1271
+ }),
1272
+ yes
1273
+ });
1274
+ await writePackageJson(packageJsonPath, pkg);
1275
+ const installOk = await maybeInstallDependencies({
1276
+ yes,
1277
+ packageManager,
1278
+ rootDir
1279
+ });
1280
+ const scriptSummary = stats.scriptsUpdated.length > 0 ? `updated scripts: ${stats.scriptsUpdated.join(", ")}` : stats.scriptsKept.length > 0 ? `kept existing scripts: ${stats.scriptsKept.join(", ")}` : "scripts already aligned";
1281
+ const depSummary = stats.addedOxfmtDependency ? "added devDependency: oxfmt" : "devDependency oxfmt already present";
1282
+ const removedDepsSummary = stats.removedDependencies.length > 0 ? `removed prettier deps: ${stats.removedDependencies.join(", ")}` : "no prettier deps removed";
1283
+ const removedPackageJsonPrettierSummary = stats.removedPackageJsonPrettierConfig ? "removed package.json#prettier" : "no package.json#prettier removed";
1284
+ const removedFilesSummary = stats.removedConfigFiles.length > 0 ? `removed prettier config files: ${stats.removedConfigFiles.join(", ")}` : "no prettier config files removed";
1285
+ const configSummary = stats.oxfmtConfigAction === "migrated" ? "migrated .oxfmtrc.json from prettier" : stats.oxfmtConfigAction === "rebuilt" ? "rebuilt .oxfmtrc.json" : "kept existing .oxfmtrc.json";
1286
+ const installSummary = packageManager === "deno" ? "skipped dependency install (deno project)" : installOk === true ? `installed dependencies with ${packageManager}` : installOk === false ? `dependency install failed with ${packageManager}` : "skipped dependency install";
1287
+ outro([
1288
+ "Done. Applied oxfmt migration.",
1289
+ `- ${scriptSummary}`,
1290
+ `- ${depSummary}`,
1291
+ `- ${removedDepsSummary}`,
1292
+ `- ${removedPackageJsonPrettierSummary}`,
1293
+ `- ${removedFilesSummary}`,
1294
+ `- ${configSummary}`,
1295
+ `- ${installSummary}`
1296
+ ].join("\n"));
1297
+ } catch (error) {
1298
+ if (error instanceof CancelledError) return;
1299
+ throw error;
1300
+ }
1301
+ }
1302
+ async function migrateToOxfmt(opts) {
1303
+ const { pkg, rootDir, oxfmtConfigPath, packageManager, configMode, yes } = opts;
1304
+ const scripts = { ...pkg.scripts };
1305
+ const conflictingScripts = Object.entries(OXFMT_SCRIPTS).filter(([name, command]) => typeof scripts[name] === "string" && scripts[name] !== command).map(([name]) => name);
1306
+ const shouldOverwriteConflicts = conflictingScripts.length === 0 ? true : yes ? true : await askConfirm({
1307
+ message: `Overwrite conflicting scripts (${conflictingScripts.join(", ")}) with oxfmt?`,
1308
+ initialValue: true
1309
+ });
1310
+ const scriptsUpdated = [];
1311
+ const scriptsKept = [];
1312
+ for (const [name, command] of Object.entries(OXFMT_SCRIPTS)) {
1313
+ const current = scripts[name];
1314
+ if (current === command) continue;
1315
+ if (current && !shouldOverwriteConflicts) {
1316
+ scriptsKept.push(name);
1317
+ continue;
1318
+ }
1319
+ scripts[name] = command;
1320
+ scriptsUpdated.push(name);
1321
+ }
1322
+ pkg.scripts = scripts;
1323
+ const devDependencies = { ...pkg.devDependencies };
1324
+ let addedOxfmtDependency = false;
1325
+ if (!devDependencies.oxfmt) {
1326
+ devDependencies.oxfmt = "latest";
1327
+ addedOxfmtDependency = true;
1328
+ }
1329
+ pkg.devDependencies = devDependencies;
1330
+ const removePrettier = yes ? true : await askConfirm({
1331
+ message: "Remove prettier dependencies and config files?",
1332
+ initialValue: true
1333
+ });
1334
+ const removedDependencies = [];
1335
+ let removedPackageJsonPrettierConfig = false;
1336
+ if (removePrettier) {
1337
+ removedDependencies.push(...removePrettierDependencies(pkg, "dependencies"));
1338
+ removedDependencies.push(...removePrettierDependencies(pkg, "devDependencies"));
1339
+ removedPackageJsonPrettierConfig = removePrettierConfigFromPackageJson(pkg);
1340
+ cleanupEmptyDependencyBuckets(pkg);
1341
+ }
1342
+ const oxfmtConfigAction = await applyOxfmtConfig({
1343
+ rootDir,
1344
+ oxfmtConfigPath,
1345
+ packageManager,
1346
+ configMode,
1347
+ yes
1348
+ });
1349
+ const removedConfigFiles = [];
1350
+ if (removePrettier) for (const file of PRETTIER_CONFIG_FILES) {
1351
+ const filePath = path.join(rootDir, file);
1352
+ if (!await pathExists(filePath)) continue;
1353
+ await unlink(filePath);
1354
+ removedConfigFiles.push(file);
1355
+ }
1356
+ return {
1357
+ scriptsUpdated,
1358
+ scriptsKept,
1359
+ addedOxfmtDependency,
1360
+ removedPackageJsonPrettierConfig,
1361
+ removedDependencies,
1362
+ removedConfigFiles,
1363
+ oxfmtConfigAction
1364
+ };
1365
+ }
1366
+ async function maybeInstallDependencies(opts) {
1367
+ const { yes, packageManager, rootDir } = opts;
1368
+ if (packageManager === "deno") return void 0;
1369
+ if (!(yes || await askConfirm({
1370
+ message: `Install dependencies now with ${packageManager}?`,
1371
+ initialValue: true
1372
+ }))) return void 0;
1373
+ const installSpinner = spinner();
1374
+ installSpinner.start(`Installing dependencies with ${packageManager}`);
1375
+ const result = await exec(packageManager, ["install"], { cwd: rootDir });
1376
+ installSpinner.stop(result.ok ? "Dependencies installed" : "Dependency install failed");
1377
+ return result.ok;
1378
+ }
1379
+ async function askConfirm(opts) {
1380
+ const answer = await confirm({
1381
+ message: opts.message,
1382
+ initialValue: opts.initialValue
1383
+ });
1384
+ if (isCancel(answer)) return abort();
1385
+ return answer;
1386
+ }
1387
+ async function askConfigMode(opts) {
1388
+ const mode = await select({
1389
+ message: "Prettier config strategy",
1390
+ initialValue: await detectPrettierConfig(opts.rootDir, opts.pkg) ? "migrate" : "rebuild",
1391
+ options: [{
1392
+ value: "migrate",
1393
+ label: "Migrate from Prettier (oxfmt --migrate=prettier)"
1394
+ }, {
1395
+ value: "rebuild",
1396
+ label: "Rebuild .oxfmtrc.json (current mode)"
1397
+ }]
1398
+ });
1399
+ if (isCancel(mode)) return abort();
1400
+ return mode;
1401
+ }
1402
+ async function detectPrettierConfig(rootDir, pkg) {
1403
+ if (Object.prototype.hasOwnProperty.call(pkg, "prettier")) return true;
1404
+ for (const file of PRETTIER_CONFIG_FILES) if (await pathExists(path.join(rootDir, file))) return true;
1405
+ return false;
1406
+ }
1407
+ async function applyOxfmtConfig(opts) {
1408
+ const { rootDir, oxfmtConfigPath, packageManager, configMode, yes } = opts;
1409
+ if (!(!await pathExists(oxfmtConfigPath) || yes || await askConfirm({
1410
+ message: configMode === "migrate" ? "Overwrite existing .oxfmtrc.json via prettier migration?" : "Overwrite existing .oxfmtrc.json?",
1411
+ initialValue: true
1412
+ }))) return "kept-existing";
1413
+ if (configMode === "migrate") {
1414
+ if (await runOxfmtPrettierMigration({
1415
+ rootDir,
1416
+ packageManager
1417
+ })) return "migrated";
1418
+ if (!(yes || await askConfirm({
1419
+ message: "Migration failed. Rebuild .oxfmtrc.json with defaults instead?",
1420
+ initialValue: true
1421
+ }))) return "kept-existing";
1422
+ }
1423
+ await writeText(oxfmtConfigPath, oxfmtConfigTemplate());
1424
+ return "rebuilt";
1425
+ }
1426
+ async function runOxfmtPrettierMigration(opts) {
1427
+ const { rootDir, packageManager } = opts;
1428
+ const migrateSpinner = spinner();
1429
+ migrateSpinner.start("Migrating prettier config to .oxfmtrc.json");
1430
+ if ((await exec("oxfmt", ["--migrate=prettier"], { cwd: rootDir })).ok) {
1431
+ migrateSpinner.stop("Migrated config with oxfmt");
1432
+ return true;
1433
+ }
1434
+ const fallbackRun = packageManager === "pnpm" ? await exec("pnpm", [
1435
+ "exec",
1436
+ "oxfmt",
1437
+ "--migrate=prettier"
1438
+ ], { cwd: rootDir }) : packageManager === "npm" ? await exec("npm", [
1439
+ "exec",
1440
+ "oxfmt",
1441
+ "--",
1442
+ "--migrate=prettier"
1443
+ ], { cwd: rootDir }) : packageManager === "yarn" ? await exec("yarn", [
1444
+ "dlx",
1445
+ "oxfmt",
1446
+ "--migrate=prettier"
1447
+ ], { cwd: rootDir }) : packageManager === "bun" ? await exec("bun", [
1448
+ "x",
1449
+ "oxfmt",
1450
+ "--migrate=prettier"
1451
+ ], { cwd: rootDir }) : { ok: false };
1452
+ migrateSpinner.stop(fallbackRun.ok ? "Migrated config with oxfmt" : "Prettier migration failed");
1453
+ return fallbackRun.ok;
1454
+ }
1455
+ function removePrettierDependencies(pkg, key) {
1456
+ const bucket = pkg[key];
1457
+ if (!bucket) return [];
1458
+ const removed = [];
1459
+ for (const name of Object.keys(bucket)) {
1460
+ if (!isPrettierDependency(name)) continue;
1461
+ delete bucket[name];
1462
+ removed.push(name);
1463
+ }
1464
+ return removed;
1465
+ }
1466
+ function cleanupEmptyDependencyBuckets(pkg) {
1467
+ if (pkg.dependencies && Object.keys(pkg.dependencies).length === 0) delete pkg.dependencies;
1468
+ if (pkg.devDependencies && Object.keys(pkg.devDependencies).length === 0) delete pkg.devDependencies;
1469
+ }
1470
+ function removePrettierConfigFromPackageJson(pkg) {
1471
+ if (!Object.prototype.hasOwnProperty.call(pkg, "prettier")) return false;
1472
+ delete pkg.prettier;
1473
+ return true;
1474
+ }
1475
+ function isPrettierDependency(name) {
1476
+ return name === "prettier" || /(^|\/)prettier-plugin-/.test(name) || name.startsWith("@prettier/plugin-");
1477
+ }
1478
+ var CancelledError = class extends Error {
1479
+ constructor() {
1480
+ super("Cancelled");
1481
+ }
1482
+ };
1483
+ function abort(opts = {}) {
1484
+ cancel(opts.message ?? "Cancelled");
1485
+ process.exitCode = opts.exitCode ?? 0;
1486
+ throw new CancelledError();
1487
+ }
1488
+
1489
+ //#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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frontpl",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "description": "Interactive CLI to scaffold standardized frontend project templates.",
5
5
  "keywords": [
6
6
  "cli",
@@ -53,11 +53,11 @@
53
53
  "tiny-bin": "^2.0.0"
54
54
  },
55
55
  "devDependencies": {
56
- "@kingsword/lint-config": "^0.1.1",
56
+ "@kingsword/lint-config": "^0.2.1",
57
57
  "@types/node": "^25.0.10",
58
- "oxfmt": "^0.31.0",
58
+ "oxfmt": "^0.32.0",
59
59
  "oxlint": "^1.46.0",
60
- "oxlint-tsgolint": "^0.11.5",
60
+ "oxlint-tsgolint": "^0.13.0",
61
61
  "tsdown": "^0.20.1",
62
62
  "typescript": "^5.9.3"
63
63
  },