Skip to content
Open
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
10 changes: 10 additions & 0 deletions src/lib/adapters/headless-adapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,14 @@ describe('HeadlessAdapter', () => {
it('spreads structured completion fields into the complete event when present', async () => {
const adapter = createAdapter();
await adapter.start();
const applicationSetup = {
clientId: 'client_app',
redirectUri: 'http://localhost:3000/callback',
signOutUri: 'http://localhost:3000/',
initiateLoginUri: 'http://localhost:3000/sign-in',
verified: false,
reason: 'No dashboard session.',
};

emitter.emit('complete', {
success: true,
Expand All @@ -423,6 +431,7 @@ describe('HeadlessAdapter', () => {
url: 'http://localhost:3000',
files: ['a.ts'],
nextSteps: ['x'],
applicationSetup,
docsUrl: 'https://d',
dashboardUrl: 'https://dash',
},
Expand All @@ -437,6 +446,7 @@ describe('HeadlessAdapter', () => {
url: 'http://localhost:3000',
files: ['a.ts'],
nextSteps: ['x'],
applicationSetup,
}),
);
await adapter.stop();
Expand Down
1 change: 1 addition & 0 deletions src/lib/adapters/headless-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ export class HeadlessAdapter implements InstallerAdapter {
url: completion.url,
files: completion.files,
nextSteps: completion.nextSteps,
...(completion.applicationSetup ? { applicationSetup: completion.applicationSetup } : {}),
}
: {}),
});
Expand Down
123 changes: 123 additions & 0 deletions src/lib/agent-runner.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { FrameworkConfig } from './framework-config.js';
import type { InstallerOptions } from '../utils/types.js';

vi.mock('./skills-assets.js', () => ({ getReference: vi.fn() }));
vi.mock('./agent-interface.js', () => ({ initializeAgent: vi.fn(), runAgent: vi.fn() }));
vi.mock('./validation/index.js', () => ({ validateInstallation: vi.fn(), quickCheckValidateAndFormat: vi.fn() }));
vi.mock('./validation/security-checks.js', () => ({
runInstallSecurityChecks: vi.fn(async () => ({ findings: [], blocking: [] })),
securityFindingsToIssues: vi.fn(() => []),
formatSecurityFindingsForAgent: vi.fn(() => ''),
}));
vi.mock('../steps/index.js', () => ({}));
vi.mock('./workos-management.js', () => ({}));
vi.mock('./env-writer.js', () => ({}));
vi.mock('../utils/ui-utils.js', () => ({
ensurePackageIsInstalled: vi.fn(),
getOrAskForWorkOSCredentials: vi.fn(async () => ({ apiKey: 'test-key', clientId: 'client_test' })),
getPackageDotJson: vi.fn(async () => ({ dependencies: { next: '16.3.5' } })),
isUsingTypeScript: vi.fn(() => true),
}));
vi.mock('../utils/analytics.js', () => ({
analytics: { setTag: vi.fn(), capture: vi.fn(), shutdown: vi.fn() },
}));

import { getReference } from './skills-assets.js';
import { initializeAgent, runAgent } from './agent-interface.js';
import { runAgentInstaller } from './agent-runner.js';
import { validateInstallation, quickCheckValidateAndFormat } from './validation/index.js';

const options: InstallerOptions = {
debug: false,
forceInstall: false,
installDir: '/tmp/test-authkit-app',
local: false,
ci: true,
skipAuth: true,
clientId: 'client_test',
noValidate: true,
};

const config: FrameworkConfig = {
metadata: {
name: 'Next.js',
integration: 'nextjs',
skillName: 'workos-authkit-nextjs',
language: 'javascript',
docsUrl: 'https://workos.com/docs/authkit/nextjs',
stability: 'stable',
priority: 100,
},
detection: { packageName: 'next', packageDisplayName: 'Next.js', getVersion: () => '16.3.5' },
environment: { requiresApiKey: true, uploadToHosting: false, getEnvVars: () => ({}) },
analytics: { getTags: () => ({}) },
prompts: { getAdditionalContextLines: () => ['Router: app'] },
ui: { successMessage: 'Installed', getOutroChanges: () => [], getOutroNextSteps: () => [] },
};

const setupContent = 'Configure and read back Sign-out URI and Initiate login URI. Report unverified flows.';

beforeEach(() => {
vi.clearAllMocks();
vi.mocked(getReference).mockImplementation(async (name) => {
if (name === 'workos-authkit-setup') return setupContent;
return `Instructions from ${name}`;
});
vi.mocked(runAgent).mockResolvedValue({});
vi.mocked(quickCheckValidateAndFormat).mockResolvedValue(null);
vi.mocked(validateInstallation).mockResolvedValue({ passed: true, framework: 'nextjs', issues: [], durationMs: 0 });
});

describe('installer prompt', () => {
it.each(['javascript', 'php'] as const)('injects shared application setup for %s integrations', async (language) => {
const framework = {
...config,
metadata: {
...config.metadata,
language,
integration: language === 'javascript' ? 'nextjs' : 'php',
skillName: language === 'javascript' ? 'workos-authkit-nextjs' : 'workos-php',
},
};
await runAgentInstaller(framework, options);

const prompt = vi.mocked(runAgent).mock.calls[0][1];
expect(getReference).toHaveBeenCalledWith('workos-authkit-setup');
expect(prompt).toContain(setupContent);
expect(prompt).toContain(`Instructions from ${framework.metadata.skillName}`);
expect(prompt).toContain('Router: app');
expect(prompt).not.toContain('test-key');
if (language === 'javascript') {
expect(prompt).toContain('NEXT_PUBLIC_WORKOS_REDIRECT_URI');
expect(prompt).toContain('Instructions from workos-authkit-base');
} else {
expect(prompt).toContain('WORKOS_REDIRECT_URI');
expect(prompt).not.toContain('NEXT_PUBLIC_WORKOS_REDIRECT_URI');
expect(getReference).not.toHaveBeenCalledWith('workos-authkit-base');
}
});

it('blocks success when an application route is still missing after retries', async () => {
vi.mocked(validateInstallation).mockResolvedValue({
passed: false,
framework: 'nextjs',
durationMs: 0,
issues: [{ type: 'file', severity: 'error', message: 'Missing sign-in route', hint: 'Create /sign-in' }],
});
await expect(runAgentInstaller(config, { ...options, noValidate: false })).rejects.toThrow('Missing sign-in route');
const retry = vi.mocked(runAgent).mock.calls[0][5];
expect(await retry!.validateAndFormat(options.installDir)).toContain('Create /sign-in');
});

it('does not start the agent when the bundled setup reference is missing', async () => {
vi.mocked(getReference).mockImplementation(async (name) => {
if (name === 'workos-authkit-setup') throw new Error('Missing bundled setup reference');
return `Instructions from ${name}`;
});

await expect(runAgentInstaller(config, options)).rejects.toThrow('Missing bundled setup reference');
expect(initializeAgent).not.toHaveBeenCalled();
expect(runAgent).not.toHaveBeenCalled();
});
});
54 changes: 46 additions & 8 deletions src/lib/agent-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { analytics } from '../utils/analytics.js';
import { INSTALLER_INTERACTION_EVENT_NAME } from './constants.js';
import { initializeAgent, runAgent, type RetryConfig } from './agent-interface.js';
import { uploadEnvironmentVariablesStep } from '../steps/index.js';
import { autoConfigureWorkOSEnvironment } from './workos-management.js';
import { autoConfigureWorkOSEnvironment, configureCallbackUri } from './workos-management.js';
import { detectPort, getCallbackPath } from './port-detection.js';
import { writeEnvLocal } from './env-writer.js';

Expand Down Expand Up @@ -64,12 +64,18 @@ export async function runAgentInstaller(config: FrameworkConfig, options: Instal

// Auto-configure WorkOS environment (redirect URI, CORS, homepage)
// Skip if caller already handled this (prevents duplicate dashboard config output)
// Next.js URL setup runs natively after code validation, with client-ID
// targeting and read-back. Do not pre-write unrelated homepage/CORS settings.
if (!callerHandledConfig && apiKey && config.environment.requiresApiKey) {
const port = detectPort(config.metadata.integration, options.installDir);
await autoConfigureWorkOSEnvironment(apiKey, config.metadata.integration, port, {
homepageUrl: options.homepageUrl,
redirectUri: options.redirectUri,
});
if (config.metadata.integration === 'nextjs') {
await configureCallbackUri(apiKey, options.redirectUri || `http://localhost:${port}${getCallbackPath('nextjs')}`);
} else {
await autoConfigureWorkOSEnvironment(apiKey, config.metadata.integration, port, {
homepageUrl: options.homepageUrl,
redirectUri: options.redirectUri,
});
}
}

// Gather framework-specific context (e.g., Next.js router, React Native platform)
Expand Down Expand Up @@ -136,8 +142,11 @@ export async function runAgentInstaller(config: FrameworkConfig, options: Instal
validateAndFormat: async (workingDirectory: string) => {
const quickPrompt = await quickCheckValidateAndFormat(workingDirectory);
const security = await runInstallSecurityChecks(integration, workingDirectory);
if (quickPrompt === null && security.blocking.length === 0) return null;
return [quickPrompt, formatSecurityFindingsForAgent(security.findings)]
const installation = await validateInstallation(integration, workingDirectory, { runBuild: false });
const errors = installation.issues.filter((issue) => issue.severity === 'error');
if (quickPrompt === null && security.blocking.length === 0 && installation.passed) return null;
const completenessPrompt = errors.map((issue) => `${issue.message}. ${issue.hint ?? ''}`).join('\n');
return [quickPrompt, completenessPrompt, formatSecurityFindingsForAgent(security.findings)]
.filter((p): p is string => Boolean(p))
.join('\n\n');
},
Expand Down Expand Up @@ -205,6 +214,15 @@ export async function runAgentInstaller(config: FrameworkConfig, options: Instal
await analytics.shutdown('error');
throw new Error(formatBlockingSecurityError(security.blocking));
}
if (!validationResult.passed) {
await analytics.shutdown('error');
throw new Error(
`Installation validation failed:\n${validationResult.issues
.filter((issue) => issue.severity === 'error')
.map((issue) => `${issue.message}. ${issue.hint ?? ''}`)
.join('\n')}`,
);
}
}

// Track retry metrics AFTER the security gate. `passed_after_retry` must
Expand Down Expand Up @@ -281,9 +299,12 @@ async function buildIntegrationPrompt(
// Base template has JS-centric assumptions (node_modules, lockfiles, AuthKitProvider)
// so only load it for JavaScript integrations; backend SDKs bypass this entirely
const isJavaScript = config.metadata.language === 'javascript';
const [baseContent, refContent] = await Promise.all([
// Inline shared setup too: relative links in the framework reference do not
// resolve from the app directory, and agents can skip them entirely.
const [baseContent, refContent, setupContent] = await Promise.all([
isJavaScript ? getReference('workos-authkit-base') : Promise.resolve(''),
getReference(skillName),
getReference('workos-authkit-setup'),
]);

// Build env var list dynamically based on what was actually configured
Expand Down Expand Up @@ -311,6 +332,23 @@ ${baseContent ? `## General Guidelines\n\n${baseContent}\n\n` : ''}## Integratio

${refContent}

## Required Application Setup and Verification

${setupContent}

## Installer execution boundary

The setup reference above is already included in this prompt. Do not read a relative workos-authkit-setup.md from the app directory.
The agent's shell permissions do not allow WorkOS management commands. Do not run workos, install another CLI, use curl or SDK scripts to bypass that boundary, or attempt dashboard authentication. Implement and validate the app code only. The installer handles supported dashboard configuration outside the agent after code validation; unavailable configuration must remain explicitly unverified.
${
config.metadata.integration === 'nextjs'
? `
Create a dedicated /sign-in GET route in the App Router using getSignInUrl() from @workos-inc/authkit-nextjs and redirect(await getSignInUrl()) from next/navigation. Keep the OAuth callback using handleAuth() separate. The Initiate login URI is the app origin plus /sign-in, NEVER the callback URI. Read existing files before editing; do not replace an unrelated existing sign-in flow. Keep /sign-in public and follow the SDK README for PKCE cookie handling.
`
Comment on lines +343 to +347

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Pages Router installs fail

Pages Router is a supported and detected Next.js mode, but this instruction always tells the agent to create an App Router /sign-in route. The new validation rule also accepts only {,src/}app/sign-in/route.*, so a correct Pages Router implementation is rejected and the installation fails, or the agent must add an unwanted App Router tree. Please make the prompt and validation depend on the detected router and support the equivalent Pages Router route.

Knowledge Base Used:

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/agent-runner.ts
Line: 343-347

Comment:
**Pages Router installs fail**

Pages Router is a supported and detected Next.js mode, but this instruction always tells the agent to create an App Router `/sign-in` route. The new validation rule also accepts only `{,src/}app/sign-in/route.*`, so a correct Pages Router implementation is rejected and the installation fails, or the agent must add an unwanted App Router tree. Please make the prompt and validation depend on the detected router and support the equivalent Pages Router route.

**Knowledge Base Used:**
- [Application installation workflows](https://app.greptile.com/workos/-/custom-context/knowledge-base/workos/cli/-/docs/application-installation.md)
- [Installer orchestration and project mutation](https://app.greptile.com/workos/-/custom-context/knowledge-base/workos/cli/-/docs/installer-orchestration.md)
- [Agent and skills workflows](https://app.greptile.com/workos/-/custom-context/knowledge-base/workos/cli/-/docs/agent-and-skills-workflows.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

: ''
}
Do not claim the full integration or browser flows are verified. Report code implementation separately from application configuration and browser testing.

Report your progress using [STATUS] prefixes.

Begin integration now.`;
Expand Down
Loading
Loading