2026 03 10 Generate Edit Deploy Pipeline
Documentation for Plans
Generate → Edit → Deploy Pipeline Implementation Plan
For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build the complete pipeline from "user describes a platform" → Vitto generates high-quality code → user edits in Studio (backed by GitLab) → one-click deploy to Cloudflare.
Architecture: Code generation produces a full NX app shell + modules. All generated code is committed to a GitLab repo (feature branch + draft MR). Vitto Studio loads files directly from GitLab (not local disk). Edits in Studio commit back to GitLab. Deploy button triggers Cloudflare Pages direct upload from GitLab source.
Tech Stack: Angular 20 (standalone, signals, OnPush), NestJS backends, GitLab REST API v4, Cloudflare Pages Direct Upload API, R2 storage, RxJS, SSE streaming.
FUTURE: Local dev mode — For power users/dev team, add optional local file sync (
SAVE_GENERATED_LOCALLY=true+ file watcher → GitLab push). Deferred to avoid overcomplicating the primary cloud-first flow.
Chunk 1: Fix Code Generation Quality
The scaffold fallback (generateScaffoldFiles() in module-gen-phase.executor.ts) produces code that violates every rule in implement.prompt.md. When the LLM fails or times out, users get broken modules. The scaffold MUST match the LLM prompt's golden examples.
Task 1.1: Fix Model Generation — Add BaseEntity
Files:
Modify:
APIs/backend-vitto-api/src/app/module-generation/phases/module-gen-phase.executor.ts:1254-1300Step 1: Fix model template to extend BaseEntity
In generateScaffoldFiles(), around line 1283-1287, change the model generation:
// BEFORE (line 1287):
content: `${statusType}export interface ${entityClass} {\n${fieldLines.join('\n')}\n}\n`,
// AFTER:
content: `import { BaseEntity } from '@libs-core/shared-models';\n\n${statusType}export interface ${entityClass} extends BaseEntity {\n${fieldLines.join('\n')}\n}\n`,
- Step 2: Remove manual id/createdAt/updatedAt injection
Remove lines 1274-1276 — BaseEntity already provides these:
// REMOVE these 3 lines:
if (!reservedFields.has('id')) fieldLines.unshift(' id: string;');
if (!reservedFields.has('createdAt')) fieldLines.push(' createdAt: string;');
if (!reservedFields.has('updatedAt')) fieldLines.push(' updatedAt: string;');
- Step 3: Verify backend-vitto-api still compiles
Run: cd APIs/backend-vitto-api && npx tsc --noEmit --pretty 2>&1 | head -30
Expected: No errors related to model generation
- Step 4: Commit
git add APIs/backend-vitto-api/src/app/module-generation/phases/module-gen-phase.executor.ts
git commit -m "[S199.1] fix: scaffold models extend BaseEntity instead of manual id/dates"
Task 1.2: Fix Service Generation — Extend BaseCrudApiService
Files:
Modify:
APIs/backend-vitto-api/src/app/module-generation/phases/module-gen-phase.executor.ts:1302-1362Step 1: Replace manual service with BaseCrudApiService pattern
Replace the service generation block (lines 1312-1354) with:
files.push({
moduleName: slug,
category,
path: `src/lib/services/${slug}-api.service.ts`,
content: [
`import { Injectable, inject } from '@angular/core';`,
`import { Observable } from 'rxjs';`,
`import { map } from 'rxjs';`,
`import { BaseCrudApiService, CrudPaginatedResult } from '@libs-core/module-base';`,
`import { PaginationRequest } from '@libs-core/shared-models';`,
`import { UniverseDataService } from '@libs-foundational/core-angular';`,
primaryEntity ? `import { ${primaryClass} } from '../models/${primarySlug}.model';` : '',
``,
`@Injectable({ providedIn: 'root' })`,
`export class ${className}ApiService extends BaseCrudApiService<${primaryClass}> {`,
` protected readonly baseUrl = '';`,
` protected readonly entityName = '${primaryClass}';`,
` private readonly dataService = inject(UniverseDataService);`,
` private readonly collectionName = '${this.pluralize(primarySlug)}';`,
``,
` override getList(params?: PaginationRequest): Observable<CrudPaginatedResult<${primaryClass}>> {`,
` return this.dataService.query<${primaryClass}>(this.collectionName, {`,
` search: params?.search,`,
` page: params?.page,`,
` pageSize: params?.limit,`,
` }).pipe(`,
` map(res => ({ items: res.data, totalItems: res.total ?? res.data.length }))`,
` );`,
` }`,
``,
` override getById(id: string): Observable<${primaryClass}> {`,
` return this.dataService.read<${primaryClass}>(this.collectionName, id).pipe(`,
` map(entity => {`,
` if (!entity) throw new Error('${primaryClass} not found');`,
` return entity;`,
` })`,
` );`,
` }`,
``,
` override create(data: Partial<${primaryClass}>): Observable<${primaryClass}> {`,
` return this.dataService.create<${primaryClass}>(this.collectionName, data as ${primaryClass}).pipe(`,
` map(res => res.data as ${primaryClass})`,
` );`,
` }`,
``,
` override update(id: string, data: Partial<${primaryClass}>): Observable<${primaryClass}> {`,
` return this.dataService.update<${primaryClass}>(this.collectionName, id, data as ${primaryClass}).pipe(`,
` map(() => ({ ...data, id } as ${primaryClass}))`,
` );`,
` }`,
``,
` override delete(id: string): Observable<void> {`,
` return this.dataService.delete(this.collectionName, id).pipe(map(() => void 0));`,
` }`,
`}`,
``,
].filter(l => l !== undefined).join('\n'),
});
- Step 2: Update services/index.ts barrel
Change the barrel export filename from ${slug}.service to ${slug}-api.service:
// Update the services/index.ts generation (around line 1361):
content: `export * from './${slug}-api.service';\n`,
- Step 3: Add pluralize helper if not exists
Check if this.pluralize() exists on the class. If not, add:
private pluralize(slug: string): string {
if (slug.endsWith('s')) return slug;
if (slug.endsWith('y')) return slug.slice(0, -1) + 'ies';
return slug + 's';
}
- Step 4: Verify compilation
Run: cd APIs/backend-vitto-api && npx tsc --noEmit --pretty 2>&1 | head -30
Expected: No errors
- Step 5: Commit
git add APIs/backend-vitto-api/src/app/module-generation/phases/module-gen-phase.executor.ts
git commit -m "[S199.2] fix: scaffold services extend BaseCrudApiService with proper CRUD"
Task 1.3: Fix List Component — Use useCrudList Composable
Files:
Modify:
APIs/backend-vitto-api/src/app/module-generation/phases/module-gen-phase.executor.ts:1364-1513Step 1: Replace manual list component with composable version
Replace the list component TS generation (lines 1365-1433) with the golden example from implement.prompt.md:
files.push({
moduleName: slug,
category,
path: `src/lib/components/${primarySlug}-list/${primarySlug}-list.component.ts`,
content: [
`import { Component, ChangeDetectionStrategy, inject } from '@angular/core';`,
`import { CommonModule } from '@angular/common';`,
`import { forkJoin, map } from 'rxjs';`,
`import { useCrudList } from '@libs-core/module-base';`,
`import { GgsCrudListShellComponent } from '@libs-core/ui-framework';`,
`import { ${className}ApiService } from '../../services/${slug}-api.service';`,
primaryEntity ? `import type { ${primaryClass} } from '../../models/${primarySlug}.model';` : '',
``,
`@Component({`,
` selector: 'mod-${primarySlug}-list',`,
` standalone: true,`,
` imports: [CommonModule, GgsCrudListShellComponent],`,
` changeDetection: ChangeDetectionStrategy.OnPush,`,
` templateUrl: './${primarySlug}-list.component.html',`,
` styleUrl: './${primarySlug}-list.component.scss',`,
`})`,
`export class ${primaryClass}ListComponent {`,
` private readonly api = inject(${className}ApiService);`,
``,
` readonly crud = useCrudList<${primaryClass}>({`,
` loadFn: (req) => this.api.getList(req),`,
` basePath: '/${slug}',`,
` entityName: '${primaryClass}',`,
` deleteFn: (ids) => forkJoin(ids.map(id => this.api.delete(id))).pipe(map(() => void 0)),`,
` });`,
`}`,
``,
].filter(l => l !== undefined).join('\n'),
});
- Step 2: Replace list HTML with shell component
Replace the list HTML generation (lines 1436-1505) with:
files.push({
moduleName: slug,
category,
path: `src/lib/components/${primarySlug}-list/${primarySlug}-list.component.html`,
content: `<ggs-crud-list-shell [crud]="crud" />\n`,
});
- Step 3: Simplify list SCSS
Replace the verbose SCSS with minimal styling (the shell component handles layout):
files.push({
moduleName: slug,
category,
path: `src/lib/components/${primarySlug}-list/${primarySlug}-list.component.scss`,
content: `:host { display: block; padding: 1.5rem; }\n`,
});
- Step 4: Verify compilation
Run: cd APIs/backend-vitto-api && npx tsc --noEmit --pretty 2>&1 | head -30
- Step 5: Commit
git add APIs/backend-vitto-api/src/app/module-generation/phases/module-gen-phase.executor.ts
git commit -m "[S199.3] fix: scaffold list component uses useCrudList + GgsCrudListShellComponent"
Task 1.4: Fix Routes — Add authGuard
Files:
Modify:
APIs/backend-vitto-api/src/app/module-generation/phases/module-gen-phase.executor.ts:1515-1552Step 1: Add authGuard import and canActivate to all routes
Replace the routes generation:
files.push({
moduleName: slug,
category,
path: `src/lib/${slug}.routes.ts`,
content: [
`import { Routes } from '@angular/router';`,
`import { authGuard } from '@libs-core/security';`,
``,
`export const ${routesConstName}: Routes = [`,
` {`,
` path: '',`,
` canActivate: [authGuard],`,
` loadComponent: () => import('./components/${primarySlug}-list/${primarySlug}-list.component')`,
` .then(m => m.${primaryClass}ListComponent),`,
` data: { title: '${displayName}', breadcrumb: '${displayName}' },`,
` },`,
` {`,
` path: 'create',`,
` canActivate: [authGuard],`,
` loadComponent: () => import('./components/${primarySlug}-form/${primarySlug}-form.component')`,
` .then(m => m.${primaryClass}FormComponent),`,
` data: { title: 'Create ${displayName}', breadcrumb: 'Create', mode: 'create' },`,
` },`,
` {`,
` path: ':id',`,
` canActivate: [authGuard],`,
` loadComponent: () => import('./components/${primarySlug}-detail/${primarySlug}-detail.component')`,
` .then(m => m.${primaryClass}DetailComponent),`,
` data: { title: '${displayName} Details', breadcrumb: 'Details' },`,
` },`,
` {`,
` path: ':id/edit',`,
` canActivate: [authGuard],`,
` loadComponent: () => import('./components/${primarySlug}-form/${primarySlug}-form.component')`,
` .then(m => m.${primaryClass}FormComponent),`,
` data: { title: 'Edit ${displayName}', breadcrumb: 'Edit', mode: 'edit' },`,
` },`,
`];`,
``,
].join('\n'),
});
- Step 2: Commit
git add APIs/backend-vitto-api/src/app/module-generation/phases/module-gen-phase.executor.ts
git commit -m "[S199.4] fix: scaffold routes include authGuard on all paths"
Task 1.5: Fix Domain Events Generation
Files:
Modify:
APIs/backend-vitto-api/src/app/module-generation/phases/module-gen-phase.executor.ts(add after routes generation, before module contract)Step 1: Add events file generation to scaffold
Add after the routes block (~line 1552):
// --- Domain Events ---
if (primaryEntity) {
files.push({
moduleName: slug,
category,
path: `src/lib/events/${primarySlug}.events.ts`,
content: [
`import { DomainEvent } from '@libs-core/event-system';`,
`import { ${primaryClass} } from '../models/${primarySlug}.model';`,
``,
`export class ${primaryClass}CreatedEvent extends DomainEvent<{ ${primaryCamel}: ${primaryClass} }> {`,
` override readonly eventName = '${slug}.${primarySlug}.created';`,
`}`,
``,
`export class ${primaryClass}UpdatedEvent extends DomainEvent<{ ${primaryCamel}: ${primaryClass} }> {`,
` override readonly eventName = '${slug}.${primarySlug}.updated';`,
`}`,
``,
`export class ${primaryClass}DeletedEvent extends DomainEvent<{ id: string }> {`,
` override readonly eventName = '${slug}.${primarySlug}.deleted';`,
`}`,
``,
].join('\n'),
});
}
- Step 2: Add events export to barrel
Update the src/index.ts barrel to include events:
// In the barrel generation (~line 1624):
content: [
`export * from './lib/${slug}.module';`,
`export * from './lib/models';`,
`export * from './lib/services';`,
`export * from './lib/${slug}.routes';`,
primaryEntity ? `export * from './lib/events/${primarySlug}.events';` : '',
``,
].filter(Boolean).join('\n'),
- Step 3: Commit
git add APIs/backend-vitto-api/src/app/module-generation/phases/module-gen-phase.executor.ts
git commit -m "[S199.5] feat: scaffold generates domain events for CRUD operations"
Task 1.6: Add Detail + Form Component Scaffolds
Currently the scaffold only generates a list component. Detail and Form are referenced in routes but never generated — guaranteed 404 at runtime.
Files:
Modify:
APIs/backend-vitto-api/src/app/module-generation/phases/module-gen-phase.executor.tsStep 1: Add detail component scaffold
Add after the list component block:
// --- Detail Component ---
files.push({
moduleName: slug,
category,
path: `src/lib/components/${primarySlug}-detail/${primarySlug}-detail.component.ts`,
content: [
`import { Component, ChangeDetectionStrategy, inject } from '@angular/core';`,
`import { CommonModule } from '@angular/common';`,
`import { RouterModule } from '@angular/router';`,
`import { useCrudDetail } from '@libs-core/module-base';`,
`import { ${className}ApiService } from '../../services/${slug}-api.service';`,
primaryEntity ? `import type { ${primaryClass} } from '../../models/${primarySlug}.model';` : '',
``,
`@Component({`,
` selector: 'mod-${primarySlug}-detail',`,
` standalone: true,`,
` imports: [CommonModule, RouterModule],`,
` changeDetection: ChangeDetectionStrategy.OnPush,`,
` templateUrl: './${primarySlug}-detail.component.html',`,
` styleUrl: './${primarySlug}-detail.component.scss',`,
`})`,
`export class ${primaryClass}DetailComponent {`,
` private readonly api = inject(${className}ApiService);`,
``,
` readonly detail = useCrudDetail<${primaryClass}>({`,
` loadFn: (id) => this.api.getById(id),`,
` basePath: '/${slug}',`,
` entityName: '${primaryClass}',`,
` });`,
`}`,
``,
].filter(l => l !== undefined).join('\n'),
});
files.push({
moduleName: slug,
category,
path: `src/lib/components/${primarySlug}-detail/${primarySlug}-detail.component.html`,
content: [
`<div class="detail-container">`,
` @if (detail.loading()) {`,
` <div class="loading">Loading...</div>`,
` }`,
` @if (detail.error(); as err) {`,
` <div class="error">{{ err }}</div>`,
` }`,
` @if (detail.entity(); as item) {`,
` <div class="detail-header">`,
` <h2>{{ item['name'] || item.id }}</h2>`,
` <a [routerLink]="['edit']" class="btn btn-primary">Edit</a>`,
` </div>`,
` <div class="detail-body">`,
` <pre>{{ item | json }}</pre>`,
` </div>`,
` }`,
`</div>`,
``,
].join('\n'),
});
files.push({
moduleName: slug,
category,
path: `src/lib/components/${primarySlug}-detail/${primarySlug}-detail.component.scss`,
content: `:host { display: block; padding: 1.5rem; }\n.detail-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }\n`,
});
- Step 2: Add form component scaffold
// --- Form Component ---
files.push({
moduleName: slug,
category,
path: `src/lib/components/${primarySlug}-form/${primarySlug}-form.component.ts`,
content: [
`import { Component, ChangeDetectionStrategy, inject } from '@angular/core';`,
`import { CommonModule } from '@angular/common';`,
`import { ReactiveFormsModule } from '@angular/forms';`,
`import { useCrudForm } from '@libs-core/module-base';`,
`import { ${className}ApiService } from '../../services/${slug}-api.service';`,
primaryEntity ? `import type { ${primaryClass} } from '../../models/${primarySlug}.model';` : '',
``,
`@Component({`,
` selector: 'mod-${primarySlug}-form',`,
` standalone: true,`,
` imports: [CommonModule, ReactiveFormsModule],`,
` changeDetection: ChangeDetectionStrategy.OnPush,`,
` templateUrl: './${primarySlug}-form.component.html',`,
` styleUrl: './${primarySlug}-form.component.scss',`,
`})`,
`export class ${primaryClass}FormComponent {`,
` private readonly api = inject(${className}ApiService);`,
``,
` readonly form = useCrudForm<${primaryClass}>({`,
` loadFn: (id) => this.api.getById(id),`,
` createFn: (data) => this.api.create(data),`,
` updateFn: (id, data) => this.api.update(id, data),`,
` basePath: '/${slug}',`,
` entityName: '${primaryClass}',`,
` });`,
`}`,
``,
].filter(l => l !== undefined).join('\n'),
});
files.push({
moduleName: slug,
category,
path: `src/lib/components/${primarySlug}-form/${primarySlug}-form.component.html`,
content: [
`<div class="form-container">`,
` <h2>{{ form.isEditMode() ? 'Edit' : 'Create' }} ${displayName}</h2>`,
` @if (form.loading()) {`,
` <div class="loading">Loading...</div>`,
` }`,
` @if (form.error(); as err) {`,
` <div class="error">{{ err }}</div>`,
` }`,
` <form [formGroup]="form.formGroup()" (ngSubmit)="form.submit()">`,
` <!-- Form fields will be generated based on entity spec -->`,
` <div class="form-actions">`,
` <button type="button" class="btn btn-secondary" (click)="form.cancel()">Cancel</button>`,
` <button type="submit" class="btn btn-primary" [disabled]="form.saving()">`,
` {{ form.saving() ? 'Saving...' : (form.isEditMode() ? 'Update' : 'Create') }}`,
` </button>`,
` </div>`,
` </form>`,
`</div>`,
``,
].join('\n'),
});
files.push({
moduleName: slug,
category,
path: `src/lib/components/${primarySlug}-form/${primarySlug}-form.component.scss`,
content: `:host { display: block; padding: 1.5rem; }\n.form-actions { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 1.5rem; }\n`,
});
- Step 3: Verify compilation, commit
cd APIs/backend-vitto-api && npx tsc --noEmit --pretty 2>&1 | head -30
git add APIs/backend-vitto-api/src/app/module-generation/phases/module-gen-phase.executor.ts
git commit -m "[S199.6] feat: scaffold generates detail + form components with composables"
Task 1.7: Add Vitto Chat Context Instructions to Generated Modules
This is the groundbreaking feature. Each generated module includes a .vitto/instructions.json file that tells Vitto what it can do when the user clicks any area of the code.
Files:
Modify:
APIs/backend-vitto-api/src/app/module-generation/phases/module-gen-phase.executor.tsStep 1: Generate .vitto/instructions.json per module
Add to generateScaffoldFiles(), inside the module loop:
// --- Vitto Chat Context Instructions ---
const vittoInstructions = {
moduleId: slug,
moduleName: displayName,
version: '1.0.0',
editableAreas: [
{
id: `${slug}:model`,
files: [`src/lib/models/${primarySlug}.model.ts`],
description: `Entity model for ${displayName}`,
vittoActions: [
{ action: 'add-field', label: 'Add a new field', prompt: `Add a new field to the ${primaryClass} entity. Ask the user what field name and type.` },
{ action: 'add-validation', label: 'Add validation rules', prompt: `Add validation rules to ${primaryClass} fields.` },
{ action: 'add-computed', label: 'Add computed property', prompt: `Add a computed/derived property to ${primaryClass}.` },
],
},
{
id: `${slug}:list`,
files: [`src/lib/components/${primarySlug}-list/${primarySlug}-list.component.ts`, `src/lib/components/${primarySlug}-list/${primarySlug}-list.component.html`],
description: `List view for ${displayName}`,
vittoActions: [
{ action: 'add-column', label: 'Add table column', prompt: `Add a new column to the ${primaryClass} list table.` },
{ action: 'add-filter', label: 'Add filter', prompt: `Add a filter to the ${primaryClass} list.` },
{ action: 'add-bulk-action', label: 'Add bulk action', prompt: `Add a bulk action (e.g., export, delete selected) to the ${primaryClass} list.` },
{ action: 'customize-layout', label: 'Change layout', prompt: `Change the list layout (table, cards, grid) for ${primaryClass}.` },
],
},
{
id: `${slug}:detail`,
files: [`src/lib/components/${primarySlug}-detail/${primarySlug}-detail.component.ts`, `src/lib/components/${primarySlug}-detail/${primarySlug}-detail.component.html`],
description: `Detail view for ${displayName}`,
vittoActions: [
{ action: 'add-section', label: 'Add detail section', prompt: `Add a new section to the ${primaryClass} detail view.` },
{ action: 'add-related', label: 'Show related data', prompt: `Show related entities on the ${primaryClass} detail page.` },
{ action: 'add-action', label: 'Add action button', prompt: `Add an action button to the ${primaryClass} detail page.` },
],
},
{
id: `${slug}:form`,
files: [`src/lib/components/${primarySlug}-form/${primarySlug}-form.component.ts`, `src/lib/components/${primarySlug}-form/${primarySlug}-form.component.html`],
description: `Create/Edit form for ${displayName}`,
vittoActions: [
{ action: 'add-field', label: 'Add form field', prompt: `Add a new form field to the ${primaryClass} form.` },
{ action: 'add-validation', label: 'Add form validation', prompt: `Add validation to the ${primaryClass} form fields.` },
{ action: 'conditional-field', label: 'Add conditional field', prompt: `Add a conditional field that shows/hides based on another field value.` },
{ action: 'add-step', label: 'Convert to wizard', prompt: `Convert the ${primaryClass} form into a multi-step wizard.` },
],
},
{
id: `${slug}:service`,
files: [`src/lib/services/${slug}-api.service.ts`],
description: `API service for ${displayName}`,
vittoActions: [
{ action: 'add-method', label: 'Add API method', prompt: `Add a custom API method to the ${className}ApiService.` },
{ action: 'add-caching', label: 'Add caching', prompt: `Add response caching to the ${className}ApiService methods.` },
],
},
{
id: `${slug}:routes`,
files: [`src/lib/${slug}.routes.ts`],
description: `Route configuration for ${displayName}`,
vittoActions: [
{ action: 'add-route', label: 'Add new page', prompt: `Add a new route/page to the ${displayName} module.` },
{ action: 'add-guard', label: 'Add route guard', prompt: `Add a custom guard to restrict access to specific ${displayName} pages.` },
],
},
],
};
files.push({
moduleName: slug,
category,
path: `.vitto/instructions.json`,
content: JSON.stringify(vittoInstructions, null, 2) + '\n',
});
- Step 2: Commit
git add APIs/backend-vitto-api/src/app/module-generation/phases/module-gen-phase.executor.ts
git commit -m "[S199.7] feat: generate .vitto/instructions.json for contextual chat editing"
Task 1.8: Enforce Auto-Corrections (Not Just Warn)
Files:
Modify:
APIs/backend-vitto-api/src/app/module-generation/validation/auto-correction.service.tsStep 1: Find composable gap detection methods
Search for warning-only methods that detect useCrudList, BaseCrudApiService, BaseEntity violations.
- Step 2: Convert warnings to corrections
For each detection that currently only logs: apply a regex-based fix. Specifically:
Services without
extends BaseCrudApiService→ injectextends BaseCrudApiService<T>after class declarationModels without
extends BaseEntity→ addextends BaseEntityto interface declarations in.model.tsfilesComponents with
signal<T[]>([])patterns → log warning with composable suggestion (auto-fix too risky for LLM-generated code)Step 3: Commit
git add APIs/backend-vitto-api/src/app/module-generation/validation/auto-correction.service.ts
git commit -m "[S199.8] fix: auto-correction enforces BaseEntity and BaseCrudApiService"
Chunk 2: App Shell Generation
Currently the pipeline generates loose modules but no app container. A generated platform needs a runnable Angular app with routing, theme, and module registration.
Task 2.1: Create App Shell Generator
Files:
Create:
APIs/backend-vitto-api/src/app/module-generation/generators/app-shell-generator.service.tsStep 1: Create the service
import { Injectable, Logger } from '@nestjs/common';
interface AppShellConfig {
platformName: string;
platformSlug: string;
modules: Array<{ slug: string; className: string; category: string; displayName: string }>;
theme?: { primaryColor?: string; accentColor?: string };
}
interface GeneratedFile {
path: string;
content: string;
moduleName: string;
category: string;
}
@Injectable()
export class AppShellGeneratorService {
private readonly logger = new Logger('AppShellGenerator');
generate(config: AppShellConfig): GeneratedFile[] {
const files: GeneratedFile[] = [];
const { platformSlug, platformName, modules } = config;
// main.ts
files.push(this.generateMain(platformSlug));
// app.config.ts
files.push(this.generateAppConfig(platformSlug, platformName, modules));
// app.routes.ts
files.push(this.generateAppRoutes(platformSlug, modules));
// app.component.ts + html + scss
files.push(...this.generateAppComponent(platformSlug, platformName));
// index.html
files.push(this.generateIndexHtml(platformSlug, platformName));
// project.json (NX app)
files.push(this.generateProjectJson(platformSlug));
// tsconfig.app.json
files.push(this.generateTsConfigApp(platformSlug));
// styles.scss (entry point)
files.push(this.generateStyles(platformSlug));
return files;
}
// ... (implementation methods follow in Step 2)
}
- Step 2: Implement each generator method
Key methods — generateAppRoutes() connects all modules:
private generateAppRoutes(slug: string, modules: AppShellConfig['modules']): GeneratedFile {
const moduleImports = modules.map(m =>
` { path: '${m.slug}', loadChildren: () => import('@module/${m.slug}').then(m => m.${m.className}Routes) },`
).join('\n');
return {
path: `apps/${slug}/src/app/app.routes.ts`,
content: [
`import { Routes } from '@angular/router';`,
`import { authGuard } from '@libs-core/security';`,
``,
`export const appRoutes: Routes = [`,
` { path: '', redirectTo: 'dashboard', pathMatch: 'full' },`,
moduleImports,
` { path: '**', redirectTo: 'dashboard' },`,
`];`,
``,
].join('\n'),
moduleName: slug,
category: 'app',
};
}
generateAppConfig() registers all module manifests:
private generateAppConfig(slug: string, name: string, modules: AppShellConfig['modules']): GeneratedFile {
return {
path: `apps/${slug}/src/app/app.config.ts`,
content: [
`import { ApplicationConfig, provideAppInitializer, inject } from '@angular/core';`,
`import { provideRouter, withComponentInputBinding } from '@angular/router';`,
`import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';`,
`import { ModuleRegistryService } from '@libs-core/module-base';`,
`import { appRoutes } from './app.routes';`,
``,
`export const appConfig: ApplicationConfig = {`,
` providers: [`,
` provideRouter(appRoutes, withComponentInputBinding()),`,
` provideHttpClient(withInterceptorsFromDi()),`,
` provideAppInitializer(() => {`,
` const registry = inject(ModuleRegistryService);`,
` // Module manifests registered dynamically at runtime`,
` }),`,
` ],`,
`};`,
``,
].join('\n'),
moduleName: slug,
category: 'app',
};
}
Step 3: Register service in module-generation NestJS module
Step 4: Wire into INTEGRATE phase — Call
appShellGenerator.generate()after all modules completeStep 5: Commit
git add APIs/backend-vitto-api/src/app/module-generation/generators/
git commit -m "[S199.9] feat: app shell generator produces runnable Angular app"
Chunk 3: GitLab-Backed Studio (Replace Local Disk)
Task 3.1: Add GitLab File Operations to StudioApiService
Files:
Modify:
libs/libs-structural/vitto-studio/src/lib/services/studio-api.service.tsStep 1: Add GitLab-backed file operations
Replace disk-based methods with GitLab API calls:
// New methods — all go through backend-ai-api which proxies to GitLab
listModulesFromRepo(spaceId: string, universeId: string): Observable<{ modules: string[] }> {
return this.http.get<{ modules: string[] }>(
`${this.aiApiUrl}/studio/repos/${spaceId}/${universeId}/modules`
);
}
getModuleStructureFromRepo(spaceId: string, universeId: string, moduleSlug: string): Observable<ModuleStructure> {
return this.http.get<ModuleStructure>(
`${this.aiApiUrl}/studio/repos/${spaceId}/${universeId}/modules/${moduleSlug}/structure`
);
}
getFileFromRepo(spaceId: string, universeId: string, filePath: string): Observable<{ content: string }> {
return this.http.get<{ content: string }>(
`${this.aiApiUrl}/studio/repos/${spaceId}/${universeId}/file`,
{ params: { path: filePath } }
);
}
commitFileToRepo(spaceId: string, universeId: string, filePath: string, content: string, message: string): Observable<{ sha: string }> {
return this.http.post<{ sha: string }>(
`${this.aiApiUrl}/studio/repos/${spaceId}/${universeId}/file`,
{ path: filePath, content, message }
);
}
- Step 2: Commit
git add libs/libs-structural/vitto-studio/src/lib/services/studio-api.service.ts
git commit -m "[S199.10] feat: studio API methods for GitLab-backed file operations"
Task 3.2: Create Backend Studio Repo Controller
Files:
Create:
APIs/backend-ai-api/src/app/studio/studio-repo.controller.tsStep 1: Create controller that proxies to UniverseGitService
@Controller('studio/repos')
export class StudioRepoController {
constructor(private readonly universeGit: UniverseGitService) {}
@Get(':spaceId/:universeId/modules')
async listModules(@Param('spaceId') spaceId: string, @Param('universeId') universeId: string) {
const tree = await this.universeGit.getFileTree(spaceId, universeId, 'modules/', 'develop');
const modules = tree.filter(f => f.type === 'tree' && f.path.startsWith('modules/') && f.path.split('/').length === 3)
.map(f => f.path.split('/')[2]);
return { modules: [...new Set(modules)] };
}
@Get(':spaceId/:universeId/modules/:slug/structure')
async getModuleStructure(...) { /* recursive tree listing */ }
@Get(':spaceId/:universeId/file')
async getFile(@Query('path') filePath: string, ...) {
const content = await this.universeGit.getFileContent(spaceId, universeId, filePath, 'develop');
return { content };
}
@Post(':spaceId/:universeId/file')
async commitFile(@Body() body: { path: string; content: string; message: string }, ...) {
const result = await this.universeGit.commitFiles(repoPath, 'develop', [{ path: body.path, content: body.content }], body.message);
return { sha: result.sha };
}
}
- Step 2: Register in StudioModule, commit
Task 3.3: Update VfsSyncService to Load from GitLab
Files:
Modify:
libs/libs-structural/vitto-studio/src/lib/services/vfs-sync.service.tsStep 1: Replace
refreshAllModulesFromDisk()withrefreshAllModulesFromRepo()
Change the hydration flow to use GitLab endpoints instead of local disk scanning. Keep fromDisk as an option for future local dev mode.
- Step 2: Update
syncFileEdit()to commit to GitLab
When a user edits a file in Studio, debounce + commit to the GitLab branch:
private syncFileEdit(node: VfsNode): void {
if (node.sourceType === 'module' && node.isGenerated) {
this.api.commitFileToRepo(this.spaceId, this.universeId, node.path, node.content, `edit: ${node.name}`)
.pipe(debounceTime(1000), takeUntilDestroyed(this.destroyRef))
.subscribe();
}
}
- Step 3: Commit
Chunk 4: Deploy Pipeline (Studio → Cloudflare)
Task 4.1: Create Studio Deploy Endpoint on Vitto
Files:
Create:
APIs/backend-vitto-api/src/app/studio/studio-deploy.controller.tsStep 1: Implement deploy endpoint
The endpoint should:
- Pull latest from GitLab repo (develop branch)
- Run
ng build(or use direct upload without build for non-compiled assets) - Upload dist to Cloudflare Pages via
DirectCloudflareDeployService - Stream progress via SSE
@Controller('studio')
export class StudioDeployController {
@Post('deploy')
async deploy(@Body() body: { spaceId: string; target: 'cloudflare' | 'subdomain' }, @Res() res: Response) {
// 1. Get GitLab repo for this space
// 2. Pull all files from develop branch
// 3. Generate .gitlab-ci.yml (or trigger direct upload)
// 4. Stream progress events
}
}
- Step 2: Wire deploy SSE events to frontend
Emit events: deploy.started, deploy.building, deploy.uploading, deploy.complete, deploy.failed
- Step 3: Commit
Task 4.2: Create Draft MR After Generation
Files:
Modify:
APIs/backend-vitto-api/src/app/module-generation/phases/module-gen-phase.executor.tsStep 1: After GitLab commit, create a draft MR
In the INTEGRATE phase, after files are committed to the feature branch:
// After commitFiles() succeeds:
const mrResult = await this.universeGit.createMergeRequest(
gitConfig.repoPath,
featureBranch,
'develop',
`Draft: Generated ${executeOutput.modules.length} modules`,
{ draft: true }
);
this.emitPhaseEvent(sseSubject, session.id, 'module-gen.mr-created' as any, {
mrUrl: mrResult.webUrl,
mrId: mrResult.id,
branch: featureBranch,
});
- Step 2: Emit MR URL to chat
The frontend should display a clickable link to the GitLab MR.
- Step 3: Commit
Task 4.3: Wire Cloudflare Preview Deploy
Files:
Modify:
APIs/backend-vitto-api/src/app/studio/studio-deploy.controller.tsStep 1: For preview deploys, use direct upload
Instead of waiting for GitLab CI, use CloudflarePagesService.uploadFiles() directly with files from the GitLab repo. This gives instant preview URLs.
- Step 2: Return preview URL in SSE stream
sseSubject.next({
type: 'deploy.complete',
data: {
url: deployResult.url,
previewUrl: deployResult.previewUrl,
projectName: cloudflareProjectName,
},
});
- Step 3: Commit
Chunk 5: Vitto Chat Contextual Editing
Task 5.1: Load .vitto/instructions.json in Chat Context
Files:
Modify:
APIs/backend-vitto-api/src/app/engine/agent-loop.service.tsor relevant prompt builderStep 1: When user asks about a module, load its instructions
When the chat context includes a module ID or the user clicks a module area in Studio:
- Read
.vitto/instructions.jsonfrom the module (via GitLab) - Inject the relevant
editableAreas[].vittoActionsinto the system prompt - Vitto now knows exactly what actions are available for that code area
- Step 2: Add
edit_module_filetool to Vitto's tool registry
{
name: 'edit_module_file',
description: 'Edit a file in a generated module. Commits the change to GitLab.',
parameters: {
spaceId: { type: 'string' },
universeId: { type: 'string' },
filePath: { type: 'string' },
content: { type: 'string' },
commitMessage: { type: 'string' },
},
}
- Step 3: Studio sends click context to chat
When user clicks an editable area in Studio, send a message to Vitto chat with:
{
"context": {
"moduleId": "player-management",
"area": "list",
"file": "src/lib/components/player-list/player-list.component.ts",
"availableActions": ["add-column", "add-filter", "add-bulk-action", "customize-layout"]
}
}
Vitto can then offer: "I can add a column, add filters, add bulk actions, or change the layout. What would you like?"
- Step 4: Commit
Chunk 6: SSE Refresh Resilience
Task 6.1: Generation Session Persistence
Files:
Modify:
APIs/backend-vitto-api/src/app/module-generation/module-generation.service.tsModify:
libs/libs-ai/vitto-chat/src/lib/services/vitto-chat-engine.service.tsStep 1: Store generation session state in MongoDB
On each phase completion, persist the session state so it survives page refresh:
await this.sessionsCollection.updateOne(
{ _id: session.id },
{ $set: { phase: currentPhase, status: 'running', lastEvent: event, updatedAt: new Date() } },
{ upsert: true }
);
- Step 2: Add poll endpoint for active generation
@Get('generation/active/:spaceId')
async getActiveGeneration(@Param('spaceId') spaceId: string) {
return this.moduleGen.getActiveSession(spaceId);
}
- Step 3: On page load, check for active generation and resubscribe
In the frontend chat engine, on initialization:
// Check if there's an active generation for this space
this.api.getActiveGeneration(spaceId).subscribe(session => {
if (session?.status === 'running') {
this.resubscribeToSSE(session.taskId);
this.showReconnectedMessage(session);
}
});
- Step 4: Commit
Execution Order
- Chunk 1 (Tasks 1.1-1.8): Fix code quality — must be first, affects all subsequent generation
- Chunk 2 (Task 2.1): App shell — needed for deployable output
- Chunk 3 (Tasks 3.1-3.3): GitLab-backed Studio — the storage backbone
- Chunk 4 (Tasks 4.1-4.3): Deploy pipeline — connects Studio to Cloudflare
- Chunk 5 (Task 5.1): Vitto contextual editing — the "groundbreaking feature"
- Chunk 6 (Task 6.1): SSE resilience — polish for production reliability