diff --git a/.specify/specs/001-vibe-git-strategy/plan.md b/.specify/specs/001-vibe-git-strategy/plan.md new file mode 100644 index 0000000..1fced2b --- /dev/null +++ b/.specify/specs/001-vibe-git-strategy/plan.md @@ -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 diff --git a/.specify/specs/001-vibe-git-strategy/spec.md b/.specify/specs/001-vibe-git-strategy/spec.md new file mode 100644 index 0000000..09e7539 --- /dev/null +++ b/.specify/specs/001-vibe-git-strategy/spec.md @@ -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" diff --git a/.specify/specs/001-vibe-git-strategy/tasks.md b/.specify/specs/001-vibe-git-strategy/tasks.md new file mode 100644 index 0000000..fdce2ae --- /dev/null +++ b/.specify/specs/001-vibe-git-strategy/tasks.md @@ -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 diff --git a/.specify/specs/001-vibe-git-strategy/workflow-state.json b/.specify/specs/001-vibe-git-strategy/workflow-state.json new file mode 100644 index 0000000..d31268c --- /dev/null +++ b/.specify/specs/001-vibe-git-strategy/workflow-state.json @@ -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" +} diff --git a/src/__tests__/add-command.test.ts b/src/__tests__/add-command.test.ts index 95f0236..ad3b60e 100644 --- a/src/__tests__/add-command.test.ts +++ b/src/__tests__/add-command.test.ts @@ -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" }); diff --git a/src/__tests__/detect-project.test.ts b/src/__tests__/detect-project.test.ts index 2467bdd..f29546d 100644 --- a/src/__tests__/detect-project.test.ts +++ b/src/__tests__/detect-project.test.ts @@ -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"), + "
", + ); + + 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")); diff --git a/src/__tests__/e2e.test.ts b/src/__tests__/e2e.test.ts index 1ab846f..70d65c2 100644 --- a/src/__tests__/e2e.test.ts +++ b/src/__tests__/e2e.test.ts @@ -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); }); diff --git a/src/__tests__/fixtures.ts b/src/__tests__/fixtures.ts index 49294d8..2df51e4 100644 --- a/src/__tests__/fixtures.ts +++ b/src/__tests__/fixtures.ts @@ -25,6 +25,7 @@ export function makeBaseConfig( claudeCode: false, speckit: false, workflowMode: "none", + gitStrategy: "pr-required", speckitPreset: null, gitInit: false, ...overrides, @@ -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", diff --git a/src/__tests__/versions.test.ts b/src/__tests__/versions.test.ts index 720a0ed..d51e4c9 100644 --- a/src/__tests__/versions.test.ts +++ b/src/__tests__/versions.test.ts @@ -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); + }); }); diff --git a/src/commands/add.ts b/src/commands/add.ts index 7fa3a95..d97054f 100644 --- a/src/commands/add.ts +++ b/src/commands/add.ts @@ -62,6 +62,11 @@ const LAYER_CONFIG_MAP: Record = { 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": { @@ -123,6 +128,7 @@ async function runLayerGenerator( case "angular": case "react": case "vue": + case "svelte": await generateFrontend(projectDir, config, versions); break; case "docker": @@ -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; @@ -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) @@ -287,6 +299,7 @@ Exemples: claudeCode: false, speckit: false, workflowMode: "none", + gitStrategy: "pr-required", speckitPreset: null, gitInit: false, }; diff --git a/src/commands/new.ts b/src/commands/new.ts index 822e17d..97f8076 100644 --- a/src/commands/new.ts +++ b/src/commands/new.ts @@ -103,6 +103,14 @@ export async function generateProject( `\r ✔ Frontend Vue.js ${versions.vue} (Vite) généré `, ), ); + } else if (config.frontend === "svelte") { + process.stdout.write(chalk.yellow(" ⏳ Frontend Svelte (Vite)...")); + await generateFrontend(projectDir, config, versions); + console.log( + chalk.green( + `\r ✔ Frontend Svelte ${versions.svelte} (Vite) généré `, + ), + ); } if (config.docker) { @@ -172,6 +180,7 @@ export const newCommand = new Command("new") .option("--frontend", "Inclure le frontend Angular") .option("--no-frontend", "Exclure le frontend Angular") .option("--react", "Inclure le frontend React (Vite + Tailwind)") + .option("--svelte", "Inclure le frontend Svelte (Vite + Tailwind)") .option("--angular", "Inclure le frontend Angular (standalone, OnPush)") .option("--auth", "Inclure l'authentification") .option("--flyway", "Inclure Flyway (migrations SQL)") @@ -206,6 +215,7 @@ Backends: Frontends: --angular Angular (standalone, OnPush) --react React (Vite + Tailwind CSS) + --svelte Svelte (Vite + Tailwind CSS) Infrastructure: --docker Docker Compose (PostgreSQL + pgAdmin) [défaut: oui si backend] @@ -218,6 +228,7 @@ Exemples: $ forgekit new my-app $ forgekit new my-api --spring-boot --no-frontend --group com.acme $ forgekit new my-app --laravel --react --auth --openapi + $ forgekit new my-app --fastapi --svelte --auth $ forgekit new my-app --fastapi --angular --ui tailwind --ngrx $ forgekit new my-app --spring-boot --angular --no-flyway --no-docker`, ) @@ -238,6 +249,7 @@ Exemples: if (options.laravel) defaults.backendType = "laravel" as BackendType; if (options.nestjs) defaults.backendType = "nestjs" as BackendType; if (options.react) defaults.frontend = "react-vite" as FrontendType; + else if (options.svelte) defaults.frontend = "svelte" as FrontendType; else if (options.angular || options.frontend === true) defaults.frontend = "angular" as FrontendType; else if (options.frontend === false) defaults.frontend = null; @@ -322,6 +334,10 @@ Exemples: console.log( chalk.cyan(" cd frontend && npm install && npm run dev"), ); + if (config.frontend === "svelte") + console.log( + chalk.cyan(" cd frontend && npm install && npm run dev"), + ); console.log(""); } catch (error) { console.log( diff --git a/src/generators/claude-code/__tests__/claude-code.test.ts b/src/generators/claude-code/__tests__/claude-code.test.ts index b4e95bc..79a26ec 100644 --- a/src/generators/claude-code/__tests__/claude-code.test.ts +++ b/src/generators/claude-code/__tests__/claude-code.test.ts @@ -474,7 +474,11 @@ describe("ClaudeCodeGenerator", () => { }); it("vibe mode → settings.json git.strategy=no-pr and CLAUDE.md uses commit skill", async () => { - const config = { ...baseConfig, workflowMode: "vibe" as const }; + const config = { + ...baseConfig, + workflowMode: "vibe" as const, + gitStrategy: "no-pr" as const, + }; await generateClaudeCode(tmpDir, config, baseVersions); const settings = await fs.readJson( path.join(tmpDir, ".claude", "settings.json"), diff --git a/src/generators/claude-code/index.ts b/src/generators/claude-code/index.ts index c942e77..b0298bb 100644 --- a/src/generators/claude-code/index.ts +++ b/src/generators/claude-code/index.ts @@ -114,6 +114,8 @@ class ClaudeCodeGenerator extends BaseGenerator { const hasFrontend = this.config.frontend !== null; const angular = this.config.frontend === "angular"; const reactVite = this.config.frontend === "react-vite"; + const vue = this.config.frontend === "vue"; + const svelte = this.config.frontend === "svelte"; const parentHooks = await this.readParentHooks(); @@ -128,6 +130,8 @@ class ClaudeCodeGenerator extends BaseGenerator { hasFrontend, angular, reactVite, + vue, + svelte, frontend: hasFrontend, docker: this.config.docker, flyway: this.config.flyway, @@ -138,9 +142,8 @@ class ClaudeCodeGenerator extends BaseGenerator { claudeDir: ".claude", workflowSpeckit: this.config.workflowMode === "speckit", workflowVibe: this.config.workflowMode === "vibe", - gitStrategy: - this.config.workflowMode === "vibe" ? "no-pr" : "pr-required", - gitStrategyNoPr: this.config.workflowMode === "vibe", + gitStrategy: this.config.gitStrategy, + gitStrategyNoPr: this.config.gitStrategy === "no-pr", hasParentSessionStart: parentHooks.has("SessionStart"), hasParentPreCompact: parentHooks.has("PreCompact"), ...this.resolveSpeckitData(), @@ -405,6 +408,16 @@ class ClaudeCodeGenerator extends BaseGenerator { ); } + if (this.config.frontend === "svelte") { + commands.push( + "Bash(npm run dev)", + "Bash(npm run build)", + "Bash(npm run lint)", + "Bash(npm install)", + "Bash(npm run)", + ); + } + if (this.config.docker) { commands.push( "Bash(docker compose up)", diff --git a/src/generators/frontend/__tests__/index.test.ts b/src/generators/frontend/__tests__/index.test.ts index 8e9191a..c720bae 100644 --- a/src/generators/frontend/__tests__/index.test.ts +++ b/src/generators/frontend/__tests__/index.test.ts @@ -32,6 +32,17 @@ describe("generateFrontend router", () => { ).toBe(true); }); + it("generates Svelte files when frontend is svelte", async () => { + const config = { ...baseConfig, frontend: "svelte" as const }; + await generateFrontend(tmpDir, config, baseVersions); + expect( + await fs.pathExists(path.join(tmpDir, "frontend", "vite.config.ts")), + ).toBe(true); + expect( + await fs.pathExists(path.join(tmpDir, "frontend", "src", "App.svelte")), + ).toBe(true); + }); + it("generates Angular files when frontend is angular (US1 regression)", async () => { const config = { ...baseConfig, frontend: "angular" as const }; await generateFrontend(tmpDir, config, baseVersions); diff --git a/src/generators/root/index.ts b/src/generators/root/index.ts index 7444014..fcf0f0a 100644 --- a/src/generators/root/index.ts +++ b/src/generators/root/index.ts @@ -20,6 +20,8 @@ class RootGenerator extends BaseGenerator { const hasFrontend = this.config.frontend !== null; const angular = this.config.frontend === "angular"; const reactVite = this.config.frontend === "react-vite"; + const vue = this.config.frontend === "vue"; + const svelte = this.config.frontend === "svelte"; const data = { name: this.config.name, @@ -31,6 +33,8 @@ class RootGenerator extends BaseGenerator { hasFrontend, angular, reactVite, + vue, + svelte, docker: this.config.docker, versions: this.versions, }; diff --git a/src/prompts/add.ts b/src/prompts/add.ts index 015485f..1cdd963 100644 --- a/src/prompts/add.ts +++ b/src/prompts/add.ts @@ -29,6 +29,9 @@ export async function promptAddLayerConfig( if (layer === "vue") { return promptVue(defaults); } + if (layer === "svelte") { + return promptSvelte(defaults); + } if (layer === "prettier") { if (existingConfig.frontend === null) { throw new Error( @@ -217,3 +220,9 @@ async function promptVue( ): Promise> { return promptAuth(defaults); } + +async function promptSvelte( + defaults: Partial, +): Promise> { + return promptAuth(defaults); +} diff --git a/src/prompts/project.ts b/src/prompts/project.ts index 69224c1..2a7e3d9 100644 --- a/src/prompts/project.ts +++ b/src/prompts/project.ts @@ -10,6 +10,7 @@ import type { BackendType, FrontendType, WorkflowMode, + GitStrategy, SpeckitPreset, } from "../types.js"; @@ -61,6 +62,7 @@ export async function promptProjectConfig( { name: "Angular (standalone, OnPush)", value: "angular" }, { name: "React (Vite + Tailwind)", value: "react-vite" }, { name: "Vue.js (Vite + Tailwind)", value: "vue" }, + { name: "Svelte (Vite + Tailwind)", value: "svelte" }, { name: "Aucun", value: null }, ], default: "angular", @@ -231,6 +233,8 @@ export async function promptProjectConfig( uiFramework = "tailwind"; } else if (frontend === "vue") { uiFramework = "tailwind"; + } else if (frontend === "svelte") { + uiFramework = "tailwind"; } // ── Section 5: Infrastructure ───────────────────────────────────────────── @@ -321,6 +325,24 @@ export async function promptProjectConfig( }); } + let gitStrategy: GitStrategy = defaults.gitStrategy ?? "pr-required"; + if (workflowMode === "vibe" && defaults.gitStrategy === undefined) { + gitStrategy = await select({ + message: "Stratégie git", + choices: [ + { + name: "PR obligatoire (plus sûr)", + value: "pr-required", + }, + { + name: "Push direct sur master (plus rapide)", + value: "no-pr", + }, + ], + default: "pr-required", + }); + } + let speckitPreset: SpeckitPreset | null = defaults.speckitPreset ?? null; if (workflowMode === "speckit" && defaults.speckitPreset === undefined) { speckitPreset = await select({ @@ -364,6 +386,7 @@ export async function promptProjectConfig( docker, speckit, workflowMode, + gitStrategy, speckitPreset, ci, claudeCode, diff --git a/src/templates/claude-code/CLAUDE.md.hbs b/src/templates/claude-code/CLAUDE.md.hbs index eab36fa..247346b 100644 --- a/src/templates/claude-code/CLAUDE.md.hbs +++ b/src/templates/claude-code/CLAUDE.md.hbs @@ -64,3 +64,31 @@ npm run build # Production build npm run lint # TypeScript check ``` {{/if}} +{{#if vue}} +## Frontend — Vue {{versions.vue}} (Vite) + +- **Architecture:** `src/router/`, `src/stores/`, `src/components/`, `src/composables/` +- **Conventions:** Composition API, typed stores, Tailwind-first styling + +### Commands +```bash +cd frontend +npm run dev # Start dev server (port 4200) +npm run build # Production build +npm run lint # TypeScript check +``` +{{/if}} +{{#if svelte}} +## Frontend — Svelte {{versions.svelte}} (Vite) + +- **Architecture:** `src/components/`, `src/stores/`, `src/lib/` +- **Conventions:** Small components, typed stores, Tailwind-first styling + +### Commands +```bash +cd frontend +npm run dev # Start dev server (port 4200) +npm run build # Production build +npm run lint # Svelte/TypeScript check +``` +{{/if}} diff --git a/src/templates/claude-code/rules/frontend.md.hbs b/src/templates/claude-code/rules/frontend.md.hbs index b895c7f..85a58f1 100644 --- a/src/templates/claude-code/rules/frontend.md.hbs +++ b/src/templates/claude-code/rules/frontend.md.hbs @@ -20,3 +20,18 @@ paths: ["**/frontend/**"] - **CSS:** Tailwind CSS v4 - **Commands:** `cd frontend` → `npm install` | `npm run dev` | `npm run build` | `npm run lint` {{/if}} +{{#if vue}} +## Frontend — Vue {{versions.vue}} (Vite + Tailwind) + +- **Vue {{versions.vue}}** — Composition API, single-file components +- **State:** Pinia +- **CSS:** Tailwind CSS v4 +- **Commands:** `cd frontend` → `npm install` | `npm run dev` | `npm run build` | `npm run lint` +{{/if}} +{{#if svelte}} +## Frontend — Svelte {{versions.svelte}} (Vite + Tailwind) + +- **Svelte {{versions.svelte}}** — component-first, minimal client-side setup +- **CSS:** Tailwind CSS v4 +- **Commands:** `cd frontend` → `npm install` | `npm run dev` | `npm run build` | `npm run lint` +{{/if}} diff --git a/src/templates/root/README.md.hbs b/src/templates/root/README.md.hbs index a797462..95880c3 100644 --- a/src/templates/root/README.md.hbs +++ b/src/templates/root/README.md.hbs @@ -16,6 +16,12 @@ {{#if reactVite}} - **Frontend:** React {{versions.react}} / Vite / Tailwind CSS {{/if}} +{{#if vue}} +- **Frontend:** Vue {{versions.vue}} / Vite / Tailwind CSS +{{/if}} +{{#if svelte}} +- **Frontend:** Svelte {{versions.svelte}} / Vite / Tailwind CSS +{{/if}} {{#if docker}} - **Infra:** Docker Compose (PostgreSQL 17 + pgAdmin) {{/if}} @@ -49,6 +55,14 @@ cd frontend && npm install && ng serve # Démarrer le frontend (port 4200) cd frontend && npm install && npm run dev {{/if}} +{{#if vue}} +# Démarrer le frontend (port 4200) +cd frontend && npm install && npm run dev +{{/if}} +{{#if svelte}} +# Démarrer le frontend (port 4200) +cd frontend && npm install && npm run dev +{{/if}} ``` --- diff --git a/src/types.ts b/src/types.ts index ac25650..fd0ee7b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,7 @@ export type UIFramework = "primeng" | "tailwind" | "none"; export type PrimeNGPreset = "Aura" | "Lara" | "Nora"; export type WorkflowMode = "speckit" | "vibe" | "none"; +export type GitStrategy = "pr-required" | "no-pr"; export type SpeckitPreset = "rigorous" | "balanced" | "fast" | "bare-metal"; export type BackendType = | "spring-boot" @@ -9,7 +10,7 @@ export type BackendType = | "nextjs" | "nestjs" | null; -export type FrontendType = "angular" | "react-vite" | "vue" | null; +export type FrontendType = "angular" | "react-vite" | "vue" | "svelte" | null; export interface ProjectConfig { name: string; @@ -36,6 +37,7 @@ export interface ProjectConfig { claudeCode: boolean; speckit: boolean; workflowMode: WorkflowMode; + gitStrategy: GitStrategy; speckitPreset: SpeckitPreset | null; gitInit: boolean; } diff --git a/src/utils/detect-project.ts b/src/utils/detect-project.ts index bb2c124..848cd88 100644 --- a/src/utils/detect-project.ts +++ b/src/utils/detect-project.ts @@ -48,6 +48,12 @@ const SENTINEL_MAP: Array<{ c.frontend = "vue"; }, }, + { + files: ["frontend/src/App.svelte", "frontend/svelte.config.js"], + apply: (c) => { + c.frontend = "svelte"; + }, + }, { files: ["frontend/vite.config.ts"], apply: (c) => { @@ -108,6 +114,7 @@ function defaultConfig(projectDir: string): ProjectConfig { claudeCode: false, speckit: false, workflowMode: "none", + gitStrategy: "pr-required", speckitPreset: null, gitInit: false, }; diff --git a/src/versions.ts b/src/versions.ts index f6aebe5..b1b1519 100644 --- a/src/versions.ts +++ b/src/versions.ts @@ -26,6 +26,8 @@ export interface ResolvedVersions { reactRouter: string; vite: string; axiosReact: string; + svelte: string; + vitePluginSvelte: string; // Backend — Next.js next: string; nextAuth: string; @@ -69,6 +71,8 @@ export const FALLBACK_VERSIONS: ResolvedVersions = { reactRouter: "7.5.0", // renovate: datasource=npm depName=react-router vite: "7.0.0", // renovate: datasource=npm depName=vite axiosReact: "1.8.0", // renovate: datasource=npm depName=axios + svelte: "5.38.7", // renovate: datasource=npm depName=svelte + vitePluginSvelte: "6.2.0", // renovate: datasource=npm depName=@sveltejs/vite-plugin-svelte next: "15.3.0", // renovate: datasource=npm depName=next nextAuth: "5.0.0-beta.31", // renovate: datasource=npm depName=next-auth prismaClient: "6.6.0", // renovate: datasource=npm depName=prisma @@ -260,6 +264,17 @@ export async function resolveVersions(opts: { ); } + if (opts.frontend === "svelte") { + tasks.push( + fetchNpmVersion("svelte").then(set("svelte")), + fetchNpmVersion("@sveltejs/vite-plugin-svelte").then( + set("vitePluginSvelte"), + ), + fetchNpmVersion("tailwindcss").then(set("tailwind")), + fetchNpmVersion("vite").then(set("vite")), + ); + } + if (opts.frontend === "react-vite") { tasks.push( fetchNpmVersion("react").then(set("react")),