ReactiveX/rxjs · error · Error

Project root is not a directory: ${projectRoot}

Error message

Project root is not a directory: ${projectRoot}

What it means

Thrown by resolveInstallation when the supplied --project-root exists but is not a directory. Every skill action resolves and validates the project root before touching anything.

Source

Thrown at packages/migrate/src/skill-install.ts:181

  options: SkillInstallOptions & { readonly operation: SkillInstallAction }
): Promise<SkillInstallResult> {
  const { operation, ...installOptions } = options;
  return manageSkillInstallation(operation, installOptions);
}

function adapterFor(harness: SkillHarness): {
  readonly adapter: SkillInstallAdapter;
  readonly localPath: string;
  readonly compatibleHarnesses: readonly SkillHarness[];
} {
  const adapter = skillHarnessAdapters[harness];
  return { adapter: adapter.adapterId, localPath: adapter.targetDirectory, compatibleHarnesses: adapter.compatibleHarnesses };
}

async function resolveInstallation(options: SkillInstallOptions): Promise<ResolvedInstallation> {
  const projectRoot = resolve(options.projectRoot);
  const canonicalProjectRoot = await realpath(projectRoot);
  if (!(await stat(canonicalProjectRoot)).isDirectory()) throw new Error(`Project root is not a directory: ${projectRoot}`);

  const canonicalSkillRoot = await realpath(resolve(options.canonicalSkillRoot));
  if (!(await stat(canonicalSkillRoot)).isDirectory()) throw new Error(`Canonical Skill root is not a directory: ${canonicalSkillRoot}`);

  const adapter = adapterFor(options.harness);
  const targetPath = resolve(projectRoot, adapter.localPath);
  assertContained(projectRoot, targetPath, 'Skill target must remain below the project root.');
  const canonicalTargetPath = await canonicalFuturePath(targetPath);
  assertContained(canonicalProjectRoot, canonicalTargetPath, 'Skill target resolves outside the project root.');
  const integrity = await inspectSkillIntegrity(canonicalSkillRoot);
  const provenance: SkillInstallProvenance = {
    ...integrity,
    installSchemaVersion: skillInstallSchemaVersion,
    adapter: adapter.adapter,
    compatibleHarnesses: adapter.compatibleHarnesses,
    targetPath: adapter.localPath.replaceAll(sep, '/'),
  };
  return {

View on GitHub (pinned to 54796b38a5)

Solutions

  1. Pass the project directory, not a file inside it
  2. Verify the path with ls -ld; fix typos
  3. Ensure the symlink target is a real directory

Example fix

# before
rxjs-migrate-skill check --harness claude --project-root ./package.json
# after
rxjs-migrate-skill check --harness claude --project-root .
Defensive patterns

Strategy: validation

Validate before calling

import { stat } from 'node:fs/promises';
if (!(await stat(projectRoot)).isDirectory()) throw new TypeError(`projectRoot not a directory: ${projectRoot}`);

Type guard

const isDir = async (p: string) => (await stat(p)).isDirectory();

Prevention

When it happens

Trigger: Passing --project-root pointing at a file (e.g. package.json or a symlink to a file); realpath resolves successfully but stat().isDirectory() is false.

Common situations: Passing the manifest file instead of its directory; a symlink project root that resolves to a file; wrong path in CI config.

Related errors


AI-assisted analysis of ReactiveX/rxjs@54796b38a5 (2026-08-28). Data as JSON: /api/errors/52736412a43fd158. Report an issue: GitHub.