diff --git a/src/NetscriptFunctions.ts b/src/NetscriptFunctions.ts index 7217912c6..88eec6892 100644 --- a/src/NetscriptFunctions.ts +++ b/src/NetscriptFunctions.ts @@ -902,7 +902,7 @@ export const ns: InternalAPI = { ]; if (!substring) return allFilenames.sort(); - return allFilenames.filter((filename) => filename.includes(substring)).sort(); + return allFilenames.filter((filename) => ("/" + filename).includes(substring)).sort(); }, getRecentScripts: () => (): RecentScript[] => { return recentScripts.map((rs) => ({ diff --git a/src/Terminal/commands/cd.ts b/src/Terminal/commands/cd.ts index c36886f1f..3dd182452 100644 --- a/src/Terminal/commands/cd.ts +++ b/src/Terminal/commands/cd.ts @@ -4,8 +4,8 @@ import { directoryExistsOnServer, resolveDirectory } from "../../Paths/Directory export function cd(args: (string | number | boolean)[], server: BaseServer): void { if (args.length > 1) return Terminal.error("Incorrect number of arguments. Usage: cd [dir]"); - // If no arg was provided, just use "". - const userInput = String(args[0] ?? ""); + // If no arg was provided, just use "/". + const userInput = String(args[0] ?? "/"); const targetDir = resolveDirectory(userInput, Terminal.currDir); // Explicitly checking null due to root being "" if (targetDir === null) return Terminal.error(`Could not resolve directory ${userInput}`); diff --git a/src/Terminal/commands/scp.ts b/src/Terminal/commands/scp.ts index 25c3a04b6..fb0f3b58d 100644 --- a/src/Terminal/commands/scp.ts +++ b/src/Terminal/commands/scp.ts @@ -5,36 +5,62 @@ import { hasScriptExtension } from "../../Paths/ScriptFilePath"; import { hasTextExtension } from "../../Paths/TextFilePath"; import { checkEnum } from "../../utils/helpers/enum"; import { LiteratureName } from "../../Literature/data/LiteratureNames"; +import { ContentFile } from "../../Paths/ContentFile"; export function scp(args: (string | number | boolean)[], server: BaseServer): void { - if (args.length !== 2) { + if (args.length < 2) { return Terminal.error("Incorrect usage of scp command. Usage: scp [source filename] [destination hostname]"); } - const [scriptname, destHostname] = args.map((arg) => arg + ""); - - const path = Terminal.getFilepath(scriptname); - if (!path) return Terminal.error(`Invalid file path: ${scriptname}`); + // Validate destination server + const destHostname = String(args.pop()); const destServer = GetServer(destHostname); - if (!destServer) return Terminal.error(`Invalid destination server: ${args[1]}`); + if (!destServer) return Terminal.error(`Invalid destination server: ${destHostname}`); - // Lit files - if (path.endsWith(".lit")) { - if (!checkEnum(LiteratureName, path) || !server.messages.includes(path)) { - return Terminal.error(`No file at path ${path}`); + // Validate filepaths + const filenames = args.map(String); + const files: (LiteratureName | ContentFile)[] = []; + + // File validation loop, handle all errors before copying any files + for (const filename of filenames) { + const path = Terminal.getFilepath(filename); + if (!path) return Terminal.error(`Invalid file path: ${filename}`); + // Validate .lit files + if (path.endsWith(".lit")) { + if (!checkEnum(LiteratureName, path) || !server.messages.includes(path)) { + return Terminal.error(`scp failed: ${path} does not exist on server ${server.hostname}`); + } + files.push(path); + continue; } - if (destServer.messages.includes(path)) return Terminal.print(`${path} was already on ${destHostname}`); - destServer.messages.push(path); - return Terminal.print(`Copied ${path} to ${destHostname}`); + // Error for invalid filetype + if (!hasScriptExtension(path) && !hasTextExtension(path)) { + return Terminal.error( + `scp failed: ${path} has invalid extension. scp only works for scripts (.js or .script), text files (.txt), and literature files (.lit)`, + ); + } + const sourceContentFile = server.getContentFile(path); + if (!sourceContentFile) return Terminal.error(`scp failed: ${path} does not exist on server ${server.hostname}`); + files.push(sourceContentFile); } - if (!hasScriptExtension(path) && !hasTextExtension(path)) { - return Terminal.error("scp only works for scripts, text files (.txt), and literature files (.lit)"); + // Actually copy the files (no more errors possible) + for (const file of files) { + // Lit files, entire "file" is just the name + if (checkEnum(LiteratureName, file)) { + if (destServer.messages.includes(file)) { + Terminal.print(`${file} was already on ${destHostname}, file skipped`); + continue; + } + destServer.messages.push(file); + Terminal.print(`${file} copied to ${destHostname}`); + continue; + } + + // Content files (script and txt) + const { filename, content } = file; + const { overwritten } = destServer.writeToContentFile(filename, content); + if (overwritten) Terminal.warn(`${filename} already existed on ${destHostname} and was overwritten`); + else Terminal.print(`${filename} copied to ${destHostname}`); } - // Text or script - const source = server.getContentFile(path); - if (!source) return Terminal.error(`No file at path ${path}`); - const { overwritten } = destServer.writeToContentFile(path, source.content); - if (overwritten) Terminal.warn(`${path} already exists on ${destHostname} and will be overwritten`); - Terminal.print(`${path} copied to ${destHostname}`); } diff --git a/src/Terminal/getTabCompletionPossibilities.ts b/src/Terminal/getTabCompletionPossibilities.ts index a0ad70e16..efd20b974 100644 --- a/src/Terminal/getTabCompletionPossibilities.ts +++ b/src/Terminal/getTabCompletionPossibilities.ts @@ -163,7 +163,7 @@ export async function getTabCompletionPossibilities(terminalText: string, baseDi // Just some booleans so the mismatch between command length and arg number are not as confusing. const onCommand = commandLength === 1; const onFirstCommandArg = commandLength === 2; - const onSecondCommandArg = commandLength === 3; + // const onSecondCommandArg = commandLength === 3; // unused // These are always added. addGlobalAliases(); @@ -239,11 +239,12 @@ export async function getTabCompletionPossibilities(terminalText: string, baseDi return possibilities; case "scp": - if (onFirstCommandArg) { - addScripts(); - addTextFiles(); - addLiterature(); - } else if (onSecondCommandArg) addServerNames(); + if (!onFirstCommandArg) { + addServerNames(); + } + addScripts(); + addTextFiles(); + addLiterature(); return possibilities; case "rm": diff --git a/src/Terminal/ui/TerminalInput.tsx b/src/Terminal/ui/TerminalInput.tsx index ba3521e91..a1c99b3f7 100644 --- a/src/Terminal/ui/TerminalInput.tsx +++ b/src/Terminal/ui/TerminalInput.tsx @@ -197,7 +197,7 @@ export function TerminalInput(): React.ReactElement { // Run command. if (event.key === KEY.ENTER && value !== "") { event.preventDefault(); - Terminal.print(`[${Player.getCurrentServer().hostname} ~${Terminal.cwd()}]> ${value}`); + Terminal.print(`[${Player.getCurrentServer().hostname} /${Terminal.cwd()}]> ${value}`); Terminal.executeCommands(value); saveValue(""); return; @@ -282,7 +282,7 @@ export function TerminalInput(): React.ReactElement { if (Settings.EnableBashHotkeys) { if (event.code === KEYCODE.C && event.ctrlKey && ref && ref.selectionStart === ref.selectionEnd) { event.preventDefault(); - Terminal.print(`[${Player.getCurrentServer().hostname} ~${Terminal.cwd()}]> ${value}`); + Terminal.print(`[${Player.getCurrentServer().hostname} /${Terminal.cwd()}]> ${value}`); modifyInput("clearall"); } @@ -361,7 +361,7 @@ export function TerminalInput(): React.ReactElement { className: classes.input, startAdornment: ( - [{Player.getCurrentServer().hostname} ~{Terminal.cwd()}]>  + [{Player.getCurrentServer().hostname} /{Terminal.cwd()}]>  ), spellCheck: false, diff --git a/test/jest/Terminal/tabCompletion.test.ts b/test/jest/Terminal/tabCompletion.test.ts index 83bd812e7..6316a215a 100644 --- a/test/jest/Terminal/tabCompletion.test.ts +++ b/test/jest/Terminal/tabCompletion.test.ts @@ -13,6 +13,7 @@ import { hasScriptExtension } from "../../../src/Paths/ScriptFilePath"; import { LiteratureName } from "../../../src/Literature/data/LiteratureNames"; import { MessageFilename } from "../../../src/Message/MessageHelpers"; import { Terminal } from "../../../src/Terminal"; +import { IPAddress } from "../../../src/Types/strings"; describe("getTabCompletionPossibilities", function () { let closeServer: Server; @@ -23,7 +24,7 @@ describe("getTabCompletionPossibilities", function () { Player.init(); closeServer = new Server({ - ip: "8.8.8.8", + ip: "8.8.8.8" as IPAddress, hostname: "near", hackDifficulty: 1, moneyAvailable: 70000, @@ -33,7 +34,7 @@ describe("getTabCompletionPossibilities", function () { serverGrowth: 3000, }); farServer = new Server({ - ip: "4.4.4.4", + ip: "4.4.4.4" as IPAddress, hostname: "far", hackDifficulty: 1, moneyAvailable: 70000, @@ -87,23 +88,22 @@ describe("getTabCompletionPossibilities", function () { it("completes the scp command", async () => { writeFiles(); let options = await getTabCompletionPossibilities("scp ", root); - expect(options.sort()).toEqual( - [ - "note.txt", - "folder1/text.txt", - "folder1/text2.txt", - "hack.js", - "weaken.js", - "grow.js", - "old.script", - "folder1/test.js", - "anotherFolder/win.js", - LiteratureName.AGreenTomorrow, - ].sort(), - ); + const filesToMatch = [ + "note.txt", + "folder1/text.txt", + "folder1/text2.txt", + "hack.js", + "weaken.js", + "grow.js", + "old.script", + "folder1/test.js", + "anotherFolder/win.js", + LiteratureName.AGreenTomorrow, + ]; + expect(options.sort()).toEqual(filesToMatch.sort()); // Test the second command argument (server name) options = await getTabCompletionPossibilities("scp note.txt ", root); - expect(options).toEqual(["home", "near", "far"]); + expect(options.sort()).toEqual(["home", "near", "far", ...filesToMatch].sort()); }); it("completes the kill, tail, mem, and check commands", async () => {