#!/usr/bin/perl
# Q Cloud -- patches continue.continue's installed extension.js so its
# run_terminal_command tool actually presses Enter and captures real
# stdout/stderr/exit status over a VS Code Remote-SSH connection.
#
# Mac/Linux counterpart to continue-remote-ssh-patch.ps1 -- same fix,
# same anchors, same safety properties. See that file's header comment
# for the full explanation of the bug. Perl is used because it ships by
# default on macOS and virtually every Linux distribution, so this asks
# nothing extra of the machine it runs on.
#
# SAFE TO RUN MORE THAN ONCE: checks whether the file is already patched
# and does nothing if so. SAFE TO RUN AFTER A CONTINUE UPDATE: re-run
# this any time Continue updates and you want the fix back. A
# timestamped backup is made before any change, and nothing is written
# if the anchors below aren't found exactly where expected -- the
# script explains why and stops rather than guessing.
#
# Usage:
#   perl continue-remote-ssh-patch.pl

use strict;
use warnings;
use File::Copy qw(copy);

my $ext_root = "$ENV{HOME}/.vscode/extensions";
opendir(my $dh, $ext_root) or die "Could not open $ext_root -- is VS Code installed for this user?\n";
my @candidates = sort grep { /^continue\.continue-/ } readdir($dh);
closedir($dh);

if (!@candidates) {
    die "Could not find a continue.continue-* folder under $ext_root -- is the Continue extension installed?\n";
}

my $dir = $candidates[-1];
my $target = "$ext_root/$dir/out/extension.js";

if (!-f $target) {
    die "Found $dir but no out/extension.js inside it -- this build may be structured differently than expected.\n";
}

print "Found: $target\n";

open(my $fh, '<:raw', $target) or die "Could not read $target: $!\n";
local $/;
my $text = <$fh>;
close($fh);

if (index($text, "runCommandCapturingOutput") != -1) {
    print "Already patched -- nothing to do.\n";
    exit 0;
}

# ---- anchor 1: the exact end of runCommand(), a plain literal search  ----
# (no regex at all here -- a literal index() search costs nothing no
# matter how large the file is, which is not true of a backtracking
# regex run over the whole thing; that mistake is what made the first
# draft of the Windows counterpart take minutes on a 55MB file).
my $anchor1 = "terminal.show();\n        terminal.sendText(command, false);\n      }";
my $anchor1_pos = index($text, $anchor1);
if ($anchor1_pos == -1) {
    die "Anchor 1 (runCommand's ending) not found -- stopping without changing anything. " .
        "This Continue version may differ from the one this script was written against; " .
        "send the exact Continue version to Q for an updated patch.\n";
}
# Make sure it is not present more than once -- a second occurrence
# would mean this anchor is not unique in this build, and inserting at
# the first match blindly would be a guess.
if (index($text, $anchor1, $anchor1_pos + 1) != -1) {
    die "Anchor 1 matched more than once -- stopping without changing anything.\n";
}

# ---- discover the local alias this build uses for the vscode module ----
# Only a small bounded window right before the anchor, not the whole
# (multi-megabyte) preceding file -- same reasoning as above.
my $lookback = 400;
my $window_start = $anchor1_pos - $lookback;
$window_start = 0 if $window_start < 0;
my $window = substr($text, $window_start, $anchor1_pos - $window_start);
my ($alias) = $window =~ /(\w+)\.window\.createTerminal\(options\?\.terminalName\)/;
if (!$alias) {
    die "Could not determine the vscode module alias -- stopping without changing anything.\n";
}
print "vscode module alias in this build: $alias\n";

# ---- anchor 2: the WHOLE fallback statement, also a plain literal   ----
# search. This must start at "await extras.ide.runCommand(command);",
# not partway into the object literal below it (a short anchor landing
# mid-object was an earlier bug here: the new code spliced in front of
# "content:" instead of in front of the whole statement, breaking the
# object literal it landed inside).
my $anchor2 = "await extras.ide.runCommand(command);\n      return [\n        {\n          name: \"Terminal\",\n          description: \"Terminal command output\",\n          content: \"Command executed in remote terminal. Output capture is not yet available for remote environments.\",\n          status: \"Command executed\"\n        }\n      ];";
my $anchor2_pos = index($text, $anchor2);
if ($anchor2_pos == -1) {
    die "Anchor 2 (the fallback message) not found -- stopping without changing anything.\n";
}
if (index($text, $anchor2, $anchor2_pos + 1) != -1) {
    die "Anchor 2 matched more than once -- stopping without changing anything.\n";
}

# ---- build the new method, against a placeholder for the alias -------
my $new_method = <<'END_METHOD';
      // PATCHED (Q Cloud, local): adds real Remote-SSH output capture via
      // VS Code's Shell Integration API, and actually presses Enter.
      async runCommandCapturingOutput(command, options = { reuseTerminal: true }) {
        let terminal;
        if (__VSCODE_ALIAS__.window.terminals.length && options.reuseTerminal) {
          if (options.terminalName) {
            terminal = __VSCODE_ALIAS__.window.terminals.find(
              (t9) => t9?.name === options.terminalName
            );
          } else {
            terminal = __VSCODE_ALIAS__.window.activeTerminal ?? __VSCODE_ALIAS__.window.terminals[0];
          }
        }
        if (!terminal) {
          terminal = __VSCODE_ALIAS__.window.createTerminal(options?.terminalName);
        }
        terminal.show();
        let integration = terminal.shellIntegration;
        if (!integration) {
          integration = await new Promise((resolvePatched) => {
            const timeoutId = setTimeout(() => {
              sub.dispose();
              resolvePatched(void 0);
            }, 5000);
            const sub = __VSCODE_ALIAS__.window.onDidChangeTerminalShellIntegration(
              (e14) => {
                if (e14.terminal === terminal) {
                  clearTimeout(timeoutId);
                  sub.dispose();
                  resolvePatched(e14.shellIntegration);
                }
              }
            );
          });
        }
        if (!integration) {
          terminal.sendText(command, true);
          return [
            {
              name: "Terminal",
              description: "Terminal command output",
              content: "Command sent to the terminal and executed (Enter was pressed), but this terminal has no shell integration active, so its output cannot be read back here. Check the terminal panel directly for the result.",
              status: "Command executed, output not captured"
            }
          ];
        }
        const execution = integration.executeCommand(command);
        // Hoisted so the timeout branch below can dispose it too --
        // left undisposed on a timeout, it stays subscribed on a
        // terminal the next call is very likely to reuse, and can fire
        // against a future unrelated execution.
        let exitSub;
        const exitPromise = new Promise((resolvePatched) => {
          exitSub = __VSCODE_ALIAS__.window.onDidEndTerminalShellExecution((e14) => {
            if (e14.execution === execution) {
              exitSub.dispose();
              resolvePatched(e14.exitCode);
            }
          });
        });
        let output = "";
        const readLoop = (async () => {
          for await (const chunk of execution.read()) {
            output += chunk;
          }
        })();
        const patchedTimeoutMs = 12e4;
        let exitCode;
        let timedOut = false;
        await Promise.race([
          Promise.all([readLoop, exitPromise]).then(([, code]) => {
            exitCode = code;
          }),
          new Promise((resolvePatched) => setTimeout(resolvePatched, patchedTimeoutMs)).then(
            () => {
              timedOut = true;
            }
          )
        ]);
        if (timedOut) {
          exitSub.dispose();
          output += `\n[Timeout after ${Math.round(patchedTimeoutMs / 1e3)}s -- it may still be running]\n`;
        }
        const status = timedOut
          ? "Command may still be running (timed out waiting for it)"
          : exitCode === 0
            ? "Command completed"
            : `Command failed with exit code ${exitCode}`;
        return [
          {
            name: "Terminal",
            description: "Terminal command output",
            content: output,
            status
          }
        ];
      }
END_METHOD

$new_method =~ s/__VSCODE_ALIAS__/$alias/g;

my $new_fallback_prefix = <<'END_FALLBACK';
if (typeof extras.ide.runCommandCapturingOutput === "function") {
        try {
          return await extras.ide.runCommandCapturingOutput(command);
        } catch (patchedError) {
        }
      }
END_FALLBACK

# ---- apply both edits by plain splicing, not regex substitution ----
# (splicing at a known byte offset can't misinterpret $1-style
# backreferences or other special characters that might appear in the
# text being inserted, the way a regex replacement string could).
#
# Whichever anchor sits at the LARGER offset is spliced first, so the
# other anchor's offset -- which is earlier in the file and therefore
# unaffected by an insertion that happens after it -- never needs to be
# adjusted. Getting this backwards silently inserts the fallback text at
# the wrong byte offset, landing mid-identifier elsewhere in the file;
# always splicing the later position first avoids that regardless of
# which anchor happens to come first in a given build.
if ($anchor1_pos > $anchor2_pos) {
    my $insert_at = $anchor1_pos + length($anchor1);
    $text = substr($text, 0, $insert_at) . "\n" . $new_method . substr($text, $insert_at);
    $text = substr($text, 0, $anchor2_pos) . $new_fallback_prefix . "      " . substr($text, $anchor2_pos);
} else {
    $text = substr($text, 0, $anchor2_pos) . $new_fallback_prefix . "      " . substr($text, $anchor2_pos);
    my $insert_at = $anchor1_pos + length($anchor1);
    $text = substr($text, 0, $insert_at) . "\n" . $new_method . substr($text, $insert_at);
}

my @t = localtime();
my $stamp = sprintf("%04d%02d%02d-%02d%02d%02d", $t[5]+1900, $t[4]+1, $t[3], $t[2], $t[1], $t[0]);
my $backup = "$target.orig-backup-$stamp";
copy($target, $backup) or die "Could not write backup $backup: $!\n";
print "Backed up original to: $backup\n";

open(my $wh, '>:raw', $target) or die "Could not write $target: $!\n";
print $wh $text;
close($wh);

print "Patched: $target\n";
print "Now reload VS Code: Cmd/Ctrl+Shift+P -> 'Developer: Reload Window'\n";
