create-skaff 0.0.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/package.json +31 -0
- package/src/components/confirm-step.tsx +33 -0
- package/src/components/name-step.tsx +30 -0
- package/src/components/package-manager-step.tsx +29 -0
- package/src/components/progress-step.tsx +40 -0
- package/src/components/step-row.tsx +24 -0
- package/src/components/wizard-app.tsx +69 -0
- package/src/index.tsx +7 -0
- package/src/lib/hooks/use-scaffold-runner.ts +46 -0
- package/src/lib/package-manager.ts +12 -0
- package/src/lib/scaffold-steps.ts +110 -0
- package/src/lib/utils/add-typography-plugin.ts +14 -0
- package/src/lib/utils/run-command.ts +27 -0
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-skaff",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Interactive Next.js project scaffolder: Tailwind, shadcn/ui, Motion, Lucide, Oxlint, Oxfmt, Ultracite, Claude and Codex config",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-skaff": "./src/index.tsx",
|
|
8
|
+
"skaff": "./src/index.tsx"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"dev": "bun src/index.tsx",
|
|
15
|
+
"typecheck": "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"bun": ">=1.3.0"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@opentui/core": "^0.5.11",
|
|
23
|
+
"@opentui/react": "^0.5.11",
|
|
24
|
+
"react": "^19.3.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/bun": "^1.4.2",
|
|
28
|
+
"@types/react": "^19.3.0",
|
|
29
|
+
"typescript": "^7.0.2"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ScaffoldConfig } from "../lib/scaffold-steps";
|
|
2
|
+
import { scaffoldSteps } from "../lib/scaffold-steps";
|
|
3
|
+
|
|
4
|
+
type ConfirmStepProps = { config: ScaffoldConfig };
|
|
5
|
+
|
|
6
|
+
export function ConfirmStep({ config }: ConfirmStepProps) {
|
|
7
|
+
return (
|
|
8
|
+
<box flexDirection="column" gap={1}>
|
|
9
|
+
<text>
|
|
10
|
+
<strong>Ready to scaffold</strong>
|
|
11
|
+
</text>
|
|
12
|
+
<box flexDirection="column" border borderStyle="rounded" paddingLeft={1} paddingRight={1}>
|
|
13
|
+
<text>
|
|
14
|
+
name: <span fg="#7dd3fc">{config.name}</span>
|
|
15
|
+
</text>
|
|
16
|
+
<text>
|
|
17
|
+
package manager: <span fg="#7dd3fc">{config.packageManager}</span>
|
|
18
|
+
</text>
|
|
19
|
+
<text>
|
|
20
|
+
directory: <span fg="#7dd3fc">{`${config.cwd}/${config.name}`}</span>
|
|
21
|
+
</text>
|
|
22
|
+
</box>
|
|
23
|
+
<box flexDirection="column">
|
|
24
|
+
{scaffoldSteps.map((step) => (
|
|
25
|
+
<text key={step.id} fg="#aaaaaa">
|
|
26
|
+
· {step.label}
|
|
27
|
+
</text>
|
|
28
|
+
))}
|
|
29
|
+
</box>
|
|
30
|
+
<text fg="#888888">Enter to start · Esc to go back</text>
|
|
31
|
+
</box>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
type NameStepProps = { initialValue: string; onSubmit: (name: string) => void };
|
|
4
|
+
|
|
5
|
+
export function NameStep({ initialValue, onSubmit }: NameStepProps) {
|
|
6
|
+
const [value, setValue] = useState(initialValue);
|
|
7
|
+
const name = value.trim();
|
|
8
|
+
const valid = /^[a-z0-9][a-z0-9._-]*$/.test(name);
|
|
9
|
+
return (
|
|
10
|
+
<box flexDirection="column" gap={1}>
|
|
11
|
+
<text>
|
|
12
|
+
<strong>Project name</strong>
|
|
13
|
+
</text>
|
|
14
|
+
<box border borderStyle="rounded" paddingLeft={1} paddingRight={1} width={40}>
|
|
15
|
+
<input
|
|
16
|
+
focused
|
|
17
|
+
placeholder="my-app"
|
|
18
|
+
value={initialValue}
|
|
19
|
+
onInput={setValue}
|
|
20
|
+
onSubmit={() => {
|
|
21
|
+
if (valid) {
|
|
22
|
+
onSubmit(name);
|
|
23
|
+
}
|
|
24
|
+
}}
|
|
25
|
+
/>
|
|
26
|
+
</box>
|
|
27
|
+
<text fg="#888888">lowercase letters, numbers, dots, dashes · Enter to continue</text>
|
|
28
|
+
</box>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { SelectOption } from "@opentui/core";
|
|
2
|
+
import { type PackageManager, packageManagers } from "../lib/package-manager";
|
|
3
|
+
|
|
4
|
+
type PackageManagerStepProps = { onSelect: (packageManager: PackageManager) => void };
|
|
5
|
+
|
|
6
|
+
const options: SelectOption[] = packageManagers.map((value) => ({ name: value, value, description: "" }));
|
|
7
|
+
|
|
8
|
+
export function PackageManagerStep({ onSelect }: PackageManagerStepProps) {
|
|
9
|
+
return (
|
|
10
|
+
<box flexDirection="column" gap={1}>
|
|
11
|
+
<text>
|
|
12
|
+
<strong>Package manager</strong>
|
|
13
|
+
</text>
|
|
14
|
+
<box border borderStyle="rounded" width={40} height={5}>
|
|
15
|
+
<select
|
|
16
|
+
focused
|
|
17
|
+
options={options}
|
|
18
|
+
showDescription={false}
|
|
19
|
+
onSelect={(_index, option) => {
|
|
20
|
+
if (option && packageManagers.includes(option.value as PackageManager)) {
|
|
21
|
+
onSelect(option.value as PackageManager);
|
|
22
|
+
}
|
|
23
|
+
}}
|
|
24
|
+
/>
|
|
25
|
+
</box>
|
|
26
|
+
<text fg="#888888">↑↓ to move · Enter to continue · Esc to go back</text>
|
|
27
|
+
</box>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useKeyboard } from "@opentui/react";
|
|
2
|
+
import { useScaffoldRunner } from "../lib/hooks/use-scaffold-runner";
|
|
3
|
+
import type { ScaffoldConfig } from "../lib/scaffold-steps";
|
|
4
|
+
import { StepRow } from "./step-row";
|
|
5
|
+
|
|
6
|
+
type ProgressStepProps = { config: ScaffoldConfig; onExit: () => void };
|
|
7
|
+
|
|
8
|
+
export function ProgressStep({ config, onExit }: ProgressStepProps) {
|
|
9
|
+
const { steps, finished, error } = useScaffoldRunner(config);
|
|
10
|
+
useKeyboard((key) => {
|
|
11
|
+
if (finished && (key.name === "q" || key.name === "return")) {
|
|
12
|
+
onExit();
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
return (
|
|
16
|
+
<box flexDirection="column" gap={1}>
|
|
17
|
+
<text>
|
|
18
|
+
<strong>{finished ? (error ? "Failed" : "Done") : "Scaffolding…"}</strong>
|
|
19
|
+
</text>
|
|
20
|
+
<box flexDirection="column">
|
|
21
|
+
{steps.map((step) => (
|
|
22
|
+
<StepRow key={step.id} step={step} />
|
|
23
|
+
))}
|
|
24
|
+
</box>
|
|
25
|
+
{error ? (
|
|
26
|
+
<box border borderStyle="rounded" borderColor="#f87171" paddingLeft={1} paddingRight={1}>
|
|
27
|
+
<text fg="#f87171">{error}</text>
|
|
28
|
+
</box>
|
|
29
|
+
) : null}
|
|
30
|
+
{finished && !error ? (
|
|
31
|
+
<box flexDirection="column">
|
|
32
|
+
<text fg="#4ade80">Next steps:</text>
|
|
33
|
+
<text>{` cd ${config.name}`}</text>
|
|
34
|
+
<text>{` ${config.packageManager} run dev`}</text>
|
|
35
|
+
</box>
|
|
36
|
+
) : null}
|
|
37
|
+
<text fg="#888888">{finished ? "Enter or q to exit" : "running… Ctrl+C to abort"}</text>
|
|
38
|
+
</box>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { StepState } from "../lib/hooks/use-scaffold-runner";
|
|
2
|
+
|
|
3
|
+
const glyphs: Record<StepState["status"], { icon: string; color: string }> = {
|
|
4
|
+
pending: { icon: "○", color: "#666666" },
|
|
5
|
+
running: { icon: "◐", color: "#facc15" },
|
|
6
|
+
done: { icon: "●", color: "#4ade80" },
|
|
7
|
+
failed: { icon: "✕", color: "#f87171" },
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type StepRowProps = { step: StepState };
|
|
11
|
+
|
|
12
|
+
export function StepRow({ step }: StepRowProps) {
|
|
13
|
+
const glyph = glyphs[step.status];
|
|
14
|
+
return (
|
|
15
|
+
<box flexDirection="column">
|
|
16
|
+
<text>
|
|
17
|
+
<span fg={glyph.color}>{glyph.icon}</span> {step.label}
|
|
18
|
+
</text>
|
|
19
|
+
{step.status === "running" && step.lastLine ? (
|
|
20
|
+
<text fg="#888888">{` ${step.lastLine.slice(0, 80)}`}</text>
|
|
21
|
+
) : null}
|
|
22
|
+
</box>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useKeyboard, useRenderer } from "@opentui/react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import type { PackageManager } from "../lib/package-manager";
|
|
4
|
+
import type { ScaffoldConfig } from "../lib/scaffold-steps";
|
|
5
|
+
import { ConfirmStep } from "./confirm-step";
|
|
6
|
+
import { NameStep } from "./name-step";
|
|
7
|
+
import { PackageManagerStep } from "./package-manager-step";
|
|
8
|
+
import { ProgressStep } from "./progress-step";
|
|
9
|
+
|
|
10
|
+
type Screen = "name" | "packageManager" | "confirm" | "progress";
|
|
11
|
+
|
|
12
|
+
type WizardAppProps = { initialName: string; cwd: string };
|
|
13
|
+
|
|
14
|
+
export function WizardApp({ initialName, cwd }: WizardAppProps) {
|
|
15
|
+
const renderer = useRenderer();
|
|
16
|
+
const [screen, setScreen] = useState<Screen>("name");
|
|
17
|
+
const [name, setName] = useState(initialName);
|
|
18
|
+
const [packageManager, setPackageManager] = useState<PackageManager>("pnpm");
|
|
19
|
+
const [config, setConfig] = useState<ScaffoldConfig | null>(null);
|
|
20
|
+
|
|
21
|
+
const exit = () => {
|
|
22
|
+
renderer.destroy();
|
|
23
|
+
process.exit(0);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
useKeyboard((key) => {
|
|
27
|
+
if (key.name === "escape") {
|
|
28
|
+
if (screen === "packageManager") {
|
|
29
|
+
setScreen("name");
|
|
30
|
+
} else if (screen === "confirm") {
|
|
31
|
+
setScreen("packageManager");
|
|
32
|
+
} else if (screen === "name") {
|
|
33
|
+
exit();
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (screen === "confirm" && key.name === "return") {
|
|
38
|
+
setConfig({ name, packageManager, cwd });
|
|
39
|
+
setScreen("progress");
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<box flexDirection="column" padding={1} gap={1}>
|
|
45
|
+
<text>
|
|
46
|
+
<strong fg="#a78bfa">skaff</strong> <span fg="#888888">· Next.js scaffolder</span>
|
|
47
|
+
</text>
|
|
48
|
+
{screen === "name" ? (
|
|
49
|
+
<NameStep
|
|
50
|
+
initialValue={name}
|
|
51
|
+
onSubmit={(value) => {
|
|
52
|
+
setName(value);
|
|
53
|
+
setScreen("packageManager");
|
|
54
|
+
}}
|
|
55
|
+
/>
|
|
56
|
+
) : null}
|
|
57
|
+
{screen === "packageManager" ? (
|
|
58
|
+
<PackageManagerStep
|
|
59
|
+
onSelect={(value) => {
|
|
60
|
+
setPackageManager(value);
|
|
61
|
+
setScreen("confirm");
|
|
62
|
+
}}
|
|
63
|
+
/>
|
|
64
|
+
) : null}
|
|
65
|
+
{screen === "confirm" ? <ConfirmStep config={{ name, packageManager, cwd }} /> : null}
|
|
66
|
+
{screen === "progress" && config ? <ProgressStep config={config} onExit={exit} /> : null}
|
|
67
|
+
</box>
|
|
68
|
+
);
|
|
69
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { createCliRenderer } from "@opentui/core";
|
|
3
|
+
import { createRoot } from "@opentui/react";
|
|
4
|
+
import { WizardApp } from "./components/wizard-app";
|
|
5
|
+
|
|
6
|
+
const renderer = await createCliRenderer({ exitOnCtrlC: true });
|
|
7
|
+
createRoot(renderer).render(<WizardApp initialName={process.argv[2] ?? ""} cwd={process.cwd()} />);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { type ScaffoldConfig, scaffoldSteps } from "../scaffold-steps";
|
|
3
|
+
|
|
4
|
+
export type StepStatus = "pending" | "running" | "done" | "failed";
|
|
5
|
+
|
|
6
|
+
export type StepState = { id: string; label: string; status: StepStatus; lastLine: string };
|
|
7
|
+
|
|
8
|
+
export type RunnerState = { steps: StepState[]; finished: boolean; error: string | null };
|
|
9
|
+
|
|
10
|
+
export function useScaffoldRunner(config: ScaffoldConfig): RunnerState {
|
|
11
|
+
const [steps, setSteps] = useState<StepState[]>(
|
|
12
|
+
scaffoldSteps.map(({ id, label }) => ({ id, label, status: "pending", lastLine: "" })),
|
|
13
|
+
);
|
|
14
|
+
const [finished, setFinished] = useState(false);
|
|
15
|
+
const [error, setError] = useState<string | null>(null);
|
|
16
|
+
const started = useRef(false);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (started.current) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
started.current = true;
|
|
23
|
+
|
|
24
|
+
const patch = (id: string, changes: Partial<StepState>) =>
|
|
25
|
+
setSteps((current) => current.map((step) => (step.id === id ? { ...step, ...changes } : step)));
|
|
26
|
+
|
|
27
|
+
const run = async () => {
|
|
28
|
+
for (const step of scaffoldSteps) {
|
|
29
|
+
patch(step.id, { status: "running" });
|
|
30
|
+
const result = await step.run(config, (line) => patch(step.id, { lastLine: line }));
|
|
31
|
+
if (!result.ok) {
|
|
32
|
+
patch(step.id, { status: "failed" });
|
|
33
|
+
setError(result.output.trim().split("\n").slice(-12).join("\n"));
|
|
34
|
+
setFinished(true);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
patch(step.id, { status: "done", lastLine: "" });
|
|
38
|
+
}
|
|
39
|
+
setFinished(true);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
run();
|
|
43
|
+
}, [config]);
|
|
44
|
+
|
|
45
|
+
return { steps, finished, error };
|
|
46
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type PackageManager = "npm" | "pnpm" | "bun";
|
|
2
|
+
|
|
3
|
+
export const packageManagers: PackageManager[] = ["npm", "pnpm", "bun"];
|
|
4
|
+
|
|
5
|
+
export const packageManagerCommands: Record<
|
|
6
|
+
PackageManager,
|
|
7
|
+
{ dlx: string[]; add: string[]; addDev: string[]; createNextFlag: string }
|
|
8
|
+
> = {
|
|
9
|
+
npm: { dlx: ["npx", "-y"], add: ["npm", "install"], addDev: ["npm", "install", "-D"], createNextFlag: "--use-npm" },
|
|
10
|
+
pnpm: { dlx: ["pnpm", "dlx"], add: ["pnpm", "add"], addDev: ["pnpm", "add", "-D"], createNextFlag: "--use-pnpm" },
|
|
11
|
+
bun: { dlx: ["bunx"], add: ["bun", "add"], addDev: ["bun", "add", "-d"], createNextFlag: "--use-bun" },
|
|
12
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { type PackageManager, packageManagerCommands } from "./package-manager";
|
|
3
|
+
import { addTypographyPlugin } from "./utils/add-typography-plugin";
|
|
4
|
+
import { type CommandResult, runCommand } from "./utils/run-command";
|
|
5
|
+
|
|
6
|
+
export type ScaffoldConfig = { name: string; packageManager: PackageManager; cwd: string };
|
|
7
|
+
|
|
8
|
+
export type ScaffoldStep = {
|
|
9
|
+
id: string;
|
|
10
|
+
label: string;
|
|
11
|
+
run: (config: ScaffoldConfig, onOutput: (line: string) => void) => Promise<CommandResult>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const projectDir = (config: ScaffoldConfig) => join(config.cwd, config.name);
|
|
15
|
+
|
|
16
|
+
const command =
|
|
17
|
+
(build: (config: ScaffoldConfig) => string[], inProject = true): ScaffoldStep["run"] =>
|
|
18
|
+
(config, onOutput) =>
|
|
19
|
+
runCommand(build(config), inProject ? projectDir(config) : config.cwd, onOutput);
|
|
20
|
+
|
|
21
|
+
export const scaffoldSteps: ScaffoldStep[] = [
|
|
22
|
+
{
|
|
23
|
+
id: "next",
|
|
24
|
+
label: "Next.js + Tailwind (create-next-app)",
|
|
25
|
+
run: command(
|
|
26
|
+
({ name, packageManager }) => [
|
|
27
|
+
...packageManagerCommands[packageManager].dlx,
|
|
28
|
+
"create-next-app@latest",
|
|
29
|
+
name,
|
|
30
|
+
"--ts",
|
|
31
|
+
"--tailwind",
|
|
32
|
+
"--app",
|
|
33
|
+
"--no-src-dir",
|
|
34
|
+
"--import-alias",
|
|
35
|
+
"@/*",
|
|
36
|
+
"--yes",
|
|
37
|
+
packageManagerCommands[packageManager].createNextFlag,
|
|
38
|
+
],
|
|
39
|
+
false,
|
|
40
|
+
),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "typography",
|
|
44
|
+
label: "Tailwind Typography plugin",
|
|
45
|
+
run: async (config, onOutput) => {
|
|
46
|
+
const result = await runCommand(
|
|
47
|
+
[...packageManagerCommands[config.packageManager].addDev, "@tailwindcss/typography"],
|
|
48
|
+
projectDir(config),
|
|
49
|
+
onOutput,
|
|
50
|
+
);
|
|
51
|
+
if (result.ok) {
|
|
52
|
+
await addTypographyPlugin(projectDir(config));
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "shadcn-init",
|
|
59
|
+
label: "shadcn/ui init (radix, vega preset)",
|
|
60
|
+
run: command(({ packageManager }) => [
|
|
61
|
+
...packageManagerCommands[packageManager].dlx,
|
|
62
|
+
"shadcn@latest",
|
|
63
|
+
"init",
|
|
64
|
+
"-y",
|
|
65
|
+
"-b",
|
|
66
|
+
"radix",
|
|
67
|
+
"-p",
|
|
68
|
+
"vega",
|
|
69
|
+
"--silent",
|
|
70
|
+
]),
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "shadcn-all",
|
|
74
|
+
label: "shadcn/ui add --all",
|
|
75
|
+
run: command(({ packageManager }) => [
|
|
76
|
+
...packageManagerCommands[packageManager].dlx,
|
|
77
|
+
"shadcn@latest",
|
|
78
|
+
"add",
|
|
79
|
+
"--all",
|
|
80
|
+
"-y",
|
|
81
|
+
"--silent",
|
|
82
|
+
]),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "motion-lucide",
|
|
86
|
+
label: "Motion + Lucide icons",
|
|
87
|
+
run: command(({ packageManager }) => [...packageManagerCommands[packageManager].add, "motion", "lucide-react"]),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "ultracite",
|
|
91
|
+
label: "Ultracite (Oxlint, Oxfmt, Claude + Codex config)",
|
|
92
|
+
run: command(({ packageManager }) => [
|
|
93
|
+
...packageManagerCommands[packageManager].dlx,
|
|
94
|
+
"ultracite@latest",
|
|
95
|
+
"init",
|
|
96
|
+
"--pm",
|
|
97
|
+
packageManager,
|
|
98
|
+
"--linter",
|
|
99
|
+
"oxlint",
|
|
100
|
+
"--agents",
|
|
101
|
+
"claude",
|
|
102
|
+
"codex",
|
|
103
|
+
"--frameworks",
|
|
104
|
+
"next",
|
|
105
|
+
"--editors",
|
|
106
|
+
"vscode",
|
|
107
|
+
"--quiet",
|
|
108
|
+
]),
|
|
109
|
+
},
|
|
110
|
+
];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const importLine = '@import "tailwindcss";';
|
|
5
|
+
const pluginLine = '@plugin "@tailwindcss/typography";';
|
|
6
|
+
|
|
7
|
+
export async function addTypographyPlugin(projectDir: string): Promise<void> {
|
|
8
|
+
const cssPath = join(projectDir, "app", "globals.css");
|
|
9
|
+
const css = await readFile(cssPath, "utf8");
|
|
10
|
+
if (css.includes(pluginLine)) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
await writeFile(cssPath, css.replace(importLine, `${importLine}\n${pluginLine}`));
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export type CommandResult = { ok: boolean; output: string };
|
|
4
|
+
|
|
5
|
+
export function runCommand(args: string[], cwd: string, onOutput: (line: string) => void): Promise<CommandResult> {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
const [command, ...rest] = args;
|
|
8
|
+
const child = spawn(command, rest, {
|
|
9
|
+
cwd,
|
|
10
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
11
|
+
env: { ...process.env, CI: "1", FORCE_COLOR: "0" },
|
|
12
|
+
});
|
|
13
|
+
const chunks: string[] = [];
|
|
14
|
+
const push = (data: Buffer) => {
|
|
15
|
+
const text = data.toString();
|
|
16
|
+
chunks.push(text);
|
|
17
|
+
const line = text.trim().split("\n").at(-1);
|
|
18
|
+
if (line) {
|
|
19
|
+
onOutput(line);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
child.stdout.on("data", push);
|
|
23
|
+
child.stderr.on("data", push);
|
|
24
|
+
child.on("error", (error) => resolve({ ok: false, output: error.message }));
|
|
25
|
+
child.on("close", (code) => resolve({ ok: code === 0, output: chunks.join("") }));
|
|
26
|
+
});
|
|
27
|
+
}
|