Skip to content

Commit bd9e068

Browse files
authored
fix: unbreak non-JS integration install flow (Django, .NET, Kotlin + others) (#125)
* fix: support Django projects end-to-end in workos install Previously `workos install` crashed with "Could not find package.json" in any project without a package.json: JS integration detection calls getPackageDotJson() which exits the process on missing file, killing the installer before non-JS integrations (Python/Django) got a turn. - Skip JS integration detection when package.json is missing - Widen Python detection: match manage.py or requirements.txt with django, not just pyproject.toml (via a new optional detect() override on FrameworkMetadata, wired to the existing isDjangoProject helper) - Write credentials to .env (not .env.local) and skip cookie password generation for non-JS projects in unclaimed env provisioning - Gate state-machine configureEnvironment actor on JS-only to prevent a second .env.local leak post-detection - Add Django port/callback defaults (8000, /auth/callback/) so detectPort returns the right values for Python Tests: +8 net covering detection matrix (no-package.json skip, pyproject.toml, manage.py alone, requirements.txt+django, non-Django python), python port defaults, and .env vs .env.local selection in unclaimed provisioning. * fix: broaden non-JS integration detection, add port defaults for all languages Beyond the Django fixes in the previous commit, several other non-JS integrations had parallel problems that would bite the next user with a .NET, Kotlin, or non-trivial project layout. - .NET detection was completely broken: manifestFile '*.csproj' went through existsSync() which does literal-filename matching, so it never matched. Now uses globExists() from language-detection.ts (which has the right logic but wasn't wired up anywhere). - Kotlin detection only matched build.gradle.kts (Kotlin DSL). Now also matches build.gradle (Groovy DSL) and pom.xml (Maven) via the existing detectKotlin() helper, which already had Gradle Groovy support. Extended it to cover Maven too. - Port defaults added for ruby (3000), php (8000), php-laravel (8000), go (8080), dotnet (5000), elixir (4000), kotlin (8080). Previously all fell back to DEFAULT_PORT=3000, which is wrong for every one of them. - ensureGitignore() now also writes .env to .gitignore for non-JS projects (previously only handled .env.local for JS). Exported globExists() and detectKotlin() from language-detection.ts so the integrations can share them. detectLanguage() itself still isn't called anywhere in the install path — that's a larger cleanup for later. * feat: parse dev-server port from config for .NET, Phoenix, Spring Boot, Rails Follow-up to the hardcoded ecosystem defaults. These four languages have a canonical config file where the dev server port lives; parse it so we don't misconfigure redirect URIs when the user has customized the port. - .NET: Properties/launchSettings.json → profiles[*].applicationUrl - Phoenix: config/dev.exs or config/runtime.exs → port: NNNN - Spring Boot: src/main/resources/application.{properties,yml} → server.port - Rails: config/puma.rb → port ENV.fetch("PORT") { N } or literal port N Falls back to the ecosystem default when the file is missing or malformed. Python (Django), Go, plain PHP, and Laravel are unchanged — none has a canonical config file; ports are CLI args or hardcoded in source. * refactor: delete dead language-detection.ts, inline helpers into callers detectLanguage() and its LANGUAGE_DETECTORS table were never called from the install path. globExists() and detectKotlin() were the only pieces still used after wiring up the dotnet/kotlin detect() overrides — both are caller-specific enough to inline: - dotnet/index.ts gets hasCsproj() (6 lines, readdirSync + endsWith) - kotlin/index.ts gets isKotlinProject() (handles kts, gradle, pom.xml) - The Language type moves into framework-config.ts where it is consumed Net: one fewer module, no lost functionality.
1 parent 524c709 commit bd9e068

14 files changed

Lines changed: 526 additions & 143 deletions

src/cli.config.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,38 @@ export const config = {
5757
port: 5173,
5858
callbackPath: '/callback',
5959
},
60+
python: {
61+
port: 8000,
62+
callbackPath: '/auth/callback/',
63+
},
64+
ruby: {
65+
port: 3000,
66+
callbackPath: '/auth/callback',
67+
},
68+
php: {
69+
port: 8000,
70+
callbackPath: '/auth/callback',
71+
},
72+
phpLaravel: {
73+
port: 8000,
74+
callbackPath: '/auth/callback',
75+
},
76+
go: {
77+
port: 8080,
78+
callbackPath: '/auth/callback',
79+
},
80+
dotnet: {
81+
port: 5000,
82+
callbackPath: '/auth/callback',
83+
},
84+
elixir: {
85+
port: 4000,
86+
callbackPath: '/auth/callback',
87+
},
88+
kotlin: {
89+
port: 8080,
90+
callbackPath: '/auth/callback',
91+
},
6092
},
6193

6294
legacy: {

src/integrations/dotnet/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
/* .NET (ASP.NET Core) integration — auto-discovered by registry */
2+
import { readdirSync } from 'node:fs';
23
import type { FrameworkConfig } from '../../lib/framework-config.js';
34
import type { InstallerOptions } from '../../utils/types.js';
45
import { enableDebugLogs } from '../../utils/debug.js';
6+
7+
function hasCsproj(installDir: string): boolean {
8+
try {
9+
return readdirSync(installDir).some((f) => f.endsWith('.csproj'));
10+
} catch {
11+
return false;
12+
}
13+
}
514
import { SPINNER_MESSAGE } from '../../lib/framework-config.js';
615
import { getOrAskForWorkOSCredentials } from '../../utils/clack-utils.js';
716
import { analytics } from '../../utils/analytics.js';
@@ -22,6 +31,8 @@ export const config: FrameworkConfig = {
2231
priority: 35,
2332
packageManager: 'dotnet',
2433
manifestFile: '*.csproj',
34+
// existsSync cannot glob, so match any *.csproj in the install dir.
35+
detect: (options) => hasCsproj(options.installDir),
2536
},
2637

2738
detection: {

src/integrations/kotlin/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,31 @@
11
/* Kotlin (Spring Boot) integration — auto-discovered by registry */
2+
import { existsSync, readFileSync } from 'node:fs';
3+
import { join } from 'node:path';
24
import type { FrameworkConfig } from '../../lib/framework-config.js';
35
import type { InstallerOptions } from '../../utils/types.js';
46
import { enableDebugLogs } from '../../utils/debug.js';
57

8+
function hasKotlinContent(path: string, pattern: RegExp): boolean {
9+
try {
10+
return pattern.test(readFileSync(path, 'utf-8'));
11+
} catch {
12+
return false;
13+
}
14+
}
15+
16+
function isKotlinProject(installDir: string): boolean {
17+
const kts = join(installDir, 'build.gradle.kts');
18+
if (existsSync(kts) && hasKotlinContent(kts, /org\.jetbrains\.kotlin|kotlin\(/)) return true;
19+
20+
const gradle = join(installDir, 'build.gradle');
21+
if (existsSync(gradle) && hasKotlinContent(gradle, /kotlin/)) return true;
22+
23+
const pom = join(installDir, 'pom.xml');
24+
if (existsSync(pom) && hasKotlinContent(pom, /kotlin/i)) return true;
25+
26+
return false;
27+
}
28+
629
export const config: FrameworkConfig = {
730
metadata: {
831
name: 'Kotlin (Spring Boot)',
@@ -14,6 +37,8 @@ export const config: FrameworkConfig = {
1437
priority: 40,
1538
packageManager: 'gradle',
1639
manifestFile: 'build.gradle.kts',
40+
// Also match Groovy DSL (build.gradle) and Maven (pom.xml) Kotlin projects.
41+
detect: (options) => isKotlinProject(options.installDir),
1742
},
1843

1944
detection: {

src/integrations/python/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export const config: FrameworkConfig = {
9999
priority: 60,
100100
packageManager: 'pip',
101101
manifestFile: 'pyproject.toml',
102+
detect: (options: Pick<InstallerOptions, 'installDir'>) => isDjangoProject(options.installDir),
102103
gatherContext: async (options: InstallerOptions) => {
103104
const pkgMgr = detectPythonPackageManager(options.installDir);
104105
return {

src/lib/env-writer.ts

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,31 @@ import { join } from 'path';
33
import { parseEnvFile } from '../utils/env-parser.js';
44

55
const ENV_LOCAL_COVERING_PATTERNS = ['.env.local', '.env*.local', '.env*'];
6+
const ENV_COVERING_PATTERNS = ['.env', '.env*'];
67

78
/**
8-
* Ensure .env.local is in .gitignore.
9+
* Ensure the given env filename is in .gitignore.
910
* Creates .gitignore if it doesn't exist.
1011
* No-ops if a covering pattern is already present.
1112
*/
12-
function ensureGitignore(installDir: string): void {
13+
function ensureGitignore(installDir: string, filename: '.env' | '.env.local'): void {
1314
const gitignorePath = join(installDir, '.gitignore');
15+
const coveringPatterns = filename === '.env' ? ENV_COVERING_PATTERNS : ENV_LOCAL_COVERING_PATTERNS;
1416

1517
if (!existsSync(gitignorePath)) {
16-
writeFileSync(gitignorePath, '.env.local\n');
18+
writeFileSync(gitignorePath, `${filename}\n`);
1719
return;
1820
}
1921

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

23-
if (lines.some((line) => ENV_LOCAL_COVERING_PATTERNS.includes(line))) {
25+
if (lines.some((line) => coveringPatterns.includes(line))) {
2426
return;
2527
}
2628

2729
const separator = content.endsWith('\n') ? '' : '\n';
28-
writeFileSync(gitignorePath, `${content}${separator}.env.local\n`);
30+
writeFileSync(gitignorePath, `${content}${separator}${filename}\n`);
2931
}
3032

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

79-
ensureGitignore(installDir);
81+
ensureGitignore(installDir, '.env.local');
82+
83+
writeFileSync(envPath, content + '\n');
84+
}
85+
86+
/**
87+
* Write WorkOS credentials to the appropriate env file for the project.
88+
* Picks `.env.local` for JS projects (package.json present) or `.env` for
89+
* everything else (Python/Django, Ruby/Rails, Go, ...). Skips cookie password
90+
* generation outside the JS branch — non-JS SDKs don't use it.
91+
*
92+
* Used by pre-detection flows that write credentials before the framework
93+
* integration is known (unclaimed env provisioning).
94+
*/
95+
export function writeCredentialsEnv(installDir: string, envVars: Partial<EnvVars>): void {
96+
const hasPackageJson = existsSync(join(installDir, 'package.json'));
97+
if (hasPackageJson) {
98+
writeEnvLocal(installDir, envVars);
99+
return;
100+
}
101+
102+
const envPath = join(installDir, '.env');
103+
let existingEnv: Record<string, string> = {};
104+
if (existsSync(envPath)) {
105+
const content = readFileSync(envPath, 'utf-8');
106+
existingEnv = parseEnvFile(content);
107+
}
108+
109+
const merged = { ...existingEnv, ...envVars };
110+
const content = Object.entries(merged)
111+
.map(([key, value]) => `${key}=${value}`)
112+
.join('\n');
113+
114+
ensureGitignore(installDir, '.env');
80115

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

src/lib/framework-config.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import type { InstallerOptions } from '../utils/types.js';
2-
import type { Language } from './language-detection.js';
2+
3+
/**
4+
* Supported programming languages for framework integrations.
5+
*/
6+
export type Language = 'javascript' | 'python' | 'ruby' | 'php' | 'go' | 'kotlin' | 'dotnet' | 'elixir';
37

48
/**
59
* Configuration interface for framework-specific agent integrations.
@@ -60,6 +64,14 @@ export interface FrameworkMetadata {
6064

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

6577
/**

src/lib/language-detection.ts

Lines changed: 0 additions & 125 deletions
This file was deleted.

0 commit comments

Comments
 (0)