plans/feat-init-project-environment-detection.md
composio initEnhance composio init to automatically detect the project environment (TypeScript/Python, package manager, monorepo vs single-package) and install Composio's SDK dependencies using the correct package manager command. This replaces the existing EnvLangDetector + JsPackageManagerDetector services with a unified ProjectEnvironmentDetector service that produces richer detection results, and adds a CommandRunner service for running install commands in tests.
Currently, composio init writes config files but does not:
@composio/core for TS, composio for Python)@composio/mastra, @composio/openai, etc.)Users must manually figure out which packages to install and run the correct package manager command. This is error-prone, especially in monorepos where the package manager may differ from what the user expects.
The composio2 branch already has working implementations of ProjectEnvironmentDetector, CommandRunner, core-dependency.ts, and an install.cmd.ts with tests. This plan imports and adapts those implementations into the current feat/cli-login-2.0 branch.
graph TD
A[composio init] --> B[Project Selection]
B --> C[Init Wizard: usage mode + framework + skills]
C --> D[ProjectEnvironmentDetector.detectProjectEnvironment]
D --> E{Detected?}
E -->|Yes| F[Show detection + confirm]
E -->|Ambiguous| G[Prompt user to choose language]
E -->|No indicators| H[Prompt user to choose language + PM]
F --> I[resolveCoreDependencyState]
G --> I
H --> I
I --> J{Already installed?}
J -->|Yes, not --force| K[Skip install, log info]
J -->|No or --force| L[Confirm install command]
L --> M[CommandRunner.run with spinner]
M --> N[Write config files]
N --> O[Optionally install skills]
ProjectEnvironmentDetector (new service) — replaces EnvLangDetector + JsPackageManagerDetector with a single service that:
ProjectEnvironment discriminated union (js | python)language, packageManager, rootDir, and evidence[]deno.json, deno.lock) as a JS package managerCommandRunner (new service) — thin wrapper around @effect/platform Command.exitCode for testability. In production, it runs the actual command; in tests, it returns a configurable exit code.
core-dependency.ts (new effect) — maps a ProjectEnvironment to a CoreDependencyPlan containing the install command string, dependency name, and package manager. Also detects if the dependency is already installed.
Updated init.cmd.ts — integrates detection + installation into the wizard flow after project selection.
ProjectEnvironmentDetector serviceFile: ts/packages/cli/src/services/project-environment-detector.ts
Import from: /Users/jkomyno/work/composio/composio2/ts/packages/cli/src/services/project-environment-detector.ts
The reference implementation is 587 lines and provides:
export type ProjectEnvironment =
| { kind: 'js'; language: JsLanguage; packageManager: JsPackageManager; rootDir: string; evidence: string[] }
| { kind: 'python'; language: 'python'; packageManager: PythonPackageManager; rootDir: string; evidence: string[] };
export type JsLanguage = 'typescript' | 'javascript';
export type JsPackageManager = 'pnpm' | 'bun' | 'yarn' | 'npm' | 'deno';
export type PythonPackageManager = 'uv' | 'pip';
Key detection strategies:
package.json, tsconfig.json, pnpm-workspace.yaml, lock files — each gets a high scorepyproject.toml, setup.py, requirements.txt, uv.lock, poetry.lock.ts, .py), subdirectory scanspackage.json: reads packageManager field (e.g., "[email protected]")uv.lock or [tool.uv] in pyproject.tomlAmbiguity handling: If both JS and Python strong indicators exist at CWD depth 0, fails with ProjectEnvironmentDetectorError. At deeper levels, skips and continues searching.
Service pattern: Uses Effect.Service class with effect: body and dependencies: [BunFileSystem.layer].
CommandRunner serviceFile: ts/packages/cli/src/services/command-runner.ts
Import from: /Users/jkomyno/work/composio/composio2/ts/packages/cli/src/services/command-runner.ts
import { Command } from '@effect/platform';
import { Effect } from 'effect';
export class CommandRunner extends Effect.Service<CommandRunner>()('services/CommandRunner', {
sync: () => ({
run: (command: Command.Command) => Command.exitCode(command),
}),
dependencies: [],
}) {}
This is intentionally thin — the real value is in testability. In tests, we provide Layer.succeed(CommandRunner, new CommandRunner({ run: () => Effect.succeed(ExitCode(0)) })).
core-dependency.ts effectFile: ts/packages/cli/src/effects/core-dependency.ts
Import from: /Users/jkomyno/work/composio/composio2/ts/packages/cli/src/effects/core-dependency.ts
Key exports:
CoreDependencyPlan — discriminated union with installCommand, dependency, packageManager, rootDirdetectCoreDependencyPlan(cwd) — maps ProjectEnvironment → CoreDependencyPlandetectCoreDependencyVersion(plan) — checks if @composio/core or composio is already installedresolveCoreDependencyState(cwd) — combines plan + version checkgetJsInstallCommand(pm, dependency) — maps package manager to install commandgetPythonInstallCommand(pm, dependency) — same for PythonInstall command mapping:
| PM | Command |
|---|---|
| pnpm | pnpm add @composio/core |
| bun | bun add @composio/core |
| yarn | yarn add @composio/core |
| npm | npm install -S @composio/core |
| deno | deno add npm:@composio/core |
| uv | uv pip install composio |
| pip | pip install composio |
composio init Commandconst dryRunOpt = Options.boolean('dry-run').pipe(
Options.withDefault(false),
Options.withDescription('Print install command without executing it')
);
const forceOpt = Options.boolean('force').pipe(
Options.withDefault(false),
Options.withDescription('Reinstall even if dependency appears installed')
);
const yesOpt = Options.boolean('yes').pipe(
Options.withAlias('y'),
Options.withDefault(false),
Options.withDescription('Skip confirmation prompts')
);
InitConfigBuilder and InitConfigAdd new builder steps and fields:
interface InitConfig {
readonly usageMode: UsageMode;
readonly framework: NativeFramework | undefined;
readonly installSkills: boolean;
readonly detectedEnv: ProjectEnvironment; // NEW
readonly installPlan: CoreDependencyPlan; // NEW
}
Add withDetectedEnv and withInstallPlan methods to InitConfigBuilder, and update the build() type constraint.
After the existing wizard steps (usage mode → framework → skills), add:
// Step 4: Detect project environment
const envDetector = yield* ProjectEnvironmentDetector;
const detectedEnv = yield* envDetector
.detectProjectEnvironment(proc.cwd)
.pipe(
Effect.catchTag('services/ProjectEnvironmentDetectorError', e =>
Effect.gen(function* () {
yield* ui.log.warn(e.message);
// Fallback: ask user to choose
const lang = yield* ui.select('What language is your project?', [
{ value: 'typescript', label: 'TypeScript' },
{ value: 'python', label: 'Python' },
]);
// ... prompt for package manager too
})
)
);
yield* ui.log.step(`Detected: ${detectedEnv.language} (${detectedEnv.packageManager})`);
// Step 5: Check if already installed + determine install plan
const { plan, installedVersion } = yield* resolveCoreDependencyState(proc.cwd);
if (installedVersion && !forceOpt) {
yield* ui.log.info(`Found ${plan.dependency}: ${installedVersion.version}`);
yield* ui.log.success('Dependency already installed.');
} else {
// Step 6: Confirm and install
if (dryRunOpt) {
yield* ui.note(plan.installCommand, 'Install Command');
} else {
const shouldInstall = yesOpt || (yield* ui.confirm(`Run: ${plan.installCommand}?`));
if (shouldInstall) {
yield* ui.withSpinner(`Installing ${plan.dependency}...`, installEffect, {
successMessage: `Installed ${plan.dependency}`,
});
}
}
}
When a framework is selected (not "skip"), also install the provider package:
const frameworkPackage = getFrameworkPackage(config.framework, detectedEnv);
// e.g., '@composio/mastra' for TS, 'composio[mastra]' for Python
| Framework | TS Package | Python Package |
|---|---|---|
| ai-sdk | @composio/vercel | composio[vercel] |
| mastra | @composio/mastra | composio[mastra] |
| openai-agents | @composio/openai | composio[openai] |
| claude-agent-sdk | @composio/anthropic | composio[anthropic] |
bin.tsAdd to the layer composition:
import { ProjectEnvironmentDetector } from 'src/services/project-environment-detector';
import { CommandRunner } from 'src/services/command-runner';
const layers = Layer.mergeAll(
// ... existing layers ...
ProjectEnvironmentDetector.Default, // NEW
CommandRunner.Default, // NEW
);
EnvLangDetector and JsPackageManagerDetectorThe new ProjectEnvironmentDetector subsumes both. However, EnvLangDetector is still used by generate.cmd.ts. Options:
ProjectEnvironmentDetector only in init.cmd.ts. Migrate generate.cmd.ts in a follow-up PR.generate.cmd.ts to use the new service.Go with Option A to keep this PR focused.
ProjectEnvironmentDetectorFile: ts/packages/cli/test/src/services/project-environment-detector.test.ts
Uses tempy.temporaryDirectory() to create isolated temp dirs with specific file layouts. Tests use BunFileSystem.layer (real filesystem).
describe('ProjectEnvironmentDetector', () => {
describe('detectProjectEnvironment', () => {
// TypeScript detection
it('[Given] package.json + tsconfig.json [Then] detects typescript + npm')
it('[Given] pnpm-lock.yaml + package.json with packageManager:pnpm@ [Then] detects typescript + pnpm')
it('[Given] bun.lockb [Then] detects javascript + bun')
it('[Given] deno.json [Then] detects typescript + deno')
it('[Given] package.json with typescript dep [Then] detects typescript')
// Python detection
it('[Given] pyproject.toml with [tool.uv] [Then] detects python + uv')
it('[Given] requirements.txt only [Then] detects python + pip')
it('[Given] uv.lock [Then] detects python + uv')
it('[Given] setup.py only [Then] detects python + pip')
// Ambiguity
it('[Given] both package.json + pyproject.toml at CWD [Then] fails with ambiguity error')
it('[Given] both indicators but only at parent level [Then] skips and continues')
// Ancestor walking
it('[Given] indicators at parent directory [Then] detects from parent')
it('[Given] no indicators anywhere [Then] fails with no-detection error')
// File extension fallback
it('[Given] only .ts files, no config files [Then] detects typescript via weak signals')
it('[Given] only .py files, no config files [Then] detects python via weak signals')
// Edge cases
it('[Given] empty directory [Then] fails with no-detection error')
it('[Given] monorepo root with pnpm-workspace.yaml [Then] detects pnpm')
})
describe('detectJsPackageManager', () => {
it('[Given] packageManager field in package.json [Then] uses that')
it('[Given] lock file present [Then] detects from lock file')
it('[Given] no lock file, no packageManager field [Then] defaults to npm')
})
describe('detectPythonPackageManager', () => {
it('[Given] uv.lock present [Then] detects uv')
it('[Given] [tool.uv] in pyproject.toml [Then] detects uv')
it('[Given] no uv indicators [Then] defaults to pip')
})
})
core-dependency.tsFile: ts/packages/cli/test/src/effects/core-dependency.test.ts
describe('core-dependency', () => {
describe('getJsInstallCommand', () => {
it('[Given] pnpm [Then] returns "pnpm add <dep>"')
it('[Given] npm [Then] returns "npm install -S <dep>"')
it('[Given] yarn [Then] returns "yarn add <dep>"')
it('[Given] bun [Then] returns "bun add <dep>"')
it('[Given] deno [Then] returns "deno add npm:<dep>"')
})
describe('getPythonInstallCommand', () => {
it('[Given] uv [Then] returns "uv pip install <dep>"')
it('[Given] pip [Then] returns "pip install <dep>"')
})
describe('detectCoreDependencyPlan', () => {
it('[Given] TS project [Then] plans @composio/core install')
it('[Given] Python project [Then] plans composio install')
})
describe('detectCoreDependencyVersion', () => {
it('[Given] @composio/core in node_modules [Then] returns version')
it('[Given] @composio/core in pnpm virtual store [Then] returns version')
it('[Given] not installed [Then] returns null')
})
})
composio initFile: ts/packages/cli/test/src/commands/init.cmd.test.ts
Uses TestLive with fixtures and mocked services.
describe('CLI: composio init', () => {
// Agent-native path (--org-id + --project-id)
describe('[Given] --org-id + --project-id flags', () => {
layer(TestLive({ fixture: 'typescript-project' }))(it => {
it.scoped('[Then] detects TS project and shows install plan', () => ...)
})
layer(TestLive({ fixture: 'python-project' }))(it => {
it.scoped('[Then] detects Python project and shows install plan', () => ...)
})
})
// Dry-run mode
describe('[Given] --dry-run flag', () => {
layer(TestLive({ fixture: 'typescript-project' }))(it => {
it.scoped('[Then] prints install command without executing', () => ...)
})
})
// Already installed
describe('[Given] @composio/core already in node_modules', () => {
layer(TestLive({ fixture: 'typescript-project-with-composio-core' }))(it => {
it.scoped('[Then] skips install', () => ...)
})
})
// --force reinstall
describe('[Given] --force flag with dependency installed', () => {
layer(TestLive({ fixture: 'typescript-project-with-composio-core', commandRunner: successRunner }))(it => {
it.scoped('[Then] reinstalls', () => ...)
})
})
// Install failure
describe('[Given] install command fails', () => {
layer(TestLive({ fixture: 'typescript-project', commandRunner: failRunner }))(it => {
it.scoped('[Then] shows error message', () => ...)
})
})
})
test/__fixtures__/
typescript-project/ # EXISTING: package.json + tsconfig.json
python-project/ # EXISTING: requirements.txt + setup.py
typescript-project-with-composio-core/ # EXISTING
python-project-with-composio-core/ # EXISTING
typescript-pnpm-monorepo/ # NEW: pnpm-workspace.yaml + package.json with packageManager:pnpm@
typescript-bun-project/ # NEW: bun.lockb + package.json
python-uv-project/ # NEW: pyproject.toml with [tool.uv] + uv.lock
ambiguous-project/ # NEW: package.json + pyproject.toml (for error case)
Update TestLive interface to accept optional overrides:
interface TestLiveInput {
// ... existing fields ...
commandRunner?: CommandRunner; // NEW: mock for CommandRunner
terminalUI?: TerminalUI; // NEW: override TerminalUI for custom prompt behavior
}
Add ProjectEnvironmentDetector.Default and CommandRunner.Default (or mock) to the test layer composition.
composio init deterministic E2E testDirectory: ts/e2e-tests/cli/init/
Constraint: E2E tests run inside Docker containers. The init command requires API access for project listing, which makes it hard to test end-to-end without mocking.
Deterministic test strategy: Use --org-id + --project-id flags to bypass API calls. Pre-create a fixture project directory inside the Docker container.
// ts/e2e-tests/cli/init/e2e.test.ts
e2e(import.meta.url, {
versions: { cli: ['current'] },
defineTests: ({ runCmd }) => {
describe('composio init --dry-run', () => {
it('detects TypeScript project', async () => {
// Create a minimal TS project in the container
await runCmd('mkdir -p /tmp/test-ts && echo \'{"name":"test"}\' > /tmp/test-ts/package.json');
await runCmd('echo \'{}\' > /tmp/test-ts/tsconfig.json');
const result = await runCmd(
'cd /tmp/test-ts && composio init --org-id test-org --project-id test-proj --dry-run'
);
expect(result.exitCode).toBe(0);
// In non-interactive (piped) mode, output() writes JSON to stdout
expect(result.stdout).toContain('typescript');
});
});
describe('composio init on Python project', () => {
it('detects Python project', async () => {
await runCmd('mkdir -p /tmp/test-py && echo "composio" > /tmp/test-py/requirements.txt');
const result = await runCmd(
'cd /tmp/test-py && composio init --org-id test-org --project-id test-proj --dry-run'
);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('python');
});
});
},
});
Note: The init command's interactive prompts are automatically skipped in piped mode (TerminalUI returns defaults). The --dry-run flag prevents actual package installation.
EnvLangDetector + JsPackageManagerDetectorRejected because: The existing services have separate interfaces that would need to be called independently and manually composed. The ProjectEnvironmentDetector provides a single, unified detection result with richer evidence tracking and proper ambiguity handling.
composio install commandReference: The composio2 branch already has a standalone install.cmd.ts.
Decision: Both approaches have merit. The composio init command should include dependency installation as part of the initialization flow. A separate composio install command could be added later for re-installation or CI use. This plan focuses on integrating detection + installation into init.
Effect.Service class vs Context.Tag for new servicesDecision: Use Effect.Service class pattern (newer pattern) for consistency with EnvLangDetector, JsPackageManagerDetector, NodeOs, NodeProcess.
composio init detects TypeScript projects (from tsconfig.json, package.json, lock files)composio init detects Python projects (from pyproject.toml, setup.py, requirements.txt)composio init detects the correct package manager (pnpm, npm, yarn, bun, deno, uv, pip)composio init walks up the directory tree to find project root (monorepo support)composio init shows the detected environment and asks for confirmation before installingcomposio init runs the correct install command (pnpm add, npm install, uv pip install, etc.)composio init --dry-run prints the install command without executingcomposio init --force reinstalls even if dependency is already presentcomposio init --yes skips confirmation promptscomposio init shows appropriate error messages when detection fails (ambiguous or empty project)composio init gracefully handles install command failurescomposio init skips installation when dependency is already installed (without --force)composio init emits structured JSON to stdout when piped (agent-native)composio init writes detectedEnv info to .composio/config.jsonpnpm typecheck passes from repo rootProjectEnvironmentDetector covering all detection strategiescore-dependency.ts covering all package managerscomposio init covering happy path + error casescomposio init --dry-run in Docker containercomposio init in any TypeScript or Python project and have dependencies installed automaticallynpm install / pnpm add / uv pip install steps needed after init@effect/platform Command module is already a dependency of the CLI packagecomposio2 reference implementations are available at /Users/jkomyno/work/composio/composio2/ts/packages/cli/src/typescript-project and python-project already exist| Risk | Impact | Mitigation |
|---|---|---|
| Ambiguous project detection (both TS + Python) | User confusion | Prompt user to choose, provide clear error message with evidence |
| Install command fails (network, permissions) | Blocked init flow | Show error with the failing command, suggest manual install |
| Package manager not installed on system | Install fails | Check binary existence before running, suggest install |
| Monorepo: init from subdirectory | Wrong root dir detected | Ancestor walking already handles this; root dir = where lock file or package.json with workspaces lives |
Breaking existing generate.cmd.ts | Regression | Keep old services (Option A), migrate generate in follow-up PR |
ProjectEnvironmentDetector service (src/services/project-environment-detector.ts)CommandRunner service (src/services/command-runner.ts)core-dependency.ts effect (src/effects/core-dependency.ts)bin.ts layer compositionProjectEnvironmentDetector tests (17 test cases)core-dependency.ts tests (7 test cases)composio init (Green)--dry-run, --force, --yes flags to init commandInitConfigBuilder with withDetectedEnv and withInstallPlan.composio/config.json to include detection infoinit.cmd.test.ts command tests (8 scenarios)TestLive to support commandRunner and terminalUI overridesProjectEnvironmentDetector.Default to test layerts/e2e-tests/cli/init/ E2E test suite (deferred to follow-up PR)--dry-run mode with TS and Python fixtures in Docker (deferred)ts/e2e-tests/cli/README.md with new test info (deferred)pnpm typecheck and fix any issuesCLAUDE.md if needed (document new services)composio install as standalone command — extract install logic for re-use outside initnpx skills add ComposioHQ/skills when installSkills: truegenerate.cmd.ts — replace EnvLangDetector usage with ProjectEnvironmentDetectordeno add npm:@composio/core patterncomposio init: ts/packages/cli/src/commands/init.cmd.tsEnvLangDetector: ts/packages/cli/src/services/env-lang-detector.tsJsPackageManagerDetector: ts/packages/cli/src/services/js-package-manager-detector.tsts/packages/cli/src/bin.ts (lines 93-112)ts/packages/cli/test/__utils__/services/test-layer.tsts/e2e-tests/cli/README.mdts/packages/cli/test/src/commands/login.cmd.test.tsProjectEnvironmentDetector: /Users/jkomyno/work/composio/composio2/ts/packages/cli/src/services/project-environment-detector.tsCommandRunner: /Users/jkomyno/work/composio/composio2/ts/packages/cli/src/services/command-runner.tscore-dependency.ts: /Users/jkomyno/work/composio/composio2/ts/packages/cli/src/effects/core-dependency.tsinstall.cmd.ts: /Users/jkomyno/work/composio/composio2/ts/packages/cli/src/commands/install.cmd.tsinstall.cmd.test.ts: /Users/jkomyno/work/composio/composio2/ts/packages/cli/test/src/commands/install.cmd.test.tsTerminalUI with prompts: /Users/jkomyno/work/composio/composio2/ts/packages/cli/src/services/terminal-ui.tsts/vendor/effect/packages/effect/src/ts/vendor/effect/packages/cli/src/Command.tsts/vendor/effect/packages/platform/src/FileSystem.tsts/vendor/effect/packages/platform/src/Command.tsts/vendor/clack/packages/prompts/src/