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
32 changes: 32 additions & 0 deletions src/cli.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,38 @@ export const config = {
port: 5173,
callbackPath: '/callback',
},
python: {
port: 8000,
callbackPath: '/auth/callback/',
},
ruby: {
port: 3000,
callbackPath: '/auth/callback',
},
php: {
port: 8000,
callbackPath: '/auth/callback',
},
phpLaravel: {
port: 8000,
callbackPath: '/auth/callback',
},
go: {
port: 8080,
callbackPath: '/auth/callback',
},
dotnet: {
port: 5000,
callbackPath: '/auth/callback',
},
elixir: {
port: 4000,
callbackPath: '/auth/callback',
},
kotlin: {
port: 8080,
callbackPath: '/auth/callback',
},
},

legacy: {
Expand Down
11 changes: 11 additions & 0 deletions src/integrations/dotnet/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
/* .NET (ASP.NET Core) integration — auto-discovered by registry */
import { readdirSync } from 'node:fs';
import type { FrameworkConfig } from '../../lib/framework-config.js';
import type { InstallerOptions } from '../../utils/types.js';
import { enableDebugLogs } from '../../utils/debug.js';

function hasCsproj(installDir: string): boolean {
try {
return readdirSync(installDir).some((f) => f.endsWith('.csproj'));
} catch {
return false;
}
}
import { SPINNER_MESSAGE } from '../../lib/framework-config.js';
import { getOrAskForWorkOSCredentials } from '../../utils/clack-utils.js';
import { analytics } from '../../utils/analytics.js';
Expand All @@ -22,6 +31,8 @@ export const config: FrameworkConfig = {
priority: 35,
packageManager: 'dotnet',
manifestFile: '*.csproj',
// existsSync cannot glob, so match any *.csproj in the install dir.
detect: (options) => hasCsproj(options.installDir),
},

detection: {
Expand Down
25 changes: 25 additions & 0 deletions src/integrations/kotlin/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,31 @@
/* Kotlin (Spring Boot) integration — auto-discovered by registry */
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import type { FrameworkConfig } from '../../lib/framework-config.js';
import type { InstallerOptions } from '../../utils/types.js';
import { enableDebugLogs } from '../../utils/debug.js';

function hasKotlinContent(path: string, pattern: RegExp): boolean {
try {
return pattern.test(readFileSync(path, 'utf-8'));
} catch {
return false;
}
}

function isKotlinProject(installDir: string): boolean {
const kts = join(installDir, 'build.gradle.kts');
if (existsSync(kts) && hasKotlinContent(kts, /org\.jetbrains\.kotlin|kotlin\(/)) return true;

const gradle = join(installDir, 'build.gradle');
if (existsSync(gradle) && hasKotlinContent(gradle, /kotlin/)) return true;

const pom = join(installDir, 'pom.xml');
if (existsSync(pom) && hasKotlinContent(pom, /kotlin/i)) return true;

return false;
}

export const config: FrameworkConfig = {
metadata: {
name: 'Kotlin (Spring Boot)',
Expand All @@ -14,6 +37,8 @@ export const config: FrameworkConfig = {
priority: 40,
packageManager: 'gradle',
manifestFile: 'build.gradle.kts',
// Also match Groovy DSL (build.gradle) and Maven (pom.xml) Kotlin projects.
detect: (options) => isKotlinProject(options.installDir),
},

detection: {
Expand Down
1 change: 1 addition & 0 deletions src/integrations/python/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export const config: FrameworkConfig = {
priority: 60,
packageManager: 'pip',
manifestFile: 'pyproject.toml',
detect: (options: Pick<InstallerOptions, 'installDir'>) => isDjangoProject(options.installDir),
gatherContext: async (options: InstallerOptions) => {
const pkgMgr = detectPythonPackageManager(options.installDir);
return {
Expand Down
47 changes: 41 additions & 6 deletions src/lib/env-writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,31 @@ import { join } from 'path';
import { parseEnvFile } from '../utils/env-parser.js';

const ENV_LOCAL_COVERING_PATTERNS = ['.env.local', '.env*.local', '.env*'];
const ENV_COVERING_PATTERNS = ['.env', '.env*'];

/**
* Ensure .env.local is in .gitignore.
* Ensure the given env filename is in .gitignore.
* Creates .gitignore if it doesn't exist.
* No-ops if a covering pattern is already present.
*/
function ensureGitignore(installDir: string): void {
function ensureGitignore(installDir: string, filename: '.env' | '.env.local'): void {
const gitignorePath = join(installDir, '.gitignore');
const coveringPatterns = filename === '.env' ? ENV_COVERING_PATTERNS : ENV_LOCAL_COVERING_PATTERNS;

if (!existsSync(gitignorePath)) {
writeFileSync(gitignorePath, '.env.local\n');
writeFileSync(gitignorePath, `${filename}\n`);
return;
}

const content = readFileSync(gitignorePath, 'utf-8');
const lines = content.split('\n').map((line) => line.trim());

if (lines.some((line) => ENV_LOCAL_COVERING_PATTERNS.includes(line))) {
if (lines.some((line) => coveringPatterns.includes(line))) {
return;
}

const separator = content.endsWith('\n') ? '' : '\n';
writeFileSync(gitignorePath, `${content}${separator}.env.local\n`);
writeFileSync(gitignorePath, `${content}${separator}${filename}\n`);
}

interface EnvVars {
Expand Down Expand Up @@ -76,7 +78,40 @@ export function writeEnvLocal(installDir: string, envVars: Partial<EnvVars>): vo
.map(([key, value]) => `${key}=${value}`)
.join('\n');

ensureGitignore(installDir);
ensureGitignore(installDir, '.env.local');

writeFileSync(envPath, content + '\n');
}

/**
* Write WorkOS credentials to the appropriate env file for the project.
* Picks `.env.local` for JS projects (package.json present) or `.env` for
* everything else (Python/Django, Ruby/Rails, Go, ...). Skips cookie password
* generation outside the JS branch — non-JS SDKs don't use it.
*
* Used by pre-detection flows that write credentials before the framework
* integration is known (unclaimed env provisioning).
*/
export function writeCredentialsEnv(installDir: string, envVars: Partial<EnvVars>): void {
const hasPackageJson = existsSync(join(installDir, 'package.json'));
if (hasPackageJson) {
writeEnvLocal(installDir, envVars);
return;
}

const envPath = join(installDir, '.env');
let existingEnv: Record<string, string> = {};
if (existsSync(envPath)) {
const content = readFileSync(envPath, 'utf-8');
existingEnv = parseEnvFile(content);
}

const merged = { ...existingEnv, ...envVars };
const content = Object.entries(merged)
.map(([key, value]) => `${key}=${value}`)
.join('\n');

ensureGitignore(installDir, '.env');

writeFileSync(envPath, content + '\n');
}
14 changes: 13 additions & 1 deletion src/lib/framework-config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { InstallerOptions } from '../utils/types.js';
import type { Language } from './language-detection.js';

/**
* Supported programming languages for framework integrations.
*/
export type Language = 'javascript' | 'python' | 'ruby' | 'php' | 'go' | 'kotlin' | 'dotnet' | 'elixir';

/**
* Configuration interface for framework-specific agent integrations.
Expand Down Expand Up @@ -60,6 +64,14 @@ export interface FrameworkMetadata {

/** Primary manifest file (e.g., 'pyproject.toml', 'Gemfile'). Optional for JS integrations. */
manifestFile?: string;

/**
* Optional custom detection override for non-JS integrations. When present,
* the registry calls this instead of falling back to `manifestFile` existence.
* Use when a single manifest file isn't enough (e.g., Django projects may
* use `manage.py` + `requirements.txt` without a `pyproject.toml`).
*/
detect?: (options: Pick<InstallerOptions, 'installDir'>) => boolean | Promise<boolean>;
}

/**
Expand Down
125 changes: 0 additions & 125 deletions src/lib/language-detection.ts

This file was deleted.

Loading
Loading