import { BaseService, type BaseServiceContext } from '@learnitive/base-utils';
import { SERVICE_NAME } from '@learnitive/constants';
import { readBrowserGitSettings } from '@learnitive/project-git';
import {
  projectDirectoryPath,
  projectFilePath,
  type ProjectEntry,
  type ProjectFilePath,
} from '@learnitive/project-storage';
import {
  assessTerminalLine,
  classifyTerminalLine,
  normalizeTerminalPath,
  parseTerminalLine,
  type TerminalCapabilityClass,
  type TerminalExecutionOrigin,
  type TerminalInvocation,
  type TerminalOutput,
  type TerminalPolicyDecision,
} from '@learnitive/terminal';
import { classifyScientificArtifact, type RuntimeExecutionRequest } from '@learnitive/scientific-runtime';
import { WsPath } from '@learnitive/ws-path';
import type { FileSystemService } from './file-system-service';
import type { LatexSourceService } from './latex-source-service';
import type { ProjectCollaborationService } from './project-collaboration-service';
import type { ProjectCompileService } from './project-compile-service';
import type { ProjectDatasetService } from './project-dataset-service';
import type { ProjectGitService } from './project-git-service';
import type { ProjectHttpMethod, ProjectHttpService } from './project-http-service';
import type { ProjectStorageService } from './project-storage-service';
import type { WorkspaceOpsService } from './workspace-ops-service';
import type { ProjectScientificRuntimeService } from './project-scientific-runtime-service';
import {
  getGeneratedResearchArtifacts,
  writeCanonicalProjectFile,
  writeGeneratedResearchArtifact,
  type GeneratedResearchArtifact,
} from './research-artifact-files';


export type TerminalExecutionOptions = {
  scientificAllowCreatePaths?: readonly string[];
  scientificInputPaths?: readonly string[];
  /** Set only after ProjectTerminalAiService has approved the exact structured step. */
  aiApproved?: boolean;
};

export type TerminalExecutionResult = {
  cwd: string;
  exitCode: number;
  outputs: readonly TerminalOutput[];
};

export type TerminalExecutionRecord = {
  id: string;
  origin: TerminalExecutionOrigin;
  command: string;
  cwdBefore: string;
  cwdAfter: string;
  startedAt: number;
  durationMs: number;
  exitCode: number;
  stdoutTail: string;
  stderrTail: string;
};

function asProjectPath(path: string): string {
  if (path === '/workspace') return '';
  return path.slice('/workspace/'.length);
}

function basename(path: string): string {
  const value = path.replace(/\/$/u, '');
  return value.slice(value.lastIndexOf('/') + 1);
}

function matchesPath(entry: ProjectEntry, projectPath: string): boolean {
  const path = String(entry.path).replace(/\/$/u, '');
  return path === projectPath.replace(/\/$/u, '');
}

const MAX_EXECUTION_RECORDS = 20;
const MAX_EXECUTION_TAIL = 16_000;
const encoder = new TextEncoder();

function tail(text: string, max = MAX_EXECUTION_TAIL): string {
  return text.length <= max ? text : `…[truncated]\n${text.slice(-max)}`;
}

const MAX_HTTP_STDOUT_BYTES = 2 * 1024 * 1024;
const MAX_GIT_TEXT_OUTPUT = 2 * 1024 * 1024;
const GIT_IDENTITY_METADATA_KEY = 'learnitiveTerminalGitIdentity';

type TerminalGitIdentity = { name: string; email: string };

function formatGitStatus(status: Awaited<ReturnType<ProjectGitService['status']>>): string {
  const lines = [`On branch ${status.branch ?? '(detached/unknown)'}`];
  if (!status.staged.length && !status.unstaged.length) return `${lines[0]}\nworking tree clean\n`;
  if (status.staged.length) {
    lines.push('', 'Changes staged for commit:');
    for (const item of status.staged) lines.push(`  ${item.status.padEnd(10)} ${item.path}`);
  }
  if (status.unstaged.length) {
    lines.push('', 'Changes not staged for commit:');
    for (const item of status.unstaged) lines.push(`  ${item.status.padEnd(10)} ${item.path}`);
  }
  return `${lines.join('\n')}\n`;
}

function formatGitDiff(diff: Awaited<ReturnType<ProjectGitService['diff']>>): string {
  if (diff.binary) return `diff -- ${diff.path}\nBinary file changed (${diff.oldSize} -> ${diff.newSize} bytes)\n`;
  const body = (diff.lines ?? []).map((line) => `${line.kind === 'add' ? '+' : line.kind === 'remove' ? '-' : ' '} ${line.text}`).join('\n');
  return `diff -- ${diff.path}\n${body}${body ? '\n' : ''}`;
}

function safeGitRemoteUrl(value: string): string {
  let url: URL;
  try { url = new URL(value); } catch { throw new Error(`git remote: invalid URL: ${value}`); }
  if (!['http:', 'https:'].includes(url.protocol)) throw new Error('Browser Git remotes must use HTTP(S).');
  if (url.username || url.password) throw new Error('Do not embed Git credentials in a remote URL. Use Learnitive Git credentials UI when authentication is required.');
  for (const key of url.searchParams.keys()) {
    if (/^(?:token|access_token|api_key|apikey|key|secret)$/iu.test(key)) {
      throw new Error('Do not embed Git credentials in remote URL query parameters. Use the Learnitive Git credentials UI when authentication is required.');
    }
  }
  return url.href;
}

function redactUrlSecrets(value: string): string {
  try {
    const url = new URL(value);
    if (url.username || url.password) { url.username = '[REDACTED]'; url.password = ''; }
    for (const key of [...url.searchParams.keys()]) {
      if (/^(?:token|access_token|api_key|apikey|key|secret)$/iu.test(key)) url.searchParams.set(key, '[REDACTED]');
    }
    return url.href;
  } catch {
    return value.replace(/([?&](?:token|access_token|api_key|apikey|key|secret)=)[^&#\s]+/giu, '$1[REDACTED]');
  }
}

function boundGitOutput(value: string): string {
  return value.length <= MAX_GIT_TEXT_OUTPUT
    ? value
    : `${value.slice(0, MAX_GIT_TEXT_OUTPUT)}\n…[Git output truncated; inspect a narrower path/range]\n`;
}

function isProbablyTextResponse(contentType: string | undefined, bytes: Uint8Array): boolean {
  if (contentType && /(?:^text\/|json|xml|javascript|yaml|csv|tab-separated-values|svg)/iu.test(contentType)) return true;
  const sample = bytes.subarray(0, Math.min(bytes.length, 8192));
  return !sample.includes(0);
}

function parsePositiveNumber(value: string | undefined, label: string): number {
  const parsed = Number(value);
  if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`${label}: expected a positive number`);
  return parsed;
}

function redactRecordedCommand(command: string): string {
  return command
    .replace(/((?:authorization|proxy-authorization|x-api-key|x-auth-token|x-access-token|api-key)\s*:\s*)([^"'\s][^\r\n]*|["'][^"']*["'])/giu, '$1[REDACTED]')
    .replace(/((?:-d|--data|--data-raw|--data-binary)\s+)(["'][^"']*["']|\S+)/giu, '$1[REDACTED_BODY]')
    .replace(/([?&](?:token|access_token|api_key|apikey|key|secret)=)[^&#\s]+/giu, '$1[REDACTED]');
}
export class ProjectTerminalService extends BaseService {
  static deps = ['fileSystem', 'latexSource', 'projectStorage', 'projectCompile', 'projectDataset', 'projectGit', 'projectHttp', 'projectCollaboration', 'workspaceOps', 'projectScientificRuntime'] as const;
  private readonly cwdByWorkspace = new Map<string, string>();
  private readonly executionsByWorkspace = new Map<string, TerminalExecutionRecord[]>();
  private executionSequence = 0;

  constructor(
    context: BaseServiceContext,
    private readonly dependencies: {
      fileSystem: FileSystemService;
      latexSource: LatexSourceService;
      projectStorage: ProjectStorageService;
      projectCompile: ProjectCompileService;
      projectDataset: ProjectDatasetService;
      projectGit: ProjectGitService;
      projectHttp: ProjectHttpService;
      projectCollaboration: ProjectCollaborationService;
      workspaceOps: WorkspaceOpsService;
      projectScientificRuntime: ProjectScientificRuntimeService;
    },
  ) {
    super(SERVICE_NAME.projectTerminalService, context, dependencies);
  }

  hookMount(): void {
    this.addCleanup(() => {
      this.cwdByWorkspace.clear();
      this.executionsByWorkspace.clear();
    });
  }

  getCwd(wsName: string): string {
    return this.cwdByWorkspace.get(wsName) ?? '/workspace';
  }

  assess(line: string): TerminalCapabilityClass {
    return classifyTerminalLine(line);
  }

  policy(line: string, origin: TerminalExecutionOrigin = 'user'): TerminalPolicyDecision {
    return assessTerminalLine(line, origin);
  }

  getRecentExecutions(wsName: string): readonly TerminalExecutionRecord[] {
    return this.executionsByWorkspace.get(wsName) ?? [];
  }

  getLastExecution(wsName: string): TerminalExecutionRecord | undefined {
    return this.executionsByWorkspace.get(wsName)?.at(-1);
  }

  private resolve(wsName: string, path?: string): string {
    return normalizeTerminalPath(this.getCwd(wsName), path ?? '.');
  }

  private wsPath(wsName: string, terminalPath: string): string {
    const projectPath = asProjectPath(terminalPath);
    if (!projectPath) throw new Error('Expected a file path below /workspace');
    return WsPath.fromParts(wsName, projectPath).wsPath;
  }

  private async readText(wsName: string, terminalPath: string): Promise<string> {
    const file = await this.dependencies.fileSystem.readFile(this.wsPath(wsName, terminalPath));
    if (!file) throw new Error(`File not found: ${terminalPath}`);
    return file.text();
  }

  async getGeneratedArtifacts(wsName: string): Promise<readonly GeneratedResearchArtifact[]> {
    return getGeneratedResearchArtifacts(this.dependencies.workspaceOps, wsName);
  }

  async writeGeneratedFile(
    wsName: string,
    terminalPath: string,
    data: Uint8Array | string,
    artifact: Omit<GeneratedResearchArtifact, 'path' | 'createdAt'>,
  ): Promise<GeneratedResearchArtifact> {
    this.dependencies.projectCollaboration.assertProjectStructureMutable(wsName, 'write generated project artifacts');
    const resolved = this.resolve(wsName, terminalPath);
    return writeGeneratedResearchArtifact(
      this.dependencies,
      wsName,
      asProjectPath(resolved),
      data,
      artifact,
    );
  }

  async writeWorkspaceFile(
    wsName: string,
    terminalPath: string,
    data: Uint8Array | string,
  ): Promise<void> {
    const resolved = this.resolve(wsName, terminalPath);
    const projectPath = asProjectPath(resolved);
    if (!projectPath) throw new Error('Expected a file path below /workspace');
    if (this.dependencies.projectCollaboration.isActive(wsName)) {
      if (typeof data !== 'string' || !this.dependencies.projectCollaboration.isSourcePathCollaborative(wsName, projectPath)) {
        throw new Error('Project structure is locked while live collaboration is active.');
      }
      this.dependencies.projectCollaboration.replaceCollaborativeText(wsName, projectPath, data, { reviewSource: 'terminal', semanticKind: 'terminal-edit' });
      await this.dependencies.projectCollaboration.flushToStorage(wsName);
      return;
    }
    await writeCanonicalProjectFile(this.dependencies, wsName, projectPath, data);
  }

  private async listEntries(wsName: string): Promise<ProjectEntry[]> {
    return (await this.dependencies.projectStorage.getProvider(wsName)).listEntries();
  }

  private async getTerminalGitIdentity(wsName: string): Promise<TerminalGitIdentity | undefined> {
    const metadata = await this.dependencies.workspaceOps.getWorkspaceMetadata(wsName);
    const value = metadata[GIT_IDENTITY_METADATA_KEY];
    const record = value && typeof value === 'object' ? value as Partial<TerminalGitIdentity> : {};
    const browserSettings = readBrowserGitSettings();
    const name = (typeof record.name === 'string' ? record.name.trim() : '') || browserSettings.authorName;
    const email = (typeof record.email === 'string' ? record.email.trim() : '') || browserSettings.authorEmail;
    if (!name || !email) return undefined;
    return { name, email };
  }

  private async setTerminalGitIdentity(wsName: string, patch: Partial<TerminalGitIdentity>): Promise<TerminalGitIdentity> {
    const metadata = await this.dependencies.workspaceOps.getWorkspaceMetadata(wsName);
    const raw = metadata[GIT_IDENTITY_METADATA_KEY];
    const record = raw && typeof raw === 'object' ? raw as Partial<TerminalGitIdentity> : {};
    const current = {
      name: typeof record.name === 'string' ? record.name : '',
      email: typeof record.email === 'string' ? record.email : '',
    };
    const next = { ...current, ...patch };
    await this.dependencies.workspaceOps.updateWorkspaceMetadata(wsName, (metadata) => ({
      ...metadata,
      [GIT_IDENTITY_METADATA_KEY]: next,
    }));
    return next;
  }

  private resolveGitPath(wsName: string, value: string): ProjectFilePath {
    const project = asProjectPath(this.resolve(wsName, value));
    if (!project) throw new Error('git: expected a project file path');
    return projectFilePath(project);
  }

  private async executeGit(
    wsName: string,
    args: readonly string[],
  ): Promise<{ exitCode: number; stdout: string; stderr: string }> {
    const fail = (message: string) => ({ exitCode: 1, stdout: '', stderr: `${message}\n` });
    const subcommand = (args[0] ?? 'status').toLowerCase();
    const rest = args.slice(1);

    if (subcommand === 'init') {
      const branchIndex = rest.findIndex((item) => item === '-b' || item === '--initial-branch');
      const branch = branchIndex >= 0 ? rest[branchIndex + 1] : rest.find((item) => !item.startsWith('-'));
      await this.dependencies.projectGit.init(wsName, branch || 'main');
      return { exitCode: 0, stdout: `Initialized empty Git repository on branch ${branch || 'main'}.\n`, stderr: '' };
    }
    if (subcommand === 'clone') {
      const rawUrl = rest.find((item) => !item.startsWith('-'));
      if (!rawUrl) return fail('git clone: expected an HTTP(S) repository URL');
      const url = safeGitRemoteUrl(rawUrl);
      const { corsProxy } = readBrowserGitSettings();
      await this.dependencies.projectGit.clone(wsName, url, {
        singleBranch: rest.includes('--single-branch'),
        ...(corsProxy ? { corsProxy } : {}),
      });
      return { exitCode: 0, stdout: `Cloned ${redactUrlSecrets(url)}\n`, stderr: '' };
    }

    if (!(await this.dependencies.projectGit.isRepository(wsName))) {
      return fail('git: this Learnitive workspace is not a repository; run `git init` or use the Git panel to clone one.');
    }

    if (subcommand === 'status') {
      const status = await this.dependencies.projectGit.status(wsName);
      if (rest.includes('--short') || rest.includes('-s')) {
        const lines = [
          ...status.staged.map((item) => `S ${item.status.padEnd(10)} ${item.path}`),
          ...status.unstaged.map((item) => `W ${item.status.padEnd(10)} ${item.path}`),
        ];
        return { exitCode: 0, stdout: `${lines.join('\n')}${lines.length ? '\n' : ''}`, stderr: '' };
      }
      return { exitCode: 0, stdout: formatGitStatus(status), stderr: '' };
    }

    if (subcommand === 'diff') {
      if (rest.includes('--staged') || rest.includes('--cached')) return fail('git diff: staged-only diff is not exposed by the browser Git adapter; this command shows HEAD versus the canonical workspace.');
      const requested = rest.filter((item) => !item.startsWith('-'));
      const paths: ProjectFilePath[] = [];
      if (requested.length) {
        for (const value of requested) paths.push(this.resolveGitPath(wsName, value));
      } else {
        const status = await this.dependencies.projectGit.status(wsName);
        const seen = new Set<string>();
        for (const change of [...status.staged, ...status.unstaged]) {
          if (!seen.has(String(change.path))) {
            seen.add(String(change.path));
            paths.push(change.path);
          }
        }
      }
      if (!paths.length) return { exitCode: 0, stdout: '', stderr: '' };
      let stdout = '';
      for (const path of paths.slice(0, 100)) stdout += formatGitDiff(await this.dependencies.projectGit.diff(wsName, path));
      return { exitCode: 0, stdout: boundGitOutput(stdout), stderr: '' };
    }

    if (subcommand === 'log') {
      let depth = 20;
      const nIndex = rest.findIndex((item) => item === '-n' || item === '--max-count');
      if (nIndex >= 0) depth = Math.min(200, Math.max(1, Number(rest[nIndex + 1]) || 20));
      const compact = rest.includes('--oneline');
      const commits = await this.dependencies.projectGit.log(wsName, depth);
      const stdout = compact
        ? commits.map((commit) => `${commit.oid.slice(0, 8)} ${commit.message.split(/\r?\n/u)[0] ?? ''}`).join('\n')
        : commits.map((commit) => [
            `commit ${commit.oid}`,
            `Author: ${commit.authorName} <${commit.authorEmail}>`,
            `Date:   ${new Date(commit.timestamp * 1000).toISOString()}`,
            '',
            `    ${commit.message.replace(/\r?\n/gu, '\n    ')}`,
          ].join('\n')).join('\n\n');
      return { exitCode: 0, stdout: boundGitOutput(`${stdout}${stdout ? '\n' : ''}`), stderr: '' };
    }

    if (subcommand === 'add' || subcommand === 'stage') {
      const requested = rest.filter((item) => !item.startsWith('-'));
      let paths: ProjectFilePath[];
      if (!requested.length || rest.includes('-A') || rest.includes('--all')) {
        const status = await this.dependencies.projectGit.status(wsName);
        const unique = new Map<string, ProjectFilePath>();
        for (const change of [...status.staged, ...status.unstaged]) unique.set(String(change.path), change.path);
        paths = [...unique.values()];
      } else if (requested.includes('.')) {
        const status = await this.dependencies.projectGit.status(wsName);
        const cwd = asProjectPath(this.getCwd(wsName)).replace(/\/$/u, '');
        const prefix = cwd ? `${cwd}/` : '';
        const unique = new Map<string, ProjectFilePath>();
        for (const change of [...status.staged, ...status.unstaged]) {
          if (!cwd || String(change.path) === cwd || String(change.path).startsWith(prefix)) unique.set(String(change.path), change.path);
        }
        paths = [...unique.values()];
      } else paths = requested.map((value) => this.resolveGitPath(wsName, value));
      if (paths.length) await this.dependencies.projectGit.stage(wsName, paths);
      return { exitCode: 0, stdout: paths.length ? `Staged ${paths.length} path${paths.length === 1 ? '' : 's'}.\n` : 'Nothing to stage.\n', stderr: '' };
    }

    if (subcommand === 'restore') {
      if (!rest.includes('--staged')) return fail('git restore: Learnitive terminal currently supports only `git restore --staged <path...>`; working-tree restore is intentionally not exposed here.');
      const requested = rest.filter((item) => item !== '--staged' && !item.startsWith('-'));
      if (!requested.length) return fail('git restore --staged: expected at least one path');
      const paths = requested.map((value) => this.resolveGitPath(wsName, value));
      await this.dependencies.projectGit.unstage(wsName, paths);
      return { exitCode: 0, stdout: `Unstaged ${paths.length} path${paths.length === 1 ? '' : 's'}.\n`, stderr: '' };
    }

    if (subcommand === 'config') {
      const key = rest[0]?.toLowerCase();
      if (key !== 'user.name' && key !== 'user.email') return fail('git config: Learnitive terminal supports only user.name and user.email.');
      const value = rest.slice(1).join(' ').trim();
      if (!value) {
        const metadata = await this.dependencies.workspaceOps.getWorkspaceMetadata(wsName);
        const raw = metadata[GIT_IDENTITY_METADATA_KEY];
        const record = raw && typeof raw === 'object' ? raw as Partial<TerminalGitIdentity> : {};
        const browserSettings = readBrowserGitSettings();
        const current = key === 'user.name'
          ? (typeof record.name === 'string' ? record.name.trim() : '') || browserSettings.authorName
          : (typeof record.email === 'string' ? record.email.trim() : '') || browserSettings.authorEmail;
        return current ? { exitCode: 0, stdout: `${current}\n`, stderr: '' } : fail(`git config: ${key} is not set for this workspace`);
      }
      const identity = await this.setTerminalGitIdentity(wsName, key === 'user.name' ? { name: value } : { email: value });
      return { exitCode: 0, stdout: `${key}=${key === 'user.name' ? identity.name : identity.email}\n`, stderr: '' };
    }

    if (subcommand === 'commit') {
      const mIndex = rest.findIndex((item) => item === '-m' || item === '--message');
      const message = mIndex >= 0 ? rest[mIndex + 1]?.trim() : undefined;
      if (!message) return fail('git commit: provide a message with `-m "..."`.');
      const identity = await this.getTerminalGitIdentity(wsName);
      if (!identity) return fail('git commit: configure this workspace first with `git config user.name "Name"` and `git config user.email "you@example.org"`.');
      const oid = await this.dependencies.projectGit.commit(wsName, {
        message,
        authorName: identity.name,
        authorEmail: identity.email,
      });
      return { exitCode: 0, stdout: `[${oid.slice(0, 8)}] ${message}\n`, stderr: '' };
    }

    if (subcommand === 'branch') {
      const branches = await this.dependencies.projectGit.listBranches(wsName);
      const deleteIndex = rest.findIndex((item) => item === '-d' || item === '-D' || item === '--delete');
      if (deleteIndex >= 0) {
        const name = rest[deleteIndex + 1];
        if (!name) return fail('git branch --delete: expected a branch name');
        await this.dependencies.projectGit.deleteBranch(wsName, name);
        return { exitCode: 0, stdout: `Deleted branch ${name}.\n`, stderr: '' };
      }
      const name = rest.find((item) => !item.startsWith('-'));
      if (name) {
        await this.dependencies.projectGit.createBranch(wsName, name);
        return { exitCode: 0, stdout: `Created branch ${name}.\n`, stderr: '' };
      }
      return { exitCode: 0, stdout: `${branches.join('\n')}${branches.length ? '\n' : ''}`, stderr: '' };
    }

    if (subcommand === 'checkout' || subcommand === 'switch') {
      const branch = rest.find((item) => !item.startsWith('-'));
      if (!branch) return fail(`git ${subcommand}: expected a branch name`);
      await this.dependencies.projectGit.checkout(wsName, branch);
      return { exitCode: 0, stdout: `Switched to branch ${branch}.\n`, stderr: '' };
    }

    if (subcommand === 'merge') {
      const branch = rest.find((item) => !item.startsWith('-'));
      if (!branch) return fail('git merge: expected a branch name');
      const identity = await this.getTerminalGitIdentity(wsName);
      const result = await this.dependencies.projectGit.merge(wsName, branch, identity ? { name: identity.name, email: identity.email } : undefined);
      if (result.conflicts.length) return fail(`git merge: conflicts in ${result.conflicts.join(', ')}`);
      return { exitCode: 0, stdout: `Merged ${branch}${result.oid ? ` (${result.oid.slice(0, 8)})` : ''}.\n`, stderr: '' };
    }

    if (subcommand === 'remote') {
      const action = rest[0]?.toLowerCase();
      if (!action || action === '-v' || action === '--verbose') {
        const remotes = await this.dependencies.projectGit.listRemotes(wsName);
        return { exitCode: 0, stdout: `${remotes.map((item) => `${item.remote}\t${redactUrlSecrets(item.url)}`).join('\n')}${remotes.length ? '\n' : ''}`, stderr: '' };
      }
      if (action === 'add') {
        if (!rest[1] || !rest[2]) return fail('git remote add: expected <name> <http(s)-url>');
        await this.dependencies.projectGit.addRemote(wsName, rest[1], safeGitRemoteUrl(rest[2]));
        return { exitCode: 0, stdout: `Added remote ${rest[1]}.\n`, stderr: '' };
      }
      if (action === 'remove' || action === 'rm') {
        if (!rest[1]) return fail('git remote remove: expected a remote name');
        await this.dependencies.projectGit.removeRemote(wsName, rest[1]);
        return { exitCode: 0, stdout: `Removed remote ${rest[1]}.\n`, stderr: '' };
      }
      return fail('git remote: supported commands are list, add and remove.');
    }

    if (subcommand === 'fetch' || subcommand === 'pull' || subcommand === 'push') {
      const remote = rest.find((item) => !item.startsWith('-'));
      const { corsProxy } = readBrowserGitSettings();
      const remoteInput = {
        ...(remote ? { remote } : {}),
        ...(corsProxy ? { corsProxy } : {}),
      };
      if (subcommand === 'fetch') {
        await this.dependencies.projectGit.fetch(wsName, remoteInput);
      } else if (subcommand === 'pull') {
        const identity = await this.getTerminalGitIdentity(wsName);
        await this.dependencies.projectGit.pull(wsName, {
          ...remoteInput,
          ...(identity ? { author: { name: identity.name, email: identity.email } } : {}),
        });
      } else {
        await this.dependencies.projectGit.push(wsName, remoteInput);
      }
      return { exitCode: 0, stdout: `${subcommand} completed${remote ? ` for ${remote}` : ''}.\n`, stderr: '' };
    }

    return fail(`git: unsupported terminal subcommand: ${subcommand}`);
  }

  private async executeCurl(
    wsName: string,
    args: readonly string[],
    signal?: AbortSignal,
  ): Promise<{ exitCode: number; stdout: string; stderr: string }> {
    const fail = (message: string) => ({ exitCode: 1, stdout: '', stderr: `${message}\n` });
    let method: ProjectHttpMethod | undefined;
    let outputPath: string | undefined;
    let body: Uint8Array | string | undefined;
    let inputProjectPath: ProjectFilePath | undefined;
    let timeoutMs: number | undefined;
    let maxResponseBytes: number | undefined;
    let includeHeaders = false;
    let failOnHttpError = false;
    const headers: Record<string, string> = {};
    let url: string | undefined;

    for (let index = 0; index < args.length; index += 1) {
      const arg = args[index]!;
      if (arg === '-I' || arg === '--head') { method = 'HEAD'; includeHeaders = true; continue; }
      if (arg === '-i' || arg === '--include') { includeHeaders = true; continue; }
      if (arg === '-f' || arg === '--fail') { failOnHttpError = true; continue; }
      if (['-s', '--silent', '-S', '--show-error', '-L', '--location'].includes(arg)) continue;
      if (arg === '-X' || arg === '--request') {
        const candidate = args[++index]?.toUpperCase();
        if (!candidate || !['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(candidate)) return fail('curl: supported request methods are GET, HEAD, POST, PUT, PATCH and DELETE');
        method = candidate as ProjectHttpMethod;
        continue;
      }
      if (arg === '-H' || arg === '--header') {
        const raw = args[++index];
        if (!raw) return fail('curl: --header expects "Name: value"');
        const separator = raw.indexOf(':');
        if (separator <= 0) return fail('curl: --header expects "Name: value"');
        headers[raw.slice(0, separator).trim()] = raw.slice(separator + 1).trim();
        continue;
      }
      if (arg === '-o' || arg === '--output') {
        outputPath = args[++index];
        if (!outputPath) return fail('curl: --output expects a /workspace-relative path');
        continue;
      }
      if (arg === '--max-time') {
        timeoutMs = Math.round(parsePositiveNumber(args[++index], 'curl --max-time') * 1000);
        continue;
      }
      if (arg === '--max-filesize') {
        maxResponseBytes = Math.round(parsePositiveNumber(args[++index], 'curl --max-filesize'));
        continue;
      }
      if (['-d', '--data', '--data-raw', '--data-binary'].includes(arg)) {
        const raw = args[++index];
        if (raw === undefined) return fail(`curl: ${arg} expects data`);
        if (arg === '--data-binary' && raw.startsWith('@')) {
          const terminalPath = this.resolve(wsName, raw.slice(1));
          inputProjectPath = projectFilePath(asProjectPath(terminalPath));
          const bytes = await (await this.dependencies.projectStorage.getProvider(wsName)).readFile(inputProjectPath, signal);
          if (!bytes) return fail(`curl: upload file not found: ${raw.slice(1)}`);
          body = bytes;
        } else body = raw;
        if (!method) method = 'POST';
        continue;
      }
      if (arg.startsWith('-')) return fail(`curl: unsupported option: ${arg}`);
      if (url) return fail('curl: exactly one URL is supported per command');
      url = arg;
    }

    if (!url) return fail('curl: expected an HTTP(S) URL');
    if (body !== undefined && (method === 'GET' || method === 'HEAD')) return fail(`curl: ${method} request bodies are not supported by the browser HTTP client`);
    const response = await this.dependencies.projectHttp.request({
      url,
      method: method ?? 'GET',
      headers,
      ...(body !== undefined ? { body } : {}),
      ...(timeoutMs !== undefined ? { timeoutMs } : {}),
      ...(maxResponseBytes !== undefined ? { maxResponseBytes } : {}),
    }, signal);

    const statusLine = `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ''}`;
    const headerText = `${statusLine}\n${Object.entries(response.headers).map(([name, value]) => `${name}: ${value}`).join('\n')}\n\n`;
    if (failOnHttpError && response.status >= 400) return fail(`curl: ${statusLine}`);

    if (outputPath) {
      const resolved = this.resolve(wsName, outputPath);
      const project = asProjectPath(resolved);
      if (!project) return fail('curl: output must be a file below /workspace');
      const artifact = await this.writeGeneratedFile(wsName, resolved, response.bytes, {
        kind: classifyScientificArtifact(project),
        generatedBy: {
          runtime: 'http',
          command: `curl ${response.url}`,
          ...(inputProjectPath ? { inputPaths: [String(inputProjectPath)] } : {}),
          runId: `http:${Date.now()}`,
        },
      });
      return {
        exitCode: 0,
        stdout: `${includeHeaders ? headerText : ''}Saved ${response.bytes.byteLength} bytes to ${artifact.path}\n`,
        stderr: '',
      };
    }

    if (response.bytes.byteLength > MAX_HTTP_STDOUT_BYTES) {
      return fail(`curl: response is ${response.bytes.byteLength} bytes; use -o <project-path> to save responses larger than ${MAX_HTTP_STDOUT_BYTES} bytes.`);
    }
    if (!isProbablyTextResponse(response.contentType, response.bytes)) {
      return fail(`curl: binary response (${response.bytes.byteLength} bytes${response.contentType ? `, ${response.contentType}` : ''}); use -o <project-path> to save it.`);
    }
    const text = new TextDecoder('utf-8', { fatal: false }).decode(response.bytes);
    return {
      exitCode: 0,
      stdout: `${includeHeaders ? headerText : ''}${text}${text && !text.endsWith('\n') ? '\n' : ''}`,
      stderr: '',
    };
  }

  private async executeInvocation(
    wsName: string,
    invocation: TerminalInvocation,
    stdin: string,
    signal?: AbortSignal,
    options: TerminalExecutionOptions = {},
  ): Promise<{ exitCode: number; stdout: string; stderr: string }> {
    signal?.throwIfAborted();
    const command = invocation.command.toLowerCase();
    const args = [...invocation.args];
    let stdout = '';
    let stderr = '';
    let exitCode = 0;

    const fail = (message: string) => ({ exitCode: 1, stdout: '', stderr: `${message}\n` });

    try {
      if (command === 'pwd') {
        stdout = `${this.getCwd(wsName)}\n`;
      } else if (command === 'cd') {
        const target = this.resolve(wsName, args[0] ?? '/workspace');
        const project = asProjectPath(target).replace(/\/$/u, '');
        if (project) {
          const entries = await this.listEntries(wsName);
          const exists = entries.some((entry) => entry.kind === 'directory' && matchesPath(entry, project));
          if (!exists) return fail(`cd: no such directory: ${args[0] ?? ''}`);
        }
        this.cwdByWorkspace.set(wsName, target);
      } else if (command === 'ls') {
        const target = this.resolve(wsName, args.find((arg) => !arg.startsWith('-')) ?? '.');
        const project = asProjectPath(target).replace(/\/$/u, '');
        const prefix = project ? `${project}/` : '';
        const entries = await this.listEntries(wsName);
        const names = new Map<string, string>();
        for (const entry of entries) {
          const path = String(entry.path);
          if (project && matchesPath(entry, project)) {
            names.set(basename(path), entry.kind === 'directory' ? `${basename(path)}/` : basename(path));
            continue;
          }
          if (!path.startsWith(prefix)) continue;
          const rest = path.slice(prefix.length);
          if (!rest) continue;
          const first = rest.split('/')[0]!;
          names.set(first, rest.includes('/') || entry.kind === 'directory' ? `${first}/` : first);
        }
        stdout = `${[...names.values()].sort().join('\n')}${names.size ? '\n' : ''}`;
      } else if (command === 'cat') {
        if (!args.length) stdout = stdin;
        else for (const arg of args) stdout += await this.readText(wsName, this.resolve(wsName, arg));
      } else if (command === 'head' || command === 'tail') {
        let count = 10;
        const n = args.indexOf('-n');
        if (n >= 0 && args[n + 1]) count = Math.max(0, Number(args[n + 1]) || 10);
        const fileArg = args.filter((arg, index) => index !== n && index !== n + 1 && !arg.startsWith('-'))[0];
        const text = fileArg ? await this.readText(wsName, this.resolve(wsName, fileArg)) : stdin;
        const lines = text.split(/\r?\n/u);
        stdout = `${(command === 'head' ? lines.slice(0, count) : lines.slice(-count)).join('\n')}\n`;
      } else if (command === 'sed') {
        const inPlace = args.includes('-i');
        const clean = args.filter((arg) => arg !== '-i');
        const expression = clean[0];
        const fileArg = clean[1];
        if (!expression) return fail('sed: expected substitution such as s/old/new/g');
        const match = expression.match(/^s(.)(.*?)\1(.*?)\1([g]?)$/u);
        if (!match) return fail('sed: only s/old/new/ and s/old/new/g are supported');
        const [, , rawPattern = '', replacement = '', flags = ''] = match;
        const text = fileArg ? await this.readText(wsName, this.resolve(wsName, fileArg)) : stdin;
        let next: string;
        try { next = text.replace(new RegExp(rawPattern, flags === 'g' ? 'gu' : 'u'), replacement); }
        catch (error) { return fail(`sed: ${error instanceof Error ? error.message : String(error)}`); }
        if (inPlace) {
          if (!fileArg) return fail('sed -i: expected a file');
          await this.writeWorkspaceFile(wsName, this.resolve(wsName, fileArg), next);
        } else stdout = next;
      } else if (command === 'wc') {
        const fileArg = args.find((arg) => !arg.startsWith('-'));
        const text = fileArg ? await this.readText(wsName, this.resolve(wsName, fileArg)) : stdin;
        const lines = text.length ? text.split(/\r?\n/u).length : 0;
        const words = text.trim() ? text.trim().split(/\s+/u).length : 0;
        stdout = `${lines} ${words} ${encoder.encode(text).byteLength}${fileArg ? ` ${fileArg}` : ''}\n`;
      } else if (command === 'grep') {
        const recursive = args.includes('-R') || args.includes('-r');
        const clean = args.filter((arg) => !['-R', '-r', '-n'].includes(arg));
        const pattern = clean.shift();
        if (!pattern) return fail('grep: missing pattern');
        const target = clean[0];
        const matchText = (text: string, label = '') => text.split(/\r?\n/u)
          .map((line, index) => line.includes(pattern) ? `${label}${index + 1}:${line}` : '')
          .filter(Boolean).join('\n');
        if (!target) stdout = matchText(stdin);
        else {
          const resolved = this.resolve(wsName, target);
          const project = asProjectPath(resolved).replace(/\/$/u, '');
          const entries = await this.listEntries(wsName);
          const files = recursive
            ? entries.filter((entry): entry is Extract<ProjectEntry, { kind: 'file' }> => entry.kind === 'file' && (!project || String(entry.path).startsWith(`${project}/`) || String(entry.path) === project))
            : entries.filter((entry): entry is Extract<ProjectEntry, { kind: 'file' }> => entry.kind === 'file' && String(entry.path) === project);
          const found: string[] = [];
          for (const file of files) {
            const wsPath = WsPath.fromParts(wsName, String(file.path)).wsPath;
            const blob = await this.dependencies.fileSystem.readFile(wsPath);
            if (!blob || blob.size > 5 * 1024 * 1024) continue;
            const text = await blob.text();
            const matches = matchText(text, `${String(file.path)}:`);
            if (matches) found.push(matches);
          }
          stdout = found.join('\n');
        }
        if (stdout && !stdout.endsWith('\n')) stdout += '\n';
      } else if (command === 'find') {
        const target = this.resolve(wsName, args[0] ?? '.');
        const project = asProjectPath(target).replace(/\/$/u, '');
        const entries = await this.listEntries(wsName);
        stdout = entries
          .filter((entry) => !project || String(entry.path).replace(/\/$/u, '') === project || String(entry.path).startsWith(`${project}/`))
          .map((entry) => `/workspace/${String(entry.path)}`)
          .sort().join('\n');
        if (stdout) stdout += '\n';
      } else if (command === 'mkdir') {
        if (this.dependencies.projectCollaboration.isActive(wsName)) return fail('Project structure is locked while live collaboration is active.');
        if (!args.length) return fail('mkdir: missing directory');
        const provider = await this.dependencies.projectStorage.getProvider(wsName);
        for (const arg of args.filter((value) => value !== '-p')) {
          const project = asProjectPath(this.resolve(wsName, arg));
          if (!project) continue;
          const directoryPath = `${project.replace(/\/$/u, '')}/`;
          await provider.createDirectory(projectDirectoryPath(directoryPath), signal);
        }
        this.dependencies.fileSystem.refreshFileTree();
      } else if (command === 'touch') {
        if (this.dependencies.projectCollaboration.isActive(wsName)) return fail('Project structure is locked while live collaboration is active.');
        if (!args.length) return fail('touch: missing file');
        for (const arg of args) {
          const resolved = this.resolve(wsName, arg);
          const wsPath = this.wsPath(wsName, resolved);
          if (!(await this.dependencies.fileSystem.exists(wsPath))) await this.writeWorkspaceFile(wsName, resolved, '');
        }
      } else if (command === 'cp') {
        if (this.dependencies.projectCollaboration.isActive(wsName)) return fail('Project structure is locked while live collaboration is active.');
        if (args.length !== 2) return fail('cp: expected source and destination');
        const source = this.resolve(wsName, args[0]);
        const destination = this.resolve(wsName, args[1]);
        const file = await this.dependencies.fileSystem.readFile(this.wsPath(wsName, source));
        if (!file) return fail(`cp: file not found: ${args[0]}`);
        await this.writeWorkspaceFile(wsName, destination, new Uint8Array(await file.arrayBuffer()));
      } else if (command === 'mv') {
        if (this.dependencies.projectCollaboration.isActive(wsName)) return fail('Project structure is locked while live collaboration is active.');
        if (args.length !== 2) return fail('mv: expected source and destination');
        const oldWsPath = this.wsPath(wsName, this.resolve(wsName, args[0]));
        const newWsPath = this.wsPath(wsName, this.resolve(wsName, args[1]));
        await this.dependencies.latexSource.flush(oldWsPath).catch(() => undefined);
        await this.dependencies.fileSystem.renameFile({ oldWsPath, newWsPath });
        if (this.dependencies.latexSource.getSnapshot(oldWsPath)) this.dependencies.latexSource.retargetLoadedDocument(oldWsPath, newWsPath);
      } else if (command === 'rm') {
        if (this.dependencies.projectCollaboration.isActive(wsName)) return fail('Project structure is locked while live collaboration is active.');
        const recursive = args.includes('-r') || args.includes('-R') || args.includes('-rf') || args.includes('-fr');
        const targets = args.filter((arg) => !arg.startsWith('-'));
        if (!targets.length) return fail('rm: missing path');
        const provider = await this.dependencies.projectStorage.getProvider(wsName);
        for (const arg of targets) {
          const resolved = this.resolve(wsName, arg);
          const project = asProjectPath(resolved).replace(/\/$/u, '');
          const entries = await provider.listEntries(signal);
          const directory = entries.find((entry) => entry.kind === 'directory' && matchesPath(entry, project));
          if (directory) {
            if (!recursive) return fail(`rm: ${arg}: is a directory (use -r)`);
            const descendants = entries
              .filter((entry): entry is Extract<ProjectEntry, { kind: 'file' }> =>
                entry.kind === 'file' && String(entry.path).startsWith(`${project}/`),
              )
              .map((entry) => this.wsPath(wsName, String(entry.path)));
            if (descendants.length) await this.dependencies.fileSystem.deleteFiles(descendants);
            await provider.deleteDirectory(projectDirectoryPath(`${project}/`), signal);
            this.dependencies.fileSystem.refreshFileTree();
          } else {
            const targetWsPath = this.wsPath(wsName, resolved);
            if (this.dependencies.latexSource.getSnapshot(targetWsPath)) return fail(`rm: close the loaded LaTeX source before deleting it: ${arg}`);
            await this.dependencies.fileSystem.deleteFile(targetWsPath);
          }
        }
      } else if (command === 'runtime') {
        const subcommand = args[0]?.toLowerCase() ?? 'status';
        if (subcommand === 'status' || subcommand === 'doctor') {
          const diagnostics = this.dependencies.projectScientificRuntime.getDiagnostics(wsName);
          if (args.includes('--json')) {
            stdout = `${JSON.stringify(diagnostics, null, 2)}\n`;
          } else {
            const caps = diagnostics.capabilities;
            const capabilityLine = (label: string, value: boolean) => `  ${value ? 'PASS' : 'MISS'} ${label}`;
            const runtimeLines = diagnostics.runtimes.flatMap((runtime) => [
              `${runtime.status === 'supported' ? 'PASS' : runtime.status === 'degraded' ? 'WARN' : 'FAIL'} ${runtime.label}`,
              `  registered=${runtime.registered ? 'yes' : 'no'} state=${runtime.state}`,
              ...runtime.blockers.map((item) => `  blocker: ${item}`),
              ...runtime.degradations.map((item) => `  degraded: ${item}`),
              ...(runtime.error ? [`  last error: ${runtime.error}`] : []),
            ]);
            stdout = [
              'Learnitive scientific runtime doctor',
              'No runtimes or packages were downloaded by this check.',
              '',
              'Browser capabilities:',
              capabilityLine('WebAssembly', caps.webAssembly),
              capabilityLine('Web Workers', caps.worker),
              capabilityLine('fetch()', caps.fetch),
              capabilityLine('AbortController', caps.abortController),
              capabilityLine('crypto.randomUUID()', caps.cryptoRandomUUID),
              capabilityLine('cross-origin isolation', caps.crossOriginIsolated),
              capabilityLine('SharedArrayBuffer', caps.sharedArrayBuffer),
              capabilityLine('Atomics', caps.atomics),
              capabilityLine('OffscreenCanvas', caps.offscreenCanvas),
              capabilityLine('DOM canvas fallback', caps.domCanvas),
              '',
              'Runtime workspace guardrails:',
              `  Max mirrored input file: ${(diagnostics.workspaceLimits.maxInputFileBytes / 1024 / 1024).toFixed(0)} MiB`,
              `  Max mirrored workspace: ${(diagnostics.workspaceLimits.maxTotalInputBytes / 1024 / 1024).toFixed(0)} MiB`,
              '',
              'Scientific runtimes:',
              ...runtimeLines,
              '',
              'Recovery: runtime reset <python|r|wasi|all>',
            ].join('\n') + '\n';
          }
        } else if (subcommand === 'reset' || subcommand === 'restart') {
          const target = (args[1] ?? '').toLowerCase();
          if (!['python', 'r', 'wasi', 'all'].includes(target)) {
            return fail('runtime reset: expected python, r, wasi, or all');
          }
          await this.dependencies.projectScientificRuntime.resetRuntime(
            wsName,
            target as 'python' | 'r' | 'wasi' | 'all',
          );
          stdout = target === 'all'
            ? 'Reset all loaded scientific runtimes. They will initialize lazily on next use.\n'
            : `Reset ${target} runtime. It will initialize lazily on next use.\n`;
        } else {
          return fail('runtime: supported commands are `runtime doctor`, `runtime status`, and `runtime reset <python|r|wasi|all>`');
        }
      } else if (command === 'dataset') {
        const subcommand = args[0]?.toLowerCase() ?? 'inspect';
        if (subcommand !== 'inspect') return fail('dataset: supported command is `dataset inspect <file.csv|file.tsv>`');
        const requested = args[1];
        if (!requested) return fail('dataset inspect: expected a CSV or TSV file');
        const resolved = this.resolve(wsName, requested);
        const inspection = await this.dependencies.projectDataset.inspect(
          wsName,
          asProjectPath(resolved),
          { sampleRows: 8, statistics: true },
          signal,
        );
        const columnLines = inspection.columns.map((column) => {
          const numeric = column.numeric
            ? ` mean=${column.numeric.mean.toPrecision(6)} std=${column.numeric.std.toPrecision(6)} min=${column.numeric.min.toPrecision(6)} max=${column.numeric.max.toPrecision(6)}`
            : '';
          return `  ${column.name}: ${column.inferredType}, missing=${column.missingCount}${numeric}`;
        });
        const sample = inspection.sampleRows.length
          ? `\nSample:\n${[inspection.columns.map((column) => column.name), ...inspection.sampleRows].map((row) => row.join('\t')).join('\n')}`
          : '';
        stdout = [
          `Dataset: ${inspection.path}`,
          `Rows: ${inspection.rowCount}`,
          `Columns: ${inspection.columnCount}`,
          `Bytes: ${inspection.bytes}`,
          ...columnLines,
          ...(inspection.warnings.length ? ['', 'Warnings:', ...inspection.warnings.map((warning) => `  ${warning}`)] : []),
        ].join('\n') + sample + '\n';
      } else if (command === 'artifact') {
        const subcommand = args[0]?.toLowerCase() ?? 'list';
        const artifacts = await this.getGeneratedArtifacts(wsName);
        if (subcommand === 'list') {
          if (!artifacts.length) stdout = 'No generated research artifacts.\n';
          else stdout = artifacts
            .slice()
            .sort((a, b) => b.createdAt - a.createdAt)
            .map((artifact) => {
              const runtime = artifact.generatedBy?.runtime ? ` via ${artifact.generatedBy.runtime}` : '';
              const source = artifact.generatedBy?.sourcePath ? ` from ${artifact.generatedBy.sourcePath}` : '';
              return `${artifact.kind}  ${artifact.path}${runtime}${source}`;
            })
            .join('\n') + '\n';
        } else if (subcommand === 'inspect') {
          const requested = args[1];
          if (!requested) return fail('artifact inspect: expected a generated artifact path');
          const resolved = asProjectPath(this.resolve(wsName, requested));
          const artifact = artifacts.find((item) => item.path === resolved);
          if (!artifact) return fail(`artifact inspect: not a registered generated artifact: ${resolved}`);
          const entries = await this.listEntries(wsName);
          const entry = entries.find((item) => item.path === artifact.path);
          const artifactSize = entry?.kind === 'file' ? entry.size : undefined;
          stdout = [
            `Path: ${artifact.path}`,
            `Kind: ${artifact.kind}`,
            `Created: ${new Date(artifact.createdAt).toISOString()}`,
            `Size: ${artifactSize ?? 'unknown'} bytes`,
            `Runtime: ${artifact.generatedBy?.runtime ?? 'unknown'}`,
            `Generator: ${artifact.generatedBy?.sourcePath ?? 'unknown'}`,
            `Command: ${artifact.generatedBy?.command ?? 'unknown'}`,
          ].join('\n') + '\n';
        } else {
          return fail('artifact: supported commands are `artifact list` and `artifact inspect <path>`');
        }
      } else if (command === 'git') {
        const result = await this.executeGit(wsName, args);
        stdout = result.stdout;
        stderr = result.stderr;
        exitCode = result.exitCode;
      } else if (command === 'curl') {
        const result = await this.executeCurl(wsName, args, signal);
        stdout = result.stdout;
        stderr = result.stderr;
        exitCode = result.exitCode;
      } else if (command === 'python' || command === 'python3') {
        if (!this.dependencies.projectScientificRuntime.isRuntimeAvailable('python')) {
          return fail('python: Pyodide runtime is unavailable in this build');
        }
        const cwd = asProjectPath(this.getCwd(wsName));
        let request: RuntimeExecutionRequest;
        if (args[0] === '-c') {
          const code = args[1];
          if (code === undefined) return fail('python -c: missing code');
          request = {
            mode: 'code' as const,
            code,
            args: args.slice(2),
            cwd,
            sourceName: '<command>',
          };
        } else if (args.length) {
          const source = this.resolve(wsName, args[0]!);
          request = {
            mode: 'file' as const,
            path: asProjectPath(source),
            args: args.slice(1),
            cwd,
          };
        } else {
          request = {
            mode: 'code' as const,
            code: "import sys; print(f'Python {sys.version.split()[0]} (Pyodide)')",
            cwd,
            sourceName: '<startup>',
          };
        }
        const result = await this.dependencies.projectScientificRuntime.execute(
          wsName,
          'python',
          request,
          { signal, commandLabel: `${command} ${args.join(' ')}`.trim(), ...(request.mode === 'file' && request.args?.length ? { executionArgs: request.args } : {}), ...(options.scientificAllowCreatePaths !== undefined ? { allowCreatePaths: options.scientificAllowCreatePaths } : {}), ...(options.scientificInputPaths?.length ? { inputPaths: options.scientificInputPaths } : {}) },
        );
        stdout = result.stdout;
        stderr = result.stderr;
        exitCode = result.exitCode;
        if (result.workspace.blockedModifiedFiles.length) {
          stderr += `Learnitive blocked runtime changes to existing project files: ${result.workspace.blockedModifiedFiles.join(', ')}\n`;
        }
        if (result.workspace.blockedCreatedFiles.length) {
          stderr += `Learnitive blocked runtime-created files outside the approved output list: ${result.workspace.blockedCreatedFiles.join(', ')}\n`;
        }
        if (result.artifacts.length) {
          stdout += `${stdout && !stdout.endsWith('\n') ? '\n' : ''}Generated:
${result.artifacts.map((artifact) => `  ${artifact.path}`).join('\n')}
`;
        }
      } else if (command === 'pyodide') {
        if (args[0] && args[0] !== 'info') return fail('pyodide: supported command is `pyodide info`');
        const state = this.dependencies.projectScientificRuntime.getRuntimeState(wsName, 'python');
        const packages = this.dependencies.projectScientificRuntime.packages.list('python');
        stdout = [
          'Pyodide scientific Python',
          `Available: ${this.dependencies.projectScientificRuntime.isRuntimeAvailable('python') ? 'yes' : 'no'}`,
          `State: ${state?.status ?? 'not-loaded'}`,
          `Packages: ${packages.map((item) => item.name).join(', ') || 'none registered'}`,
        ].join('\n') + '\n';
      } else if (command === 'r' || command === 'rscript') {
        if (!this.dependencies.projectScientificRuntime.isRuntimeAvailable('r')) {
          return fail('R: webR runtime is unavailable in this build');
        }
        const cwd = asProjectPath(this.getCwd(wsName));
        let request: RuntimeExecutionRequest;
        if ((command === 'r' && args[0] === '-e') || (command === 'rscript' && args[0] === '-e')) {
          const code = args[1];
          if (code === undefined) return fail(`${command} -e: missing code`);
          request = {
            mode: 'code' as const,
            code,
            args: args.slice(2),
            cwd,
            sourceName: '<command>',
          };
        } else if (command === 'rscript' && args.length) {
          const source = this.resolve(wsName, args[0]!);
          request = {
            mode: 'file' as const,
            path: asProjectPath(source),
            args: args.slice(1),
            cwd,
          };
        } else if (command === 'r' && args.length) {
          return fail('R: supported forms are `R`, `R -e "code"`, and `Rscript file.R [args...]`');
        } else {
          request = {
            mode: 'code' as const,
            code: "cat(R.version.string, ' (webR)\n', sep='')",
            cwd,
            sourceName: '<startup>',
          };
        }
        const result = await this.dependencies.projectScientificRuntime.execute(
          wsName,
          'r',
          request,
          { signal, commandLabel: `${invocation.command} ${args.join(' ')}`.trim(), ...(request.mode === 'file' && request.args?.length ? { executionArgs: request.args } : {}), ...(options.scientificAllowCreatePaths !== undefined ? { allowCreatePaths: options.scientificAllowCreatePaths } : {}), ...(options.scientificInputPaths?.length ? { inputPaths: options.scientificInputPaths } : {}) },
        );
        stdout = result.stdout;
        stderr = result.stderr;
        exitCode = result.exitCode;
        if (result.workspace.blockedModifiedFiles.length) {
          stderr += `Learnitive blocked runtime changes to existing project files: ${result.workspace.blockedModifiedFiles.join(', ')}\n`;
        }
        if (result.workspace.blockedCreatedFiles.length) {
          stderr += `Learnitive blocked runtime-created files outside the approved output list: ${result.workspace.blockedCreatedFiles.join(', ')}\n`;
        }
        if (result.artifacts.length) {
          stdout += `${stdout && !stdout.endsWith('\n') ? '\n' : ''}Generated:\n${result.artifacts.map((artifact) => `  ${artifact.path}`).join('\n')}\n`;
        }
      } else if (command === 'webr') {
        const subcommand = args[0]?.toLowerCase() ?? 'info';
        if (subcommand === 'install') {
          const requested = args.slice(1);
          if (!requested.length) return fail('webr install: provide one or more Learnitive-certified R package names');
          const installed = await this.dependencies.projectScientificRuntime.installPackages(wsName, 'r', requested, signal);
          stdout = `Installed webR package${installed.length === 1 ? '' : 's'}: ${installed.join(', ')}\n`;
        } else if (subcommand === 'info') {
          const state = this.dependencies.projectScientificRuntime.getRuntimeState(wsName, 'r');
          const packages = this.dependencies.projectScientificRuntime.packages.list('r');
          stdout = [
            'webR scientific R',
            `Available: ${this.dependencies.projectScientificRuntime.isRuntimeAvailable('r') ? 'yes' : 'no'}`,
            `State: ${state?.status ?? 'not-loaded'}`,
            `Packages: ${packages.map((item) => `${item.name} (${item.state})`).join(', ') || 'none registered'}`,
            'Package installs use precompiled WebAssembly binaries from the configured webR repository.',
          ].join('\n') + '\n';
        } else {
          return fail('webr: supported commands are `webr info` and `webr install <package...>`');
        }
      } else if (['clang', 'clang++', 'cc', 'c++'].includes(command)) {
        if (!this.dependencies.projectScientificRuntime.isRuntimeAvailable('wasi')) {
          return fail(`${invocation.command}: Wasmer/WASIX runtime is unavailable in this build`);
        }
        const wasiState = this.dependencies.projectScientificRuntime.getRuntimeState(wsName, 'wasi');
        if (wasiState?.status !== 'ready') {
          return fail(`${invocation.command}: browser Clang is not loaded. Run \`wasix install clang\` first; that explicit network-approved step loads the Wasmer SDK and pinned compiler toolchain.`);
        }
        const tool = command === 'clang++' || command === 'c++' ? 'clang++' : 'clang';
        const cwd = asProjectPath(this.getCwd(wsName));
        const sourceArg = args.find((arg) => !arg.startsWith('-') && /\.(?:c|cc|cpp|cxx)$/iu.test(arg));
        const sourcePath = sourceArg ? asProjectPath(this.resolve(wsName, sourceArg)) : undefined;
        const result = await this.dependencies.projectScientificRuntime.execute(
          wsName,
          'wasi',
          { mode: 'tool', tool, args, cwd },
          {
            signal,
            commandLabel: `${invocation.command} ${args.join(' ')}`.trim(),
            ...(sourcePath ? { sourcePath, inputPaths: [sourcePath] } : {}),
            ...(options.scientificAllowCreatePaths !== undefined ? { allowCreatePaths: options.scientificAllowCreatePaths } : {}),
          },
        );
        stdout = result.stdout;
        stderr = result.stderr;
        exitCode = result.exitCode;
        if (result.workspace.blockedModifiedFiles.length) stderr += `Learnitive blocked runtime changes to existing project files: ${result.workspace.blockedModifiedFiles.join(', ')}\n`;
        if (result.workspace.blockedCreatedFiles.length) stderr += `Learnitive blocked runtime-created files outside the approved output list: ${result.workspace.blockedCreatedFiles.join(', ')}\n`;
        if (result.artifacts.length) {
          stdout += `${stdout && !stdout.endsWith('\n') ? '\n' : ''}Generated:\n${result.artifacts.map((artifact) => `  ${artifact.path}`).join('\n')}\n`;
        }
      } else if (command === 'wasm-run') {
        if (!this.dependencies.projectScientificRuntime.isRuntimeAvailable('wasi')) return fail('wasm-run: Wasmer/WASIX runtime is unavailable in this build');
        const wasiState = this.dependencies.projectScientificRuntime.getRuntimeState(wsName, 'wasi');
        if (wasiState?.status !== 'ready') {
          return fail('wasm-run: Wasmer/WASIX is not loaded. Run `wasix install clang` first; network initialization must be explicitly approved.');
        }
        const requested = args[0];
        if (!requested) return fail('wasm-run: expected a compiled .wasm program');
        const program = asProjectPath(this.resolve(wsName, requested));
        if (!/\.wasm$/iu.test(program)) return fail('wasm-run: expected a .wasm program');
        const cwd = asProjectPath(this.getCwd(wsName));
        const result = await this.dependencies.projectScientificRuntime.execute(
          wsName,
          'wasi',
          { mode: 'file', path: program, args: args.slice(1), cwd },
          {
            signal,
            commandLabel: `${invocation.command} ${args.join(' ')}`.trim(),
            inputPaths: [program],
            ...(args.length > 1 ? { executionArgs: args.slice(1) } : {}),
            ...(options.scientificAllowCreatePaths !== undefined ? { allowCreatePaths: options.scientificAllowCreatePaths } : {}),
          },
        );
        stdout = result.stdout;
        stderr = result.stderr;
        exitCode = result.exitCode;
        if (result.workspace.blockedModifiedFiles.length) stderr += `Learnitive blocked runtime changes to existing project files: ${result.workspace.blockedModifiedFiles.join(', ')}\n`;
        if (result.workspace.blockedCreatedFiles.length) stderr += `Learnitive blocked runtime-created files outside the approved output list: ${result.workspace.blockedCreatedFiles.join(', ')}\n`;
        if (result.artifacts.length) stdout += `${stdout && !stdout.endsWith('\n') ? '\n' : ''}Generated:\n${result.artifacts.map((artifact) => `  ${artifact.path}`).join('\n')}\n`;
      } else if (['pandoc', 'jaq', 'jq', 'sqlite3'].includes(command)) {
        const state = this.dependencies.projectScientificRuntime.getRuntimeState(wsName, 'wasi');
        if (!state || state.status === 'not-loaded') {
          const utility = command === 'jq' ? 'jaq' : command === 'sqlite3' ? 'sqlite' : command;
          return fail(`${command}: browser WASI utility is not loaded. Run \`wasix install ${utility}\` first; network initialization must be explicitly approved.`);
        }
        const tool = command === 'jq' ? 'jaq' : command === 'sqlite3' ? 'sqlite' : command;
        const cwd = asProjectPath(this.getCwd(wsName));
        const scientificOptions: Parameters<ProjectScientificRuntimeService['execute']>[3] = {
          signal,
          commandLabel: `${invocation.command} ${args.join(' ')}`.trim(),
          ...(options.scientificAllowCreatePaths !== undefined ? { allowCreatePaths: options.scientificAllowCreatePaths } : {}),
          ...(options.scientificInputPaths !== undefined ? { inputPaths: options.scientificInputPaths } : {}),
        };
        if (command === 'sqlite3') {
          const databaseArg = args.find((arg) => !arg.startsWith('-') && /\.(?:sqlite|sqlite3|db)$/iu.test(arg));
          if (databaseArg) {
            const databasePath = asProjectPath(this.resolve(wsName, databaseArg));
            scientificOptions.allowOverwritePaths = [databasePath];
            scientificOptions.inputPaths = [...new Set([...(scientificOptions.inputPaths ?? []), databasePath])];
          }
        }
        const result = await this.dependencies.projectScientificRuntime.execute(
          wsName,
          'wasi',
          { mode: 'tool', tool, args, cwd, ...(stdin ? { stdin } : {}) },
          scientificOptions,
        );
        stdout = result.stdout;
        stderr = result.stderr;
        exitCode = result.exitCode;
        if (result.workspace.blockedModifiedFiles.length) stderr += `Learnitive blocked runtime changes to existing project files: ${result.workspace.blockedModifiedFiles.join(', ')}\n`;
        if (result.workspace.blockedCreatedFiles.length) stderr += `Learnitive blocked runtime-created files outside the approved output list: ${result.workspace.blockedCreatedFiles.join(', ')}\n`;
        if (result.artifacts.length) stdout += `${stdout && !stdout.endsWith('\n') ? '\n' : ''}Generated:\n${result.artifacts.map((artifact) => `  ${artifact.path}`).join('\n')}\n`;
      } else if (command === 'wasix') {
        const subcommand = args[0]?.toLowerCase() ?? 'info';
        if (subcommand === 'install') {
          const target = args[1]?.toLowerCase();
          const certifiedUtilities = ['pandoc', 'jaq', 'sqlite'];
          if (target !== 'clang' && !certifiedUtilities.includes(target ?? '')) {
            return fail('wasix install: certified targets are `clang`, `pandoc`, `jaq`, and `sqlite`');
          }
          const result = await this.dependencies.projectScientificRuntime.execute(
            wsName,
            'wasi',
            target === 'clang'
              ? { mode: 'tool', tool: 'install-clang', args: [], cwd: asProjectPath(this.getCwd(wsName)) }
              : { mode: 'tool', tool: 'install-utility', args: [target!], cwd: asProjectPath(this.getCwd(wsName)) },
            { signal, commandLabel: `wasix install ${target}`, allowCreatePaths: [] },
          );
          stdout = result.stdout;
          stderr = result.stderr;
          exitCode = result.exitCode;
        } else if (subcommand === 'info') {
          const state = this.dependencies.projectScientificRuntime.getRuntimeState(wsName, 'wasi');
          const isolated = typeof globalThis.crossOriginIsolated === 'boolean' ? globalThis.crossOriginIsolated : false;
          stdout = [
            'Learnitive C/C++ · Wasmer WASIX',
            `Available: ${this.dependencies.projectScientificRuntime.isRuntimeAvailable('wasi') ? 'yes' : 'no'}`,
            `State: ${state?.status ?? 'not-loaded'}`,
            `Cross-origin isolated: ${isolated ? 'yes' : 'no'}`,
            'Toolchain: clang/clang (install explicitly with `wasix install clang`)',
            'Certified utilities: pandoc, jaq (jq alias), sqlite3',
            'Install: wasix install pandoc | jaq | sqlite',
            'Gnuplot: not certified in this release; use Matplotlib or R/webR plotting.',
            'Build: clang source.c -o build/source.wasm',
            'C++: clang++ source.cpp -o build/source.wasm',
            'Run: wasm-run build/source.wasm [args...]',
          ].join('\n') + '\n';
        } else {
          return fail('wasix: supported commands are `wasix info` and `wasix install <clang|pandoc|jaq|sqlite>`');
        }
      } else if (['tex', 'pdflatex', 'xelatex', 'lualatex'].includes(command)) {
        const root = (await this.dependencies.projectStorage.getProjectConfig(wsName)).rootFile;
        const requested = args.find((arg) => !arg.startsWith('-'));
        if (requested && requested.replace(/^\.\//u, '') !== root) {
          return fail(`${command}: Learnitive currently compiles the configured root document (${root}); change the project root before compiling another file.`);
        }
        if (command !== 'tex') {
          await this.dependencies.projectCompile.setProjectCompiler(wsName, command as 'pdflatex' | 'xelatex' | 'lualatex');
        }
        const result = await this.dependencies.projectCompile.compile(wsName, signal);
        stdout = `Compiled ${root} with ${result.compiler}.\n`;
        const diagnostics = result.diagnostics ?? [];
        if (diagnostics.length) {
          stdout += diagnostics.map((d) => `${d.file ?? root}${d.line ? `:${d.line}${d.column ? `:${d.column}` : ''}` : ''}: ${d.severity}: ${d.message}`).join('\n');
          stdout += '\n';
        }
      } else if (command === 'help') {
        stdout = 'Learnitive browser terminal\nCommands: pwd cd ls cat head tail sed wc find grep runtime dataset artifact git curl mkdir touch cp mv rm tex pdflatex xelatex lualatex python python3 pyodide R Rscript webr clang clang++ cc c++ wasm-run wasix pandoc jaq jq sqlite3 help clear\nAI (terminal UI): ai "question" | ai explain | ai fix | ai suggest | ai plan "task" | ai agent "task" | ai workflow "task" | ai analyse <dataset> | ai plot <dataset> | ai git "question"\nPipelines: |   Redirection: > >>   Statements: ;\nGit uses Learnitive browser Git; curl uses browser fetch and remains subject to CORS.\nC/C++ compiles to WebAssembly/WASI; certified WASI utilities are finite and explicitly installed.\nWorkspace capability: /workspace only.\n';
      } else if (command === 'clear') {
        stdout = '';
      } else {
        return fail(`${invocation.command}: command not found`);
      }
    } catch (error) {
      return fail(error instanceof Error ? error.message : String(error));
    }

    if (invocation.redirect) {
      const target = this.resolve(wsName, invocation.redirect.path);
      let text = stdout;
      if (invocation.redirect.append) {
        try { text = `${await this.readText(wsName, target)}${stdout}`; } catch { /* new file */ }
      }
      await this.writeWorkspaceFile(wsName, target, text);
      stdout = '';
    }
    return { exitCode, stdout, stderr };
  }

  async execute(
    wsName: string,
    line: string,
    signal?: AbortSignal,
    origin: TerminalExecutionOrigin = 'user',
    options: TerminalExecutionOptions = {},
  ): Promise<TerminalExecutionResult> {
    await this.mountPromise;
    const decision = this.policy(line, origin);
    if (decision.action === 'deny') {
      const result: TerminalExecutionResult = {
        cwd: this.getCwd(wsName),
        exitCode: 127,
        outputs: [{ stream: 'stderr', text: `${decision.reason ?? 'Command denied by terminal policy'}\n` }],
      };
      this.recordExecution(wsName, line, origin, this.getCwd(wsName), Date.now(), result);
      return result;
    }
    if (origin === 'ai' && decision.action === 'confirm' && !options.aiApproved) {
      const result: TerminalExecutionResult = {
        cwd: this.getCwd(wsName),
        exitCode: 126,
        outputs: [{ stream: 'system', text: `AI execution requires explicit approval (${decision.capability}).\n` }],
      };
      this.recordExecution(wsName, line, origin, this.getCwd(wsName), Date.now(), result);
      return result;
    }
    const startedAt = Date.now();
    const cwdBefore = this.getCwd(wsName);
    const outputs: TerminalOutput[] = [];
    let exitCode = 0;
    try {
      const statements = parseTerminalLine(line);
      for (const statement of statements) {
        let stdin = '';
        for (const invocation of statement.pipeline) {
          const result = await this.executeInvocation(wsName, invocation, stdin, signal, options);
          exitCode = result.exitCode;
          if (result.stderr) outputs.push({ stream: 'stderr', text: result.stderr });
          stdin = result.stdout;
          if (result.exitCode !== 0) break;
        }
        if (stdin) outputs.push({ stream: 'stdout', text: stdin });
        if (exitCode !== 0) break;
      }
    } catch (error) {
      exitCode = 2;
      outputs.push({ stream: 'stderr', text: `${error instanceof Error ? error.message : String(error)}\n` });
    }
    const result = { cwd: this.getCwd(wsName), exitCode, outputs } satisfies TerminalExecutionResult;
    this.recordExecution(wsName, line, origin, cwdBefore, startedAt, result);
    return result;
  }

  private recordExecution(
    wsName: string,
    command: string,
    origin: TerminalExecutionOrigin,
    cwdBefore: string,
    startedAt: number,
    result: TerminalExecutionResult,
  ): void {
    const stdout = result.outputs.filter((item) => item.stream === 'stdout').map((item) => item.text).join('');
    const stderr = result.outputs.filter((item) => item.stream === 'stderr').map((item) => item.text).join('');
    const record: TerminalExecutionRecord = {
      id: `${startedAt}-${++this.executionSequence}`,
      origin,
      command: redactRecordedCommand(command),
      cwdBefore,
      cwdAfter: result.cwd,
      startedAt,
      durationMs: Math.max(0, Date.now() - startedAt),
      exitCode: result.exitCode,
      stdoutTail: tail(stdout),
      stderrTail: tail(stderr),
    };
    const current = this.executionsByWorkspace.get(wsName) ?? [];
    this.executionsByWorkspace.set(wsName, [...current, record].slice(-MAX_EXECUTION_RECORDS));
  }
}
