583 lines
21 KiB
TypeScript
583 lines
21 KiB
TypeScript
import { execFile } from 'child_process'
|
|
import { execa } from 'execa'
|
|
import { mkdir, stat } from 'fs/promises'
|
|
import * as os from 'os'
|
|
import { join } from 'path'
|
|
import { logEvent } from 'src/services/analytics/index.js'
|
|
import { registerCleanup } from '../cleanupRegistry.js'
|
|
import { getCwd } from '../cwd.js'
|
|
import { logForDebugging } from '../debug.js'
|
|
import {
|
|
embeddedSearchToolsBinaryPath,
|
|
hasEmbeddedSearchTools,
|
|
} from '../embeddedTools.js'
|
|
import { getClaudeConfigHomeDir } from '../envUtils.js'
|
|
import { pathExists } from '../file.js'
|
|
import { getFsImplementation } from '../fsOperations.js'
|
|
import { logError } from '../log.js'
|
|
import { getPlatform } from '../platform.js'
|
|
import { ripgrepCommand } from '../ripgrep.js'
|
|
import { subprocessEnv } from '../subprocessEnv.js'
|
|
import { quote } from './shellQuote.js'
|
|
|
|
const LITERAL_BACKSLASH = '\\'
|
|
const SNAPSHOT_CREATION_TIMEOUT = 10000 // 10 seconds
|
|
|
|
/**
|
|
* Creates a shell function that invokes `binaryPath` with a specific argv[0].
|
|
* This uses the bun-internal ARGV0 dispatch trick: the bun binary checks its
|
|
* argv[0] and runs the embedded tool (rg, bfs, ugrep) that matches.
|
|
*
|
|
* @param prependArgs - Arguments to inject before the user's args (e.g.,
|
|
* default flags). Injected literally; each element must be a valid shell
|
|
* word (no spaces/special chars).
|
|
*/
|
|
function createArgv0ShellFunction(
|
|
funcName: string,
|
|
argv0: string,
|
|
binaryPath: string,
|
|
prependArgs: string[] = [],
|
|
): string {
|
|
const quotedPath = quote([binaryPath])
|
|
const argSuffix =
|
|
prependArgs.length > 0 ? `${prependArgs.join(' ')} "$@"` : '"$@"'
|
|
return [
|
|
`function ${funcName} {`,
|
|
' if [[ -n $ZSH_VERSION ]]; then',
|
|
` ARGV0=${argv0} ${quotedPath} ${argSuffix}`,
|
|
' elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "win32" ]]; then',
|
|
// On Windows (git bash), exec -a does not work, so use ARGV0 env var instead
|
|
// The bun binary reads from ARGV0 natively to set argv[0]
|
|
` ARGV0=${argv0} ${quotedPath} ${argSuffix}`,
|
|
' elif [[ $BASHPID != $$ ]]; then',
|
|
` exec -a ${argv0} ${quotedPath} ${argSuffix}`,
|
|
' else',
|
|
` (exec -a ${argv0} ${quotedPath} ${argSuffix})`,
|
|
' fi',
|
|
'}',
|
|
].join('\n')
|
|
}
|
|
|
|
/**
|
|
* Creates ripgrep shell integration (alias or function)
|
|
* @returns Object with type and the shell snippet to use
|
|
*/
|
|
export function createRipgrepShellIntegration(): {
|
|
type: 'alias' | 'function'
|
|
snippet: string
|
|
} {
|
|
const rgCommand = ripgrepCommand()
|
|
|
|
// For embedded ripgrep (bun-internal), we need a shell function that sets argv0
|
|
if (rgCommand.argv0) {
|
|
return {
|
|
type: 'function',
|
|
snippet: createArgv0ShellFunction(
|
|
'rg',
|
|
rgCommand.argv0,
|
|
rgCommand.rgPath,
|
|
),
|
|
}
|
|
}
|
|
|
|
// For regular ripgrep, use a simple alias target
|
|
const quotedPath = quote([rgCommand.rgPath])
|
|
const quotedArgs = rgCommand.rgArgs.map(arg => quote([arg]))
|
|
const aliasTarget =
|
|
rgCommand.rgArgs.length > 0
|
|
? `${quotedPath} ${quotedArgs.join(' ')}`
|
|
: quotedPath
|
|
|
|
return { type: 'alias', snippet: aliasTarget }
|
|
}
|
|
|
|
/**
|
|
* VCS directories to exclude from grep searches. Matches the list in
|
|
* GrepTool (see GrepTool.ts: VCS_DIRECTORIES_TO_EXCLUDE).
|
|
*/
|
|
const VCS_DIRECTORIES_TO_EXCLUDE = [
|
|
'.git',
|
|
'.svn',
|
|
'.hg',
|
|
'.bzr',
|
|
'.jj',
|
|
'.sl',
|
|
] as const
|
|
|
|
/**
|
|
* Creates shell integration for `find` and `grep`, backed by bfs and ugrep
|
|
* embedded in the bun binary (ant-native only). Unlike the rg integration,
|
|
* this always shadows the system find/grep since bfs/ugrep are drop-in
|
|
* replacements and we want consistent fast behavior.
|
|
*
|
|
* These wrappers replace the GlobTool/GrepTool dedicated tools (which are
|
|
* removed from the tool registry when embedded search tools are available),
|
|
* so they're tuned to match those tools' semantics, not GNU find/grep.
|
|
*
|
|
* `find` ↔ GlobTool:
|
|
* - Inject `-regextype findutils-default`: bfs defaults to POSIX BRE for
|
|
* -regex, but GNU find defaults to emacs-flavor (which supports `\|`
|
|
* alternation). Without this, `find . -regex '.*\.\(js\|ts\)'` silently
|
|
* returns zero results. A later user-supplied -regextype still overrides.
|
|
* - No gitignore filtering: GlobTool passes `--no-ignore` to rg. bfs has no
|
|
* gitignore support anyway, so this matches by default.
|
|
* - Hidden files included: both GlobTool (`--hidden`) and bfs's default.
|
|
*
|
|
* Caveat: even with findutils-default, Oniguruma (bfs's regex engine) uses
|
|
* leftmost-first alternation, not POSIX leftmost-longest. Patterns where
|
|
* one alternative is a prefix of another (e.g., `\(ts\|tsx\)`) may miss
|
|
* matches that GNU find catches. Workaround: put the longer alternative first.
|
|
*
|
|
* `grep` ↔ GrepTool (file filtering) + GNU grep (regex syntax):
|
|
* - `-G` (basic regex / BRE): GNU grep defaults to BRE where `\|` is
|
|
* alternation. ugrep defaults to ERE where `|` is alternation and `\|` is a
|
|
* literal pipe. Without -G, `grep "foo\|bar"` silently returns zero results.
|
|
* User-supplied `-E`, `-F`, or `-P` later in argv overrides this.
|
|
* - `--ignore-files`: respect .gitignore (GrepTool uses rg's default, which
|
|
* respects gitignore). Override with `grep --no-ignore-files`.
|
|
* - `--hidden`: include hidden files (GrepTool passes `--hidden` to rg).
|
|
* Override with `grep --no-hidden`.
|
|
* - `--exclude-dir` for VCS dirs: GrepTool passes `--glob '!.git'` etc. to rg.
|
|
* - `-I`: skip binary files. rg's recursion silently skips binary matches
|
|
* by default (different from direct-file-arg behavior); ugrep doesn't, so
|
|
* we inject -I to match. Override with `grep -a`.
|
|
*
|
|
* Not replicated from GrepTool:
|
|
* - `--max-columns 500`: ugrep's `--width` hard-truncates output which could
|
|
* break pipelines; rg's version replaces the line with a placeholder.
|
|
* - Read deny rules / plugin cache exclusions: require toolPermissionContext
|
|
* which isn't available at shell-snapshot creation time.
|
|
*
|
|
* Returns null if embedded search tools are not available in this build.
|
|
*/
|
|
export function createFindGrepShellIntegration(): string | null {
|
|
if (!hasEmbeddedSearchTools()) {
|
|
return null
|
|
}
|
|
const binaryPath = embeddedSearchToolsBinaryPath()
|
|
return [
|
|
// User shell configs may define aliases like `alias find=gfind` or
|
|
// `alias grep=ggrep` (common on macOS with Homebrew GNU tools). The
|
|
// snapshot sources user aliases before these function definitions, and
|
|
// bash expands aliases before function lookup — so a renaming alias
|
|
// would silently bypass the embedded bfs/ugrep dispatch. Clear them first
|
|
// (same fix the rg integration uses).
|
|
'unalias find 2>/dev/null || true',
|
|
'unalias grep 2>/dev/null || true',
|
|
createArgv0ShellFunction('find', 'bfs', binaryPath, [
|
|
'-regextype',
|
|
'findutils-default',
|
|
]),
|
|
createArgv0ShellFunction('grep', 'ugrep', binaryPath, [
|
|
'-G',
|
|
'--ignore-files',
|
|
'--hidden',
|
|
'-I',
|
|
...VCS_DIRECTORIES_TO_EXCLUDE.map(d => `--exclude-dir=${d}`),
|
|
]),
|
|
].join('\n')
|
|
}
|
|
|
|
function getConfigFile(shellPath: string): string {
|
|
const fileName = shellPath.includes('zsh')
|
|
? '.zshrc'
|
|
: shellPath.includes('bash')
|
|
? '.bashrc'
|
|
: '.profile'
|
|
|
|
const configPath = join(os.homedir(), fileName)
|
|
|
|
return configPath
|
|
}
|
|
|
|
/**
|
|
* Generates user-specific snapshot content (functions, options, aliases)
|
|
* This content is derived from the user's shell configuration file
|
|
*/
|
|
function getUserSnapshotContent(configFile: string): string {
|
|
const isZsh = configFile.endsWith('.zshrc')
|
|
|
|
let content = ''
|
|
|
|
// User functions
|
|
if (isZsh) {
|
|
content += `
|
|
echo "# Functions" >> "$SNAPSHOT_FILE"
|
|
|
|
# Force autoload all functions first
|
|
typeset -f > /dev/null 2>&1
|
|
|
|
# Now get user function names - filter completion functions (single underscore prefix)
|
|
# but keep double-underscore helpers (e.g. __zsh_like_cd from mise, __pyenv_init)
|
|
typeset +f | grep -vE '^_[^_]' | while read func; do
|
|
typeset -f "$func" >> "$SNAPSHOT_FILE"
|
|
done
|
|
`
|
|
} else {
|
|
content += `
|
|
echo "# Functions" >> "$SNAPSHOT_FILE"
|
|
|
|
# Force autoload all functions first
|
|
declare -f > /dev/null 2>&1
|
|
|
|
# Now get user function names - filter completion functions (single underscore prefix)
|
|
# but keep double-underscore helpers (e.g. __zsh_like_cd from mise, __pyenv_init)
|
|
declare -F | cut -d' ' -f3 | grep -vE '^_[^_]' | while read func; do
|
|
# Encode the function to base64, preserving all special characters
|
|
encoded_func=$(declare -f "$func" | base64 )
|
|
# Write the function definition to the snapshot
|
|
echo "eval ${LITERAL_BACKSLASH}"${LITERAL_BACKSLASH}$(echo '$encoded_func' | base64 -d)${LITERAL_BACKSLASH}" > /dev/null 2>&1" >> "$SNAPSHOT_FILE"
|
|
done
|
|
`
|
|
}
|
|
|
|
// Shell options
|
|
if (isZsh) {
|
|
content += `
|
|
echo "# Shell Options" >> "$SNAPSHOT_FILE"
|
|
setopt | sed 's/^/setopt /' | head -n 1000 >> "$SNAPSHOT_FILE"
|
|
`
|
|
} else {
|
|
content += `
|
|
echo "# Shell Options" >> "$SNAPSHOT_FILE"
|
|
shopt -p | head -n 1000 >> "$SNAPSHOT_FILE"
|
|
set -o | grep "on" | awk '{print "set -o " $1}' | head -n 1000 >> "$SNAPSHOT_FILE"
|
|
echo "shopt -s expand_aliases" >> "$SNAPSHOT_FILE"
|
|
`
|
|
}
|
|
|
|
// User aliases
|
|
content += `
|
|
echo "# Aliases" >> "$SNAPSHOT_FILE"
|
|
# Filter out winpty aliases on Windows to avoid "stdin is not a tty" errors
|
|
# Git Bash automatically creates aliases like "alias node='winpty node.exe'" for
|
|
# programs that need Win32 Console in mintty, but winpty fails when there's no TTY
|
|
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then
|
|
alias | grep -v "='winpty " | sed 's/^alias //g' | sed 's/^/alias -- /' | head -n 1000 >> "$SNAPSHOT_FILE"
|
|
else
|
|
alias | sed 's/^alias //g' | sed 's/^/alias -- /' | head -n 1000 >> "$SNAPSHOT_FILE"
|
|
fi
|
|
`
|
|
|
|
return content
|
|
}
|
|
|
|
/**
|
|
* Generates Claude Code specific snapshot content
|
|
* This content is always included regardless of user configuration
|
|
*/
|
|
async function getClaudeCodeSnapshotContent(): Promise<string> {
|
|
// Get the appropriate PATH based on platform
|
|
let pathValue = process.env.PATH
|
|
if (getPlatform() === 'windows') {
|
|
// On Windows with git-bash, read the Cygwin PATH
|
|
const cygwinResult = await execa('echo $PATH', {
|
|
shell: true,
|
|
reject: false,
|
|
})
|
|
if (cygwinResult.exitCode === 0 && cygwinResult.stdout) {
|
|
pathValue = cygwinResult.stdout.trim()
|
|
}
|
|
// Fall back to process.env.PATH if we can't get Cygwin PATH
|
|
}
|
|
|
|
const rgIntegration = createRipgrepShellIntegration()
|
|
|
|
let content = ''
|
|
|
|
// Check if rg is available, if not create an alias/function to bundled ripgrep
|
|
// We use a subshell to unalias rg before checking, so that user aliases like
|
|
// `alias rg='rg --smart-case'` don't shadow the real binary check. The subshell
|
|
// ensures we don't modify the user's aliases in the parent shell.
|
|
content += `
|
|
# Check for rg availability
|
|
echo "# Check for rg availability" >> "$SNAPSHOT_FILE"
|
|
echo "if ! (unalias rg 2>/dev/null; command -v rg) >/dev/null 2>&1; then" >> "$SNAPSHOT_FILE"
|
|
`
|
|
|
|
if (rgIntegration.type === 'function') {
|
|
// For embedded ripgrep, write the function definition using heredoc
|
|
content += `
|
|
cat >> "$SNAPSHOT_FILE" << 'RIPGREP_FUNC_END'
|
|
${rgIntegration.snippet}
|
|
RIPGREP_FUNC_END
|
|
`
|
|
} else {
|
|
// For regular ripgrep, write a simple alias
|
|
const escapedSnippet = rgIntegration.snippet.replace(/'/g, "'\\''")
|
|
content += `
|
|
echo ' alias rg='"'${escapedSnippet}'" >> "$SNAPSHOT_FILE"
|
|
`
|
|
}
|
|
|
|
content += `
|
|
echo "fi" >> "$SNAPSHOT_FILE"
|
|
`
|
|
|
|
// For ant-native builds, shadow find/grep with bfs/ugrep embedded in the bun
|
|
// binary. Unlike rg (which only activates if system rg is absent), we always
|
|
// shadow find/grep since bfs/ugrep are drop-in replacements and we want
|
|
// consistent fast behavior in Claude's shell.
|
|
const findGrepIntegration = createFindGrepShellIntegration()
|
|
if (findGrepIntegration !== null) {
|
|
content += `
|
|
# Shadow find/grep with embedded bfs/ugrep (ant-native only)
|
|
echo "# Shadow find/grep with embedded bfs/ugrep" >> "$SNAPSHOT_FILE"
|
|
cat >> "$SNAPSHOT_FILE" << 'FIND_GREP_FUNC_END'
|
|
${findGrepIntegration}
|
|
FIND_GREP_FUNC_END
|
|
`
|
|
}
|
|
|
|
// Add PATH to the file
|
|
content += `
|
|
|
|
# Add PATH to the file
|
|
echo "export PATH=${quote([pathValue || ''])}" >> "$SNAPSHOT_FILE"
|
|
`
|
|
|
|
return content
|
|
}
|
|
|
|
/**
|
|
* Creates the appropriate shell script for capturing environment
|
|
*/
|
|
async function getSnapshotScript(
|
|
shellPath: string,
|
|
snapshotFilePath: string,
|
|
configFileExists: boolean,
|
|
): Promise<string> {
|
|
const configFile = getConfigFile(shellPath)
|
|
const isZsh = configFile.endsWith('.zshrc')
|
|
|
|
// Generate the user content and Claude Code content
|
|
const userContent = configFileExists
|
|
? getUserSnapshotContent(configFile)
|
|
: !isZsh
|
|
? // we need to manually force alias expansion in bash - normally `getUserSnapshotContent` takes care of this
|
|
'echo "shopt -s expand_aliases" >> "$SNAPSHOT_FILE"'
|
|
: ''
|
|
const claudeCodeContent = await getClaudeCodeSnapshotContent()
|
|
|
|
const script = `SNAPSHOT_FILE=${quote([snapshotFilePath])}
|
|
${configFileExists ? `source "${configFile}" < /dev/null` : '# No user config file to source'}
|
|
|
|
# First, create/clear the snapshot file
|
|
echo "# Snapshot file" >| "$SNAPSHOT_FILE"
|
|
|
|
# When this file is sourced, we first unalias to avoid conflicts
|
|
# This is necessary because aliases get "frozen" inside function definitions at definition time,
|
|
# which can cause unexpected behavior when functions use commands that conflict with aliases
|
|
echo "# Unset all aliases to avoid conflicts with functions" >> "$SNAPSHOT_FILE"
|
|
echo "unalias -a 2>/dev/null || true" >> "$SNAPSHOT_FILE"
|
|
|
|
${userContent}
|
|
|
|
${claudeCodeContent}
|
|
|
|
# Exit silently on success, only report errors
|
|
if [ ! -f "$SNAPSHOT_FILE" ]; then
|
|
echo "Error: Snapshot file was not created at $SNAPSHOT_FILE" >&2
|
|
exit 1
|
|
fi
|
|
`
|
|
|
|
return script
|
|
}
|
|
|
|
/**
|
|
* Creates and saves the shell environment snapshot by loading the user's shell configuration
|
|
*
|
|
* This function is a critical part of Claude CLI's shell integration strategy. It:
|
|
*
|
|
* 1. Identifies the user's shell config file (.zshrc, .bashrc, etc.)
|
|
* 2. Creates a temporary script that sources this configuration file
|
|
* 3. Captures the resulting shell environment state including:
|
|
* - Functions defined in the user's shell configuration
|
|
* - Shell options and settings that affect command behavior
|
|
* - Aliases that the user has defined
|
|
*
|
|
* The snapshot is saved to a temporary file that can be sourced by subsequent shell
|
|
* commands, ensuring they run with the user's expected environment, aliases, and functions.
|
|
*
|
|
* This approach allows Claude CLI to execute commands as if they were run in the user's
|
|
* interactive shell, while avoiding the overhead of creating a new login shell for each command.
|
|
* It handles both Bash and Zsh shells with their different syntax for functions, options, and aliases.
|
|
*
|
|
* If the snapshot creation fails (e.g., timeout, permissions issues), the CLI will still
|
|
* function but without the user's custom shell environment, potentially missing aliases
|
|
* and functions the user relies on.
|
|
*
|
|
* @returns Promise that resolves to the snapshot file path or undefined if creation failed
|
|
*/
|
|
export const createAndSaveSnapshot = async (
|
|
binShell: string,
|
|
): Promise<string | undefined> => {
|
|
const shellType = binShell.includes('zsh')
|
|
? 'zsh'
|
|
: binShell.includes('bash')
|
|
? 'bash'
|
|
: 'sh'
|
|
|
|
logForDebugging(`Creating shell snapshot for ${shellType} (${binShell})`)
|
|
|
|
return new Promise(async resolve => {
|
|
try {
|
|
const configFile = getConfigFile(binShell)
|
|
logForDebugging(`Looking for shell config file: ${configFile}`)
|
|
const configFileExists = await pathExists(configFile)
|
|
|
|
if (!configFileExists) {
|
|
logForDebugging(
|
|
`Shell config file not found: ${configFile}, creating snapshot with Claude Code defaults only`,
|
|
)
|
|
}
|
|
|
|
// Create unique snapshot path with timestamp and random ID
|
|
const timestamp = Date.now()
|
|
const randomId = Math.random().toString(36).substring(2, 8)
|
|
const snapshotsDir = join(getClaudeConfigHomeDir(), 'shell-snapshots')
|
|
logForDebugging(`Snapshots directory: ${snapshotsDir}`)
|
|
const shellSnapshotPath = join(
|
|
snapshotsDir,
|
|
`snapshot-${shellType}-${timestamp}-${randomId}.sh`,
|
|
)
|
|
|
|
// Ensure snapshots directory exists
|
|
await mkdir(snapshotsDir, { recursive: true })
|
|
|
|
const snapshotScript = await getSnapshotScript(
|
|
binShell,
|
|
shellSnapshotPath,
|
|
configFileExists,
|
|
)
|
|
logForDebugging(`Creating snapshot at: ${shellSnapshotPath}`)
|
|
logForDebugging(`Execution timeout: ${SNAPSHOT_CREATION_TIMEOUT}ms`)
|
|
execFile(
|
|
binShell,
|
|
['-c', '-l', snapshotScript],
|
|
{
|
|
env: {
|
|
...((process.env.CLAUDE_CODE_DONT_INHERIT_ENV
|
|
? {}
|
|
: subprocessEnv()) as typeof process.env),
|
|
SHELL: binShell,
|
|
GIT_EDITOR: 'true',
|
|
CLAUDECODE: '1',
|
|
},
|
|
timeout: SNAPSHOT_CREATION_TIMEOUT,
|
|
maxBuffer: 1024 * 1024, // 1MB buffer
|
|
encoding: 'utf8',
|
|
},
|
|
async (error, stdout, stderr) => {
|
|
if (error) {
|
|
const execError = error as Error & {
|
|
killed?: boolean
|
|
signal?: string
|
|
code?: number
|
|
}
|
|
logForDebugging(`Shell snapshot creation failed: ${error.message}`)
|
|
logForDebugging(`Error details:`)
|
|
logForDebugging(` - Error code: ${execError?.code}`)
|
|
logForDebugging(` - Error signal: ${execError?.signal}`)
|
|
logForDebugging(` - Error killed: ${execError?.killed}`)
|
|
logForDebugging(` - Shell path: ${binShell}`)
|
|
logForDebugging(` - Config file: ${getConfigFile(binShell)}`)
|
|
logForDebugging(` - Config file exists: ${configFileExists}`)
|
|
logForDebugging(` - Working directory: ${getCwd()}`)
|
|
logForDebugging(` - Claude home: ${getClaudeConfigHomeDir()}`)
|
|
logForDebugging(`Full snapshot script:\n${snapshotScript}`)
|
|
if (stdout) {
|
|
logForDebugging(
|
|
`stdout output (${stdout.length} chars):\n${stdout}`,
|
|
)
|
|
} else {
|
|
logForDebugging(`No stdout output captured`)
|
|
}
|
|
if (stderr) {
|
|
logForDebugging(
|
|
`stderr output (${stderr.length} chars): ${stderr}`,
|
|
)
|
|
} else {
|
|
logForDebugging(`No stderr output captured`)
|
|
}
|
|
logError(
|
|
new Error(`Failed to create shell snapshot: ${error.message}`),
|
|
)
|
|
// Convert signal name to number if present
|
|
const signalNumber = execError?.signal
|
|
? os.constants.signals[
|
|
execError.signal as keyof typeof os.constants.signals
|
|
]
|
|
: undefined
|
|
logEvent('tengu_shell_snapshot_failed', {
|
|
stderr_length: stderr?.length || 0,
|
|
has_error_code: !!execError?.code,
|
|
error_signal_number: signalNumber,
|
|
error_killed: execError?.killed,
|
|
})
|
|
resolve(undefined)
|
|
} else {
|
|
let snapshotSize: number | undefined
|
|
try {
|
|
snapshotSize = (await stat(shellSnapshotPath)).size
|
|
} catch {
|
|
// Snapshot file not found
|
|
}
|
|
|
|
if (snapshotSize !== undefined) {
|
|
logForDebugging(
|
|
`Shell snapshot created successfully (${snapshotSize} bytes)`,
|
|
)
|
|
|
|
// Register cleanup to remove snapshot on graceful shutdown
|
|
registerCleanup(async () => {
|
|
try {
|
|
await getFsImplementation().unlink(shellSnapshotPath)
|
|
logForDebugging(
|
|
`Cleaned up session snapshot: ${shellSnapshotPath}`,
|
|
)
|
|
} catch (error) {
|
|
logForDebugging(
|
|
`Error cleaning up session snapshot: ${error}`,
|
|
)
|
|
}
|
|
})
|
|
|
|
resolve(shellSnapshotPath)
|
|
} else {
|
|
logForDebugging(
|
|
`Shell snapshot file not found after creation: ${shellSnapshotPath}`,
|
|
)
|
|
logForDebugging(
|
|
`Checking if parent directory still exists: ${snapshotsDir}`,
|
|
)
|
|
try {
|
|
const dirContents =
|
|
await getFsImplementation().readdir(snapshotsDir)
|
|
logForDebugging(
|
|
`Directory contains ${dirContents.length} files`,
|
|
)
|
|
} catch {
|
|
logForDebugging(
|
|
`Parent directory does not exist or is not accessible: ${snapshotsDir}`,
|
|
)
|
|
}
|
|
logEvent('tengu_shell_unknown_error', {})
|
|
resolve(undefined)
|
|
}
|
|
}
|
|
},
|
|
)
|
|
} catch (error) {
|
|
logForDebugging(`Unexpected error during snapshot creation: ${error}`)
|
|
if (error instanceof Error) {
|
|
logForDebugging(`Error stack trace: ${error.stack}`)
|
|
}
|
|
logError(error)
|
|
logEvent('tengu_shell_snapshot_error', {})
|
|
resolve(undefined)
|
|
}
|
|
})
|
|
}
|