Skip to content
Merged
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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ VibeStack v1 targets a single-host Docker Compose installation with:
- App lifecycle controls: deploy, update, start, stop, delete, rollback.
- Latest version plus two previous versions available for rollback.
- App logs, deployment history, audit logs, and lifecycle events.
- VibeStack Doctor diagnostics with root-cause categories, evidence, and copyable coding-agent fix prompts.
- App secrets as environment variables, never revealed after creation.
- Optional Postgres per app, using separate databases in the same Postgres server as VibeStack metadata.
- Persistent app volumes.
- Maintenance mode and admin-configurable announcement banner.
- Optional OpenRouter-powered Doctor enrichment using `openai/gpt-5.5`.
- OpenAPI-first API design.

## Quickstart Paths
Expand All @@ -55,6 +57,8 @@ The initial Claude Code companion skill lives in:

It describes how an AI coding agent should prepare a web app for VibeStack, create or validate `vibestack.json`, package the source as a tarball, submit it to the deployment API, and poll for status.

When a deployment fails, the helper now fetches VibeStack Doctor output from the server. Doctor classifies common generated-app failures such as missing health routes, wrong bind hosts, port mismatches, missing environment secrets, hard-coded localhost Postgres connections, missing database tables or migrations, build failures, and container startup failures. The management UI shows the same diagnosis and includes a copyable fix prompt for Claude Code, Codex, or another coding agent.

The skill also includes a reference API contract and a helper script:

- [skills/deploy-to-vibestack/references/api.md](skills/deploy-to-vibestack/references/api.md)
Expand Down Expand Up @@ -100,7 +104,7 @@ The Cloudflare token must be able to edit DNS records in the zone used by the ho

## Current Status

This repository is an initial public release. It includes the VibeStack API, worker, web app, shared package, deployment skill, and sample application fixtures. APIs and operational behavior may change before a 1.0 release.
This repository is an initial public release. It includes the VibeStack API, worker, web app, shared package, deployment skill, VibeStack Doctor troubleshooting workflow, and sample application fixtures. APIs and operational behavior may change before a 1.0 release.

## Contributing

Expand Down
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@vibestack/api",
"version": "0.2.2-alpha.0",
"version": "0.2.3-alpha.0",
"private": true,
"type": "module",
"main": "dist/server.js",
Expand Down
136 changes: 136 additions & 0 deletions apps/api/src/doctor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { describe, expect, it } from 'vitest';
import { buildDoctorPacket, normalizeOpenRouterSetting, publicOpenRouterSetting } from './doctor.js';
import { loadConfig } from './config.js';
import { decryptSecret } from './crypto.js';
import type { Db } from './db.js';
import type { AppRow, DeploymentRow } from './types.js';

const app: AppRow = {
id: 'de52380f-282b-44de-a741-17118f331b01',
team_id: '8f90c863-78f2-4837-a98b-02b812ef765d',
name: 'okr-dashboard',
slug: 'okr-dashboard',
hostname: 'platform-admins-okr-dashboard.example.com',
status: 'failed',
creator_user_id: 'b2f2f26f-3a5c-4226-844f-b54808bd7baf',
last_updated_by_user_id: null,
current_deployment_id: null,
postgres_enabled: false,
external_password_enabled: false,
login_access_enabled: true,
created_at: new Date(),
updated_at: new Date(),
deleted_at: null
};

function deployment(overrides: Partial<DeploymentRow>): DeploymentRow {
return {
id: 'f5c483c3-9466-4eef-8854-cab7de56c657',
app_id: app.id,
version_number: 1,
type: 'deploy',
source_commit_sha: null,
source_tarball_sha256: null,
docker_image_tag: null,
manifest: { name: 'okr-dashboard', port: 3000, healthCheckPath: '/health', requiredSecrets: [] },
status: 'failed',
started_by_user_id: app.creator_user_id,
rollback_source_deployment_id: null,
error_code: null,
error_message: null,
error_details_json: null,
log_excerpt: null,
started_at: new Date(),
finished_at: new Date(),
created_at: new Date(),
...overrides
};
}

describe('VibeStack Doctor', () => {
it('classifies missing health routes from failed health checks', async () => {
const packet = await buildDoctorPacket({
app,
deployments: [
deployment({
error_code: 'HEALTH_CHECK_FAILED',
error_message: 'The container did not return a successful response.',
error_details_json: {
checkedUrl: 'http://127.0.0.1:49152/health',
port: 3000,
healthCheckPath: '/health',
logExcerpt: 'GET /health 404\nCannot GET /health'
}
})
],
secrets: [],
appLogs: [],
postgres: { enabled: false, logs: [] }
});

expect(packet.rootCauseCategory).toBe('missing_health_route');
expect(packet.healthCheckResult.status).toBe('failed');
expect(packet.suggestedFixPrompt).toContain('health route');
});

it('classifies missing manifest required secrets', async () => {
const packet = await buildDoctorPacket({
app,
deployments: [
deployment({
manifest: { name: 'okr-dashboard', port: 3000, healthCheckPath: '/health', requiredSecrets: ['OPENAI_API_KEY'] },
error_code: 'HEALTH_CHECK_FAILED'
})
],
secrets: [],
appLogs: [],
postgres: { enabled: false, logs: [] }
});

expect(packet.rootCauseCategory).toBe('missing_env_secret');
expect(packet.evidence.some((item) => item.value === 'OPENAI_API_KEY')).toBe(true);
});

it('classifies localhost database connections from logs', async () => {
const packet = await buildDoctorPacket({
app: { ...app, postgres_enabled: true },
deployments: [deployment({ error_code: 'HEALTH_CHECK_FAILED' })],
secrets: [],
appLogs: ['Error: connect ECONNREFUSED 127.0.0.1:5432'],
postgres: { enabled: true, logs: [] }
});

expect(packet.rootCauseCategory).toBe('database_connection_localhost');
expect(packet.suggestedFixPrompt).toContain('DATABASE_URL');
});

it('stores OpenRouter API keys encrypted and exposes only configured flags', async () => {
const config = loadConfig({
DATABASE_URL: 'postgres://vibestack:vibestack@localhost:5432/vibestack',
VIBESTACK_SECRET_KEY: 'test-secret-key-for-doctor-settings'
});
const rows = new Map<string, unknown>();
const db = {
maybeOne: async () => {
const value = rows.get('openRouter');
return value ? { value_json: value } : null;
}
} as unknown as Db;

const setting = await normalizeOpenRouterSetting(db, config, {
enabled: true,
model: 'openai/gpt-5.5',
apiKey: 'sk-or-test'
});
rows.set('openRouter', setting);

expect(setting.encryptedApiKey).toMatch(/^v1:/);
expect(decryptSecret(setting.encryptedApiKey ?? '', config.secretKey)).toBe('sk-or-test');
expect(publicOpenRouterSetting(setting)).toEqual({
enabled: true,
model: 'openai/gpt-5.5',
configured: true,
apiKeyConfigured: true
});
});
});
Loading
Loading