create-skaff 0.0.4 → 0.0.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-skaff",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Interactive Next.js project scaffolder: Tailwind, shadcn/ui, Motion, Lucide, Oxlint, Oxfmt, Ultracite, Claude and Codex config",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
+ }
@@ -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
- type Screen = "name" | "packageManager" | "confirm" | "progress";
13
+ const screens = ["name", "packageManager", "features", "confirm", "progress"] as const;
12
14
 
13
- type WizardAppProps = { initialName: string; cwd: string };
15
+ type Screen = (typeof screens)[number];
14
16
 
15
- export function WizardApp({ initialName, cwd }: WizardAppProps) {
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 === "packageManager") {
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 nameAnswered = screen !== "name";
45
- const packageManagerAnswered = screen === "confirm" || screen === "progress";
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
- {nameAnswered ? <AnsweredRow label="Project name" value={name} /> : null}
54
- {packageManagerAnswered ? <AnsweredRow label="Package manager" value={packageManager} /> : null}
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}
@@ -66,11 +78,20 @@ export function WizardApp({ initialName, cwd }: WizardAppProps) {
66
78
  initialValue={packageManager}
67
79
  onSelect={(value) => {
68
80
  setPackageManager(value);
81
+ setScreen("features");
82
+ }}
83
+ />
84
+ ) : null}
85
+ {screen === "features" ? (
86
+ <FeaturesPrompt
87
+ initialValue={features}
88
+ onSubmit={(value) => {
89
+ setFeatures(value);
69
90
  setScreen("confirm");
70
91
  }}
71
92
  />
72
93
  ) : null}
73
- {screen === "confirm" ? <ConfirmPrompt config={{ name, packageManager, cwd }} /> : null}
94
+ {screen === "confirm" ? <ConfirmPrompt config={{ name, packageManager, cwd, features, dryRun }} /> : null}
74
95
  {screen === "progress" && config ? <ProgressTimeline config={config} onExit={exit} /> : null}
75
96
  </box>
76
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={process.argv[2] ?? ""} cwd={process.cwd()} />);
11
+ createRoot(renderer).render(<WizardApp initialName={initialName} cwd={process.cwd()} dryRun={dryRun} />);
@@ -0,0 +1,38 @@
1
+ export type FeatureId = "typography" | "shadcn" | "motion" | "lucide" | "hugeicons" | "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
+ { id: "hugeicons", label: "Hugeicons (free)" },
21
+ ],
22
+ },
23
+ {
24
+ title: "Code quality",
25
+ features: [{ id: "ultracite", label: "Ultracite (Oxlint + Oxfmt)" }],
26
+ },
27
+ {
28
+ title: "AI agents",
29
+ features: [
30
+ { id: "claude", label: "Claude config" },
31
+ { id: "codex", label: "Codex config" },
32
+ ],
33
+ },
34
+ ];
35
+
36
+ export const allFeatures: Feature[] = featureGroups.flatMap((group) => group.features);
37
+
38
+ 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, scaffoldSteps } from "../scaffold-steps";
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
- scaffoldSteps.map(({ id, label }) => ({ id, label, status: "pending", lastLine: "" })),
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
- for (const step of scaffoldSteps) {
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,162 @@
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";
5
+ import { extendGitignore } from "./utils/extend-gitignore";
4
6
  import { type CommandResult, runCommand } from "./utils/run-command";
7
+ import { writeAgentConfig } from "./utils/write-agent-config";
5
8
 
6
- export type ScaffoldConfig = { name: string; packageManager: PackageManager; cwd: string };
9
+ export type ScaffoldConfig = {
10
+ name: string;
11
+ packageManager: PackageManager;
12
+ cwd: string;
13
+ features: FeatureId[];
14
+ dryRun: boolean;
15
+ };
7
16
 
8
17
  export type ScaffoldStep = {
9
18
  id: string;
10
19
  label: string;
20
+ describe: (config: ScaffoldConfig) => string;
11
21
  run: (config: ScaffoldConfig, onOutput: (line: string) => void) => Promise<CommandResult>;
12
22
  };
13
23
 
14
24
  export const projectDir = (config: ScaffoldConfig) => (config.name === "." ? config.cwd : join(config.cwd, config.name));
15
25
 
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);
26
+ const command = (
27
+ build: (config: ScaffoldConfig) => string[],
28
+ inProject = true,
29
+ ): Pick<ScaffoldStep, "describe" | "run"> => ({
30
+ describe: (config) => `$ ${build(config).join(" ")}`,
31
+ run: (config, onOutput) => runCommand(build(config), inProject ? projectDir(config) : config.cwd, onOutput),
32
+ });
20
33
 
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
- ]),
34
+ const createNextApp = command(
35
+ ({ name, packageManager }) => [
36
+ ...packageManagerCommands[packageManager].dlx,
37
+ "create-next-app@latest",
38
+ name,
39
+ "--ts",
40
+ "--tailwind",
41
+ "--app",
42
+ "--no-src-dir",
43
+ "--import-alias",
44
+ "@/*",
45
+ "--yes",
46
+ packageManagerCommands[packageManager].createNextFlag,
47
+ ],
48
+ false,
49
+ );
50
+
51
+ const nextStep: ScaffoldStep = {
52
+ id: "next",
53
+ label: "Next.js + Tailwind (create-next-app)",
54
+ describe: (config) => `${createNextApp.describe(config)}, then extend .gitignore`,
55
+ run: async (config, onOutput) => {
56
+ const result = await createNextApp.run(config, onOutput);
57
+ if (result.ok) {
58
+ await extendGitignore(projectDir(config));
59
+ }
60
+ return result;
83
61
  },
84
- {
85
- id: "motion-lucide",
86
- label: "Motion + Lucide icons",
87
- run: command(({ packageManager }) => [...packageManagerCommands[packageManager].add, "motion", "lucide-react"]),
62
+ };
63
+
64
+ const typographyStep: ScaffoldStep = {
65
+ id: "typography",
66
+ label: "Tailwind Typography plugin",
67
+ describe: (config) =>
68
+ `$ ${[...packageManagerCommands[config.packageManager].addDev, "@tailwindcss/typography"].join(" ")}, then add @plugin to app/globals.css`,
69
+ run: async (config, onOutput) => {
70
+ const result = await runCommand(
71
+ [...packageManagerCommands[config.packageManager].addDev, "@tailwindcss/typography"],
72
+ projectDir(config),
73
+ onOutput,
74
+ );
75
+ if (result.ok) {
76
+ await addTypographyPlugin(projectDir(config));
77
+ }
78
+ return result;
88
79
  },
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
- ]),
80
+ };
81
+
82
+ const shadcnInitStep: ScaffoldStep = {
83
+ id: "shadcn-init",
84
+ label: "shadcn/ui init (radix, vega preset)",
85
+ ...command(({ packageManager }) => [
86
+ ...packageManagerCommands[packageManager].dlx,
87
+ "shadcn@latest",
88
+ "init",
89
+ "-y",
90
+ "-b",
91
+ "radix",
92
+ "-p",
93
+ "vega",
94
+ "--silent",
95
+ ]),
96
+ };
97
+
98
+ const shadcnAllStep: ScaffoldStep = {
99
+ id: "shadcn-all",
100
+ label: "shadcn/ui add --all",
101
+ ...command(({ packageManager }) => [
102
+ ...packageManagerCommands[packageManager].dlx,
103
+ "shadcn@latest",
104
+ "add",
105
+ "--all",
106
+ "-y",
107
+ "--silent",
108
+ ]),
109
+ };
110
+
111
+ const librariesStep = (packages: string[]): ScaffoldStep => ({
112
+ id: "libraries",
113
+ label: packages.join(" + "),
114
+ ...command(({ packageManager }) => [...packageManagerCommands[packageManager].add, ...packages]),
115
+ });
116
+
117
+ const ultraciteStep = (agents: FeatureId[]): ScaffoldStep => ({
118
+ id: "ultracite",
119
+ label: agents.length > 0 ? `Ultracite (Oxlint, Oxfmt, ${agents.join(" + ")} config)` : "Ultracite (Oxlint, Oxfmt)",
120
+ ...command(({ packageManager }) => [
121
+ ...packageManagerCommands[packageManager].dlx,
122
+ "ultracite@latest",
123
+ "init",
124
+ "--pm",
125
+ packageManager,
126
+ "--linter",
127
+ "oxlint",
128
+ ...(agents.length > 0 ? ["--agents", ...agents] : []),
129
+ "--frameworks",
130
+ "next",
131
+ "--editors",
132
+ "vscode",
133
+ "--quiet",
134
+ ]),
135
+ });
136
+
137
+ const agentConfigStep = (agents: FeatureId[]): ScaffoldStep => ({
138
+ id: "agents",
139
+ label: `${agents.join(" + ")} config`,
140
+ describe: () => `write ${agents.map((agent) => (agent === "claude" ? "CLAUDE.md" : "AGENTS.md")).join(" and ")}`,
141
+ run: async (config) => {
142
+ await writeAgentConfig(projectDir(config), agents);
143
+ return { ok: true, output: "" };
109
144
  },
110
- ];
145
+ });
146
+
147
+ export function buildScaffoldSteps(features: FeatureId[]): ScaffoldStep[] {
148
+ const has = (id: FeatureId) => features.includes(id);
149
+ const libraries = [
150
+ ...(has("motion") ? ["motion"] : []),
151
+ ...(has("lucide") ? ["lucide-react"] : []),
152
+ ...(has("hugeicons") ? ["@hugeicons/react", "@hugeicons/core-free-icons"] : []),
153
+ ];
154
+ const agents = features.filter((id): id is "claude" | "codex" => id === "claude" || id === "codex");
155
+ return [
156
+ nextStep,
157
+ ...(has("typography") ? [typographyStep] : []),
158
+ ...(has("shadcn") ? [shadcnInitStep, shadcnAllStep] : []),
159
+ ...(libraries.length > 0 ? [librariesStep(libraries)] : []),
160
+ ...(has("ultracite") ? [ultraciteStep(agents)] : agents.length > 0 ? [agentConfigStep(agents)] : []),
161
+ ];
162
+ }
@@ -0,0 +1,15 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ const extraSections = [["# playwright mcp", "/.playwright-mcp/"]];
5
+
6
+ export async function extendGitignore(projectDir: string): Promise<void> {
7
+ const path = join(projectDir, ".gitignore");
8
+ const current = await readFile(path, "utf8").catch(() => "");
9
+ const missing = extraSections.filter((section) => !current.includes(section[1]));
10
+ if (missing.length === 0) {
11
+ return;
12
+ }
13
+ const body = missing.map((section) => section.join("\n")).join("\n\n");
14
+ await writeFile(path, `${current.trimEnd()}\n\n${body}\n`);
15
+ }
@@ -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
+ }