frontpl 0.4.0 → 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
@@ -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`.
@@ -53,6 +54,32 @@ Generated output includes (based on options):
53
54
  - Optional configs: `oxlint.config.ts`, `.oxfmtrc.json`, `tsdown.config.ts`
54
55
  - Optional GitHub Actions workflows in `.github/workflows/`
55
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
+ - 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.
82
+
56
83
  ### `frontpl ci`
57
84
 
58
85
  Add or update CI/Release workflows for an existing project (run it in your repo root).
@@ -102,8 +129,6 @@ What it does:
102
129
  - Ensures `package.json` scripts use:
103
130
  - `format`: `oxfmt`
104
131
  - `format:check`: `oxfmt --check`
105
- - `fmt`: `oxfmt`
106
- - `fmt:check`: `oxfmt --check`
107
132
  - Ensures `devDependencies.oxfmt` exists (defaults to `latest` when missing)
108
133
  - Creates or updates `.oxfmtrc.json`
109
134
  - Optionally removes `prettier` / `prettier-plugin-*` / `@prettier/plugin-*` dependencies, `package.json#prettier`, and Prettier config files (`.prettierrc*`, `prettier.config.*`)
@@ -138,6 +163,7 @@ When CI workflows are enabled, frontpl can also generate `.github/dependabot.yml
138
163
  - Keeps `github-actions` updates enabled
139
164
  - Adds grouped dependencies updates (`groups.dependencies`)
140
165
  - Uses the selected `workingDirectory` (`.` -> `/`, monorepo package -> `/packages/<name>`)
166
+ - In `frontpl init` + `pnpm workspace mode`, default `workingDirectory` is workspace root (`/`)
141
167
  - Maps JavaScript package managers (`npm`/`pnpm`/`yarn`/`bun`) to Dependabot `package-ecosystem: "npm"`
142
168
 
143
169
  ## Development
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-Bgtl3zwv.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;
@@ -46,6 +57,18 @@ declare function packageJsonTemplate(opts: {
46
57
  useTsdown: boolean;
47
58
  tsdownVersion?: string;
48
59
  }): string;
60
+ declare function workspaceRootPackageJsonTemplate(opts: {
61
+ name: string;
62
+ packageManager: string;
63
+ useOxlint: boolean;
64
+ oxlintVersion?: string;
65
+ oxlintTsgolintVersion?: string;
66
+ kingswordLintConfigVersion?: string;
67
+ useOxfmt: boolean;
68
+ oxfmtVersion?: string;
69
+ useVitest: boolean;
70
+ useTsdown: boolean;
71
+ }): string;
49
72
  declare function githubCliCiWorkflowTemplate(opts: {
50
73
  packageManager: "npm" | "pnpm" | "yarn" | "bun" | "deno";
51
74
  nodeVersion: number;
@@ -65,4 +88,4 @@ declare function githubDependabotTemplate(opts: {
65
88
  workingDirectory: string;
66
89
  }): string;
67
90
  //#endregion
68
- export { githubCliCiWorkflowTemplate, githubDependabotTemplate, oxlintConfigTemplate, packageJsonTemplate, runCi, runInit, runOxfmt, runOxlint, validateProjectName };
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 } from "./oxfmt-Bgtl3zwv.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 };
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
@@ -152,28 +149,33 @@ function tsdownConfigTemplate() {
152
149
  ""
153
150
  ].join("\n");
154
151
  }
155
- function packageJsonTemplate(opts) {
156
- const scripts = {};
152
+ function applyLintAndFormatScripts(scripts, opts) {
157
153
  if (opts.useOxlint) {
158
154
  const oxlintCmd = "oxlint --type-aware --type-check";
159
155
  scripts.lint = oxlintCmd;
160
156
  scripts["lint:fix"] = `${oxlintCmd} --fix`;
161
- } else scripts.typecheck = "tsc --noEmit";
157
+ }
162
158
  if (opts.useOxfmt) {
163
159
  scripts.format = "oxfmt";
164
160
  scripts["format:check"] = "oxfmt --check";
165
- scripts.fmt = "oxfmt";
166
- scripts["fmt:check"] = "oxfmt --check";
167
161
  }
168
- if (opts.useVitest) scripts.test = "vitest";
169
- if (opts.useTsdown) scripts.build = "tsdown";
170
- const devDependencies = { typescript: opts.typescriptVersion };
162
+ }
163
+ function applyLintAndFormatDependencies(devDependencies, opts) {
171
164
  if (opts.useOxlint) {
172
165
  if (opts.oxlintVersion) devDependencies.oxlint = opts.oxlintVersion;
173
166
  if (opts.oxlintTsgolintVersion) devDependencies["oxlint-tsgolint"] = opts.oxlintTsgolintVersion;
174
167
  if (opts.kingswordLintConfigVersion) devDependencies["@kingsword/lint-config"] = opts.kingswordLintConfigVersion;
175
168
  }
176
169
  if (opts.useOxfmt && opts.oxfmtVersion) devDependencies.oxfmt = opts.oxfmtVersion;
170
+ }
171
+ function packageJsonTemplate(opts) {
172
+ const scripts = {};
173
+ if (!opts.useOxlint && opts.includeTypecheckWithoutOxlint !== false) scripts.typecheck = "tsc --noEmit";
174
+ applyLintAndFormatScripts(scripts, opts);
175
+ if (opts.useVitest) scripts.test = "vitest";
176
+ if (opts.useTsdown) scripts.build = "tsdown";
177
+ const devDependencies = { typescript: opts.typescriptVersion };
178
+ applyLintAndFormatDependencies(devDependencies, opts);
177
179
  if (opts.useVitest && opts.vitestVersion) devDependencies.vitest = opts.vitestVersion;
178
180
  if (opts.useTsdown && opts.tsdownVersion) devDependencies.tsdown = opts.tsdownVersion;
179
181
  return JSON.stringify({
@@ -186,6 +188,22 @@ function packageJsonTemplate(opts) {
186
188
  packageManager: opts.packageManager
187
189
  }, null, 2) + "\n";
188
190
  }
191
+ function workspaceRootPackageJsonTemplate(opts) {
192
+ const scripts = {};
193
+ applyLintAndFormatScripts(scripts, opts);
194
+ if (opts.useVitest) scripts.test = "pnpm -r --if-present run test";
195
+ if (opts.useTsdown) scripts.build = "pnpm -r --if-present run build";
196
+ const devDependencies = {};
197
+ applyLintAndFormatDependencies(devDependencies, opts);
198
+ const manifest = {
199
+ name: opts.name,
200
+ private: true,
201
+ packageManager: opts.packageManager
202
+ };
203
+ if (Object.keys(scripts).length > 0) manifest.scripts = scripts;
204
+ if (Object.keys(devDependencies).length > 0) manifest.devDependencies = devDependencies;
205
+ return JSON.stringify(manifest, null, 2) + "\n";
206
+ }
189
207
  const DEFAULT_WORKFLOWS_REF = "7320d30bcd47cee17cc2d8d28250ba1ab1f742b8";
190
208
  const DEFAULT_WORKFLOWS_VERSION = "v1.0.3";
191
209
  function resolveWorkflowsPin(opts) {
@@ -376,267 +394,58 @@ function yamlString(value) {
376
394
  }
377
395
 
378
396
  //#endregion
379
- //#region src/commands/ci.ts
380
- async function runCi() {
381
- try {
382
- intro("frontpl (ci)");
383
- const rootDir = process.cwd();
384
- const detectedPackageManager = await detectPackageManager(rootDir);
385
- const packageManager = await select({
386
- message: detectedPackageManager ? `Package manager (detected: ${detectedPackageManager})` : "Package manager",
387
- initialValue: detectedPackageManager ?? "pnpm",
388
- options: [
389
- {
390
- value: "npm",
391
- label: "npm"
392
- },
393
- {
394
- value: "yarn",
395
- label: "yarn"
396
- },
397
- {
398
- value: "pnpm",
399
- label: "pnpm"
400
- },
401
- {
402
- value: "bun",
403
- label: "bun"
404
- },
405
- {
406
- value: "deno",
407
- label: "deno"
408
- }
409
- ]
410
- });
411
- if (isCancel(packageManager)) return abort$2();
412
- const candidates = await listPackageCandidates(rootDir, packageManager);
413
- if (candidates.length === 0) {
414
- cancel("No package found. Run this command in a project root (with package.json or deno.json).");
415
- process.exitCode = 1;
416
- return;
417
- }
418
- const initialWorkingDirectory = await detectWorkingDirectory(rootDir, candidates);
419
- const workingDirectory = candidates.length === 1 ? candidates[0] : await select({
420
- message: "Working directory (package folder)",
421
- initialValue: initialWorkingDirectory,
422
- options: candidates.map((c) => ({
423
- value: c,
424
- label: c
425
- }))
426
- });
427
- if (isCancel(workingDirectory)) return abort$2();
428
- const nodeVersionDefault = await detectNodeMajorVersion(rootDir) ?? 22;
429
- const nodeVersionText = await text({
430
- message: "Node.js major version (for GitHub Actions)",
431
- initialValue: String(nodeVersionDefault),
432
- validate: (value = "") => {
433
- const major = Number.parseInt(value.trim(), 10);
434
- if (!Number.isFinite(major) || major <= 0) return "Enter a valid major version (e.g. 22)";
435
- }
436
- });
437
- if (isCancel(nodeVersionText)) return abort$2();
438
- const nodeVersion = Number.parseInt(String(nodeVersionText).trim(), 10);
439
- const { runLint, runFormatCheck, runTests, lintCommand, formatCheckCommand, testCommand } = await resolveCiCommands(rootDir, workingDirectory, packageManager);
440
- const addRelease = await confirm({
441
- message: "Add release workflow too?",
442
- initialValue: true
443
- });
444
- if (isCancel(addRelease)) return abort$2();
445
- const releaseMode = addRelease ? await select({
446
- message: "Release workflows",
447
- initialValue: "tag",
448
- options: [
449
- {
450
- value: "tag",
451
- label: "Tag push (vX.Y.Z) — recommended"
452
- },
453
- {
454
- value: "commit",
455
- label: "Release commit (chore(release): vX.Y.Z) — legacy"
456
- },
457
- {
458
- value: "both",
459
- label: "Both (tag + commit)"
460
- }
461
- ]
462
- }) : void 0;
463
- if (isCancel(releaseMode)) return abort$2();
464
- const trustedPublishing = addRelease && packageManager !== "deno" ? await confirm({
465
- message: "Release: npm trusted publishing (OIDC)?",
466
- initialValue: true
467
- }) : void 0;
468
- if (isCancel(trustedPublishing)) return abort$2();
469
- const addDependabot = await pathExists(path.join(rootDir, ".git")) ? await confirm({
470
- message: "Add/update Dependabot config (.github/dependabot.yml)?",
471
- initialValue: true
472
- }) : false;
473
- if (isCancel(addDependabot)) return abort$2();
474
- const ciWorkflowPath = path.join(rootDir, ".github/workflows/ci.yml");
475
- const releaseWorkflowPath = path.join(rootDir, ".github/workflows/release.yml");
476
- const dependabotPath = path.join(rootDir, ".github/dependabot.yml");
477
- if (!await confirmOverwriteIfExists(ciWorkflowPath, ".github/workflows/ci.yml")) {
478
- cancel("Skipped CI workflow");
479
- process.exitCode = 0;
480
- return;
481
- }
482
- await writeText(ciWorkflowPath, githubCliCiWorkflowTemplate({
483
- packageManager,
484
- nodeVersion,
485
- workingDirectory,
486
- runLint,
487
- runFormatCheck,
488
- runTests,
489
- lintCommand,
490
- formatCheckCommand,
491
- testCommand
492
- }));
493
- if (addRelease) {
494
- if (await confirmOverwriteIfExists(releaseWorkflowPath, ".github/workflows/release.yml")) await writeText(releaseWorkflowPath, (releaseMode === "both" ? githubCliReleaseBothWorkflowTemplate : releaseMode === "commit" ? githubCliReleaseWorkflowTemplate : githubCliReleaseTagWorkflowTemplate)({
495
- packageManager,
496
- nodeVersion,
497
- workingDirectory,
498
- trustedPublishing
499
- }));
500
- }
501
- if (addDependabot) {
502
- if (await confirmOverwriteIfExists(dependabotPath, ".github/dependabot.yml")) await writeText(dependabotPath, githubDependabotTemplate({
503
- packageManager,
504
- workingDirectory
505
- }));
506
- }
507
- outro(addRelease ? "Done. Generated CI + release workflows (and optional Dependabot)." : "Done. Generated CI workflow (and optional Dependabot).");
508
- } catch (err) {
509
- if (err instanceof CancelledError$2) return;
510
- throw err;
511
- }
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");
512
401
  }
513
- var CancelledError$2 = class extends Error {
514
- constructor() {
515
- super("Cancelled");
402
+
403
+ //#endregion
404
+ //#region src/lib/utils.ts
405
+ async function pathExists(pathname) {
406
+ try {
407
+ await access(pathname);
408
+ return true;
409
+ } catch {
410
+ return false;
516
411
  }
517
- };
518
- function abort$2(opts = {}) {
519
- cancel(opts.message ?? "Cancelled");
520
- process.exitCode = opts.exitCode ?? 0;
521
- throw new CancelledError$2();
522
412
  }
523
- async function confirmOverwriteIfExists(absPath, label) {
524
- if (!await pathExists(absPath)) return true;
525
- const overwrite = await confirm({
526
- message: `Overwrite existing ${label}?`,
527
- initialValue: true
528
- });
529
- if (isCancel(overwrite)) return abort$2();
530
- return overwrite;
531
- }
532
- async function listPackageCandidates(rootDir, packageManager) {
533
- const candidates = /* @__PURE__ */ new Set();
534
- if (await pathExists(path.join(rootDir, "package.json"))) candidates.add(".");
535
- if (packageManager === "deno" && (await pathExists(path.join(rootDir, "deno.json")) || await pathExists(path.join(rootDir, "deno.jsonc")))) candidates.add(".");
536
- for (const base of ["packages", "apps"]) {
537
- const baseDir = path.join(rootDir, base);
538
- if (!await pathExists(baseDir)) continue;
539
- const entries = await readdir(baseDir, { withFileTypes: true });
540
- for (const entry of entries) {
541
- if (!entry.isDirectory()) continue;
542
- if (await pathExists(path.join(baseDir, entry.name, "package.json"))) candidates.add(path.posix.join(base, entry.name));
543
- }
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;
544
421
  }
545
- return [...candidates];
546
422
  }
547
- async function detectWorkingDirectory(rootDir, candidates) {
548
- if (candidates.length === 1) return candidates[0];
549
- const rootScripts = (await readPackageJson(path.join(rootDir, "package.json")))?.scripts ?? {};
550
- const rootHasScripts = Object.keys(rootScripts).length > 0;
551
- const nonRoot = candidates.filter((c) => c !== ".");
552
- if (!rootHasScripts && nonRoot.length === 1) return nonRoot[0];
553
- return ".";
423
+ async function writePackageJson(filePath, value) {
424
+ await writeText(filePath, JSON.stringify(value, null, 2) + "\n");
554
425
  }
555
- async function detectNodeMajorVersion(rootDir) {
556
- for (const file of [".nvmrc", ".node-version"]) {
557
- const filePath = path.join(rootDir, file);
558
- if (!await pathExists(filePath)) continue;
559
- const major = parseMajorVersion((await readFile(filePath, "utf8")).split("\n")[0]?.trim() ?? "");
560
- 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;
561
431
  }
562
- const engine = (await readPackageJson(path.join(rootDir, "package.json")))?.engines?.node;
563
- if (!engine) return;
564
- const match = engine.match(/([0-9]{2,})/);
565
- if (!match) return;
566
- return Number.parseInt(match[1], 10);
567
- }
568
- function parseMajorVersion(input) {
569
- const trimmed = input.trim().replace(/^v/, "");
570
- const major = Number.parseInt(trimmed.split(".")[0] ?? "", 10);
571
- if (!Number.isFinite(major) || major <= 0) return;
572
- return major;
573
- }
574
- async function resolveCiCommands(rootDir, workingDirectory, packageManager) {
575
- if (packageManager === "deno") return {
576
- runLint: true,
577
- runFormatCheck: true,
578
- runTests: true
579
- };
580
- const pkg = await readPackageJson(path.join(rootDir, workingDirectory, "package.json"));
581
- if (!pkg) return abort$2({
582
- message: `Missing package.json in ${workingDirectory}`,
583
- exitCode: 1
584
- });
585
- const scripts = pkg.scripts ?? {};
586
- const hasLint = typeof scripts.lint === "string";
587
- const hasTest = typeof scripts.test === "string";
588
- const hasFormatCheck = typeof scripts["format:check"] === "string";
589
- const hasFmtCheck = typeof scripts["fmt:check"] === "string";
590
- const runLintDefault = hasLint;
591
- const runFormatCheckDefault = hasFormatCheck || hasFmtCheck;
592
- const runTestsDefault = hasTest;
593
- const runLint = await confirm({
594
- message: `CI: run lint${hasLint ? "" : " (no lint script detected)"}`,
595
- initialValue: runLintDefault
596
- });
597
- if (isCancel(runLint)) return abort$2();
598
- const runFormatCheck = await confirm({
599
- message: `CI: run format check${runFormatCheckDefault ? "" : " (no format check script detected)"}`,
600
- initialValue: runFormatCheckDefault
601
- });
602
- if (isCancel(runFormatCheck)) return abort$2();
603
- const runTests = await confirm({
604
- message: `CI: run tests${hasTest ? "" : " (no test script detected)"}`,
605
- initialValue: runTestsDefault
606
- });
607
- if (isCancel(runTests)) return abort$2();
608
- return {
609
- runLint,
610
- runFormatCheck,
611
- runTests,
612
- lintCommand: runLint && hasLint ? pmRun$1(packageManager, "lint") : runLint ? await promptCommand("Lint command", pmRun$1(packageManager, "lint")) : void 0,
613
- 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,
614
- testCommand: runTests && hasTest ? pmRun$1(packageManager, "test") : runTests ? await promptCommand("Test command", pmRun$1(packageManager, "test")) : void 0
615
- };
616
- }
617
- async function promptCommand(message, initialValue) {
618
- const value = await text({
619
- message,
620
- initialValue,
621
- validate: (v = "") => !v.trim() ? "Command is required" : void 0
622
- });
623
- if (isCancel(value)) return abort$2();
624
- 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;
625
440
  }
626
- function pmRun$1(pm, script) {
627
- switch (pm) {
628
- case "npm": return `npm run ${script}`;
629
- case "pnpm": return `pnpm run ${script}`;
630
- case "yarn": return `yarn ${script}`;
631
- case "bun": return `bun run ${script}`;
632
- case "deno": return script;
633
- }
441
+ function isPackageManager(value) {
442
+ return value === "npm" || value === "pnpm" || value === "yarn" || value === "bun" || value === "deno";
634
443
  }
635
444
 
636
445
  //#endregion
637
446
  //#region src/lib/exec.ts
638
447
  async function exec(command, args, opts = {}) {
639
- const resolved = resolveCommand$1(command);
448
+ const resolved = resolveCommand(command);
640
449
  return new Promise((resolve) => {
641
450
  const child = spawn(resolved, args, {
642
451
  cwd: opts.cwd,
@@ -648,52 +457,6 @@ async function exec(command, args, opts = {}) {
648
457
  child.on("error", () => resolve({ ok: false }));
649
458
  });
650
459
  }
651
- function resolveCommand$1(command) {
652
- if (process.platform !== "win32") return command;
653
- if (command === "npm") return "npm.cmd";
654
- if (command === "pnpm") return "pnpm.cmd";
655
- if (command === "yarn") return "yarn.cmd";
656
- return command;
657
- }
658
-
659
- //#endregion
660
- //#region src/lib/versions.ts
661
- async function detectPackageManagerVersion(pm) {
662
- switch (pm) {
663
- case "npm": return (await execCapture("npm", ["--version"])).stdout.trim() || void 0;
664
- case "pnpm": return (await execCapture("pnpm", ["--version"])).stdout.trim() || void 0;
665
- case "yarn": return (await execCapture("yarn", ["--version"])).stdout.trim() || void 0;
666
- case "bun": return (await execCapture("bun", ["--version"])).stdout.trim() || void 0;
667
- case "deno": return ((await execCapture("deno", ["--version"])).stdout.trim().split("\n")[0] ?? "").match(/deno\\s+([0-9]+\\.[0-9]+\\.[0-9]+)/)?.[1];
668
- }
669
- }
670
- async function execCapture(command, args) {
671
- const resolved = resolveCommand(command);
672
- return new Promise((resolve) => {
673
- const child = spawn(resolved, args, {
674
- cwd: os.tmpdir(),
675
- stdio: [
676
- "ignore",
677
- "pipe",
678
- "ignore"
679
- ],
680
- shell: false,
681
- env: process.env
682
- });
683
- const chunks = [];
684
- child.stdout.on("data", (d) => chunks.push(Buffer.from(d)));
685
- child.on("close", (code) => {
686
- resolve({
687
- ok: code === 0,
688
- stdout: Buffer.concat(chunks).toString("utf8")
689
- });
690
- });
691
- child.on("error", () => resolve({
692
- ok: false,
693
- stdout: ""
694
- }));
695
- });
696
- }
697
460
  function resolveCommand(command) {
698
461
  if (process.platform !== "win32") return command;
699
462
  if (command === "npm") return "npm.cmd";
@@ -704,7 +467,7 @@ function resolveCommand(command) {
704
467
 
705
468
  //#endregion
706
469
  //#region src/commands/init.ts
707
- function pmRun(pm, script) {
470
+ function pmRun$1(pm, script) {
708
471
  switch (pm) {
709
472
  case "npm": return `npm run ${script}`;
710
473
  case "pnpm": return `pnpm run ${script}`;
@@ -720,7 +483,7 @@ async function runInit({ nameArg }) {
720
483
  initialValue: nameArg ?? "my-frontend",
721
484
  validate: validateProjectName
722
485
  });
723
- if (isCancel(projectName)) return onCancel();
486
+ if (isCancel(projectName)) return onCancel$1();
724
487
  const packageManager = await select({
725
488
  message: "Package manager",
726
489
  initialValue: "pnpm",
@@ -747,37 +510,37 @@ async function runInit({ nameArg }) {
747
510
  }
748
511
  ]
749
512
  });
750
- if (isCancel(packageManager)) return onCancel();
513
+ if (isCancel(packageManager)) return onCancel$1();
751
514
  const pnpmWorkspace = packageManager === "pnpm" ? await confirm({
752
515
  message: "pnpm workspace mode (monorepo skeleton)?",
753
516
  initialValue: false
754
517
  }) : false;
755
- if (isCancel(pnpmWorkspace)) return onCancel();
518
+ if (isCancel(pnpmWorkspace)) return onCancel$1();
756
519
  const useOxlint = await confirm({
757
520
  message: "Enable oxlint (@kingsword/lint-config preset)?",
758
521
  initialValue: true
759
522
  });
760
- if (isCancel(useOxlint)) return onCancel();
523
+ if (isCancel(useOxlint)) return onCancel$1();
761
524
  const useOxfmt = await confirm({
762
525
  message: "Enable oxfmt (code formatting)?",
763
526
  initialValue: true
764
527
  });
765
- if (isCancel(useOxfmt)) return onCancel();
528
+ if (isCancel(useOxfmt)) return onCancel$1();
766
529
  const useVitest = await confirm({
767
530
  message: "Add Vitest?",
768
531
  initialValue: false
769
532
  });
770
- if (isCancel(useVitest)) return onCancel();
533
+ if (isCancel(useVitest)) return onCancel$1();
771
534
  const useTsdown = await confirm({
772
535
  message: "Add tsdown build?",
773
536
  initialValue: true
774
537
  });
775
- if (isCancel(useTsdown)) return onCancel();
538
+ if (isCancel(useTsdown)) return onCancel$1();
776
539
  const initGit = await confirm({
777
540
  message: "Initialize a git repository?",
778
541
  initialValue: true
779
542
  });
780
- if (isCancel(initGit)) return onCancel();
543
+ if (isCancel(initGit)) return onCancel$1();
781
544
  const githubActions = await select({
782
545
  message: "GitHub Actions workflows",
783
546
  initialValue: "ci",
@@ -796,7 +559,7 @@ async function runInit({ nameArg }) {
796
559
  }
797
560
  ]
798
561
  });
799
- if (isCancel(githubActions)) return onCancel();
562
+ if (isCancel(githubActions)) return onCancel$1();
800
563
  const releaseMode = githubActions === "ci+release" ? await select({
801
564
  message: "Release workflows",
802
565
  initialValue: "tag",
@@ -815,17 +578,17 @@ async function runInit({ nameArg }) {
815
578
  }
816
579
  ]
817
580
  }) : void 0;
818
- if (isCancel(releaseMode)) return onCancel();
581
+ if (isCancel(releaseMode)) return onCancel$1();
819
582
  const addDependabot = initGit && githubActions !== "none" ? await confirm({
820
583
  message: "Add Dependabot config (.github/dependabot.yml)?",
821
584
  initialValue: true
822
585
  }) : false;
823
- if (isCancel(addDependabot)) return onCancel();
586
+ if (isCancel(addDependabot)) return onCancel$1();
824
587
  const trustedPublishing = githubActions === "ci+release" && packageManager !== "deno" ? await confirm({
825
588
  message: "Release: npm trusted publishing (OIDC)?",
826
589
  initialValue: true
827
590
  }) : void 0;
828
- if (isCancel(trustedPublishing)) return onCancel();
591
+ if (isCancel(trustedPublishing)) return onCancel$1();
829
592
  const rootDir = path.resolve(process.cwd(), projectName);
830
593
  if (await pathExists(rootDir)) {
831
594
  cancel(`Directory already exists: ${rootDir}`);
@@ -833,6 +596,10 @@ async function runInit({ nameArg }) {
833
596
  return;
834
597
  }
835
598
  const pkgDir = pnpmWorkspace ? path.join(rootDir, "packages", projectName) : rootDir;
599
+ const toolingDir = pnpmWorkspace ? rootDir : pkgDir;
600
+ const packageUseOxlint = pnpmWorkspace ? false : useOxlint;
601
+ const packageUseOxfmt = pnpmWorkspace ? false : useOxfmt;
602
+ const packageIncludeTypecheckWithoutOxlint = !(pnpmWorkspace && useOxlint);
836
603
  const pmVersion = await detectPackageManagerVersion(packageManager);
837
604
  const packageManagerField = pmVersion ? `${packageManager}@${pmVersion}` : `${packageManager}@latest`;
838
605
  await mkdir(path.join(pkgDir, "src"), { recursive: true });
@@ -847,11 +614,18 @@ async function runInit({ nameArg }) {
847
614
  " - \"packages/*\"",
848
615
  ""
849
616
  ].join("\n"));
850
- await writeText(path.join(rootDir, "package.json"), JSON.stringify({
617
+ await writeText(path.join(rootDir, "package.json"), workspaceRootPackageJsonTemplate({
851
618
  name: projectName,
852
- private: true,
853
- packageManager: packageManagerField
854
- }, null, 2) + "\n");
619
+ packageManager: packageManagerField,
620
+ useOxlint,
621
+ oxlintVersion: "latest",
622
+ oxlintTsgolintVersion: "latest",
623
+ kingswordLintConfigVersion: "latest",
624
+ useOxfmt,
625
+ oxfmtVersion: "latest",
626
+ useVitest,
627
+ useTsdown
628
+ }));
855
629
  }
856
630
  await Promise.all([
857
631
  writeText(path.join(pkgDir, "README.md"), readmeTemplate(projectName)),
@@ -861,11 +635,12 @@ async function runInit({ nameArg }) {
861
635
  name: projectName,
862
636
  packageManager: packageManagerField,
863
637
  typescriptVersion: "latest",
864
- useOxlint,
638
+ useOxlint: packageUseOxlint,
639
+ includeTypecheckWithoutOxlint: packageIncludeTypecheckWithoutOxlint,
865
640
  oxlintVersion: "latest",
866
641
  oxlintTsgolintVersion: "latest",
867
642
  kingswordLintConfigVersion: "latest",
868
- useOxfmt,
643
+ useOxfmt: packageUseOxfmt,
869
644
  oxfmtVersion: "latest",
870
645
  useVitest,
871
646
  vitestVersion: "latest",
@@ -873,71 +648,423 @@ async function runInit({ nameArg }) {
873
648
  tsdownVersion: "latest"
874
649
  }))
875
650
  ]);
876
- if (useOxlint) await writeText(path.join(pkgDir, "oxlint.config.ts"), oxlintConfigTemplate({ useVitest }));
877
- if (useOxfmt) await writeText(path.join(pkgDir, ".oxfmtrc.json"), oxfmtConfigTemplate());
651
+ if (useOxlint) await writeText(path.join(toolingDir, "oxlint.config.ts"), oxlintConfigTemplate({ useVitest }));
652
+ if (useOxfmt) await writeText(path.join(toolingDir, ".oxfmtrc.json"), oxfmtConfigTemplate());
878
653
  if (useVitest) await writeText(path.join(pkgDir, "src/index.test.ts"), srcVitestTemplate());
879
654
  if (useTsdown) await writeText(path.join(pkgDir, "tsdown.config.ts"), tsdownConfigTemplate());
880
655
  if (packageManager === "deno") await writeText(path.join(rootDir, "deno.json"), JSON.stringify({ nodeModulesDir: "auto" }, null, 2) + "\n");
881
656
  if (githubActions !== "none") {
882
- const workingDirectory = pnpmWorkspace ? path.posix.join("packages", projectName) : ".";
883
- const lintCommand = useOxlint && packageManager !== "deno" ? pmRun(packageManager, "lint") : void 0;
884
- const formatCheckCommand = useOxfmt && packageManager !== "deno" ? pmRun(packageManager, "format:check") : void 0;
885
- const testCommand = useVitest && packageManager !== "deno" ? pmRun(packageManager, "test") : void 0;
657
+ const workingDirectory = ".";
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;
886
661
  await writeText(path.join(rootDir, ".github/workflows/ci.yml"), githubCliCiWorkflowTemplate({
887
662
  packageManager,
888
- 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,
889
921
  workingDirectory,
890
- runLint: useOxlint,
891
- runFormatCheck: useOxfmt,
892
- runTests: useVitest,
922
+ runLint,
923
+ runFormatCheck,
924
+ runTests,
893
925
  lintCommand,
894
926
  formatCheckCommand,
895
927
  testCommand
896
928
  }));
897
- if (addDependabot) await writeText(path.join(rootDir, ".github/dependabot.yml"), githubDependabotTemplate({
898
- packageManager,
899
- workingDirectory
900
- }));
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;
901
947
  }
902
- if (githubActions === "ci+release") {
903
- const workingDirectory = pnpmWorkspace ? path.posix.join("packages", projectName) : ".";
904
- await writeText(path.join(rootDir, ".github/workflows/release.yml"), (releaseMode === "both" ? githubCliReleaseBothWorkflowTemplate : releaseMode === "commit" ? githubCliReleaseWorkflowTemplate : githubCliReleaseTagWorkflowTemplate)({
905
- packageManager,
906
- nodeVersion: 22,
907
- workingDirectory,
908
- trustedPublishing
909
- }));
948
+ }
949
+ var CancelledError$2 = class extends Error {
950
+ constructor() {
951
+ super("Cancelled");
910
952
  }
911
- const canInstall = Boolean(pmVersion);
912
- let installOk = false;
913
- if (canInstall) {
914
- const installSpinner = spinner();
915
- installSpinner.start(`Installing dependencies with ${packageManager}`);
916
- installOk = (await exec(packageManager, ["install"], { cwd: rootDir })).ok;
917
- 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
+ }
918
980
  }
919
- if (initGit) await exec("git", ["init"], { cwd: rootDir });
920
- 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];
921
982
  }
922
- function validateProjectName(value) {
923
- const name = (value ?? "").trim();
924
- if (!name) return "Project name is required";
925
- if (name.length > 214) return "Project name is too long";
926
- if (name.startsWith(".")) return "Project name cannot start with '.'";
927
- if (name.startsWith("_")) return "Project name cannot start with '_'";
928
- 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 ".";
929
990
  }
930
- function onCancel() {
931
- cancel("Cancelled");
932
- 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);
933
1003
  }
934
- 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) {
935
1062
  switch (pm) {
936
- case "npm": return "npm run lint";
937
- case "pnpm": return "pnpm run lint";
938
- case "yarn": return "yarn lint";
939
- case "bun": return "bun run lint";
940
- 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;
941
1068
  }
942
1069
  }
943
1070
 
@@ -1190,7 +1317,9 @@ function abort$1(opts = {}) {
1190
1317
  //#region src/commands/oxfmt.ts
1191
1318
  const OXFMT_SCRIPTS = {
1192
1319
  format: "oxfmt",
1193
- "format:check": "oxfmt --check",
1320
+ "format:check": "oxfmt --check"
1321
+ };
1322
+ const OXFMT_LEGACY_SCRIPTS = {
1194
1323
  fmt: "oxfmt",
1195
1324
  "fmt:check": "oxfmt --check"
1196
1325
  };
@@ -1245,6 +1374,7 @@ async function runOxfmt({ yes = false } = {}) {
1245
1374
  rootDir
1246
1375
  });
1247
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";
1248
1378
  const depSummary = stats.addedOxfmtDependency ? "added devDependency: oxfmt" : "devDependency oxfmt already present";
1249
1379
  const removedDepsSummary = stats.removedDependencies.length > 0 ? `removed prettier deps: ${stats.removedDependencies.join(", ")}` : "no prettier deps removed";
1250
1380
  const removedPackageJsonPrettierSummary = stats.removedPackageJsonPrettierConfig ? "removed package.json#prettier" : "no package.json#prettier removed";
@@ -1254,6 +1384,7 @@ async function runOxfmt({ yes = false } = {}) {
1254
1384
  outro([
1255
1385
  "Done. Applied oxfmt migration.",
1256
1386
  `- ${scriptSummary}`,
1387
+ `- ${legacyScriptSummary}`,
1257
1388
  `- ${depSummary}`,
1258
1389
  `- ${removedDepsSummary}`,
1259
1390
  `- ${removedPackageJsonPrettierSummary}`,
@@ -1286,6 +1417,12 @@ async function migrateToOxfmt(opts) {
1286
1417
  scripts[name] = command;
1287
1418
  scriptsUpdated.push(name);
1288
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
+ }
1289
1426
  pkg.scripts = scripts;
1290
1427
  const devDependencies = { ...pkg.devDependencies };
1291
1428
  let addedOxfmtDependency = false;
@@ -1323,6 +1460,7 @@ async function migrateToOxfmt(opts) {
1323
1460
  return {
1324
1461
  scriptsUpdated,
1325
1462
  scriptsKept,
1463
+ removedLegacyScripts,
1326
1464
  addedOxfmtDependency,
1327
1465
  removedPackageJsonPrettierConfig,
1328
1466
  removedDependencies,
@@ -1454,4 +1592,4 @@ function abort(opts = {}) {
1454
1592
  }
1455
1593
 
1456
1594
  //#endregion
1457
- 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 };
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.0",
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": {