Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .specify/specs/001-vibe-git-strategy/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Implementation Plan: Git Strategy Configuration for Vibe Workflow

## Tech Stack & Libraries

- **Language**: TypeScript (ESM)
- **Frameworks**: Commander.js (CLI), Inquirer.js (prompts)
- **Storage**: JSON (settings.json template via Handlebars)

## Project Structure Impact

```
src/
├── types.ts # Add gitStrategy field to ProjectConfig
├── prompts/
│ └── project.ts # Add conditional prompt for git strategy
├── generators/
│ └── claude-code/
│ └── index.ts # Pass gitStrategy to template
└── templates/
└── claude-code/
└── settings.json.hbs # Conditionally include git.strategy
```

## Implementation Strategy

### Phase 1: Type Definitions
- Add `gitStrategy: "pr-required" | "direct-push"` to `ProjectConfig` interface in `src/types.ts`
- Default value: `"pr-required"` (conservative, safe default)

### Phase 2: Prompts
- Modify `src/prompts/project.ts` to add conditional prompt after `workflowMode` selection
- Condition: only ask if `workflowMode === "vibe"`
- Question: "Which git strategy will you use?"
- Options:
- "PR required before merge (safer)" → `"pr-required"`
- "Push directly to master (faster)" → `"no-pr"`
- Default: `"pr-required"`

### Phase 3: Generator & Template
- Update `src/generators/claude-code/index.ts` to pass `gitStrategy` to the settings.json template
- Update `src/templates/claude-code/settings.json.hbs` to include the `git.strategy` field based on the value

## Dependencies & Sequencing

1. **T001** - Update types (no dependencies)
2. **T002** - Add prompt logic (depends on T001)
3. **T003** - Update generator and template (depends on T001, T002)

## Testing Strategy

- Unit tests in existing test files (`src/__tests__/new-command.test.ts`)
- Verify prompt appears only when `workflowMode === "vibe"`
- Verify `ProjectConfig.gitStrategy` is set correctly
- Verify settings.json contains correct `git.strategy` value
- E2E: Generate a project with Vibe + PR workflow, verify settings.json

## Parallel Opportunities

None — tasks are sequential due to dependencies on type definitions.

## MVP Scope

- All three tasks (types, prompts, template) are minimal and required for the feature
- No optional enhancements in Phase 1
33 changes: 33 additions & 0 deletions .specify/specs/001-vibe-git-strategy/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Feature Specification: Git Strategy Configuration for Vibe Workflow

## Overview

When users select "Vibe" workflow mode during ForgeKit project setup, add an additional prompt to determine their git strategy preference. This configuration should be written to the generated project's `settings.json` to guide Claude Code behavior.

## User Stories

### US1: Prompt for Git Strategy (P1)
As a user setting up a ForgeKit project with Vibe workflow, I want to be asked whether I'll use a PR-based workflow or push directly to master, so that my project's Claude Code configuration reflects my development process.

**Acceptance Criteria:**
- When user selects "Vibe" in the workflow selection prompt, a follow-up question appears
- Question offers two clear choices: "PR workflow" or "Direct push to master"
- User can select one of these options
- Selection is stored in the config and passed to the claude-code generator

### US2: Update settings.json with Git Strategy (P1)
As a user with a configured git strategy preference, I want the generated `settings.json` to include the `git.strategy` setting, so that Claude Code enforces the appropriate workflow.

**Acceptance Criteria:**
- If "PR workflow" is selected: `settings.json` contains `"git.strategy": "pr-required"`
- If "Direct push to master" is selected: `settings.json` contains `"git.strategy": "no-pr"`
- The setting is placed in the correct location in settings.json
- The setting is present in every generated project regardless of other options

## Implementation Notes

- The prompt should appear only when `workflowMode === "vibe"`
- The question should be asked during the main project configuration wizard (in `prompts/project.ts`)
- The answer should be stored in `ProjectConfig.gitStrategy` (new field)
- The claude-code generator should pass this value to the settings.json template
- Backward compatibility: Projects without this setting should default to "pr-required"
111 changes: 111 additions & 0 deletions .specify/specs/001-vibe-git-strategy/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Tasks: Git Strategy Configuration for Vibe Workflow

## Implementation Summary

Add `gitStrategy` configuration prompt to ForgeKit's Vibe workflow mode, allowing users to choose between PR-required or direct-push workflows. The selection updates the generated project's `settings.json`.

**Total tasks**: 3
**Estimated effort**: ~30 mins
**MVP scope**: All 3 tasks

---

## Phase 1: Setup (Foundation)

*No setup tasks needed — changes are localized to existing files.*

---

## Phase 2: Implementation

### T001 — Add gitStrategy type to ProjectConfig
- [x] T001 Add `gitStrategy: "pr-required" | "no-pr"` field to `ProjectConfig` interface in `src/types.ts`
- Default value when creating ProjectConfig instances: `"pr-required"`
- Rationale: Conservative default ensures safety (PR workflow) if unset

**Test criteria**: TypeScript compilation passes, no type errors

---

## Phase 3: User Story 1 — Vibe Git Strategy Prompt

### T002 — Add conditional git strategy prompt in project wizard
- [x] T002 [US1] Add git strategy question to `src/prompts/project.ts` after `workflowMode` selection
- Condition: Only ask if `workflowMode === "vibe"`
- Question text: "Which git strategy will you use?"
- Options:
- "PR required before merge (safer)" → sets `gitStrategy = "pr-required"`
- "Push directly to master (faster)" → sets `gitStrategy = "no-pr"`
- Default: `"pr-required"`
- Store result in returned `ProjectConfig`

**Test criteria**:
- Prompt appears only when Vibe workflow selected
- Both options work correctly
- Default is "pr-required"
- ProjectConfig.gitStrategy is set accurately

### T003 — Update claude-code generator and settings.json template
- [x] T003 [US1] Update `src/generators/claude-code/index.ts` to pass `gitStrategy` from `ProjectConfig` to the settings.json template
- [x] T003b [US1] Update `src/templates/claude-code/settings.json.hbs` to include:
```json
"git": {
"strategy": "{{gitStrategy}}"
}
```
- Ensure the `git` object exists (may need to merge with existing config)
- Template should render `"pr-required"` or `"direct-push"` based on the config value

**Test criteria**:
- Generated project's `settings.json` contains `"git.strategy": "pr-required"` when user selects PR mode
- Generated project's `settings.json` contains `"git.strategy": "no-pr"` when user selects direct-push mode
- All other settings remain intact

---

## Testing Strategy

### Unit Tests
Update `src/__tests__/new-command.test.ts`:
- Test that `gitStrategy` defaults to `"pr-required"` when not explicitly set
- Test that prompt is skipped when `workflowMode !== "vibe"`
- Test that prompt appears and sets correct value when `workflowMode === "vibe"`

### E2E Test (optional, use existing e2e framework)
- Run `forgekit new test-pr-mode --vibe --git-strategy=pr-required` (if flag support added)
- Verify generated `settings.json` contains correct `git.strategy`
- Run `forgekit new test-no-pr-mode --vibe --git-strategy=no-pr`
- Verify generated `settings.json` contains correct `git.strategy`

---

## Implementation Notes

### Dependencies
- T001 must complete before T002 and T003 (types needed for prompt config)
- T002 and T003 are sequential (prompt result needs to reach template)

### Constitution Alignment
- **Principle 3** (ProjectConfig is source of truth): gitStrategy flows through ProjectConfig only
- **Principle 2** (Templates contain zero logic): Settings.json template receives flat value, no conditionals
- **Principle 1** (Each generator owns one layer): claude-code generator handles passing config to template

### File Impact Summary
- `src/types.ts` — 1 line addition (gitStrategy field)
- `src/prompts/project.ts` — ~15 lines (conditional prompt block)
- `src/generators/claude-code/index.ts` — 1 line (pass gitStrategy)
- `src/templates/claude-code/settings.json.hbs` — 2 lines (git.strategy object)

### Backward Compatibility
- Existing projects without `gitStrategy` in `ProjectConfig` will use default `"pr-required"`
- No breaking changes to CLI flags or existing workflows

---

## Completion Criteria

- [ ] All three tasks marked `[x]`
- [ ] TypeScript compiles with no errors
- [ ] Tests pass (unit + optional e2e)
- [ ] Code follows constitution principles
- [ ] PR reviewed and merged
8 changes: 8 additions & 0 deletions .specify/specs/001-vibe-git-strategy/workflow-state.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"routing": "fast-track",
"phase": "fast-track",
"branch": null,
"completed_steps": ["tasks"],
"skills_used": ["sk:tasks"],
"last_updated": "2026-04-17T15:50:00Z"
}
33 changes: 33 additions & 0 deletions src/__tests__/add-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,39 @@ describe("forgekit add — integration", () => {
).toBe(true);
});

it("adds svelte frontend to a spring-boot-only project", async () => {
await generateBase({ backendType: "spring-boot" });

const existingResult = await detectProject(projectDir);
const updatedConfig: ProjectConfig = {
...existingResult.config,
frontend: "svelte",
auth: false,
};

const addTmpDir = await fs.mkdtemp(
path.join(os.tmpdir(), "forgekit-add-svelte-gen-"),
);
try {
const { generateFrontend } =
await import("../generators/frontend/index.js");
await generateFrontend(addTmpDir, updatedConfig, BASE_VERSIONS);
await fs.copy(addTmpDir, projectDir, { overwrite: false });
} finally {
await fs.remove(addTmpDir);
}

expect(
await fs.pathExists(path.join(projectDir, "frontend", "package.json")),
).toBe(true);
expect(
await fs.pathExists(path.join(projectDir, "frontend", "src", "App.svelte")),
).toBe(true);
expect(
await fs.pathExists(path.join(projectDir, "backend", "pom.xml")),
).toBe(true);
});

it("detects conflict when adding spring-boot to project with existing backend", async () => {
await generateBase({ backendType: "fastapi" });

Expand Down
12 changes: 12 additions & 0 deletions src/__tests__/detect-project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@ describe("detectProject", () => {
expect(result.config.frontend).toBe("react-vite");
});

it("detects svelte frontend from App.svelte", async () => {
await fs.ensureDir(path.join(tmpDir, "frontend", "src"));
await fs.writeFile(
path.join(tmpDir, "frontend", "src", "App.svelte"),
"<main />",
);

const result = await detectProject(tmpDir);
expect(result.source).toBe("filesystem");
expect(result.config.frontend).toBe("svelte");
});

it("detects multiple layers from sentinel files", async () => {
await fs.writeFile(path.join(tmpDir, "docker-compose.yml"), "version: 3");
await fs.ensureDir(path.join(tmpDir, ".github", "workflows"));
Expand Down
30 changes: 30 additions & 0 deletions src/__tests__/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,4 +380,34 @@ describe("ForgeKit e2e — generation pipeline", () => {
);
expect(dockerCompose).toContain("postgres:");
}, 15_000);

it("S11: Svelte only — generates key files and package.json contains svelte", async () => {
const projectDir = await run(
baseConfig({
frontend: "svelte",
uiFramework: "tailwind",
}),
);

const frontendDir = path.join(projectDir, "frontend");
const srcDir = path.join(frontendDir, "src");

expect(await fs.pathExists(path.join(frontendDir, "package.json"))).toBe(
true,
);
expect(await fs.pathExists(path.join(frontendDir, "vite.config.ts"))).toBe(
true,
);
expect(await fs.pathExists(path.join(frontendDir, "svelte.config.js"))).toBe(
true,
);
expect(await fs.pathExists(path.join(srcDir, "App.svelte"))).toBe(true);
expect(await fs.pathExists(path.join(srcDir, "main.ts"))).toBe(true);
expect(
await fs.pathExists(path.join(srcDir, "components", "Layout.svelte")),
).toBe(true);

const pkg = await fs.readJson(path.join(frontendDir, "package.json"));
expect(pkg.dependencies["svelte"]).toBeDefined();
}, 15_000);
});
3 changes: 3 additions & 0 deletions src/__tests__/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export function makeBaseConfig(
claudeCode: false,
speckit: false,
workflowMode: "none",
gitStrategy: "pr-required",
speckitPreset: null,
gitInit: false,
...overrides,
Expand Down Expand Up @@ -53,6 +54,8 @@ export const BASE_VERSIONS: ResolvedVersions = {
reactRouter: "7.5.0",
vite: "6.3.0",
axiosReact: "1.8.0",
svelte: "5.38.7",
vitePluginSvelte: "6.2.0",
next: "15.3.0",
nextAuth: "5.0.0",
prismaClient: "6.6.0",
Expand Down
10 changes: 10 additions & 0 deletions src/__tests__/versions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,14 @@ describe("resolveVersions", () => {
);
});
});

it("resolves Svelte frontend versions with fallback data", async () => {
vi.stubGlobal("fetch", () => Promise.resolve({ ok: false } as Response));

const result = await resolveVersions({ backendType: null, frontend: "svelte" });

expect(result.svelte).toBe(FALLBACK_VERSIONS.svelte);
expect(result.vitePluginSvelte).toBe(FALLBACK_VERSIONS.vitePluginSvelte);
expect(result.tailwind).toBe(FALLBACK_VERSIONS.tailwind);
});
});
15 changes: 14 additions & 1 deletion src/commands/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ const LAYER_CONFIG_MAP: Record<string, LayerDef> = {
configValue: "vue",
conflictGroup: "frontend",
},
svelte: {
configField: "frontend",
configValue: "svelte",
conflictGroup: "frontend",
},
docker: { configField: "docker", configValue: true, conflictGroup: null },
ci: { configField: "ci", configValue: true, conflictGroup: null },
"claude-code": {
Expand Down Expand Up @@ -123,6 +128,7 @@ async function runLayerGenerator(
case "angular":
case "react":
case "vue":
case "svelte":
await generateFrontend(projectDir, config, versions);
break;
case "docker":
Expand Down Expand Up @@ -157,7 +163,10 @@ async function regenerateDependentLayers(
layer === "laravel" ||
layer === "nextjs";
const isFrontendLayer =
layer === "angular" || layer === "react" || layer === "vue";
layer === "angular" ||
layer === "react" ||
layer === "vue" ||
layer === "svelte";

if (!isBackendLayer && !isFrontendLayer) return;

Expand Down Expand Up @@ -218,6 +227,9 @@ Layers:
react Frontend React (Vite + Tailwind CSS)
Options : --auth

svelte Frontend Svelte (Vite + Tailwind CSS)
Options : --auth

docker Docker Compose (PostgreSQL + pgAdmin)
ci GitHub Actions CI (lint + test + build)
claude-code Config Claude Code (.claude/, CLAUDE.md, skills, hooks)
Expand Down Expand Up @@ -287,6 +299,7 @@ Exemples:
claudeCode: false,
speckit: false,
workflowMode: "none",
gitStrategy: "pr-required",
speckitPreset: null,
gitInit: false,
};
Expand Down
Loading
Loading