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
114 changes: 114 additions & 0 deletions scripts/test-github-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/usr/bin/env -S deno run --allow-net --allow-env

/**
* Manual test script for GitHubFSAdapter with a real repository
*
* Usage:
* GITHUB_TOKEN=$(gh auth token) deno run --allow-net --allow-env scripts/test-github-adapter.ts
*/

import { GitHubFSAdapter } from "../src/platform/adapters/github-fs-adapter/index.ts";

const token = Deno.env.get("GITHUB_TOKEN");
if (!token) {
console.error("ERROR: GITHUB_TOKEN environment variable required");
console.error("Run: GITHUB_TOKEN=$(gh auth token) deno run --allow-net --allow-env scripts/test-github-adapter.ts");
Deno.exit(1);
}

const owner = "veryfront";
const repo = "codersociety";

console.log(`\n🧪 Testing GitHubFSAdapter with ${owner}/${repo}\n`);

const adapter = new GitHubFSAdapter({
type: "github",
github: {
token,
owner,
repo,
ref: "main",
},
});

try {
// Test 1: Initialize
console.log("1️⃣ Initializing adapter...");
const startInit = Date.now();
await adapter.initialize();
console.log(` ✅ Initialized in ${Date.now() - startInit}ms`);

// Test 2: List root directory
console.log("\n2️⃣ Listing root directory...");
const rootEntries = await adapter.readdir("");
console.log(` ✅ Found ${rootEntries.length} entries:`);
for (const entry of rootEntries.slice(0, 10)) {
const icon = entry.isDirectory ? "📁" : "📄";
console.log(` ${icon} ${entry.name}`);
}
if (rootEntries.length > 10) {
console.log(` ... and ${rootEntries.length - 10} more`);
}

// Test 3: Check if common files exist
console.log("\n3️⃣ Checking common files...");
const filesToCheck = ["package.json", "README.md", "veryfront.config.ts", "tsconfig.json"];
for (const file of filesToCheck) {
const exists = await adapter.exists(file);
console.log(` ${exists ? "✅" : "❌"} ${file}`);
}

// Test 4: Read a file
console.log("\n4️⃣ Reading package.json...");
if (await adapter.exists("package.json")) {
const content = await adapter.readTextFile("package.json");
const pkg = JSON.parse(content);
console.log(` ✅ Project: ${pkg.name || "unnamed"}`);
console.log(` ✅ Content length: ${content.length} bytes`);
} else {
console.log(" ⏭️ Skipped (file doesn't exist)");
}

// Test 5: File resolution
console.log("\n5️⃣ Testing file resolution...");
const pathsToResolve = ["pages/index", "app/page", "src/index"];
for (const path of pathsToResolve) {
const resolved = await adapter.resolveFile(path);
if (resolved) {
console.log(` ✅ ${path} → ${resolved}`);
} else {
console.log(` ➖ ${path} → not found`);
}
}

// Test 6: Stat a file
console.log("\n6️⃣ Getting file stats...");
const firstFile = rootEntries.find(e => e.isFile);
if (firstFile) {
const stat = await adapter.stat(firstFile.path);
console.log(` ✅ ${firstFile.name}: ${stat.size} bytes, isFile=${stat.isFile}`);
}

// Test 7: Cache stats
console.log("\n7️⃣ Cache statistics:");
const cacheStats = adapter.getCacheStats();
console.log(` 📊 Entries: ${cacheStats.cache.size}`);
console.log(` 📊 Memory: ${(cacheStats.cache.memoryUsed / 1024).toFixed(1)} KB`);
console.log(` 📊 Hit rate: ${(cacheStats.cache.hitRate * 100).toFixed(1)}%`);

// Test 8: Rate limit info
console.log("\n8️⃣ Rate limit status:");
const rateLimit = adapter.getRateLimitInfo();
if (rateLimit) {
console.log(` 📊 Remaining: ${rateLimit.remaining}/${rateLimit.limit}`);
console.log(` 📊 Resets: ${rateLimit.reset.toLocaleTimeString()}`);
} else {
console.log(" ➖ No rate limit info yet");
}

console.log("\n✅ All tests passed!\n");

} catch (error) {
console.error("\n❌ Test failed:", error);
Deno.exit(1);
}
5 changes: 2 additions & 3 deletions src/core/config/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,12 @@ class ConfigValidationError extends Error {
}

/**
* Check if the adapter is using a virtual filesystem (e.g., Veryfront API)
* Supports both single-project (VeryfrontFSAdapter) and multi-project (MultiProjectFSAdapter) modes
* Check if the adapter is using a virtual filesystem (e.g., Veryfront API, GitHub)
*/
function isVirtualFilesystem(adapter: RuntimeAdapter): boolean {
const wrappedAdapter = (adapter?.fs as { fsAdapter?: unknown })?.fsAdapter;
const adapterName = (wrappedAdapter as { constructor?: { name?: string } })?.constructor?.name;
return adapterName === "VeryfrontFSAdapter" || adapterName === "MultiProjectFSAdapter";
return adapterName === "VeryfrontFSAdapter" || adapterName === "MultiProjectFSAdapter" || adapterName === "GitHubFSAdapter";
}

/**
Expand Down
28 changes: 27 additions & 1 deletion src/core/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export const veryfrontConfigSchema = z
.optional(),
fs: z
.object({
type: z.enum(["local", "veryfront-api", "memory"]).optional(),
type: z.enum(["local", "veryfront-api", "memory", "github"]).optional(),
local: z
.object({
baseDir: z.string().optional(),
Expand Down Expand Up @@ -195,6 +195,32 @@ export const veryfrontConfigSchema = z
})
.partial()
.optional(),
github: z
.object({
token: z.string(),
owner: z.string(),
repo: z.string(),
ref: z.string().optional(),
cache: z
.object({
enabled: z.boolean().optional(),
ttl: z.number().int().positive().optional(),
maxSize: z.number().int().positive().optional(),
maxMemory: z.number().int().positive().optional(),
})
.partial()
.optional(),
retry: z
.object({
maxRetries: z.number().int().min(0).optional(),
initialDelay: z.number().int().positive().optional(),
maxDelay: z.number().int().positive().optional(),
})
.partial()
.optional(),
})
.partial()
.optional(),
})
.partial()
.optional(),
Expand Down
23 changes: 22 additions & 1 deletion src/core/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export interface VeryfrontConfig {
};
};
fs?: {
type?: "local" | "veryfront-api" | "memory";
type?: "local" | "veryfront-api" | "memory" | "github";
local?: {
baseDir?: string;
};
Expand Down Expand Up @@ -152,6 +152,27 @@ export interface VeryfrontConfig {
memory?: {
files?: Record<string, string | Uint8Array>;
};
github?: {
/** GitHub Personal Access Token */
token: string;
/** Repository owner (user or organization) */
owner: string;
/** Repository name */
repo: string;
/** Branch, tag, or commit SHA (default: "main") */
ref?: string;
cache?: {
enabled?: boolean;
ttl?: number;
maxSize?: number;
maxMemory?: number;
};
retry?: {
maxRetries?: number;
initialDelay?: number;
maxDelay?: number;
};
};
};
ai?: {
enabled?: boolean;
Expand Down
19 changes: 18 additions & 1 deletion src/platform/adapters/fs-adapter-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,28 @@ export async function createFSAdapter(config: FSAdapterConfig): Promise<FSAdapte
return adapter;
}

if (type === "github") {
if (!config.github) {
throw toError(
createError({
type: "config",
message: "GitHub adapter requires github configuration. " +
"Provide github.owner, github.repo, and github.token (or GITHUB_TOKEN env var).",
}),
);
}

const { GitHubFSAdapter } = await import("./github-fs-adapter/index.ts");
const adapter = new GitHubFSAdapter(config);
await adapter.initialize?.();
return adapter;
}

throw toError(
createError({
type: "config",
message: `FSAdapter type "${type}" is not implemented. ` +
`Supported types: "local" (default, uses RuntimeAdapter.fs), "veryfront-api".`,
`Supported types: "local" (default, uses RuntimeAdapter.fs), "veryfront-api", "github".`,
}),
);
}
54 changes: 54 additions & 0 deletions src/platform/adapters/github-fs-adapter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# GitHub FS Adapter

Serves Veryfront projects directly from a GitHub repository. Read-only access via GitHub API.

## Configuration

```typescript
// veryfront.config.ts
export default {
fs: {
type: "github",
github: {
token: Deno.env.get("GITHUB_TOKEN"), // Required
owner: "myorg", // Required
repo: "myrepo", // Required
ref: "main", // Optional (default: "main")
cache: {
enabled: true, // Optional (default: true)
ttl: 60000, // Optional (default: 60s)
},
},
},
};
```

## Environment Variables

| Variable | Required | Description |
| -------------- | -------- | -------------------------------------------- |
| `GITHUB_TOKEN` | Yes | Personal Access Token with repo read access |
| `GITHUB_OWNER` | No | Repository owner (fallback if not in config) |
| `GITHUB_REPO` | No | Repository name (fallback if not in config) |
| `GITHUB_REF` | No | Branch/tag/SHA (fallback, default: "main") |

## How It Works

1. On initialization, fetches the full repository tree via Git Trees API
2. Builds an in-memory index of all files and directories
3. File reads use Contents API (or Blob API for files >1MB)
4. Results are cached with configurable TTL

## Rate Limits

GitHub API allows 5,000 requests/hour for authenticated requests. The adapter:

- Caches aggressively to minimize API calls
- Warns when approaching rate limit
- Includes rate limit info in errors

## Limitations

- **Read-only**: No write operations supported
- **Single repo**: One repository per adapter instance
- **No webhooks**: Cache invalidation is TTL-based only
Loading
Loading