create-skaff 0.0.3 → 0.0.5
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 +1 -1
- package/src/components/confirm-prompt.tsx +1 -1
- package/src/components/features-prompt.tsx +59 -0
- package/src/components/package-manager-prompt.tsx +22 -20
- package/src/components/progress-timeline.tsx +6 -1
- package/src/components/step-row.tsx +1 -1
- package/src/components/wizard-app.tsx +37 -15
- package/src/index.tsx +5 -1
- package/src/lib/features.ts +37 -0
- package/src/lib/hooks/use-scaffold-runner.ts +16 -4
- package/src/lib/scaffold-steps.ts +130 -92
- package/src/lib/utils/write-agent-config.ts +19 -0
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@ export function ConfirmPrompt({ config }: ConfirmPromptProps) {
|
|
|
6
6
|
return (
|
|
7
7
|
<box flexDirection="column">
|
|
8
8
|
<text>
|
|
9
|
-
<span fg="#a78bfa">◆</span> <strong>Scaffold into {projectDir(config)}?</strong>
|
|
9
|
+
<span fg="#a78bfa">◆</span> <strong>{config.dryRun ? "Dry run: preview commands for " : "Scaffold into "}{projectDir(config)}?</strong>
|
|
10
10
|
</text>
|
|
11
11
|
<text>
|
|
12
12
|
<span fg="#a78bfa">│ </span>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useKeyboard } from "@opentui/react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { allFeatures, type FeatureId, featureGroups } from "../lib/features";
|
|
4
|
+
|
|
5
|
+
type FeaturesPromptProps = { initialValue: FeatureId[]; onSubmit: (features: FeatureId[]) => void };
|
|
6
|
+
|
|
7
|
+
export function FeaturesPrompt({ initialValue, onSubmit }: FeaturesPromptProps) {
|
|
8
|
+
const [cursor, setCursor] = useState(0);
|
|
9
|
+
const [selected, setSelected] = useState<FeatureId[]>(initialValue);
|
|
10
|
+
|
|
11
|
+
useKeyboard((key) => {
|
|
12
|
+
if (key.name === "up" || key.name === "k") {
|
|
13
|
+
setCursor((current) => (current + allFeatures.length - 1) % allFeatures.length);
|
|
14
|
+
} else if (key.name === "down" || key.name === "j") {
|
|
15
|
+
setCursor((current) => (current + 1) % allFeatures.length);
|
|
16
|
+
} else if (key.name === "space") {
|
|
17
|
+
const id = allFeatures[cursor].id;
|
|
18
|
+
setSelected((current) => (current.includes(id) ? current.filter((item) => item !== id) : [...current, id]));
|
|
19
|
+
} else if (key.name === "a") {
|
|
20
|
+
setSelected((current) => (current.length === allFeatures.length ? [] : allFeatures.map((feature) => feature.id)));
|
|
21
|
+
} else if (key.name === "return") {
|
|
22
|
+
onSubmit(allFeatures.map((feature) => feature.id).filter((id) => selected.includes(id)));
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
let index = -1;
|
|
27
|
+
return (
|
|
28
|
+
<box flexDirection="column">
|
|
29
|
+
<text>
|
|
30
|
+
<span fg="#a78bfa">◆</span> <strong>What to set up</strong>
|
|
31
|
+
</text>
|
|
32
|
+
{featureGroups.map((group) => (
|
|
33
|
+
<box key={group.title} flexDirection="column">
|
|
34
|
+
<text>
|
|
35
|
+
<span fg="#a78bfa">│ </span>
|
|
36
|
+
<span fg="#888888">{group.title}</span>
|
|
37
|
+
</text>
|
|
38
|
+
{group.features.map((feature) => {
|
|
39
|
+
index += 1;
|
|
40
|
+
const active = index === cursor;
|
|
41
|
+
const checked = selected.includes(feature.id);
|
|
42
|
+
return (
|
|
43
|
+
<text key={feature.id}>
|
|
44
|
+
<span fg="#a78bfa">│ </span>
|
|
45
|
+
<span fg={active ? "#a78bfa" : "#555555"}>{active ? "❯ " : " "}</span>
|
|
46
|
+
<span fg={checked ? "#4ade80" : "#555555"}>{checked ? "◼ " : "◻ "}</span>
|
|
47
|
+
<span fg={active ? "#ffffff" : "#aaaaaa"}>{feature.label}</span>
|
|
48
|
+
</text>
|
|
49
|
+
);
|
|
50
|
+
})}
|
|
51
|
+
</box>
|
|
52
|
+
))}
|
|
53
|
+
<text>
|
|
54
|
+
<span fg="#a78bfa">│ </span>
|
|
55
|
+
<span fg="#666666">↑↓ to move · Space to toggle · a to toggle all · Enter to continue · Esc to go back</span>
|
|
56
|
+
</text>
|
|
57
|
+
</box>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -1,32 +1,34 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useKeyboard } from "@opentui/react";
|
|
2
|
+
import { useState } from "react";
|
|
2
3
|
import { type PackageManager, packageManagers } from "../lib/package-manager";
|
|
3
4
|
|
|
4
|
-
type PackageManagerPromptProps = { onSelect: (packageManager: PackageManager) => void };
|
|
5
|
+
type PackageManagerPromptProps = { initialValue: PackageManager; onSelect: (packageManager: PackageManager) => void };
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
export function PackageManagerPrompt({ initialValue, onSelect }: PackageManagerPromptProps) {
|
|
8
|
+
const [index, setIndex] = useState(Math.max(0, packageManagers.indexOf(initialValue)));
|
|
9
|
+
|
|
10
|
+
useKeyboard((key) => {
|
|
11
|
+
if (key.name === "up" || key.name === "k") {
|
|
12
|
+
setIndex((current) => (current + packageManagers.length - 1) % packageManagers.length);
|
|
13
|
+
} else if (key.name === "down" || key.name === "j") {
|
|
14
|
+
setIndex((current) => (current + 1) % packageManagers.length);
|
|
15
|
+
} else if (key.name === "return") {
|
|
16
|
+
onSelect(packageManagers[index]);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
7
19
|
|
|
8
|
-
export function PackageManagerPrompt({ onSelect }: PackageManagerPromptProps) {
|
|
9
20
|
return (
|
|
10
21
|
<box flexDirection="column">
|
|
11
22
|
<text>
|
|
12
23
|
<span fg="#a78bfa">◆</span> <strong>Package manager</strong>
|
|
13
24
|
</text>
|
|
14
|
-
|
|
15
|
-
<text
|
|
16
|
-
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
selectedIndex={1}
|
|
22
|
-
onSelect={(_index, option) => {
|
|
23
|
-
if (option && packageManagers.includes(option.value as PackageManager)) {
|
|
24
|
-
onSelect(option.value as PackageManager);
|
|
25
|
-
}
|
|
26
|
-
}}
|
|
27
|
-
/>
|
|
28
|
-
</box>
|
|
29
|
-
</box>
|
|
25
|
+
{packageManagers.map((value, i) => (
|
|
26
|
+
<text key={value}>
|
|
27
|
+
<span fg="#a78bfa">│ </span>
|
|
28
|
+
{i === index ? <span fg="#4ade80">◉ </span> : <span fg="#555555">○ </span>}
|
|
29
|
+
<span fg={i === index ? "#ffffff" : "#888888"}>{value}</span>
|
|
30
|
+
</text>
|
|
31
|
+
))}
|
|
30
32
|
<text>
|
|
31
33
|
<span fg="#a78bfa">│ </span>
|
|
32
34
|
<span fg="#666666">↑↓ to move · Enter to continue · Esc to go back</span>
|
|
@@ -25,7 +25,12 @@ export function ProgressTimeline({ config, onExit }: ProgressTimelineProps) {
|
|
|
25
25
|
<text fg="#f87171">{error}</text>
|
|
26
26
|
</box>
|
|
27
27
|
) : null}
|
|
28
|
-
{finished && !error ? (
|
|
28
|
+
{finished && !error && config.dryRun ? (
|
|
29
|
+
<text>
|
|
30
|
+
<span fg="#4ade80">└ Dry run.</span> <span fg="#888888">Nothing was written.</span>
|
|
31
|
+
</text>
|
|
32
|
+
) : null}
|
|
33
|
+
{finished && !error && !config.dryRun ? (
|
|
29
34
|
<box flexDirection="column">
|
|
30
35
|
<text>
|
|
31
36
|
<span fg="#4ade80">└ Done.</span> <span fg="#888888">Next steps:</span>
|
|
@@ -18,7 +18,7 @@ export function StepRow({ step }: StepRowProps) {
|
|
|
18
18
|
</text>
|
|
19
19
|
<text>
|
|
20
20
|
<span fg="#555555">│ </span>
|
|
21
|
-
<span fg="#666666">{step.status === "running" ? step.lastLine.slice(0, 80) : ""}</span>
|
|
21
|
+
<span fg="#666666">{step.detail || (step.status === "running" ? step.lastLine.slice(0, 80) : "")}</span>
|
|
22
22
|
</text>
|
|
23
23
|
</box>
|
|
24
24
|
);
|
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
import { useKeyboard, useRenderer } from "@opentui/react";
|
|
2
2
|
import { useState } from "react";
|
|
3
|
+
import { allFeatureIds, allFeatures, type FeatureId } from "../lib/features";
|
|
3
4
|
import type { PackageManager } from "../lib/package-manager";
|
|
4
5
|
import type { ScaffoldConfig } from "../lib/scaffold-steps";
|
|
5
6
|
import { AnsweredRow } from "./answered-row";
|
|
6
7
|
import { ConfirmPrompt } from "./confirm-prompt";
|
|
8
|
+
import { FeaturesPrompt } from "./features-prompt";
|
|
7
9
|
import { NamePrompt } from "./name-prompt";
|
|
8
10
|
import { PackageManagerPrompt } from "./package-manager-prompt";
|
|
9
11
|
import { ProgressTimeline } from "./progress-timeline";
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
const screens = ["name", "packageManager", "features", "confirm", "progress"] as const;
|
|
12
14
|
|
|
13
|
-
type
|
|
15
|
+
type Screen = (typeof screens)[number];
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
type WizardAppProps = { initialName: string; cwd: string; dryRun: boolean };
|
|
18
|
+
|
|
19
|
+
export function WizardApp({ initialName, cwd, dryRun }: WizardAppProps) {
|
|
16
20
|
const renderer = useRenderer();
|
|
17
21
|
const [screen, setScreen] = useState<Screen>("name");
|
|
18
22
|
const [name, setName] = useState(initialName);
|
|
19
23
|
const [packageManager, setPackageManager] = useState<PackageManager>("pnpm");
|
|
24
|
+
const [features, setFeatures] = useState<FeatureId[]>(allFeatureIds);
|
|
20
25
|
const [config, setConfig] = useState<ScaffoldConfig | null>(null);
|
|
21
26
|
|
|
22
27
|
const exit = () => {
|
|
@@ -24,34 +29,41 @@ export function WizardApp({ initialName, cwd }: WizardAppProps) {
|
|
|
24
29
|
process.exit(0);
|
|
25
30
|
};
|
|
26
31
|
|
|
32
|
+
const step = screens.indexOf(screen);
|
|
33
|
+
const answered = (target: Screen) => step > screens.indexOf(target);
|
|
34
|
+
|
|
27
35
|
useKeyboard((key) => {
|
|
28
36
|
if (key.name === "escape") {
|
|
29
|
-
if (screen === "
|
|
30
|
-
setScreen("name");
|
|
31
|
-
} else if (screen === "confirm") {
|
|
32
|
-
setScreen("packageManager");
|
|
33
|
-
} else if (screen === "name") {
|
|
37
|
+
if (screen === "name") {
|
|
34
38
|
exit();
|
|
39
|
+
} else if (screen !== "progress") {
|
|
40
|
+
setScreen(screens[step - 1]);
|
|
35
41
|
}
|
|
36
42
|
return;
|
|
37
43
|
}
|
|
38
44
|
if (screen === "confirm" && key.name === "return") {
|
|
39
|
-
setConfig({ name, packageManager, cwd });
|
|
45
|
+
setConfig({ name, packageManager, cwd, features, dryRun });
|
|
40
46
|
setScreen("progress");
|
|
41
47
|
}
|
|
42
48
|
});
|
|
43
49
|
|
|
44
|
-
const
|
|
45
|
-
|
|
50
|
+
const featureSummary =
|
|
51
|
+
features.length === 0
|
|
52
|
+
? "none"
|
|
53
|
+
: allFeatures
|
|
54
|
+
.filter((feature) => features.includes(feature.id))
|
|
55
|
+
.map((feature) => feature.label)
|
|
56
|
+
.join(", ");
|
|
46
57
|
|
|
47
58
|
return (
|
|
48
59
|
<box flexDirection="column" paddingLeft={1} paddingTop={1}>
|
|
49
60
|
<text>
|
|
50
|
-
<span fg="#555555">┌</span> <strong fg="#a78bfa">skaff</strong> <span fg="#888888">· Next.js scaffolder</span>
|
|
61
|
+
<span fg="#555555">┌</span> <strong fg="#a78bfa">skaff</strong> <span fg="#888888">· Next.js scaffolder{dryRun ? " · dry run" : ""}</span>
|
|
51
62
|
</text>
|
|
52
63
|
<text fg="#555555">│</text>
|
|
53
|
-
{
|
|
54
|
-
{
|
|
64
|
+
{answered("name") ? <AnsweredRow label="Project name" value={name} /> : null}
|
|
65
|
+
{answered("packageManager") ? <AnsweredRow label="Package manager" value={packageManager} /> : null}
|
|
66
|
+
{answered("features") ? <AnsweredRow label="What to set up" value={featureSummary} /> : null}
|
|
55
67
|
{screen === "name" ? (
|
|
56
68
|
<NamePrompt
|
|
57
69
|
initialValue={name}
|
|
@@ -63,13 +75,23 @@ export function WizardApp({ initialName, cwd }: WizardAppProps) {
|
|
|
63
75
|
) : null}
|
|
64
76
|
{screen === "packageManager" ? (
|
|
65
77
|
<PackageManagerPrompt
|
|
78
|
+
initialValue={packageManager}
|
|
66
79
|
onSelect={(value) => {
|
|
67
80
|
setPackageManager(value);
|
|
81
|
+
setScreen("features");
|
|
82
|
+
}}
|
|
83
|
+
/>
|
|
84
|
+
) : null}
|
|
85
|
+
{screen === "features" ? (
|
|
86
|
+
<FeaturesPrompt
|
|
87
|
+
initialValue={features}
|
|
88
|
+
onSubmit={(value) => {
|
|
89
|
+
setFeatures(value);
|
|
68
90
|
setScreen("confirm");
|
|
69
91
|
}}
|
|
70
92
|
/>
|
|
71
93
|
) : null}
|
|
72
|
-
{screen === "confirm" ? <ConfirmPrompt config={{ name, packageManager, cwd }} /> : null}
|
|
94
|
+
{screen === "confirm" ? <ConfirmPrompt config={{ name, packageManager, cwd, features, dryRun }} /> : null}
|
|
73
95
|
{screen === "progress" && config ? <ProgressTimeline config={config} onExit={exit} /> : null}
|
|
74
96
|
</box>
|
|
75
97
|
);
|
package/src/index.tsx
CHANGED
|
@@ -3,5 +3,9 @@ import { createCliRenderer } from "@opentui/core";
|
|
|
3
3
|
import { createRoot } from "@opentui/react";
|
|
4
4
|
import { WizardApp } from "./components/wizard-app";
|
|
5
5
|
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const dryRun = args.includes("--dry-run");
|
|
8
|
+
const initialName = args.find((arg) => !arg.startsWith("--")) ?? "";
|
|
9
|
+
|
|
6
10
|
const renderer = await createCliRenderer({ exitOnCtrlC: true });
|
|
7
|
-
createRoot(renderer).render(<WizardApp initialName={
|
|
11
|
+
createRoot(renderer).render(<WizardApp initialName={initialName} cwd={process.cwd()} dryRun={dryRun} />);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type FeatureId = "typography" | "shadcn" | "motion" | "lucide" | "ultracite" | "claude" | "codex";
|
|
2
|
+
|
|
3
|
+
export type Feature = { id: FeatureId; label: string };
|
|
4
|
+
|
|
5
|
+
export type FeatureGroup = { title: string; features: Feature[] };
|
|
6
|
+
|
|
7
|
+
export const featureGroups: FeatureGroup[] = [
|
|
8
|
+
{
|
|
9
|
+
title: "Styling",
|
|
10
|
+
features: [
|
|
11
|
+
{ id: "typography", label: "Tailwind Typography" },
|
|
12
|
+
{ id: "shadcn", label: "shadcn/ui (all components)" },
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
title: "Libraries",
|
|
17
|
+
features: [
|
|
18
|
+
{ id: "motion", label: "Motion (framer-motion)" },
|
|
19
|
+
{ id: "lucide", label: "Lucide icons" },
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
title: "Code quality",
|
|
24
|
+
features: [{ id: "ultracite", label: "Ultracite (Oxlint + Oxfmt)" }],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
title: "AI agents",
|
|
28
|
+
features: [
|
|
29
|
+
{ id: "claude", label: "Claude config" },
|
|
30
|
+
{ id: "codex", label: "Codex config" },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export const allFeatures: Feature[] = featureGroups.flatMap((group) => group.features);
|
|
36
|
+
|
|
37
|
+
export const allFeatureIds: FeatureId[] = allFeatures.map((feature) => feature.id);
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
|
-
import { type ScaffoldConfig
|
|
2
|
+
import { buildScaffoldSteps, type ScaffoldConfig } from "../scaffold-steps";
|
|
3
3
|
|
|
4
4
|
export type StepStatus = "pending" | "running" | "done" | "failed";
|
|
5
5
|
|
|
6
|
-
export type StepState = { id: string; label: string; status: StepStatus; lastLine: string };
|
|
6
|
+
export type StepState = { id: string; label: string; status: StepStatus; lastLine: string; detail: string };
|
|
7
7
|
|
|
8
8
|
export type RunnerState = { steps: StepState[]; finished: boolean; error: string | null };
|
|
9
9
|
|
|
10
10
|
export function useScaffoldRunner(config: ScaffoldConfig): RunnerState {
|
|
11
|
+
const plan = useRef(buildScaffoldSteps(config.features));
|
|
11
12
|
const [steps, setSteps] = useState<StepState[]>(
|
|
12
|
-
|
|
13
|
+
plan.current.map((step) => ({
|
|
14
|
+
id: step.id,
|
|
15
|
+
label: step.label,
|
|
16
|
+
status: "pending",
|
|
17
|
+
lastLine: "",
|
|
18
|
+
detail: config.dryRun ? step.describe(config) : "",
|
|
19
|
+
})),
|
|
13
20
|
);
|
|
14
21
|
const [finished, setFinished] = useState(false);
|
|
15
22
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -25,7 +32,12 @@ export function useScaffoldRunner(config: ScaffoldConfig): RunnerState {
|
|
|
25
32
|
setSteps((current) => current.map((step) => (step.id === id ? { ...step, ...changes } : step)));
|
|
26
33
|
|
|
27
34
|
const run = async () => {
|
|
28
|
-
|
|
35
|
+
if (config.dryRun) {
|
|
36
|
+
setSteps((current) => current.map((step) => ({ ...step, status: "done" })));
|
|
37
|
+
setFinished(true);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
for (const step of plan.current) {
|
|
29
41
|
patch(step.id, { status: "running" });
|
|
30
42
|
const result = await step.run(config, (line) => patch(step.id, { lastLine: line }));
|
|
31
43
|
if (!result.ok) {
|
|
@@ -1,110 +1,148 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
import type { FeatureId } from "./features";
|
|
2
3
|
import { type PackageManager, packageManagerCommands } from "./package-manager";
|
|
3
4
|
import { addTypographyPlugin } from "./utils/add-typography-plugin";
|
|
4
5
|
import { type CommandResult, runCommand } from "./utils/run-command";
|
|
6
|
+
import { writeAgentConfig } from "./utils/write-agent-config";
|
|
5
7
|
|
|
6
|
-
export type ScaffoldConfig = {
|
|
8
|
+
export type ScaffoldConfig = {
|
|
9
|
+
name: string;
|
|
10
|
+
packageManager: PackageManager;
|
|
11
|
+
cwd: string;
|
|
12
|
+
features: FeatureId[];
|
|
13
|
+
dryRun: boolean;
|
|
14
|
+
};
|
|
7
15
|
|
|
8
16
|
export type ScaffoldStep = {
|
|
9
17
|
id: string;
|
|
10
18
|
label: string;
|
|
19
|
+
describe: (config: ScaffoldConfig) => string;
|
|
11
20
|
run: (config: ScaffoldConfig, onOutput: (line: string) => void) => Promise<CommandResult>;
|
|
12
21
|
};
|
|
13
22
|
|
|
14
23
|
export const projectDir = (config: ScaffoldConfig) => (config.name === "." ? config.cwd : join(config.cwd, config.name));
|
|
15
24
|
|
|
16
|
-
const command =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
const command = (
|
|
26
|
+
build: (config: ScaffoldConfig) => string[],
|
|
27
|
+
inProject = true,
|
|
28
|
+
): Pick<ScaffoldStep, "describe" | "run"> => ({
|
|
29
|
+
describe: (config) => `$ ${build(config).join(" ")}`,
|
|
30
|
+
run: (config, onOutput) => runCommand(build(config), inProject ? projectDir(config) : config.cwd, onOutput),
|
|
31
|
+
});
|
|
20
32
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 }) => [
|
|
33
|
+
const nextStep: ScaffoldStep = {
|
|
34
|
+
id: "next",
|
|
35
|
+
label: "Next.js + Tailwind (create-next-app)",
|
|
36
|
+
...command(
|
|
37
|
+
({ name, packageManager }) => [
|
|
76
38
|
...packageManagerCommands[packageManager].dlx,
|
|
77
|
-
"
|
|
78
|
-
|
|
79
|
-
"--
|
|
80
|
-
"
|
|
81
|
-
"--
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
39
|
+
"create-next-app@latest",
|
|
40
|
+
name,
|
|
41
|
+
"--ts",
|
|
42
|
+
"--tailwind",
|
|
43
|
+
"--app",
|
|
44
|
+
"--no-src-dir",
|
|
45
|
+
"--import-alias",
|
|
46
|
+
"@/*",
|
|
47
|
+
"--yes",
|
|
48
|
+
packageManagerCommands[packageManager].createNextFlag,
|
|
49
|
+
],
|
|
50
|
+
false,
|
|
51
|
+
),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const typographyStep: ScaffoldStep = {
|
|
55
|
+
id: "typography",
|
|
56
|
+
label: "Tailwind Typography plugin",
|
|
57
|
+
describe: (config) =>
|
|
58
|
+
`$ ${[...packageManagerCommands[config.packageManager].addDev, "@tailwindcss/typography"].join(" ")}, then add @plugin to app/globals.css`,
|
|
59
|
+
run: async (config, onOutput) => {
|
|
60
|
+
const result = await runCommand(
|
|
61
|
+
[...packageManagerCommands[config.packageManager].addDev, "@tailwindcss/typography"],
|
|
62
|
+
projectDir(config),
|
|
63
|
+
onOutput,
|
|
64
|
+
);
|
|
65
|
+
if (result.ok) {
|
|
66
|
+
await addTypographyPlugin(projectDir(config));
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
88
69
|
},
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const shadcnInitStep: ScaffoldStep = {
|
|
73
|
+
id: "shadcn-init",
|
|
74
|
+
label: "shadcn/ui init (radix, vega preset)",
|
|
75
|
+
...command(({ packageManager }) => [
|
|
76
|
+
...packageManagerCommands[packageManager].dlx,
|
|
77
|
+
"shadcn@latest",
|
|
78
|
+
"init",
|
|
79
|
+
"-y",
|
|
80
|
+
"-b",
|
|
81
|
+
"radix",
|
|
82
|
+
"-p",
|
|
83
|
+
"vega",
|
|
84
|
+
"--silent",
|
|
85
|
+
]),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const shadcnAllStep: ScaffoldStep = {
|
|
89
|
+
id: "shadcn-all",
|
|
90
|
+
label: "shadcn/ui add --all",
|
|
91
|
+
...command(({ packageManager }) => [
|
|
92
|
+
...packageManagerCommands[packageManager].dlx,
|
|
93
|
+
"shadcn@latest",
|
|
94
|
+
"add",
|
|
95
|
+
"--all",
|
|
96
|
+
"-y",
|
|
97
|
+
"--silent",
|
|
98
|
+
]),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const librariesStep = (packages: string[]): ScaffoldStep => ({
|
|
102
|
+
id: "libraries",
|
|
103
|
+
label: packages.join(" + "),
|
|
104
|
+
...command(({ packageManager }) => [...packageManagerCommands[packageManager].add, ...packages]),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const ultraciteStep = (agents: FeatureId[]): ScaffoldStep => ({
|
|
108
|
+
id: "ultracite",
|
|
109
|
+
label: agents.length > 0 ? `Ultracite (Oxlint, Oxfmt, ${agents.join(" + ")} config)` : "Ultracite (Oxlint, Oxfmt)",
|
|
110
|
+
...command(({ packageManager }) => [
|
|
111
|
+
...packageManagerCommands[packageManager].dlx,
|
|
112
|
+
"ultracite@latest",
|
|
113
|
+
"init",
|
|
114
|
+
"--pm",
|
|
115
|
+
packageManager,
|
|
116
|
+
"--linter",
|
|
117
|
+
"oxlint",
|
|
118
|
+
...(agents.length > 0 ? ["--agents", ...agents] : []),
|
|
119
|
+
"--frameworks",
|
|
120
|
+
"next",
|
|
121
|
+
"--editors",
|
|
122
|
+
"vscode",
|
|
123
|
+
"--quiet",
|
|
124
|
+
]),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const agentConfigStep = (agents: FeatureId[]): ScaffoldStep => ({
|
|
128
|
+
id: "agents",
|
|
129
|
+
label: `${agents.join(" + ")} config`,
|
|
130
|
+
describe: () => `write ${agents.map((agent) => (agent === "claude" ? "CLAUDE.md" : "AGENTS.md")).join(" and ")}`,
|
|
131
|
+
run: async (config) => {
|
|
132
|
+
await writeAgentConfig(projectDir(config), agents);
|
|
133
|
+
return { ok: true, output: "" };
|
|
109
134
|
},
|
|
110
|
-
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
export function buildScaffoldSteps(features: FeatureId[]): ScaffoldStep[] {
|
|
138
|
+
const has = (id: FeatureId) => features.includes(id);
|
|
139
|
+
const libraries = [...(has("motion") ? ["motion"] : []), ...(has("lucide") ? ["lucide-react"] : [])];
|
|
140
|
+
const agents = features.filter((id): id is "claude" | "codex" => id === "claude" || id === "codex");
|
|
141
|
+
return [
|
|
142
|
+
nextStep,
|
|
143
|
+
...(has("typography") ? [typographyStep] : []),
|
|
144
|
+
...(has("shadcn") ? [shadcnInitStep, shadcnAllStep] : []),
|
|
145
|
+
...(libraries.length > 0 ? [librariesStep(libraries)] : []),
|
|
146
|
+
...(has("ultracite") ? [ultraciteStep(agents)] : agents.length > 0 ? [agentConfigStep(agents)] : []),
|
|
147
|
+
];
|
|
148
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { FeatureId } from "../features";
|
|
4
|
+
|
|
5
|
+
const agentsMarkdown = `# Project notes for coding agents
|
|
6
|
+
|
|
7
|
+
- Next.js App Router with TypeScript and Tailwind CSS v4.
|
|
8
|
+
- Run \`dev\` for the local server and \`build\` before shipping.
|
|
9
|
+
- Keep components in \`components/\`, shared helpers in \`lib/\`.
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
export async function writeAgentConfig(projectDir: string, features: FeatureId[]): Promise<void> {
|
|
13
|
+
if (features.includes("codex")) {
|
|
14
|
+
await writeFile(join(projectDir, "AGENTS.md"), agentsMarkdown);
|
|
15
|
+
}
|
|
16
|
+
if (features.includes("claude")) {
|
|
17
|
+
await writeFile(join(projectDir, "CLAUDE.md"), features.includes("codex") ? "@AGENTS.md\n" : agentsMarkdown);
|
|
18
|
+
}
|
|
19
|
+
}
|