Skip to content

Commit 97d1113

Browse files
refactor(cli): share create and init command modules (#12)
* refactor(cli): add shared manifest module Centralize VernoManifest types, builders, writers, and detection in commands/shared. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(cli): add shared post-scaffold runners Move install, shadcn, and ultracite execution helpers to commands/shared. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(cli): unify create and init plan step builders Extract shared install, shadcn, and ultracite plan steps used by both commands. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(cli): share input primitives between create and init Deduplicate UiMode, package manager guards, and shadcn preset defaults. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(cli): split init detection and restructure modules Replace monolithic init/actions with detect.ts, restructure.ts, and a thin re-export barrel. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(cli): narrow create actions to scaffold responsibilities Drop duplicated manifest and post-scaffold logic now provided by shared modules. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(cli): extract post-setup pipeline for create and init Share dry-run printing, install tasks, and shadcn/ultracite/manifest orchestration. Co-authored-by: Cursor <cursoragent@cursor.com> * test(cli): use complete manifest fixture for detection Align detectVernoManifest tests with stricter manifest validation. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: add changeset for cli shared commands refactor Co-authored-by: Cursor <cursoragent@cursor.com> * style(cli): format init-actions test file Apply ultracite formatting to detectVernoManifest test setup. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent ce5d91c commit 97d1113

18 files changed

Lines changed: 1122 additions & 1117 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@vernostudio/cli": patch
3+
---
4+
5+
Refactor create and init commands to share manifest, post-scaffold, and post-setup pipeline modules (no user-facing behavior change).

packages/cli/__tests__/init-actions.test.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,27 @@ describe("detectPackageJson", () => {
3838
});
3939
});
4040

41+
const sampleManifest = {
42+
addons: ["turborepo"],
43+
createdAt: "2024-01-01T00:00:00.000Z",
44+
frontend: "next",
45+
generator: "verno",
46+
generatorVersion: "0.0.0",
47+
packageManager: "bun",
48+
packages: [],
49+
projectName: "test",
50+
studio: "Verno Studio",
51+
ui: "shadcn",
52+
};
53+
4154
describe("detectVernoManifest", () => {
4255
test("returns parsed manifest when .verno/manifest.json exists with verno generator", () => {
4356
mkdirSync(join(TMP_DIR, ".verno"), { recursive: true });
44-
writeFileSync(
45-
join(TMP_DIR, ".verno", "manifest.json"),
46-
JSON.stringify({ addons: ["turborepo"], generator: "verno", projectName: "test" }),
47-
);
57+
writeFileSync(join(TMP_DIR, ".verno", "manifest.json"), JSON.stringify(sampleManifest));
4858
const result = detectVernoManifest(TMP_DIR);
4959
expect(result).not.toBeNull();
50-
const addons = (result as PackageJsonRecord | null)?.addons;
51-
expect(addons).toContain("turborepo");
60+
expect(result?.addons).toContain("turborepo");
61+
expect(result?.projectName).toBe("test");
5262
});
5363

5464
test("returns null when manifest does not exist", () => {

packages/cli/src/commands/create/actions.ts

Lines changed: 9 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,17 @@
11
import { existsSync } from "node:fs";
2-
import { mkdir, rm, writeFile } from "node:fs/promises";
3-
import { join, resolve } from "node:path";
2+
import { resolve } from "node:path";
43
import { generate, writeTree } from "@vernostudio/template-generator";
5-
import type {
6-
AddonId,
7-
FrontendId,
8-
PackageId,
9-
PackageManager,
10-
ProjectConfig,
11-
} from "@vernostudio/template-generator";
12-
import {
13-
getPmInstallCommand,
14-
getShadcnAddAllCommand,
15-
getShadcnBootstrapCommand,
16-
getUltraciteInitCommand,
17-
} from "../../pm-exec";
18-
import type { UltraciteInitMode } from "../../pm-exec";
4+
import type { PackageManager, ProjectConfig } from "@vernostudio/template-generator";
195
import { runProcess } from "../../run";
20-
import type { ResolvedCreateInputs, UiMode } from "./args";
21-
import type { UltraciteLinterId } from "../../ultracite-linter";
22-
import { readCliPackageVersion } from "../../cli-version";
23-
import { getShadcnWorkingDirectory } from "./plan";
24-
import {
25-
VERNO_INITIAL_COMMIT_BODY,
26-
VERNO_INITIAL_COMMIT_SUBJECT,
27-
VERNO_MANIFEST_DIR,
28-
} from "../../constants";
6+
import type { ResolvedCreateInputs } from "./args";
7+
import { VERNO_INITIAL_COMMIT_BODY, VERNO_INITIAL_COMMIT_SUBJECT } from "../../constants";
298

309
export { ensureAppGlobalsBaseLayerAtEnd } from "../../app-globals";
31-
32-
export interface VernoManifest {
33-
readonly addons: readonly AddonId[];
34-
readonly createdAt: string;
35-
readonly frontend: FrontendId;
36-
readonly generator: "verno";
37-
readonly generatorVersion: string;
38-
readonly packageManager: PackageManager;
39-
readonly packages: readonly PackageId[];
40-
readonly projectName: string;
41-
readonly shadcnPreset?: string;
42-
readonly studio: "Verno Studio";
43-
readonly ui: UiMode;
44-
readonly ultraciteLinter?: UltraciteLinterId;
45-
}
46-
47-
export const buildVernoManifest = (args: {
48-
readonly resolved: ResolvedCreateInputs;
49-
readonly projectName: string;
50-
}): VernoManifest => ({
51-
addons: args.resolved.addons,
52-
createdAt: new Date().toISOString(),
53-
frontend: args.resolved.frontend,
54-
generator: "verno",
55-
generatorVersion: readCliPackageVersion(),
56-
packageManager: args.resolved.packageManager,
57-
packages: args.resolved.packages,
58-
projectName: args.projectName,
59-
shadcnPreset: args.resolved.useShadcn ? args.resolved.shadcnPreset : undefined,
60-
studio: "Verno Studio",
61-
ui: args.resolved.ui,
62-
ultraciteLinter: args.resolved.runUltracite ? args.resolved.ultraciteLinter : undefined,
63-
});
64-
65-
export const writeVernoManifest = async (
66-
projectDir: string,
67-
manifest: VernoManifest,
68-
): Promise<void> => {
69-
const dir = join(projectDir, VERNO_MANIFEST_DIR);
70-
await mkdir(dir, { recursive: true });
71-
const out = join(dir, "manifest.json");
72-
await writeFile(out, `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
73-
};
10+
export {
11+
buildVernoManifestForCreate as buildVernoManifest,
12+
writeVernoManifest,
13+
} from "../shared/manifest";
14+
export type { VernoManifest } from "../shared/manifest";
7415

7516
export const getProjectPath = (name: string): string => resolve(process.cwd(), name);
7617

@@ -100,77 +41,6 @@ export const scaffold = async (
10041
return { filesWritten: filesWritten.length };
10142
};
10243

103-
export const runInstallIfEnabled = async (
104-
enabled: boolean,
105-
packageManager: PackageManager,
106-
projectDir: string,
107-
): Promise<void> => {
108-
if (!enabled) {
109-
return;
110-
}
111-
const { args: installArgs, file } = getPmInstallCommand(packageManager);
112-
await runProcess(file, installArgs, { cwd: projectDir, stepId: "install" });
113-
};
114-
115-
export const runShadcnIfEnabled = async (options: {
116-
readonly enabled: boolean;
117-
readonly packageManager: PackageManager;
118-
readonly preset: string;
119-
readonly projectDir: string;
120-
readonly monorepoWithDesignSystem: boolean;
121-
}): Promise<void> => {
122-
if (!options.enabled) {
123-
return;
124-
}
125-
126-
const workingDir = getShadcnWorkingDirectory(
127-
options.projectDir,
128-
options.monorepoWithDesignSystem,
129-
);
130-
131-
// shadcn apply/add requires a detected framework (Next.js, Vite, etc.).
132-
// We write a temporary dummy config to ensure detection passes in all environments.
133-
const dummyConfigPath = join(workingDir, "vite.config.ts");
134-
await writeFile(dummyConfigPath, "export default {};\n", "utf-8");
135-
136-
try {
137-
const bootstrap = getShadcnBootstrapCommand(options.packageManager, {
138-
monorepoWithDesignSystem: options.monorepoWithDesignSystem,
139-
preset: options.preset,
140-
});
141-
const addAll = getShadcnAddAllCommand(options.packageManager, {
142-
monorepoWithDesignSystem: options.monorepoWithDesignSystem,
143-
});
144-
145-
for (const cmd of [bootstrap, addAll]) {
146-
await runProcess(cmd.file, cmd.args, {
147-
ciSafe: false,
148-
cwd: options.projectDir,
149-
stepId: "shadcn",
150-
});
151-
}
152-
} finally {
153-
await rm(dummyConfigPath, { force: true }).catch(() => {
154-
/* ignore cleanup errors */
155-
});
156-
}
157-
};
158-
159-
export const runUltraciteIfEnabled = async (
160-
enabled: boolean,
161-
packageManager: PackageManager,
162-
projectDir: string,
163-
mode: UltraciteInitMode,
164-
runOptions?: { readonly ciSafe?: boolean; readonly linter?: UltraciteLinterId },
165-
): Promise<void> => {
166-
if (!enabled) {
167-
return;
168-
}
169-
const u = getUltraciteInitCommand(packageManager, mode, { linter: runOptions?.linter });
170-
const ciSafe = runOptions?.ciSafe ?? mode === "quiet";
171-
await runProcess(u.file, u.args, { ciSafe, cwd: projectDir, stepId: "ultracite" });
172-
};
173-
17444
export const runGitInitAndCommitIfEnabled = async (
17545
enabled: boolean,
17646
projectDir: string,

packages/cli/src/commands/create/args.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,20 @@ import { DEFAULT_ULTRACITE_LINTER } from "../../ultracite-linter";
99
import type { UltraciteLinterId } from "../../ultracite-linter";
1010
import { parseAddonsArg } from "../shared/addons";
1111
import { splitCommaList } from "../shared/comma-list";
12+
import {
13+
DEFAULT_SHADCN_PRESET,
14+
isPackageManager,
15+
isUiMode,
16+
PACKAGE_MANAGERS,
17+
} from "../shared/input-primitives";
18+
import type { UiMode } from "../shared/input-primitives";
1219
import { parseUltraciteLinterFlag } from "../shared/ultracite";
1320

14-
export const PACKAGE_MANAGERS: readonly PackageManager[] = ["bun", "pnpm", "npm"];
15-
export const DEFAULT_SHADCN_PRESET = "nova";
21+
export { DEFAULT_SHADCN_PRESET, isPackageManager, isUiMode, PACKAGE_MANAGERS, type UiMode };
1622

1723
export const isFrontendId = (value: string | undefined): value is FrontendId =>
1824
value !== undefined && (FRONTENDS as readonly string[]).includes(value);
1925

20-
export const isPackageManager = (value: string | undefined): value is PackageManager =>
21-
value === "bun" || value === "pnpm" || value === "npm";
22-
23-
export type UiMode = "none" | "shadcn";
24-
25-
export const isUiMode = (value: string | undefined): value is UiMode =>
26-
value === "shadcn" || value === "none";
27-
2826
export const isPackageId = (value: string): value is PackageId =>
2927
(PACKAGE_IDS as readonly string[]).includes(value);
3028

0 commit comments

Comments
 (0)