From a4c673afd69f5cd0095d24956882627613f7a385 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 2 Apr 2025 16:51:19 +0000 Subject: [PATCH 01/42] chore: add more git commands --- package.json | 2 +- src/services/codestyle.ts | 3 +-- src/services/git.ts | 14 ++++++++++++++ tests/services/codestyle.test.ts | 3 +-- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 10c4175..145d231 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@appwrite.io/synapse", - "version": "0.2.0", + "version": "0.2.1", "description": "Operating system gateway for remote serverless environments", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/services/codestyle.ts b/src/services/codestyle.ts index e4aa845..07488b3 100644 --- a/src/services/codestyle.ts +++ b/src/services/codestyle.ts @@ -97,8 +97,7 @@ export class CodeStyle { */ async lint(code: string, options: LintOptions): Promise { const eslintOptions = { - useEslintrc: false, - overrideConfig: { + baseConfig: { parser: "@typescript-eslint/parser", rules: options.rules || {}, }, diff --git a/src/services/git.ts b/src/services/git.ts index 2054916..861fa05 100644 --- a/src/services/git.ts +++ b/src/services/git.ts @@ -37,6 +37,20 @@ export class Git { }); } + async init() { + await this.execute(["init"]); + } + + /** + * Add a remote repository + * @param name - The name of the remote (e.g., "origin") + * @param url - The URL of the remote repository + * @returns The output of the git remote add command + */ + async addRemote(name: string, url: string): Promise { + return this.execute(["remote", "add", name, url]); + } + /** * Get the current branch name * @returns The current branch name diff --git a/tests/services/codestyle.test.ts b/tests/services/codestyle.test.ts index b72cd1b..b6797e5 100644 --- a/tests/services/codestyle.test.ts +++ b/tests/services/codestyle.test.ts @@ -184,8 +184,7 @@ describe("CodeStyle", () => { expect(result.issues).toHaveLength(1); expect(result.issues[0].rule).toBe("prefer-const"); expect(MockESLint).toHaveBeenCalledWith({ - useEslintrc: false, - overrideConfig: { + baseConfig: { parser: "@typescript-eslint/parser", rules: customRules, }, From 4d951df47e9e0225901db6308e5cab85c999a9f2 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 2 Apr 2025 17:00:42 +0000 Subject: [PATCH 02/42] fix: error trying to look for eslintrc --- README.md | 2 +- src/services/codestyle.ts | 6 ++++++ tests/services/codestyle.test.ts | 6 ++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8eee851..53b7e57 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Synapse -Operating system gateway for remote serverless environments. Synapse provides a WebSocket-based interface to interact with terminal sessions, manage files, and monitor system resources remotely. +Operating system gateway for remote serverless environments. Synapse provides a WebSocket-based interface to interact with terminal sessions, manage files, monitor system resources, perform Git operations, and more. ## Features diff --git a/src/services/codestyle.ts b/src/services/codestyle.ts index 07488b3..3f87114 100644 --- a/src/services/codestyle.ts +++ b/src/services/codestyle.ts @@ -99,8 +99,14 @@ export class CodeStyle { const eslintOptions = { baseConfig: { parser: "@typescript-eslint/parser", + parserOptions: { + ecmaVersion: 2020, + sourceType: "module", + }, rules: options.rules || {}, }, + useEslintrc: false, + resolvePluginsRelativeTo: __dirname, }; const linter = new ESLint(eslintOptions); diff --git a/tests/services/codestyle.test.ts b/tests/services/codestyle.test.ts index b6797e5..f6b7d4f 100644 --- a/tests/services/codestyle.test.ts +++ b/tests/services/codestyle.test.ts @@ -186,8 +186,14 @@ describe("CodeStyle", () => { expect(MockESLint).toHaveBeenCalledWith({ baseConfig: { parser: "@typescript-eslint/parser", + parserOptions: { + ecmaVersion: 2020, + sourceType: "module", + }, rules: customRules, }, + useEslintrc: false, + resolvePluginsRelativeTo: "/workspace/synapse/src/services", }); }); From 8bdb7d1121dd0345654eda0e07c52d13704aa93b Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 2 Apr 2025 17:13:25 +0000 Subject: [PATCH 03/42] fix: type errors --- src/services/codestyle.ts | 10 ++++------ tests/services/codestyle.test.ts | 8 +++----- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/services/codestyle.ts b/src/services/codestyle.ts index 3f87114..ff0861b 100644 --- a/src/services/codestyle.ts +++ b/src/services/codestyle.ts @@ -97,17 +97,15 @@ export class CodeStyle { */ async lint(code: string, options: LintOptions): Promise { const eslintOptions = { - baseConfig: { - parser: "@typescript-eslint/parser", - parserOptions: { + overrideConfig: { + languageOptions: { ecmaVersion: 2020, sourceType: "module", }, rules: options.rules || {}, }, - useEslintrc: false, - resolvePluginsRelativeTo: __dirname, - }; + overrideConfigFile: true, + } as ESLint.Options; const linter = new ESLint(eslintOptions); const eslintResult = await linter.lintText(code); diff --git a/tests/services/codestyle.test.ts b/tests/services/codestyle.test.ts index f6b7d4f..09fe66e 100644 --- a/tests/services/codestyle.test.ts +++ b/tests/services/codestyle.test.ts @@ -184,16 +184,14 @@ describe("CodeStyle", () => { expect(result.issues).toHaveLength(1); expect(result.issues[0].rule).toBe("prefer-const"); expect(MockESLint).toHaveBeenCalledWith({ - baseConfig: { - parser: "@typescript-eslint/parser", - parserOptions: { + overrideConfig: { + languageOptions: { ecmaVersion: 2020, sourceType: "module", }, rules: customRules, }, - useEslintrc: false, - resolvePluginsRelativeTo: "/workspace/synapse/src/services", + overrideConfigFile: true, }); }); From 336be7d7f7a7ad94f53a21c4d5b1de13f37138c4 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 2 Apr 2025 17:23:02 +0000 Subject: [PATCH 04/42] chore: standardize return format --- src/services/codestyle.ts | 36 ++++++----- src/services/git.ts | 34 +++++++---- tests/services/codestyle.test.ts | 10 +-- tests/services/git.test.ts | 102 ++++++++++++++++++++++++++----- 4 files changed, 134 insertions(+), 48 deletions(-) diff --git a/src/services/codestyle.ts b/src/services/codestyle.ts index ff0861b..5a604b1 100644 --- a/src/services/codestyle.ts +++ b/src/services/codestyle.ts @@ -23,13 +23,15 @@ export interface FormatResult { export interface LintResult { success: boolean; - issues: Array<{ - line: number; - column: number; - severity: "error" | "warning"; - rule: string; - message: string; - }>; + data: { + issues: Array<{ + line: number; + column: number; + severity: "error" | "warning"; + rule: string; + message: string; + }>; + }; } export class CodeStyle { @@ -112,15 +114,17 @@ export class CodeStyle { return { success: true, - issues: eslintResult.flatMap((result) => - result.messages.map((message) => ({ - line: message.line, - column: message.column, - severity: message.severity === 2 ? "error" : "warning", - rule: message.ruleId || "", - message: message.message, - })), - ), + data: { + issues: eslintResult.flatMap((result) => + result.messages.map((message) => ({ + line: message.line, + column: message.column, + severity: message.severity === 2 ? "error" : "warning", + rule: message.ruleId || "", + message: message.message, + })), + ), + }, }; } } diff --git a/src/services/git.ts b/src/services/git.ts index 861fa05..cf95022 100644 --- a/src/services/git.ts +++ b/src/services/git.ts @@ -1,6 +1,11 @@ import { spawn } from "child_process"; import { Synapse } from "../synapse"; +export type GitOperationResult = { + success: boolean; + data: string; +}; + export class Git { private synapse: Synapse; @@ -13,8 +18,8 @@ export class Git { * @param args - The arguments to pass to the git command * @returns The output of the git command */ - private async execute(args: string[]): Promise { - return new Promise((resolve, reject) => { + private async execute(args: string[]): Promise { + return new Promise((resolve) => { const git = spawn("git", args); let output = ""; let errorOutput = ""; @@ -29,16 +34,19 @@ export class Git { git.on("close", (code) => { if (code !== 0) { - reject(new Error(`Git error: ${errorOutput}`)); + resolve({ + success: false, + data: errorOutput.trim() || "Git command failed", + }); } else { - resolve(output.trim()); + resolve({ success: true, data: output.trim() }); } }); }); } - async init() { - await this.execute(["init"]); + async init(): Promise { + return this.execute(["init"]); } /** @@ -47,7 +55,7 @@ export class Git { * @param url - The URL of the remote repository * @returns The output of the git remote add command */ - async addRemote(name: string, url: string): Promise { + async addRemote(name: string, url: string): Promise { return this.execute(["remote", "add", name, url]); } @@ -55,7 +63,7 @@ export class Git { * Get the current branch name * @returns The current branch name */ - async getCurrentBranch(): Promise { + async getCurrentBranch(): Promise { return this.execute(["rev-parse", "--abbrev-ref", "HEAD"]); } @@ -63,7 +71,7 @@ export class Git { * Get the status of the repository * @returns The status of the repository */ - async status(): Promise { + async status(): Promise { return this.execute(["status"]); } @@ -72,7 +80,7 @@ export class Git { * @param files - The files to add to staging * @returns The output of the git add command */ - async add(files: string[]): Promise { + async add(files: string[]): Promise { return this.execute(["add", ...files]); } @@ -81,7 +89,7 @@ export class Git { * @param message - The commit message * @returns The output of the git commit command */ - async commit(message: string): Promise { + async commit(message: string): Promise { return this.execute(["commit", "-m", message]); } @@ -89,7 +97,7 @@ export class Git { * Pull changes from remote * @returns The output of the git pull command */ - async pull(): Promise { + async pull(): Promise { return this.execute(["pull"]); } @@ -97,7 +105,7 @@ export class Git { * Push changes to remote * @returns The output of the git push command */ - async push(): Promise { + async push(): Promise { return this.execute(["push"]); } } diff --git a/tests/services/codestyle.test.ts b/tests/services/codestyle.test.ts index 09fe66e..858e594 100644 --- a/tests/services/codestyle.test.ts +++ b/tests/services/codestyle.test.ts @@ -128,8 +128,8 @@ describe("CodeStyle", () => { const result = await codeStyle.lint(input, { language: "javascript" }); expect(result.success).toBe(true); - expect(result.issues).toHaveLength(1); - expect(result.issues[0]).toEqual({ + expect(result.data.issues).toHaveLength(1); + expect(result.data.issues[0]).toEqual({ line: 1, column: 1, severity: "error", @@ -181,8 +181,8 @@ describe("CodeStyle", () => { }); expect(result.success).toBe(true); - expect(result.issues).toHaveLength(1); - expect(result.issues[0].rule).toBe("prefer-const"); + expect(result.data.issues).toHaveLength(1); + expect(result.data.issues[0].rule).toBe("prefer-const"); expect(MockESLint).toHaveBeenCalledWith({ overrideConfig: { languageOptions: { @@ -231,7 +231,7 @@ describe("CodeStyle", () => { const result = await codeStyle.lint(input, { language: "javascript" }); expect(result.success).toBe(true); - expect(result.issues[0].severity).toBe("warning"); + expect(result.data.issues[0].severity).toBe("warning"); }); }); }); diff --git a/tests/services/git.test.ts b/tests/services/git.test.ts index ab63dcd..7472db7 100644 --- a/tests/services/git.test.ts +++ b/tests/services/git.test.ts @@ -61,7 +61,10 @@ describe("Git Service", () => { const result = await git.getCurrentBranch(); - expect(result).toBe("main"); + expect(result).toEqual({ + success: true, + data: "main", + }); expect(mockSpawn).toHaveBeenCalledWith("git", [ "rev-parse", "--abbrev-ref", @@ -72,9 +75,12 @@ describe("Git Service", () => { it("should handle error when getting current branch", async () => { setupMockProcess("", "fatal: not a git repository", 128); - await expect(git.getCurrentBranch()).rejects.toThrow( - "Git error: fatal: not a git repository", - ); + const result = await git.getCurrentBranch(); + + expect(result).toEqual({ + success: false, + data: "fatal: not a git repository", + }); }); }); @@ -86,38 +92,86 @@ describe("Git Service", () => { const result = await git.status(); - expect(result).toBe(statusOutput); + expect(result).toEqual({ + success: true, + data: statusOutput, + }); expect(mockSpawn).toHaveBeenCalledWith("git", ["status"]); }); + + it("should handle error when repository is not initialized", async () => { + setupMockProcess("", "fatal: not a git repository", 128); + + const result = await git.status(); + + expect(result).toEqual({ + success: false, + data: "fatal: not a git repository", + }); + }); }); describe("add", () => { it("should add files to staging", async () => { setupMockProcess(""); - await git.add(["file1.txt", "file2.txt"]); + const result = await git.add(["file1.txt", "file2.txt"]); + expect(result).toEqual({ + success: true, + data: "", + }); expect(mockSpawn).toHaveBeenCalledWith("git", [ "add", "file1.txt", "file2.txt", ]); }); + + it("should handle error when adding non-existent files", async () => { + setupMockProcess( + "", + "fatal: pathspec 'nonexistent.txt' did not match any files", + 128, + ); + + const result = await git.add(["nonexistent.txt"]); + + expect(result).toEqual({ + success: false, + data: "fatal: pathspec 'nonexistent.txt' did not match any files", + }); + }); }); describe("commit", () => { it("should commit changes with message", async () => { - setupMockProcess("[main abc1234] test commit\n 1 file changed"); + const commitOutput = "[main abc1234] test commit\n 1 file changed"; + setupMockProcess(commitOutput); const result = await git.commit("test commit"); - expect(result).toBe("[main abc1234] test commit\n 1 file changed"); + expect(result).toEqual({ + success: true, + data: commitOutput, + }); expect(mockSpawn).toHaveBeenCalledWith("git", [ "commit", "-m", "test commit", ]); }); + + it("should handle error when there are no changes to commit", async () => { + setupMockProcess("", "nothing to commit, working tree clean", 1); + + const result = await git.commit("test commit"); + + expect(result).toEqual({ + success: false, + data: "nothing to commit, working tree clean", + }); + }); }); describe("pull", () => { @@ -126,9 +180,23 @@ describe("Git Service", () => { const result = await git.pull(); - expect(result).toBe("Already up to date."); + expect(result).toEqual({ + success: true, + data: "Already up to date.", + }); expect(mockSpawn).toHaveBeenCalledWith("git", ["pull"]); }); + + it("should handle error when there is no remote configured", async () => { + setupMockProcess("", "fatal: no remote repository specified", 1); + + const result = await git.pull(); + + expect(result).toEqual({ + success: false, + data: "fatal: no remote repository specified", + }); + }); }); describe("push", () => { @@ -137,20 +205,26 @@ describe("Git Service", () => { const result = await git.push(); - expect(result).toBe("Everything up-to-date"); + expect(result).toEqual({ + success: true, + data: "Everything up-to-date", + }); expect(mockSpawn).toHaveBeenCalledWith("git", ["push"]); }); - it("should handle push error", async () => { + it("should handle error when there is no upstream branch", async () => { setupMockProcess( "", "fatal: The current branch has no upstream branch", 1, ); - await expect(git.push()).rejects.toThrow( - "Git error: fatal: The current branch has no upstream branch", - ); + const result = await git.push(); + + expect(result).toEqual({ + success: false, + data: "fatal: The current branch has no upstream branch", + }); }); }); }); From a36d4a50a9b8e993327bbea9a59184eb011f7146 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 2 Apr 2025 17:45:04 +0000 Subject: [PATCH 05/42] chore: improve git service --- src/services/git.ts | 74 +++++++++++++++++++++- tests/services/git.test.ts | 124 +++++++++++++++++++++++++++++++------ 2 files changed, 177 insertions(+), 21 deletions(-) diff --git a/src/services/git.ts b/src/services/git.ts index cf95022..e7201fb 100644 --- a/src/services/git.ts +++ b/src/services/git.ts @@ -1,4 +1,6 @@ import { spawn } from "child_process"; +import * as fs from "fs"; +import * as path from "path"; import { Synapse } from "../synapse"; export type GitOperationResult = { @@ -8,9 +10,18 @@ export type GitOperationResult = { export class Git { private synapse: Synapse; + private workingDir: string; constructor(synapse: Synapse) { this.synapse = synapse; + this.workingDir = process.cwd(); + } + + /** + * Check if an error is a NodeJS.ErrnoException + */ + private isErrnoException(error: unknown): error is NodeJS.ErrnoException { + return error instanceof Error && "code" in error; } /** @@ -20,7 +31,7 @@ export class Git { */ private async execute(args: string[]): Promise { return new Promise((resolve) => { - const git = spawn("git", args); + const git = spawn("git", args, { cwd: this.workingDir }); let output = ""; let errorOutput = ""; @@ -32,6 +43,13 @@ export class Git { errorOutput += data.toString(); }); + git.on("error", (error: Error) => { + resolve({ + success: false, + data: `Failed to execute git command: ${error.message}`, + }); + }); + git.on("close", (code) => { if (code !== 0) { resolve({ @@ -45,8 +63,60 @@ export class Git { }); } + /** + * Check if the current directory is already a git repository + */ + private async isGitRepository(): Promise { + try { + const gitDir = path.join(this.workingDir, ".git"); + return fs.existsSync(gitDir) && fs.statSync(gitDir).isDirectory(); + } catch (error: unknown) { + if (this.isErrnoException(error)) { + console.error(`Error checking git repository: ${error.message}`); + } + return false; + } + } + + /** + * Initialize a new git repository + * @returns Result of the git init operation + */ async init(): Promise { - return this.execute(["init"]); + try { + // Check if we're already in a git repository + if (await this.isGitRepository()) { + return { + success: false, + data: "Git repository already exists in this directory", + }; + } + + // Check if we have write permissions in the current directory + try { + await fs.promises.access(this.workingDir, fs.constants.W_OK); + } catch (error: unknown) { + if (this.isErrnoException(error)) { + return { + success: false, + data: `No write permission in the current directory: ${error.message}`, + }; + } + return { + success: false, + data: "No write permission in the current directory", + }; + } + + return this.execute(["init"]); + } catch (error: unknown) { + const message = + error instanceof Error ? error.message : "Unknown error occurred"; + return { + success: false, + data: `Failed to initialize git repository: ${message}`, + }; + } } /** diff --git a/tests/services/git.test.ts b/tests/services/git.test.ts index 7472db7..fcae3fe 100644 --- a/tests/services/git.test.ts +++ b/tests/services/git.test.ts @@ -1,21 +1,38 @@ import { jest } from "@jest/globals"; import { spawn } from "child_process"; +import * as fs from "fs"; import { Git, Synapse } from "../../src"; jest.mock("child_process", () => ({ spawn: jest.fn(), })); +jest.mock("fs", () => { + const actual = jest.requireActual("fs") as typeof fs; + return { + ...actual, + existsSync: jest.fn(), + statSync: jest.fn(), + }; +}); + describe("Git Service", () => { let git: Git; let mockSpawn: jest.Mock; let mockSynapse: jest.Mocked; + const mockWorkingDir = "/workspace/synapse"; beforeEach(() => { jest.clearAllMocks(); - mockSynapse = {} as jest.Mocked; + // Mock process.cwd + jest.spyOn(process, "cwd").mockReturnValue(mockWorkingDir); + + // Mock fs.existsSync to return false by default (no git repo) + (fs.existsSync as jest.Mock).mockReturnValue(false); + (fs.statSync as jest.Mock).mockReturnValue({ isDirectory: () => true }); + mockSynapse = {} as jest.Mocked; mockSpawn = spawn as jest.Mock; git = new Git(mockSynapse); }); @@ -55,6 +72,69 @@ describe("Git Service", () => { return mockChildProcess; }; + describe("init", () => { + it("should initialize a new git repository", async () => { + setupMockProcess("Initialized empty Git repository"); + + const result = await git.init(); + + expect(result).toEqual({ + success: true, + data: "Initialized empty Git repository", + }); + expect(mockSpawn).toHaveBeenCalledWith("git", ["init"], { + cwd: mockWorkingDir, + }); + }); + + it("should handle error when repository already exists", async () => { + // Mock fs.existsSync to return true (git repo exists) + (fs.existsSync as jest.Mock).mockReturnValue(true); + + const result = await git.init(); + + expect(result).toEqual({ + success: false, + data: "Git repository already exists in this directory", + }); + }); + }); + + describe("addRemote", () => { + it("should add a remote repository", async () => { + setupMockProcess(""); + + const result = await git.addRemote( + "origin", + "https://github.com/user/repo.git", + ); + + expect(result).toEqual({ + success: true, + data: "", + }); + expect(mockSpawn).toHaveBeenCalledWith( + "git", + ["remote", "add", "origin", "https://github.com/user/repo.git"], + { cwd: mockWorkingDir }, + ); + }); + + it("should handle error when remote already exists", async () => { + setupMockProcess("", "remote origin already exists", 128); + + const result = await git.addRemote( + "origin", + "https://github.com/user/repo.git", + ); + + expect(result).toEqual({ + success: false, + data: "remote origin already exists", + }); + }); + }); + describe("getCurrentBranch", () => { it("should return current branch name", async () => { setupMockProcess("main\n"); @@ -65,11 +145,11 @@ describe("Git Service", () => { success: true, data: "main", }); - expect(mockSpawn).toHaveBeenCalledWith("git", [ - "rev-parse", - "--abbrev-ref", - "HEAD", - ]); + expect(mockSpawn).toHaveBeenCalledWith( + "git", + ["rev-parse", "--abbrev-ref", "HEAD"], + { cwd: mockWorkingDir }, + ); }); it("should handle error when getting current branch", async () => { @@ -96,7 +176,9 @@ describe("Git Service", () => { success: true, data: statusOutput, }); - expect(mockSpawn).toHaveBeenCalledWith("git", ["status"]); + expect(mockSpawn).toHaveBeenCalledWith("git", ["status"], { + cwd: mockWorkingDir, + }); }); it("should handle error when repository is not initialized", async () => { @@ -121,11 +203,11 @@ describe("Git Service", () => { success: true, data: "", }); - expect(mockSpawn).toHaveBeenCalledWith("git", [ - "add", - "file1.txt", - "file2.txt", - ]); + expect(mockSpawn).toHaveBeenCalledWith( + "git", + ["add", "file1.txt", "file2.txt"], + { cwd: mockWorkingDir }, + ); }); it("should handle error when adding non-existent files", async () => { @@ -155,11 +237,11 @@ describe("Git Service", () => { success: true, data: commitOutput, }); - expect(mockSpawn).toHaveBeenCalledWith("git", [ - "commit", - "-m", - "test commit", - ]); + expect(mockSpawn).toHaveBeenCalledWith( + "git", + ["commit", "-m", "test commit"], + { cwd: mockWorkingDir }, + ); }); it("should handle error when there are no changes to commit", async () => { @@ -184,7 +266,9 @@ describe("Git Service", () => { success: true, data: "Already up to date.", }); - expect(mockSpawn).toHaveBeenCalledWith("git", ["pull"]); + expect(mockSpawn).toHaveBeenCalledWith("git", ["pull"], { + cwd: mockWorkingDir, + }); }); it("should handle error when there is no remote configured", async () => { @@ -209,7 +293,9 @@ describe("Git Service", () => { success: true, data: "Everything up-to-date", }); - expect(mockSpawn).toHaveBeenCalledWith("git", ["push"]); + expect(mockSpawn).toHaveBeenCalledWith("git", ["push"], { + cwd: mockWorkingDir, + }); }); it("should handle error when there is no upstream branch", async () => { From 166251daab909e2a24f391749790c43a11432514 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 3 Apr 2025 05:56:14 +0000 Subject: [PATCH 06/42] chore: standardize output --- src/services/codestyle.ts | 110 +++++++++++++++++++++--------- src/services/filesystem.ts | 46 ++++++++++--- src/services/git.ts | 15 ++-- src/services/system.ts | 8 ++- tests/services/filesystem.test.ts | 10 +-- tests/services/git.test.ts | 16 ++--- 6 files changed, 142 insertions(+), 63 deletions(-) diff --git a/src/services/codestyle.ts b/src/services/codestyle.ts index 5a604b1..e6f52a9 100644 --- a/src/services/codestyle.ts +++ b/src/services/codestyle.ts @@ -18,12 +18,13 @@ export interface LintOptions { export interface FormatResult { success: boolean; - data: string; + data?: string; + error?: string; } export interface LintResult { success: boolean; - data: { + data?: { issues: Array<{ line: number; column: number; @@ -32,6 +33,7 @@ export interface LintResult { message: string; }>; }; + error?: string; } export class CodeStyle { @@ -82,13 +84,34 @@ export class CodeStyle { * @returns A promise resolving to the formatting result */ async format(code: string, options: FormatOptions): Promise { - const prettierOptions = this.toPrettierOptions(options.language, options); - const formattedCode = await format(code, prettierOptions); + try { + if (!code || typeof code !== "string") { + return { + success: false, + error: "Invalid code input: code must be a non-empty string", + }; + } - return { - success: true, - data: formattedCode, - }; + if (!options.language) { + return { + success: false, + error: "Language must be specified in format options", + }; + } + + const prettierOptions = this.toPrettierOptions(options.language, options); + const formattedCode = await format(code, prettierOptions); + + return { + success: true, + data: formattedCode, + }; + } catch (error) { + return { + success: false, + error: `Formatting failed: ${error instanceof Error ? error.message : "Unknown error"}`, + }; + } } /** @@ -98,33 +121,54 @@ export class CodeStyle { * @returns A promise resolving to the linting result */ async lint(code: string, options: LintOptions): Promise { - const eslintOptions = { - overrideConfig: { - languageOptions: { - ecmaVersion: 2020, - sourceType: "module", + try { + if (!code || typeof code !== "string") { + return { + success: false, + error: "Invalid code input: code must be a non-empty string", + }; + } + + if (!options.language) { + return { + success: false, + error: "Language must be specified in lint options", + }; + } + + const eslintOptions = { + overrideConfig: { + languageOptions: { + ecmaVersion: 2020, + sourceType: "module", + }, + rules: options.rules || {}, }, - rules: options.rules || {}, - }, - overrideConfigFile: true, - } as ESLint.Options; + overrideConfigFile: true, + } as ESLint.Options; - const linter = new ESLint(eslintOptions); - const eslintResult = await linter.lintText(code); + const linter = new ESLint(eslintOptions); + const eslintResult = await linter.lintText(code); - return { - success: true, - data: { - issues: eslintResult.flatMap((result) => - result.messages.map((message) => ({ - line: message.line, - column: message.column, - severity: message.severity === 2 ? "error" : "warning", - rule: message.ruleId || "", - message: message.message, - })), - ), - }, - }; + return { + success: true, + data: { + issues: eslintResult.flatMap((result) => + result.messages.map((message) => ({ + line: message.line, + column: message.column, + severity: message.severity === 2 ? "error" : "warning", + rule: message.ruleId || "", + message: message.message, + })), + ), + }, + }; + } catch (error) { + return { + success: false, + error: `Linting failed: ${error instanceof Error ? error.message : "Unknown error"}`, + }; + } } } diff --git a/src/services/filesystem.ts b/src/services/filesystem.ts index 610cffb..fcf0854 100644 --- a/src/services/filesystem.ts +++ b/src/services/filesystem.ts @@ -10,6 +10,7 @@ export type FileItem = { export type FileOperationResult = { success: boolean; data?: string | FileItem[]; + error?: string; }; export class Filesystem { @@ -56,7 +57,10 @@ export class Filesystem { "createFile", `Error: ${error instanceof Error ? error.message : String(error)}`, ); - throw error; + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; } } @@ -79,7 +83,10 @@ export class Filesystem { "getFile", `Error: ${error instanceof Error ? error.message : String(error)}`, ); - throw error; + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; } } @@ -112,7 +119,10 @@ export class Filesystem { "updateFile", `Error: ${error instanceof Error ? error.message : String(error)}`, ); - throw error; + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; } } @@ -139,7 +149,10 @@ export class Filesystem { "updateFilePath", `Error: ${error instanceof Error ? error.message : String(error)}`, ); - throw error; + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; } } @@ -162,7 +175,10 @@ export class Filesystem { "deleteFile", `Error: ${error instanceof Error ? error.message : String(error)}`, ); - throw error; + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; } } @@ -185,7 +201,10 @@ export class Filesystem { "createFolder", `Error: ${error instanceof Error ? error.message : String(error)}`, ); - throw error; + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; } } @@ -245,7 +264,10 @@ export class Filesystem { "updateFolderName", `Error: ${error instanceof Error ? error.message : String(error)}`, ); - throw error; + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; } } @@ -275,7 +297,10 @@ export class Filesystem { "updateFolderPath", `Error: ${error instanceof Error ? error.message : String(error)}`, ); - throw error; + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; } } @@ -298,7 +323,10 @@ export class Filesystem { "deleteFolder", `Error: ${error instanceof Error ? error.message : String(error)}`, ); - throw error; + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; } } } diff --git a/src/services/git.ts b/src/services/git.ts index e7201fb..c4de107 100644 --- a/src/services/git.ts +++ b/src/services/git.ts @@ -5,7 +5,8 @@ import { Synapse } from "../synapse"; export type GitOperationResult = { success: boolean; - data: string; + data?: string; + error?: string; }; export class Git { @@ -46,7 +47,7 @@ export class Git { git.on("error", (error: Error) => { resolve({ success: false, - data: `Failed to execute git command: ${error.message}`, + error: `Failed to execute git command: ${error.message}`, }); }); @@ -54,7 +55,7 @@ export class Git { if (code !== 0) { resolve({ success: false, - data: errorOutput.trim() || "Git command failed", + error: errorOutput.trim() || "Git command failed", }); } else { resolve({ success: true, data: output.trim() }); @@ -88,7 +89,7 @@ export class Git { if (await this.isGitRepository()) { return { success: false, - data: "Git repository already exists in this directory", + error: "Git repository already exists in this directory", }; } @@ -99,12 +100,12 @@ export class Git { if (this.isErrnoException(error)) { return { success: false, - data: `No write permission in the current directory: ${error.message}`, + error: `No write permission in the current directory: ${error.message}`, }; } return { success: false, - data: "No write permission in the current directory", + error: "No write permission in the current directory", }; } @@ -114,7 +115,7 @@ export class Git { error instanceof Error ? error.message : "Unknown error occurred"; return { success: false, - data: `Failed to initialize git repository: ${message}`, + error: `Failed to initialize git repository: ${message}`, }; } } diff --git a/src/services/system.ts b/src/services/system.ts index a0c29c8..e567f07 100644 --- a/src/services/system.ts +++ b/src/services/system.ts @@ -11,7 +11,7 @@ export type CPUTimes = { export type SystemUsageData = { success: boolean; - data: { + data?: { cpuCores: number; cpuUsagePerCore: number[]; cpuUsagePercent: number; @@ -23,6 +23,7 @@ export type SystemUsageData = { memoryUsedBytes: number; memoryUsagePercent: number; }; + error?: string; }; export class System { @@ -106,7 +107,10 @@ export class System { this.synapse.logger( `Error in getUsage: ${error instanceof Error ? error.message : String(error)}`, ); - throw error; + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; } } } diff --git a/tests/services/filesystem.test.ts b/tests/services/filesystem.test.ts index 7d52336..5354833 100644 --- a/tests/services/filesystem.test.ts +++ b/tests/services/filesystem.test.ts @@ -54,9 +54,9 @@ describe("Filesystem", () => { (fs.mkdir as jest.Mock).mockResolvedValue(undefined); (fs.writeFile as jest.Mock).mockRejectedValue(error); - await expect(filesystem.createFile(filePath, content)).rejects.toThrow( - error, - ); + const result = await filesystem.createFile(filePath, content); + expect(result.success).toBe(false); + expect(result.error).toBe("Failed to create file"); expect(mockSynapse.logger).toHaveBeenCalledWith( expect.stringContaining("Error: Failed to create file"), ); @@ -85,7 +85,9 @@ describe("Filesystem", () => { (fs.readFile as jest.Mock).mockRejectedValue(error); - await expect(filesystem.getFile(filePath)).rejects.toThrow(error); + const result = await filesystem.getFile(filePath); + expect(result.success).toBe(false); + expect(result.error).toBe("File not found"); expect(mockSynapse.logger).toHaveBeenCalledWith( expect.stringContaining("Error: File not found"), ); diff --git a/tests/services/git.test.ts b/tests/services/git.test.ts index fcae3fe..d6420c8 100644 --- a/tests/services/git.test.ts +++ b/tests/services/git.test.ts @@ -95,7 +95,7 @@ describe("Git Service", () => { expect(result).toEqual({ success: false, - data: "Git repository already exists in this directory", + error: "Git repository already exists in this directory", }); }); }); @@ -130,7 +130,7 @@ describe("Git Service", () => { expect(result).toEqual({ success: false, - data: "remote origin already exists", + error: "remote origin already exists", }); }); }); @@ -159,7 +159,7 @@ describe("Git Service", () => { expect(result).toEqual({ success: false, - data: "fatal: not a git repository", + error: "fatal: not a git repository", }); }); }); @@ -188,7 +188,7 @@ describe("Git Service", () => { expect(result).toEqual({ success: false, - data: "fatal: not a git repository", + error: "fatal: not a git repository", }); }); }); @@ -221,7 +221,7 @@ describe("Git Service", () => { expect(result).toEqual({ success: false, - data: "fatal: pathspec 'nonexistent.txt' did not match any files", + error: "fatal: pathspec 'nonexistent.txt' did not match any files", }); }); }); @@ -251,7 +251,7 @@ describe("Git Service", () => { expect(result).toEqual({ success: false, - data: "nothing to commit, working tree clean", + error: "nothing to commit, working tree clean", }); }); }); @@ -278,7 +278,7 @@ describe("Git Service", () => { expect(result).toEqual({ success: false, - data: "fatal: no remote repository specified", + error: "fatal: no remote repository specified", }); }); }); @@ -309,7 +309,7 @@ describe("Git Service", () => { expect(result).toEqual({ success: false, - data: "fatal: The current branch has no upstream branch", + error: "fatal: The current branch has no upstream branch", }); }); }); From b01f38995a43709bfe20de9fc6e37c97a01d9dd5 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 3 Apr 2025 06:06:51 +0000 Subject: [PATCH 07/42] chore: improve error handling for terminal --- src/services/terminal.ts | 76 ++++++++++++++++++++++++++------- tests/services/terminal.test.ts | 66 ++++++++++++++++++++-------- 2 files changed, 109 insertions(+), 33 deletions(-) diff --git a/src/services/terminal.ts b/src/services/terminal.ts index 404ea75..9481025 100644 --- a/src/services/terminal.ts +++ b/src/services/terminal.ts @@ -13,6 +13,7 @@ export class Terminal { private synapse: Synapse; private term: pty.IPty | null = null; private onDataCallback: ((data: string) => void) | null = null; + private isAlive: boolean = false; /** * Creates a new Terminal instance @@ -29,19 +30,42 @@ export class Terminal { }, ) { this.synapse = synapse; - this.term = pty.spawn(terminalOptions.shell, [], { - name: "xterm-color", - cols: terminalOptions.cols, - rows: terminalOptions.rows, - cwd: terminalOptions.workdir, - env: process.env, - }); + try { + this.term = pty.spawn(terminalOptions.shell, [], { + name: "xterm-color", + cols: terminalOptions.cols, + rows: terminalOptions.rows, + cwd: terminalOptions.workdir, + env: process.env, + }); - this.term.onData((data: string) => { - if (this.onDataCallback) { - this.onDataCallback(data); - } - }); + this.isAlive = true; + + this.term.onData((data: string) => { + if (this.onDataCallback) { + this.onDataCallback(data); + } + }); + + this.term.onExit(() => { + this.isAlive = false; + this.term = null; + }); + } catch (error) { + console.error("Failed to spawn terminal:", error); + this.isAlive = false; + this.term = null; + } + } + + /** + * Checks if the terminal is alive and ready + * @throws Error if terminal is not alive + */ + private checkTerminal(): void { + if (!this.isAlive || !this.term) { + throw new Error("Terminal is not alive or has been terminated"); + } } /** @@ -50,7 +74,12 @@ export class Terminal { * @param rows - The number of rows */ updateSize(cols: number, rows: number): void { - this.term?.resize(Math.max(cols, 1), Math.max(rows, 1)); + try { + this.checkTerminal(); + this.term?.resize(Math.max(cols, 1), Math.max(rows, 1)); + } catch (error) { + console.error("Failed to resize terminal:", error); + } } /** @@ -58,7 +87,12 @@ export class Terminal { * @param command - The command to write */ createCommand(command: string): void { - this.term?.write(command); + try { + this.checkTerminal(); + this.term?.write(command); + } catch (error) { + console.error("Failed to write command to terminal:", error); + } } /** @@ -73,6 +107,18 @@ export class Terminal { * Kills the terminal */ kill(): void { - this.term?.kill(); + if (this.isAlive && this.term) { + this.term.kill(); + this.isAlive = false; + this.term = null; + } + } + + /** + * Checks if the terminal is still alive + * @returns boolean indicating if the terminal is alive + */ + isTerminalAlive(): boolean { + return this.isAlive && this.term !== null; } } diff --git a/tests/services/terminal.test.ts b/tests/services/terminal.test.ts index 87add6a..a0f85af 100644 --- a/tests/services/terminal.test.ts +++ b/tests/services/terminal.test.ts @@ -9,11 +9,20 @@ describe("Terminal", () => { let terminal: Terminal; let mockSynapse: Synapse; let mockPty: jest.Mocked; + let onDataHandler: (data: string) => void; + let onExitHandler: () => void; beforeEach(() => { mockSynapse = new Synapse(); mockPty = { - onData: jest.fn(), + onData: jest.fn((callback) => { + onDataHandler = callback; + return mockPty; + }), + onExit: jest.fn((callback) => { + onExitHandler = callback; + return mockPty; + }), write: jest.fn(), resize: jest.fn(), kill: jest.fn(), @@ -42,6 +51,7 @@ describe("Terminal", () => { env: process.env, }), ); + expect(mockPty.onExit).toHaveBeenCalled(); }); it("should create terminal with custom options", () => { @@ -66,6 +76,16 @@ describe("Terminal", () => { }), ); }); + + it("should handle spawn failure", () => { + const error = new Error("Spawn failed"); + (pty.spawn as jest.Mock).mockImplementationOnce(() => { + throw error; + }); + + terminal = new Terminal(mockSynapse); + expect(terminal.isTerminalAlive()).toBe(false); + }); }); describe("terminal operations", () => { @@ -74,30 +94,25 @@ describe("Terminal", () => { }); it("should handle resize operation with minimum values", () => { + expect(terminal.isTerminalAlive()).toBe(true); terminal.updateSize(0, -5); - expect(mockPty.resize).toHaveBeenCalledWith(1, 1); }); it("should handle write operation with command", () => { const command = "ls -la\n"; - + expect(terminal.isTerminalAlive()).toBe(true); terminal.createCommand(command); - expect(mockPty.write).toHaveBeenCalledWith(command); }); it("should handle data callback", () => { const mockCallback = jest.fn(); - terminal.onData(mockCallback); - // Simulate data event by calling the onData handler - const dataHandler = (mockPty.onData as jest.Mock).mock.calls[0][0]; - const testOutput = "test output"; - dataHandler(testOutput); - - expect(mockCallback).toHaveBeenCalledWith(testOutput); + // Simulate data event + onDataHandler("test output"); + expect(mockCallback).toHaveBeenCalledWith("test output"); }); it("should override previous data callback", () => { @@ -107,20 +122,35 @@ describe("Terminal", () => { terminal.onData(mockCallback1); terminal.onData(mockCallback2); - // Simulate data event by calling the onData handler - const dataHandler = (mockPty.onData as jest.Mock).mock.calls[0][0]; - const testOutput = "test output"; - dataHandler(testOutput); + // Simulate data event + onDataHandler("test output"); - // First callback should not be called, only the second one expect(mockCallback1).not.toHaveBeenCalled(); - expect(mockCallback2).toHaveBeenCalledWith(testOutput); + expect(mockCallback2).toHaveBeenCalledWith("test output"); }); it("should handle kill operation", () => { + expect(terminal.isTerminalAlive()).toBe(true); terminal.kill(); - expect(mockPty.kill).toHaveBeenCalled(); + expect(terminal.isTerminalAlive()).toBe(false); + }); + + it("should handle terminal exit", () => { + expect(terminal.isTerminalAlive()).toBe(true); + onExitHandler(); + expect(terminal.isTerminalAlive()).toBe(false); + }); + + it("should not perform operations on dead terminal", () => { + terminal.kill(); + expect(terminal.isTerminalAlive()).toBe(false); + + terminal.updateSize(80, 24); + expect(mockPty.resize).not.toHaveBeenCalled(); + + terminal.createCommand("test"); + expect(mockPty.write).not.toHaveBeenCalled(); }); }); }); From 8bde53e0d8d1bdc93f64ff71558fb2044d308c27 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 3 Apr 2025 06:18:25 +0000 Subject: [PATCH 08/42] fix: error handling --- src/services/terminal.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/services/terminal.ts b/src/services/terminal.ts index 9481025..0a9cb9b 100644 --- a/src/services/terminal.ts +++ b/src/services/terminal.ts @@ -78,7 +78,9 @@ export class Terminal { this.checkTerminal(); this.term?.resize(Math.max(cols, 1), Math.max(rows, 1)); } catch (error) { - console.error("Failed to resize terminal:", error); + if (this.onDataCallback) { + this.onDataCallback("Failed to resize terminal"); + } } } @@ -91,7 +93,9 @@ export class Terminal { this.checkTerminal(); this.term?.write(command); } catch (error) { - console.error("Failed to write command to terminal:", error); + if (this.onDataCallback) { + this.onDataCallback("Failed to write command to terminal"); + } } } From aeaa3c34bd3509faa3e931374b3f81f70a239c52 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 3 Apr 2025 06:37:19 +0000 Subject: [PATCH 09/42] chore: update on data callback for termina; --- src/services/terminal.ts | 11 ++++++----- tests/services/terminal.test.ts | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/services/terminal.ts b/src/services/terminal.ts index 0a9cb9b..59e60a2 100644 --- a/src/services/terminal.ts +++ b/src/services/terminal.ts @@ -12,7 +12,8 @@ export type TerminalOptions = { export class Terminal { private synapse: Synapse; private term: pty.IPty | null = null; - private onDataCallback: ((data: string) => void) | null = null; + private onDataCallback: ((success: boolean, data: string) => void) | null = + null; private isAlive: boolean = false; /** @@ -43,7 +44,7 @@ export class Terminal { this.term.onData((data: string) => { if (this.onDataCallback) { - this.onDataCallback(data); + this.onDataCallback(true, data); } }); @@ -79,7 +80,7 @@ export class Terminal { this.term?.resize(Math.max(cols, 1), Math.max(rows, 1)); } catch (error) { if (this.onDataCallback) { - this.onDataCallback("Failed to resize terminal"); + this.onDataCallback(false, "Failed to resize terminal"); } } } @@ -94,7 +95,7 @@ export class Terminal { this.term?.write(command); } catch (error) { if (this.onDataCallback) { - this.onDataCallback("Failed to write command to terminal"); + this.onDataCallback(false, "Failed to write command to terminal"); } } } @@ -103,7 +104,7 @@ export class Terminal { * Sets the callback for when data is received from the terminal * @param callback - The callback to set */ - onData(callback: (data: string) => void): void { + onData(callback: (success: boolean, data: string) => void): void { this.onDataCallback = callback; } diff --git a/tests/services/terminal.test.ts b/tests/services/terminal.test.ts index a0f85af..3c970c7 100644 --- a/tests/services/terminal.test.ts +++ b/tests/services/terminal.test.ts @@ -112,7 +112,7 @@ describe("Terminal", () => { // Simulate data event onDataHandler("test output"); - expect(mockCallback).toHaveBeenCalledWith("test output"); + expect(mockCallback).toHaveBeenCalledWith(true, "test output"); }); it("should override previous data callback", () => { @@ -126,7 +126,7 @@ describe("Terminal", () => { onDataHandler("test output"); expect(mockCallback1).not.toHaveBeenCalled(); - expect(mockCallback2).toHaveBeenCalledWith("test output"); + expect(mockCallback2).toHaveBeenCalledWith(true, "test output"); }); it("should handle kill operation", () => { From b2002bbb46daa67749f4378c0075ce766cf46183 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 3 Apr 2025 07:49:10 +0000 Subject: [PATCH 10/42] chore: add username and email commands to git --- src/services/git.ts | 18 ++++++++++++ tests/services/git.test.ts | 58 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/src/services/git.ts b/src/services/git.ts index c4de107..c61d8bc 100644 --- a/src/services/git.ts +++ b/src/services/git.ts @@ -138,6 +138,24 @@ export class Git { return this.execute(["rev-parse", "--abbrev-ref", "HEAD"]); } + /** + * Set Git user name + * @param name - The user name to set for Git + * @returns The output of the git config command + */ + async setUserName(name: string): Promise { + return this.execute(["config", "user.name", name]); + } + + /** + * Set Git user email + * @param email - The email to set for Git + * @returns The output of the git config command + */ + async setUserEmail(email: string): Promise { + return this.execute(["config", "user.email", email]); + } + /** * Get the status of the repository * @returns The status of the repository diff --git a/tests/services/git.test.ts b/tests/services/git.test.ts index d6420c8..54e9fea 100644 --- a/tests/services/git.test.ts +++ b/tests/services/git.test.ts @@ -283,6 +283,64 @@ describe("Git Service", () => { }); }); + describe("setUserName", () => { + it("should set git user name", async () => { + setupMockProcess(""); // Git config doesn't return output on success + + const result = await git.setUserName("John Doe"); + + expect(result).toEqual({ + success: true, + data: "", + }); + expect(mockSpawn).toHaveBeenCalledWith( + "git", + ["config", "user.name", "John Doe"], + { cwd: mockWorkingDir }, + ); + }); + + it("should handle error when setting user name fails", async () => { + setupMockProcess("", "error: could not set user.name", 1); + + const result = await git.setUserName("John Doe"); + + expect(result).toEqual({ + success: false, + error: "error: could not set user.name", + }); + }); + }); + + describe("setUserEmail", () => { + it("should set git user email", async () => { + setupMockProcess(""); // Git config doesn't return output on success + + const result = await git.setUserEmail("john.doe@example.com"); + + expect(result).toEqual({ + success: true, + data: "", + }); + expect(mockSpawn).toHaveBeenCalledWith( + "git", + ["config", "user.email", "john.doe@example.com"], + { cwd: mockWorkingDir }, + ); + }); + + it("should handle error when setting user email fails", async () => { + setupMockProcess("", "error: could not set user.email", 1); + + const result = await git.setUserEmail("john.doe@example.com"); + + expect(result).toEqual({ + success: false, + error: "error: could not set user.email", + }); + }); + }); + describe("push", () => { it("should push changes to remote", async () => { setupMockProcess("Everything up-to-date"); From 724f36459c1c48ae1de7edd347223aa8303fd670 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 3 Apr 2025 08:02:09 +0000 Subject: [PATCH 11/42] chore: add different work dir --- src/services/git.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/git.ts b/src/services/git.ts index c61d8bc..b4c9bbc 100644 --- a/src/services/git.ts +++ b/src/services/git.ts @@ -13,9 +13,9 @@ export class Git { private synapse: Synapse; private workingDir: string; - constructor(synapse: Synapse) { + constructor(synapse: Synapse, workingDir: string = process.cwd()) { this.synapse = synapse; - this.workingDir = process.cwd(); + this.workingDir = workingDir; } /** From 77645fa05dabf4ea08ec7ad4965e50bc43128015 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 3 Apr 2025 08:18:45 +0000 Subject: [PATCH 12/42] chore: add working dir to filesystem --- src/services/filesystem.ts | 39 +++++++++++++++++++++---------- src/services/git.ts | 5 ++++ tests/services/filesystem.test.ts | 10 ++++---- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/services/filesystem.ts b/src/services/filesystem.ts index fcf0854..cb09559 100644 --- a/src/services/filesystem.ts +++ b/src/services/filesystem.ts @@ -15,13 +15,16 @@ export type FileOperationResult = { export class Filesystem { private synapse: Synapse; + private workingDir: string; /** * Creates a new Filesystem instance * @param synapse - The Synapse instance to use + * @param workingDir - The working directory to use */ - constructor(synapse: Synapse) { + constructor(synapse: Synapse, workingDir: string = process.cwd()) { this.synapse = synapse; + this.workingDir = workingDir; } private log(method: string, message: string): void { @@ -41,6 +44,7 @@ export class Filesystem { ): Promise { try { this.log("createFile", `Creating file at path: ${filePath}`); + const fullPath = path.join(this.workingDir, filePath); const dirPath = path.dirname(filePath); this.log("createFile", `Ensuring directory exists: ${dirPath}`); @@ -48,7 +52,7 @@ export class Filesystem { await this.createFolder(dirPath); this.log("createFile", "Writing file content..."); - await fs.writeFile(filePath, content); + await fs.writeFile(fullPath, content); this.log("createFile", "File created successfully"); return { success: true }; @@ -73,8 +77,9 @@ export class Filesystem { async getFile(filePath: string): Promise { try { this.log("getFile", `Reading file at path: ${filePath}`); + const fullPath = path.join(this.workingDir, filePath); - const data = await fs.readFile(filePath, "utf-8"); + const data = await fs.readFile(fullPath, "utf-8"); this.log("getFile", "File read successfully"); return { success: true, data }; @@ -103,6 +108,7 @@ export class Filesystem { ): Promise { try { this.log("updateFile", `Updating file at path: ${filePath}`); + const fullPath = path.join(this.workingDir, filePath); const dirPath = path.dirname(filePath); this.log("updateFile", `Ensuring directory exists: ${dirPath}`); @@ -110,7 +116,7 @@ export class Filesystem { await this.createFolder(dirPath); this.log("updateFile", "Writing file content..."); - await fs.writeFile(filePath, content); + await fs.writeFile(fullPath, content); this.log("updateFile", "File updated successfully"); return { success: true }; @@ -139,8 +145,10 @@ export class Filesystem { ): Promise { try { this.log("updateFilePath", `Moving file from ${oldPath} to ${newPath}`); + const fullOldPath = path.join(this.workingDir, oldPath); + const fullNewPath = path.join(this.workingDir, newPath); - await fs.rename(oldPath, newPath); + await fs.rename(fullOldPath, fullNewPath); this.log("updateFilePath", "File moved successfully"); return { success: true }; @@ -165,8 +173,9 @@ export class Filesystem { async deleteFile(filePath: string): Promise { try { this.log("deleteFile", `Deleting file at path: ${filePath}`); + const fullPath = path.join(this.workingDir, filePath); - await fs.unlink(filePath); + await fs.unlink(fullPath); this.log("deleteFile", "File deleted successfully"); return { success: true }; @@ -191,8 +200,9 @@ export class Filesystem { async createFolder(dirPath: string): Promise { try { this.log("createFolder", `Creating directory at path: ${dirPath}`); + const fullPath = path.join(this.workingDir, dirPath); - await fs.mkdir(dirPath, { recursive: true }); + await fs.mkdir(fullPath, { recursive: true }); this.log("createFolder", "Directory created successfully"); return { success: true }; @@ -217,8 +227,9 @@ export class Filesystem { async getFolder(dirPath: string): Promise { try { this.log("getFolder", `Reading directory at path: ${dirPath}`); + const fullPath = path.join(this.workingDir, dirPath); - const items = await fs.readdir(dirPath, { withFileTypes: true }); + const items = await fs.readdir(fullPath, { withFileTypes: true }); const data: FileItem[] = items.map((item) => ({ name: item.name, isDirectory: item.isDirectory(), @@ -251,11 +262,12 @@ export class Filesystem { ): Promise { try { this.log("updateFolderName", `Renaming folder at ${dirPath} to ${name}`); + const fullPath = path.join(this.workingDir, dirPath); - const dir = path.dirname(dirPath); + const dir = path.dirname(fullPath); const newPath = path.join(dir, name); - await fs.rename(dirPath, newPath); + await fs.rename(fullPath, newPath); this.log("updateFolderName", "Folder renamed successfully"); return { success: true }; @@ -287,8 +299,10 @@ export class Filesystem { "updateFolderPath", `Moving folder from ${oldPath} to ${newPath}`, ); + const fullOldPath = path.join(this.workingDir, oldPath); + const fullNewPath = path.join(this.workingDir, newPath); - await fs.rename(oldPath, newPath); + await fs.rename(fullOldPath, fullNewPath); this.log("updateFolderPath", "Folder moved successfully"); return { success: true }; @@ -313,8 +327,9 @@ export class Filesystem { async deleteFolder(dirPath: string): Promise { try { this.log("deleteFolder", `Deleting folder at path: ${dirPath}`); + const fullPath = path.join(this.workingDir, dirPath); - await fs.rm(dirPath, { recursive: true, force: true }); + await fs.rm(fullPath, { recursive: true, force: true }); this.log("deleteFolder", "Folder deleted successfully"); return { success: true }; diff --git a/src/services/git.ts b/src/services/git.ts index b4c9bbc..0afd8cd 100644 --- a/src/services/git.ts +++ b/src/services/git.ts @@ -13,6 +13,11 @@ export class Git { private synapse: Synapse; private workingDir: string; + /** + * Creates a new Git instance + * @param synapse - The Synapse instance to use + * @param workingDir - The working directory to use + */ constructor(synapse: Synapse, workingDir: string = process.cwd()) { this.synapse = synapse; this.workingDir = workingDir; diff --git a/tests/services/filesystem.test.ts b/tests/services/filesystem.test.ts index 5354833..15de5a4 100644 --- a/tests/services/filesystem.test.ts +++ b/tests/services/filesystem.test.ts @@ -17,12 +17,12 @@ describe("Filesystem", () => { sendCommand: jest.fn(), } as unknown as Synapse); - filesystem = new Filesystem(mockSynapse); + filesystem = new Filesystem(mockSynapse, "/test"); }); describe("createFile", () => { it("should create a file with content and verify its existence", async () => { - const filePath = "/test/file.txt"; + const filePath = "/file.txt"; const content = "test content"; (fs.mkdir as jest.Mock).mockResolvedValue(undefined); @@ -37,7 +37,7 @@ describe("Filesystem", () => { recursive: true, }); - expect(fs.writeFile).toHaveBeenCalledWith(filePath, content); + expect(fs.writeFile).toHaveBeenCalledWith(`/test${filePath}`, content); await expect(fs.access(filePath)).resolves.toBeUndefined(); @@ -65,7 +65,7 @@ describe("Filesystem", () => { describe("getFile", () => { it("should read file content and verify data", async () => { - const filePath = "/test/file.txt"; + const filePath = "/file.txt"; const content = "test content"; (fs.readFile as jest.Mock).mockResolvedValue(content); @@ -76,7 +76,7 @@ describe("Filesystem", () => { const result = await filesystem.getFile(filePath); expect(result.success).toBe(true); expect(result.data).toBe(content); - expect(fs.readFile).toHaveBeenCalledWith(filePath, "utf-8"); + expect(fs.readFile).toHaveBeenCalledWith(`/test${filePath}`, "utf-8"); }); it("should handle file reading errors properly", async () => { From bb480ed0061fccaad8556a129bbfce564a74fcbd Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 3 Apr 2025 08:47:00 +0000 Subject: [PATCH 13/42] chore: improve logging for terminal error --- src/services/terminal.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/services/terminal.ts b/src/services/terminal.ts index 59e60a2..aabc384 100644 --- a/src/services/terminal.ts +++ b/src/services/terminal.ts @@ -80,7 +80,9 @@ export class Terminal { this.term?.resize(Math.max(cols, 1), Math.max(rows, 1)); } catch (error) { if (this.onDataCallback) { - this.onDataCallback(false, "Failed to resize terminal"); + const errorMessage = + error instanceof Error ? error.message : "Unknown error occurred"; + this.onDataCallback(false, errorMessage); } } } @@ -95,7 +97,9 @@ export class Terminal { this.term?.write(command); } catch (error) { if (this.onDataCallback) { - this.onDataCallback(false, "Failed to write command to terminal"); + const errorMessage = + error instanceof Error ? error.message : "Unknown error occurred"; + this.onDataCallback(false, errorMessage); } } } From 1d8afd45bd93872a6d47fd70f01e1a98e2602593 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 3 Apr 2025 08:55:07 +0000 Subject: [PATCH 14/42] chore: add more error logging --- src/services/terminal.ts | 56 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/src/services/terminal.ts b/src/services/terminal.ts index aabc384..0830901 100644 --- a/src/services/terminal.ts +++ b/src/services/terminal.ts @@ -15,6 +15,7 @@ export class Terminal { private onDataCallback: ((success: boolean, data: string) => void) | null = null; private isAlive: boolean = false; + private initializationError: Error | null = null; /** * Creates a new Terminal instance @@ -31,6 +32,8 @@ export class Terminal { }, ) { this.synapse = synapse; + console.log("Terminal constructor called with options:", terminalOptions); + try { this.term = pty.spawn(terminalOptions.shell, [], { name: "xterm-color", @@ -40,6 +43,7 @@ export class Terminal { env: process.env, }); + console.log("Terminal spawned successfully"); this.isAlive = true; this.term.onData((data: string) => { @@ -48,17 +52,46 @@ export class Terminal { } }); - this.term.onExit(() => { + this.term.onExit((e?: { exitCode: number; signal?: number }) => { + console.log( + `Terminal exited with code ${e?.exitCode ?? "unknown"} and signal ${e?.signal ?? "none"}`, + ); this.isAlive = false; this.term = null; + + if (this.onDataCallback) { + this.onDataCallback( + false, + `Terminal process exited with code ${e?.exitCode ?? "unknown"}`, + ); + } }); } catch (error) { + this.initializationError = + error instanceof Error ? error : new Error(String(error)); console.error("Failed to spawn terminal:", error); this.isAlive = false; this.term = null; + + if (this.onDataCallback) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error occurred"; + this.onDataCallback( + false, + `Terminal initialization failed: ${errorMessage}`, + ); + } } } + /** + * Get initialization error if any + * @returns The initialization error or null + */ + getInitializationError(): Error | null { + return this.initializationError; + } + /** * Checks if the terminal is alive and ready * @throws Error if terminal is not alive @@ -75,10 +108,15 @@ export class Terminal { * @param rows - The number of rows */ updateSize(cols: number, rows: number): void { + console.log( + `Updating terminal size to ${cols}x${rows}, isAlive=${this.isAlive}`, + ); try { this.checkTerminal(); this.term?.resize(Math.max(cols, 1), Math.max(rows, 1)); + console.log("Terminal size updated successfully"); } catch (error) { + console.error("Failed to update terminal size:", error); if (this.onDataCallback) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; @@ -92,10 +130,14 @@ export class Terminal { * @param command - The command to write */ createCommand(command: string): void { + console.log( + `Executing command: ${command.substring(0, 50)}${command.length > 50 ? "..." : ""}`, + ); try { this.checkTerminal(); this.term?.write(command); } catch (error) { + console.error("Failed to execute command:", error); if (this.onDataCallback) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; @@ -110,16 +152,28 @@ export class Terminal { */ onData(callback: (success: boolean, data: string) => void): void { this.onDataCallback = callback; + + // If there was an initialization error, notify the callback immediately + if (this.initializationError && callback) { + callback( + false, + `Terminal initialization failed: ${this.initializationError.message}`, + ); + } } /** * Kills the terminal */ kill(): void { + console.log("Killing terminal"); if (this.isAlive && this.term) { this.term.kill(); this.isAlive = false; this.term = null; + console.log("Terminal killed successfully"); + } else { + console.log("Terminal was already not alive, nothing to kill"); } } From ba0ebedbe9f6f68d2d1206f6e766d95988ece824 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 3 Apr 2025 10:44:57 +0000 Subject: [PATCH 15/42] chore: add cleanup for ANSI characters --- src/services/terminal.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/services/terminal.ts b/src/services/terminal.ts index 0830901..5d8a86c 100644 --- a/src/services/terminal.ts +++ b/src/services/terminal.ts @@ -146,18 +146,35 @@ export class Terminal { } } + /** + * Cleans terminal output data by removing ANSI escape sequences, carriage returns, and extra whitespace + * @param data - The raw terminal output data + * @returns The cleaned data string + */ + private cleanData(data: string): string { + return data + .replace(/\u001b\[[0-9;?]*[a-zA-Z]/g, "") // Remove ANSI escape sequences + .replace(/\r/g, "") // Remove carriage returns + .trim(); // Trim whitespace + } + /** * Sets the callback for when data is received from the terminal * @param callback - The callback to set */ onData(callback: (success: boolean, data: string) => void): void { - this.onDataCallback = callback; + // Wrap the callback to clean the data before sending + this.onDataCallback = (success: boolean, data: string) => { + callback(success, this.cleanData(data)); + }; // If there was an initialization error, notify the callback immediately if (this.initializationError && callback) { callback( false, - `Terminal initialization failed: ${this.initializationError.message}`, + this.cleanData( + `Terminal initialization failed: ${this.initializationError.message}`, + ), ); } } From 77e0e0c9ecbe11bf547e474ced08a6dbdbd6d1ae Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 3 Apr 2025 11:14:42 +0000 Subject: [PATCH 16/42] chore: handle not send last message --- src/services/terminal.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/services/terminal.ts b/src/services/terminal.ts index 5d8a86c..2507bc5 100644 --- a/src/services/terminal.ts +++ b/src/services/terminal.ts @@ -16,6 +16,7 @@ export class Terminal { null; private isAlive: boolean = false; private initializationError: Error | null = null; + private lastCommand: string = ""; /** * Creates a new Terminal instance @@ -135,6 +136,7 @@ export class Terminal { ); try { this.checkTerminal(); + this.lastCommand = command.trim(); this.term?.write(command); } catch (error) { console.error("Failed to execute command:", error); @@ -148,14 +150,20 @@ export class Terminal { /** * Cleans terminal output data by removing ANSI escape sequences, carriage returns, and extra whitespace + * Also filters out the echoed command from the output * @param data - The raw terminal output data * @returns The cleaned data string */ private cleanData(data: string): string { - return data + let cleaned = data .replace(/\u001b\[[0-9;?]*[a-zA-Z]/g, "") // Remove ANSI escape sequences - .replace(/\r/g, "") // Remove carriage returns - .trim(); // Trim whitespace + .replace(/\r/g, ""); // Remove carriage returns + + if (this.lastCommand && cleaned.trim().startsWith(this.lastCommand)) { + cleaned = cleaned.slice(this.lastCommand.length); + } + + return cleaned.trim(); } /** From db4fc6a11dc52f132a1d666990fcb7bedb3b9972 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 3 Apr 2025 11:26:48 +0000 Subject: [PATCH 17/42] chore: link message ids --- src/services/terminal.ts | 61 +++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/src/services/terminal.ts b/src/services/terminal.ts index 2507bc5..2cd27d8 100644 --- a/src/services/terminal.ts +++ b/src/services/terminal.ts @@ -12,8 +12,9 @@ export type TerminalOptions = { export class Terminal { private synapse: Synapse; private term: pty.IPty | null = null; - private onDataCallback: ((success: boolean, data: string) => void) | null = - null; + private onDataCallback: + | ((success: boolean, data: string, messageId: string | null) => void) + | null = null; private isAlive: boolean = false; private initializationError: Error | null = null; private lastCommand: string = ""; @@ -33,7 +34,6 @@ export class Terminal { }, ) { this.synapse = synapse; - console.log("Terminal constructor called with options:", terminalOptions); try { this.term = pty.spawn(terminalOptions.shell, [], { @@ -44,19 +44,15 @@ export class Terminal { env: process.env, }); - console.log("Terminal spawned successfully"); this.isAlive = true; this.term.onData((data: string) => { if (this.onDataCallback) { - this.onDataCallback(true, data); + this.onDataCallback(true, data, null); } }); this.term.onExit((e?: { exitCode: number; signal?: number }) => { - console.log( - `Terminal exited with code ${e?.exitCode ?? "unknown"} and signal ${e?.signal ?? "none"}`, - ); this.isAlive = false; this.term = null; @@ -64,6 +60,7 @@ export class Terminal { this.onDataCallback( false, `Terminal process exited with code ${e?.exitCode ?? "unknown"}`, + null, ); } }); @@ -80,6 +77,7 @@ export class Terminal { this.onDataCallback( false, `Terminal initialization failed: ${errorMessage}`, + null, ); } } @@ -108,20 +106,27 @@ export class Terminal { * @param cols - The number of columns * @param rows - The number of rows */ - updateSize(cols: number, rows: number): void { - console.log( - `Updating terminal size to ${cols}x${rows}, isAlive=${this.isAlive}`, - ); + updateSize( + cols: number, + rows: number, + messageId: string | null = null, + ): void { try { this.checkTerminal(); this.term?.resize(Math.max(cols, 1), Math.max(rows, 1)); - console.log("Terminal size updated successfully"); + if (this.onDataCallback) { + this.onDataCallback( + true, + "Terminal size updated successfully", + messageId, + ); + } } catch (error) { console.error("Failed to update terminal size:", error); if (this.onDataCallback) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; - this.onDataCallback(false, errorMessage); + this.onDataCallback(false, errorMessage, messageId); } } } @@ -130,10 +135,7 @@ export class Terminal { * Writes a command to the terminal * @param command - The command to write */ - createCommand(command: string): void { - console.log( - `Executing command: ${command.substring(0, 50)}${command.length > 50 ? "..." : ""}`, - ); + createCommand(command: string, messageId: string | null = null): void { try { this.checkTerminal(); this.lastCommand = command.trim(); @@ -143,7 +145,7 @@ export class Terminal { if (this.onDataCallback) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; - this.onDataCallback(false, errorMessage); + this.onDataCallback(false, errorMessage, messageId); } } } @@ -170,10 +172,20 @@ export class Terminal { * Sets the callback for when data is received from the terminal * @param callback - The callback to set */ - onData(callback: (success: boolean, data: string) => void): void { + onData( + callback: ( + success: boolean, + data: string, + messageId: string | null, + ) => void, + ): void { // Wrap the callback to clean the data before sending - this.onDataCallback = (success: boolean, data: string) => { - callback(success, this.cleanData(data)); + this.onDataCallback = ( + success: boolean, + data: string, + messageId: string | null, + ) => { + callback(success, this.cleanData(data), messageId); }; // If there was an initialization error, notify the callback immediately @@ -183,6 +195,7 @@ export class Terminal { this.cleanData( `Terminal initialization failed: ${this.initializationError.message}`, ), + null, ); } } @@ -191,14 +204,10 @@ export class Terminal { * Kills the terminal */ kill(): void { - console.log("Killing terminal"); if (this.isAlive && this.term) { this.term.kill(); this.isAlive = false; this.term = null; - console.log("Terminal killed successfully"); - } else { - console.log("Terminal was already not alive, nothing to kill"); } } From f40bf4bccd674ff18da3c109407130d29fab3bf8 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 3 Apr 2025 11:45:26 +0000 Subject: [PATCH 18/42] chore: add success callback --- src/services/terminal.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/services/terminal.ts b/src/services/terminal.ts index 2cd27d8..47d32b6 100644 --- a/src/services/terminal.ts +++ b/src/services/terminal.ts @@ -140,6 +140,9 @@ export class Terminal { this.checkTerminal(); this.lastCommand = command.trim(); this.term?.write(command); + if (this.onDataCallback) { + this.onDataCallback(true, "Command executed successfully", messageId); + } } catch (error) { console.error("Failed to execute command:", error); if (this.onDataCallback) { From 47b0fc3659a2f770395e52c29ccf7c775ac0a34b Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 3 Apr 2025 11:57:43 +0000 Subject: [PATCH 19/42] chore: fix tests --- tests/services/terminal.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/services/terminal.test.ts b/tests/services/terminal.test.ts index 3c970c7..4e4f67f 100644 --- a/tests/services/terminal.test.ts +++ b/tests/services/terminal.test.ts @@ -112,7 +112,7 @@ describe("Terminal", () => { // Simulate data event onDataHandler("test output"); - expect(mockCallback).toHaveBeenCalledWith(true, "test output"); + expect(mockCallback).toHaveBeenCalledWith(true, "test output", null); }); it("should override previous data callback", () => { @@ -126,7 +126,7 @@ describe("Terminal", () => { onDataHandler("test output"); expect(mockCallback1).not.toHaveBeenCalled(); - expect(mockCallback2).toHaveBeenCalledWith(true, "test output"); + expect(mockCallback2).toHaveBeenCalledWith(true, "test output", null); }); it("should handle kill operation", () => { From f755f707bf817b2ae68e522f72e414d2d50bf25c Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 3 Apr 2025 12:09:28 +0000 Subject: [PATCH 20/42] fix: test --- tests/services/git.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/services/git.test.ts b/tests/services/git.test.ts index 54e9fea..ebfcb30 100644 --- a/tests/services/git.test.ts +++ b/tests/services/git.test.ts @@ -13,6 +13,10 @@ jest.mock("fs", () => { ...actual, existsSync: jest.fn(), statSync: jest.fn(), + promises: { + ...actual.promises, + access: jest.fn(() => Promise.resolve()), + }, }; }); From 8d5e239f9f7d7379245b5187a3bf2bca2462a31d Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 3 Apr 2025 14:17:38 +0000 Subject: [PATCH 21/42] refactor: reduce tests --- tests/services/codestyle.test.ts | 153 +++++-------------- tests/services/filesystem.test.ts | 67 +++------ tests/services/git.test.ts | 222 +++++----------------------- tests/services/system.test.ts | 36 +++-- tests/services/terminal.test.ts | 123 ++++----------- tests/synapse.test.ts | 238 ++++++------------------------ 6 files changed, 191 insertions(+), 648 deletions(-) diff --git a/tests/services/codestyle.test.ts b/tests/services/codestyle.test.ts index 858e594..775d0d7 100644 --- a/tests/services/codestyle.test.ts +++ b/tests/services/codestyle.test.ts @@ -14,39 +14,34 @@ describe("CodeStyle", () => { beforeEach(() => { jest.clearAllMocks(); - mockSynapse = new Synapse() as jest.Mocked; codeStyle = new CodeStyle(mockSynapse); }); describe("format", () => { - test("should format JavaScript code with default options", async () => { + test("should format JavaScript code correctly", async () => { const input = "const x=1;const y=2;"; const expected = "const x = 1;\nconst y = 2;\n"; - const mockFormat = format as jest.MockedFunction; - mockFormat.mockResolvedValue(expected); + (format as jest.MockedFunction).mockResolvedValue( + expected, + ); const result = await codeStyle.format(input, { language: "javascript" }); - expect(result.success).toBe(true); - expect(result.data).toBe(expected); - expect(format).toHaveBeenCalledWith(input, { - parser: "babel", - tabWidth: 2, - useTabs: false, - semi: true, - singleQuote: false, - printWidth: 80, + expect(result).toEqual({ + success: true, + data: expected, }); }); test("should format TypeScript code with custom options", async () => { - const input = 'const x:number=1;const y:string="test";'; - const expected = "const x: number = 1\nconst y: string = 'test'\n"; + const input = "const x:number=1;"; + const expected = "const x: number = 1;\n"; - const mockFormat = format as jest.MockedFunction; - mockFormat.mockResolvedValue(expected); + (format as jest.MockedFunction).mockResolvedValue( + expected, + ); const result = await codeStyle.format(input, { language: "typescript", @@ -54,46 +49,18 @@ describe("CodeStyle", () => { useTabs: true, semi: false, singleQuote: true, - printWidth: 100, }); - expect(result.success).toBe(true); - expect(result.data).toBe(expected); - expect(format).toHaveBeenCalledWith(input, { - parser: "typescript", - tabWidth: 4, - useTabs: true, - semi: false, - singleQuote: true, - printWidth: 100, - }); - }); - - test("should handle unsupported language gracefully", async () => { - const input = "some code"; - const expected = "formatted code"; - - const mockFormat = format as jest.MockedFunction; - mockFormat.mockResolvedValue(expected); - - const result = await codeStyle.format(input, { language: "unsupported" }); - - expect(result.success).toBe(true); - expect(result.data).toBe(expected); - expect(format).toHaveBeenCalledWith(input, { - parser: "babel", // Should default to babel - tabWidth: 2, - useTabs: false, - semi: true, - singleQuote: false, - printWidth: 80, + expect(result).toEqual({ + success: true, + data: expected, }); }); }); describe("lint", () => { - test("should lint JavaScript code with default rules", async () => { - const input = "const x = 1;\nconst y = 2;"; + test("should lint JavaScript code and return issues", async () => { + const input = "const x = 1;"; const mockLintResult: ESLint.LintResult[] = [ { messages: [ @@ -127,75 +94,23 @@ describe("CodeStyle", () => { const result = await codeStyle.lint(input, { language: "javascript" }); - expect(result.success).toBe(true); - expect(result.data.issues).toHaveLength(1); - expect(result.data.issues[0]).toEqual({ - line: 1, - column: 1, - severity: "error", - rule: "no-unused-vars", - message: "x is defined but never used", - }); - }); - - test("should lint with custom rules", async () => { - const input = "let x = 1;"; - const customRules = { - "prefer-const": "error" as const, - }; - - const mockLintResult: ESLint.LintResult[] = [ - { - messages: [ + expect(result).toEqual({ + success: true, + data: { + issues: [ { line: 1, column: 1, - severity: 2, - ruleId: "prefer-const", - message: "Use const instead of let", + severity: "error", + rule: "no-unused-vars", + message: "x is defined but never used", }, ], - filePath: "", - errorCount: 1, - warningCount: 0, - fixableErrorCount: 0, - fixableWarningCount: 0, - source: input, - suppressedMessages: [], - fatalErrorCount: 0, - usedDeprecatedRules: [], - }, - ]; - - const mockLintText = jest - .fn<() => Promise>() - .mockResolvedValue(mockLintResult); - const MockESLint = jest.fn().mockImplementation(() => ({ - lintText: mockLintText, - })); - (ESLint as unknown) = MockESLint; - - const result = await codeStyle.lint(input, { - language: "javascript", - rules: customRules, - }); - - expect(result.success).toBe(true); - expect(result.data.issues).toHaveLength(1); - expect(result.data.issues[0].rule).toBe("prefer-const"); - expect(MockESLint).toHaveBeenCalledWith({ - overrideConfig: { - languageOptions: { - ecmaVersion: 2020, - sourceType: "module", - }, - rules: customRules, }, - overrideConfigFile: true, }); }); - test("should handle warnings correctly", async () => { + test("should handle warnings in lint results", async () => { const input = 'console.log("test");'; const mockLintResult: ESLint.LintResult[] = [ { @@ -203,7 +118,7 @@ describe("CodeStyle", () => { { line: 1, column: 1, - severity: 1, // Warning severity + severity: 1, ruleId: "no-console", message: "Unexpected console statement", }, @@ -230,8 +145,20 @@ describe("CodeStyle", () => { const result = await codeStyle.lint(input, { language: "javascript" }); - expect(result.success).toBe(true); - expect(result.data.issues[0].severity).toBe("warning"); + expect(result).toEqual({ + success: true, + data: { + issues: [ + { + line: 1, + column: 1, + severity: "warning", + rule: "no-console", + message: "Unexpected console statement", + }, + ], + }, + }); }); }); }); diff --git a/tests/services/filesystem.test.ts b/tests/services/filesystem.test.ts index 15de5a4..c489e0a 100644 --- a/tests/services/filesystem.test.ts +++ b/tests/services/filesystem.test.ts @@ -11,86 +11,63 @@ describe("Filesystem", () => { beforeEach(() => { mockSynapse = jest.mocked({ logger: jest.fn(), - setLogger: jest.fn(), - connect: jest.fn(), - disconnect: jest.fn(), - sendCommand: jest.fn(), } as unknown as Synapse); filesystem = new Filesystem(mockSynapse, "/test"); }); describe("createFile", () => { - it("should create a file with content and verify its existence", async () => { + it("should successfully create a file", async () => { const filePath = "/file.txt"; const content = "test content"; (fs.mkdir as jest.Mock).mockResolvedValue(undefined); (fs.writeFile as jest.Mock).mockResolvedValue(undefined); - (fs.readFile as jest.Mock).mockResolvedValue(content); - (fs.access as jest.Mock).mockResolvedValue(undefined); - - const createResult = await filesystem.createFile(filePath, content); - expect(createResult.success).toBe(true); - - expect(fs.mkdir).toHaveBeenCalledWith(expect.any(String), { - recursive: true, - }); - expect(fs.writeFile).toHaveBeenCalledWith(`/test${filePath}`, content); - - await expect(fs.access(filePath)).resolves.toBeUndefined(); - - const readResult = await filesystem.getFile(filePath); - expect(readResult.success).toBe(true); - expect(readResult.data).toBe(content); + const result = await filesystem.createFile(filePath, content); + expect(result).toEqual({ success: true }); }); - it("should handle file creation errors properly", async () => { + it("should handle file creation errors", async () => { const filePath = "/test/error.txt"; const content = "test content"; - const error = new Error("Failed to create file"); - (fs.mkdir as jest.Mock).mockResolvedValue(undefined); - (fs.writeFile as jest.Mock).mockRejectedValue(error); + (fs.writeFile as jest.Mock).mockRejectedValue( + new Error("Failed to create file"), + ); const result = await filesystem.createFile(filePath, content); - expect(result.success).toBe(false); - expect(result.error).toBe("Failed to create file"); - expect(mockSynapse.logger).toHaveBeenCalledWith( - expect.stringContaining("Error: Failed to create file"), - ); + expect(result).toEqual({ + success: false, + error: "Failed to create file", + }); }); }); describe("getFile", () => { - it("should read file content and verify data", async () => { + it("should successfully read file content", async () => { const filePath = "/file.txt"; const content = "test content"; (fs.readFile as jest.Mock).mockResolvedValue(content); - (fs.access as jest.Mock).mockResolvedValue(undefined); - - await expect(fs.access(filePath)).resolves.toBeUndefined(); const result = await filesystem.getFile(filePath); - expect(result.success).toBe(true); - expect(result.data).toBe(content); - expect(fs.readFile).toHaveBeenCalledWith(`/test${filePath}`, "utf-8"); + expect(result).toEqual({ + success: true, + data: content, + }); }); - it("should handle file reading errors properly", async () => { + it("should handle file reading errors", async () => { const filePath = "/test/nonexistent.txt"; - const error = new Error("File not found"); - (fs.readFile as jest.Mock).mockRejectedValue(error); + (fs.readFile as jest.Mock).mockRejectedValue(new Error("File not found")); const result = await filesystem.getFile(filePath); - expect(result.success).toBe(false); - expect(result.error).toBe("File not found"); - expect(mockSynapse.logger).toHaveBeenCalledWith( - expect.stringContaining("Error: File not found"), - ); + expect(result).toEqual({ + success: false, + error: "File not found", + }); }); }); }); diff --git a/tests/services/git.test.ts b/tests/services/git.test.ts index ebfcb30..6050c84 100644 --- a/tests/services/git.test.ts +++ b/tests/services/git.test.ts @@ -28,14 +28,9 @@ describe("Git Service", () => { beforeEach(() => { jest.clearAllMocks(); - - // Mock process.cwd jest.spyOn(process, "cwd").mockReturnValue(mockWorkingDir); - - // Mock fs.existsSync to return false by default (no git repo) (fs.existsSync as jest.Mock).mockReturnValue(false); (fs.statSync as jest.Mock).mockReturnValue({ isDirectory: () => true }); - mockSynapse = {} as jest.Mocked; mockSpawn = spawn as jest.Mock; git = new Git(mockSynapse); @@ -77,27 +72,17 @@ describe("Git Service", () => { }; describe("init", () => { - it("should initialize a new git repository", async () => { + it("success - new repository", async () => { setupMockProcess("Initialized empty Git repository"); - - const result = await git.init(); - - expect(result).toEqual({ + expect(await git.init()).toEqual({ success: true, data: "Initialized empty Git repository", }); - expect(mockSpawn).toHaveBeenCalledWith("git", ["init"], { - cwd: mockWorkingDir, - }); }); - it("should handle error when repository already exists", async () => { - // Mock fs.existsSync to return true (git repo exists) + it("error - repository exists", async () => { (fs.existsSync as jest.Mock).mockReturnValue(true); - - const result = await git.init(); - - expect(result).toEqual({ + expect(await git.init()).toEqual({ success: false, error: "Git repository already exists in this directory", }); @@ -105,34 +90,21 @@ describe("Git Service", () => { }); describe("addRemote", () => { - it("should add a remote repository", async () => { + it("success", async () => { setupMockProcess(""); - - const result = await git.addRemote( - "origin", - "https://github.com/user/repo.git", - ); - - expect(result).toEqual({ + expect( + await git.addRemote("origin", "https://github.com/user/repo.git"), + ).toEqual({ success: true, data: "", }); - expect(mockSpawn).toHaveBeenCalledWith( - "git", - ["remote", "add", "origin", "https://github.com/user/repo.git"], - { cwd: mockWorkingDir }, - ); }); - it("should handle error when remote already exists", async () => { + it("error - remote exists", async () => { setupMockProcess("", "remote origin already exists", 128); - - const result = await git.addRemote( - "origin", - "https://github.com/user/repo.git", - ); - - expect(result).toEqual({ + expect( + await git.addRemote("origin", "https://github.com/user/repo.git"), + ).toEqual({ success: false, error: "remote origin already exists", }); @@ -140,28 +112,17 @@ describe("Git Service", () => { }); describe("getCurrentBranch", () => { - it("should return current branch name", async () => { + it("success", async () => { setupMockProcess("main\n"); - - const result = await git.getCurrentBranch(); - - expect(result).toEqual({ + expect(await git.getCurrentBranch()).toEqual({ success: true, data: "main", }); - expect(mockSpawn).toHaveBeenCalledWith( - "git", - ["rev-parse", "--abbrev-ref", "HEAD"], - { cwd: mockWorkingDir }, - ); }); - it("should handle error when getting current branch", async () => { + it("error", async () => { setupMockProcess("", "fatal: not a git repository", 128); - - const result = await git.getCurrentBranch(); - - expect(result).toEqual({ + expect(await git.getCurrentBranch()).toEqual({ success: false, error: "fatal: not a git repository", }); @@ -169,61 +130,33 @@ describe("Git Service", () => { }); describe("status", () => { - it("should return git status", async () => { + it("success", async () => { const statusOutput = "On branch main\nnothing to commit, working tree clean"; setupMockProcess(statusOutput); - - const result = await git.status(); - - expect(result).toEqual({ + expect(await git.status()).toEqual({ success: true, data: statusOutput, }); - expect(mockSpawn).toHaveBeenCalledWith("git", ["status"], { - cwd: mockWorkingDir, - }); - }); - - it("should handle error when repository is not initialized", async () => { - setupMockProcess("", "fatal: not a git repository", 128); - - const result = await git.status(); - - expect(result).toEqual({ - success: false, - error: "fatal: not a git repository", - }); }); }); describe("add", () => { - it("should add files to staging", async () => { + it("success", async () => { setupMockProcess(""); - - const result = await git.add(["file1.txt", "file2.txt"]); - - expect(result).toEqual({ + expect(await git.add(["file1.txt"])).toEqual({ success: true, data: "", }); - expect(mockSpawn).toHaveBeenCalledWith( - "git", - ["add", "file1.txt", "file2.txt"], - { cwd: mockWorkingDir }, - ); }); - it("should handle error when adding non-existent files", async () => { + it("error - non-existent files", async () => { setupMockProcess( "", "fatal: pathspec 'nonexistent.txt' did not match any files", 128, ); - - const result = await git.add(["nonexistent.txt"]); - - expect(result).toEqual({ + expect(await git.add(["nonexistent.txt"])).toEqual({ success: false, error: "fatal: pathspec 'nonexistent.txt' did not match any files", }); @@ -231,29 +164,18 @@ describe("Git Service", () => { }); describe("commit", () => { - it("should commit changes with message", async () => { + it("success", async () => { const commitOutput = "[main abc1234] test commit\n 1 file changed"; setupMockProcess(commitOutput); - - const result = await git.commit("test commit"); - - expect(result).toEqual({ + expect(await git.commit("test commit")).toEqual({ success: true, data: commitOutput, }); - expect(mockSpawn).toHaveBeenCalledWith( - "git", - ["commit", "-m", "test commit"], - { cwd: mockWorkingDir }, - ); }); - it("should handle error when there are no changes to commit", async () => { + it("error - nothing to commit", async () => { setupMockProcess("", "nothing to commit, working tree clean", 1); - - const result = await git.commit("test commit"); - - expect(result).toEqual({ + expect(await git.commit("test commit")).toEqual({ success: false, error: "nothing to commit, working tree clean", }); @@ -261,115 +183,39 @@ describe("Git Service", () => { }); describe("pull", () => { - it("should pull changes from remote", async () => { + it("success", async () => { setupMockProcess("Already up to date."); - - const result = await git.pull(); - - expect(result).toEqual({ + expect(await git.pull()).toEqual({ success: true, data: "Already up to date.", }); - expect(mockSpawn).toHaveBeenCalledWith("git", ["pull"], { - cwd: mockWorkingDir, - }); }); - it("should handle error when there is no remote configured", async () => { + it("error - no remote", async () => { setupMockProcess("", "fatal: no remote repository specified", 1); - - const result = await git.pull(); - - expect(result).toEqual({ + expect(await git.pull()).toEqual({ success: false, error: "fatal: no remote repository specified", }); }); }); - describe("setUserName", () => { - it("should set git user name", async () => { - setupMockProcess(""); // Git config doesn't return output on success - - const result = await git.setUserName("John Doe"); - - expect(result).toEqual({ - success: true, - data: "", - }); - expect(mockSpawn).toHaveBeenCalledWith( - "git", - ["config", "user.name", "John Doe"], - { cwd: mockWorkingDir }, - ); - }); - - it("should handle error when setting user name fails", async () => { - setupMockProcess("", "error: could not set user.name", 1); - - const result = await git.setUserName("John Doe"); - - expect(result).toEqual({ - success: false, - error: "error: could not set user.name", - }); - }); - }); - - describe("setUserEmail", () => { - it("should set git user email", async () => { - setupMockProcess(""); // Git config doesn't return output on success - - const result = await git.setUserEmail("john.doe@example.com"); - - expect(result).toEqual({ - success: true, - data: "", - }); - expect(mockSpawn).toHaveBeenCalledWith( - "git", - ["config", "user.email", "john.doe@example.com"], - { cwd: mockWorkingDir }, - ); - }); - - it("should handle error when setting user email fails", async () => { - setupMockProcess("", "error: could not set user.email", 1); - - const result = await git.setUserEmail("john.doe@example.com"); - - expect(result).toEqual({ - success: false, - error: "error: could not set user.email", - }); - }); - }); - describe("push", () => { - it("should push changes to remote", async () => { + it("success", async () => { setupMockProcess("Everything up-to-date"); - - const result = await git.push(); - - expect(result).toEqual({ + expect(await git.push()).toEqual({ success: true, data: "Everything up-to-date", }); - expect(mockSpawn).toHaveBeenCalledWith("git", ["push"], { - cwd: mockWorkingDir, - }); }); - it("should handle error when there is no upstream branch", async () => { + it("error - no upstream", async () => { setupMockProcess( "", "fatal: The current branch has no upstream branch", 1, ); - - const result = await git.push(); - - expect(result).toEqual({ + expect(await git.push()).toEqual({ success: false, error: "fatal: The current branch has no upstream branch", }); diff --git a/tests/services/system.test.ts b/tests/services/system.test.ts index 2cef577..0175ff6 100644 --- a/tests/services/system.test.ts +++ b/tests/services/system.test.ts @@ -6,16 +6,12 @@ jest.mock("os"); describe("System", () => { let system: System; - let mockSynapse: jest.Mocked; + let mockSynapse: Synapse; beforeEach(() => { - mockSynapse = jest.mocked({ + mockSynapse = { logger: jest.fn(), - setLogger: jest.fn(), - connect: jest.fn(), - disconnect: jest.fn(), - sendCommand: jest.fn(), - } as unknown as Synapse); + } as unknown as Synapse; system = new System(mockSynapse); }); @@ -39,13 +35,29 @@ describe("System", () => { (os.freemem as jest.Mock).mockReturnValue(4000000000); (os.loadavg as jest.Mock).mockReturnValue([1.5, 1.0, 0.5]); - const result = await system.getUsage(100); // Shorter interval for testing + const result = await system.getUsage(100); expect(result.success).toBe(true); - expect(result.data).toHaveProperty("cpuCores", 1); - expect(result.data).toHaveProperty("memoryTotalBytes", 8000000000); - expect(result.data).toHaveProperty("memoryFreeBytes", 4000000000); - expect(result.data).toHaveProperty("memoryUsagePercent", 50); + expect(result.data).toBeDefined(); + + if (result.data) { + const data = result.data; + expect(data.cpuCores).toBe(1); + expect(Array.isArray(data.cpuUsagePerCore)).toBe(true); + expect(data.cpuUsagePerCore.length).toBe(1); + expect(typeof data.cpuUsagePercent).toBe("number"); + expect(data.cpuUsagePercent).toBeGreaterThanOrEqual(0); + expect(data.cpuUsagePercent).toBeLessThanOrEqual(100); + + expect(data.loadAverage1m).toBe(1.5); + expect(data.loadAverage5m).toBe(1.0); + expect(data.loadAverage15m).toBe(0.5); + + expect(data.memoryTotalBytes).toBe(8000000000); + expect(data.memoryFreeBytes).toBe(4000000000); + expect(data.memoryUsedBytes).toBe(4000000000); + expect(data.memoryUsagePercent).toBe(50); + } }); }); }); diff --git a/tests/services/terminal.test.ts b/tests/services/terminal.test.ts index 4e4f67f..df17a24 100644 --- a/tests/services/terminal.test.ts +++ b/tests/services/terminal.test.ts @@ -1,5 +1,4 @@ import * as pty from "node-pty"; -import * as os from "os"; import { Terminal, TerminalOptions } from "../../src/services/terminal"; import { Synapse } from "../../src/synapse"; @@ -33,57 +32,27 @@ describe("Terminal", () => { (pty.spawn as jest.Mock).mockReturnValue(mockPty); }); - describe("constructor", () => { - it("should create terminal with default options", () => { - const defaultShell = - os.platform() === "win32" ? "powershell.exe" : "bash"; - + describe("basic terminal functionality", () => { + beforeEach(() => { terminal = new Terminal(mockSynapse); - - expect(pty.spawn).toHaveBeenCalledWith( - defaultShell, - [], - expect.objectContaining({ - name: "xterm-color", - cols: 80, - rows: 24, - cwd: process.cwd(), - env: process.env, - }), - ); - expect(mockPty.onExit).toHaveBeenCalled(); }); - it("should create terminal with custom options", () => { - const customOptions: TerminalOptions = { - shell: "zsh", - cols: 100, - rows: 30, - workdir: "/custom/path", - }; - - terminal = new Terminal(mockSynapse, customOptions); - - expect(pty.spawn).toHaveBeenCalledWith( - "zsh", - [], - expect.objectContaining({ - name: "xterm-color", - cols: 100, - rows: 30, - cwd: "/custom/path", - env: process.env, - }), - ); + it("should initialize with correct state", () => { + expect(terminal.isTerminalAlive()).toBe(true); }); - it("should handle spawn failure", () => { - const error = new Error("Spawn failed"); - (pty.spawn as jest.Mock).mockImplementationOnce(() => { - throw error; + it("should handle data events", () => { + let receivedData = ""; + terminal.onData((success, data) => { + receivedData = data; }); - terminal = new Terminal(mockSynapse); + onDataHandler("test output"); + expect(receivedData).toBe("test output"); + }); + + it("should handle terminal death", () => { + terminal.kill(); expect(terminal.isTerminalAlive()).toBe(false); }); }); @@ -93,64 +62,24 @@ describe("Terminal", () => { terminal = new Terminal(mockSynapse); }); - it("should handle resize operation with minimum values", () => { - expect(terminal.isTerminalAlive()).toBe(true); - terminal.updateSize(0, -5); - expect(mockPty.resize).toHaveBeenCalledWith(1, 1); - }); - - it("should handle write operation with command", () => { - const command = "ls -la\n"; - expect(terminal.isTerminalAlive()).toBe(true); - terminal.createCommand(command); - expect(mockPty.write).toHaveBeenCalledWith(command); - }); - - it("should handle data callback", () => { - const mockCallback = jest.fn(); - terminal.onData(mockCallback); - - // Simulate data event - onDataHandler("test output"); - expect(mockCallback).toHaveBeenCalledWith(true, "test output", null); - }); - - it("should override previous data callback", () => { - const mockCallback1 = jest.fn(); - const mockCallback2 = jest.fn(); - - terminal.onData(mockCallback1); - terminal.onData(mockCallback2); - - // Simulate data event - onDataHandler("test output"); - - expect(mockCallback1).not.toHaveBeenCalled(); - expect(mockCallback2).toHaveBeenCalledWith(true, "test output", null); - }); - - it("should handle kill operation", () => { - expect(terminal.isTerminalAlive()).toBe(true); + it("should not operate when terminal is dead", () => { terminal.kill(); - expect(mockPty.kill).toHaveBeenCalled(); - expect(terminal.isTerminalAlive()).toBe(false); - }); + terminal.updateSize(80, 24); + terminal.createCommand("test"); - it("should handle terminal exit", () => { - expect(terminal.isTerminalAlive()).toBe(true); - onExitHandler(); expect(terminal.isTerminalAlive()).toBe(false); }); - it("should not perform operations on dead terminal", () => { - terminal.kill(); - expect(terminal.isTerminalAlive()).toBe(false); - - terminal.updateSize(80, 24); - expect(mockPty.resize).not.toHaveBeenCalled(); + it("should handle custom initialization", () => { + const customOptions: TerminalOptions = { + shell: "zsh", + cols: 100, + rows: 30, + workdir: "/custom/path", + }; - terminal.createCommand("test"); - expect(mockPty.write).not.toHaveBeenCalled(); + const customTerminal = new Terminal(mockSynapse, customOptions); + expect(customTerminal.isTerminalAlive()).toBe(true); }); }); }); diff --git a/tests/synapse.test.ts b/tests/synapse.test.ts index 183acef..99139ae 100644 --- a/tests/synapse.test.ts +++ b/tests/synapse.test.ts @@ -33,156 +33,85 @@ describe("Synapse", () => { }); describe("connect", () => { - it("should establish websocket connection successfully", async () => { + it("should connect successfully", async () => { const mockWs = createMockWebSocket(); - (WebSocket as unknown as jest.Mock).mockImplementation(() => mockWs); const connectPromise = synapse.connect("/terminal"); - mockWs.onopen!({} as WebSocket.Event); + setTimeout(() => mockWs.onopen!({} as WebSocket.Event), 0); - await expect(connectPromise).resolves.toBe(synapse); - expect(WebSocket).toHaveBeenCalledWith("ws://localhost:8080/terminal"); - }); - - it("should use default host and port when not provided", async () => { - const defaultSynapse = new Synapse(); - const mockWs = createMockWebSocket(); - - (WebSocket as unknown as jest.Mock).mockImplementation(() => mockWs); - - const connectPromise = defaultSynapse.connect("/"); - mockWs.onopen!({} as WebSocket.Event); - - await expect(connectPromise).resolves.toBe(defaultSynapse); - expect(WebSocket).toHaveBeenCalledWith("ws://localhost:3000/"); + const result = await connectPromise; + expect(result).toBe(synapse); }); it("should reject on connection error", async () => { const mockWs = createMockWebSocket(); - (WebSocket as unknown as jest.Mock).mockImplementation(() => mockWs); const connectPromise = synapse.connect("/"); - mockWs.onerror!({ - error: new Error("Connection failed"), - } as WebSocket.ErrorEvent); - - await expect(connectPromise).rejects.toThrow( - "WebSocket error: Unknown error. Failed to connect to ws://localhost:8080/", + setTimeout( + () => + mockWs.onerror!({ + error: new Error("Connection failed"), + } as WebSocket.ErrorEvent), + 0, ); + + await expect(connectPromise).rejects.toThrow("WebSocket error"); }); }); - describe("ping-pong", () => { - it("should handle ping-pong messages correctly", async () => { - const mockWs = createMockWebSocket(); + describe("message handling", () => { + let mockWs: ReturnType; + beforeEach(async () => { + mockWs = createMockWebSocket(); (WebSocket as unknown as jest.Mock).mockImplementation(() => mockWs); - const connectPromise = synapse.connect("/"); - mockWs.onopen!({} as WebSocket.Event); + setTimeout(() => mockWs.onopen!({} as WebSocket.Event), 0); await connectPromise; - - mockWs.onmessage!({ - data: "ping", - } as WebSocket.MessageEvent); - - expect(mockWs.send).toHaveBeenCalledWith("pong"); }); - }); - describe("message handling", () => { - it("should handle incoming messages correctly and pass exact message data", async () => { + it("should handle messages correctly", async () => { const handler = jest.fn(); synapse.onMessageType("test", handler); - const mockWs = createMockWebSocket(); - - (WebSocket as unknown as jest.Mock).mockImplementation(() => mockWs); - const connectPromise = synapse.connect("/"); - mockWs.onopen!({} as WebSocket.Event); - await connectPromise; - const testMessage = { type: "test", requestId: "123", - data: { foo: "bar", count: 42 }, + data: { foo: "bar" }, }; - mockWs.onmessage!({ - data: JSON.stringify(testMessage), - } as WebSocket.MessageEvent); - - expect(handler).toHaveBeenCalledTimes(1); - expect(handler).toHaveBeenCalledWith( - expect.objectContaining({ - type: "test", - requestId: "123", - data: { foo: "bar", count: 42 }, - }), - ); - }); - - it("should not trigger handlers for different message types", async () => { - const testHandler = jest.fn(); - const otherHandler = jest.fn(); - synapse.onMessageType("test", testHandler); - synapse.onMessageType("other", otherHandler); - - const mockWs = createMockWebSocket(); - - (WebSocket as unknown as jest.Mock).mockImplementation(() => mockWs); - const connectPromise = synapse.connect("/"); - mockWs.onopen!({} as WebSocket.Event); - await connectPromise; - const testMessage = { type: "test", requestId: "123", data: "test-data" }; mockWs.onmessage!({ data: JSON.stringify(testMessage), } as WebSocket.MessageEvent); - - expect(testHandler).toHaveBeenCalledTimes(1); - expect(otherHandler).not.toHaveBeenCalled(); + expect(handler).toHaveBeenCalledWith(testMessage); }); - it("should handle malformed JSON messages gracefully", async () => { + it("should ignore malformed messages", async () => { const handler = jest.fn(); synapse.onMessageType("test", handler); - const mockWs = createMockWebSocket(); - - (WebSocket as unknown as jest.Mock).mockImplementation(() => mockWs); - const connectPromise = synapse.connect("/"); - mockWs.onopen!({} as WebSocket.Event); - await connectPromise; - - mockWs.onmessage!({ - data: "invalid json{", - } as WebSocket.MessageEvent); - + mockWs.onmessage!({ data: "invalid json{" } as WebSocket.MessageEvent); expect(handler).not.toHaveBeenCalled(); }); }); describe("send", () => { - it("should send messages with correct format and return promise with message payload", async () => { - const mockWs = createMockWebSocket(); + let mockWs: ReturnType; + beforeEach(async () => { + mockWs = createMockWebSocket(); (WebSocket as unknown as jest.Mock).mockImplementation(() => mockWs); const connectPromise = synapse.connect("/"); - mockWs.onopen!({} as WebSocket.Event); + setTimeout(() => mockWs.onopen!({} as WebSocket.Event), 0); await connectPromise; + }); + it("should send messages and return payload", async () => { const payload = { data: "test-data" }; const result = await synapse.send("test-type", payload); - expect(mockWs.send).toHaveBeenCalledTimes(1); - const sentMessage = JSON.parse(mockWs.send.mock.calls[0][0]); - expect(sentMessage).toEqual({ - type: "test-type", - requestId: expect.any(String), - data: "test-data", - }); expect(result).toEqual({ type: "test-type", requestId: expect.any(String), @@ -190,20 +119,8 @@ describe("Synapse", () => { }); }); - it("should throw error when WebSocket is not connected", () => { - expect(() => synapse.send("test-type")).toThrow( - "WebSocket is not connected", - ); - }); - - it("should throw error when WebSocket is not in OPEN state", async () => { - const mockWs = createMockWebSocket({ readyState: WebSocket.CLOSED }); - - (WebSocket as unknown as jest.Mock).mockImplementation(() => mockWs); - const connectPromise = synapse.connect("/"); - mockWs.onopen!({} as WebSocket.Event); - await connectPromise; - + it("should throw when not connected", () => { + synapse.disconnect(); expect(() => synapse.send("test-type")).toThrow( "WebSocket is not connected", ); @@ -211,100 +128,35 @@ describe("Synapse", () => { }); describe("handleUpgrade", () => { - it("should handle upgrade requests and establish WebSocket connection", () => { - const mockReq = { - headers: { - upgrade: "websocket", - }, - } as IncomingMessage; - - const mockSocket = {} as Socket; - const mockHead = Buffer.alloc(0); + it("should handle upgrade and setup handlers", () => { const mockWs = createMockWebSocket(); - - const mockHandleUpgrade = jest.fn((req, socket, head, cb) => { - cb(mockWs); - }); - const mockEmit = jest.fn(); - const mockWss = { - handleUpgrade: mockHandleUpgrade, - emit: mockEmit, - on: jest.fn((event, callback) => { - if (event === "connection") { - callback(mockWs); - } - }), - close: jest.fn(), - } as unknown as WebSocketServer; - - jest.mocked(WebSocketServer).mockImplementation(() => mockWss); - - synapse = new Synapse(); // Recreate synapse with mocked WSS - synapse.handleUpgrade(mockReq, mockSocket, mockHead); - - expect(mockHandleUpgrade).toHaveBeenCalledWith( - mockReq, - mockSocket, - mockHead, - expect.any(Function), - ); - - expect(mockEmit).toHaveBeenCalledWith("connection", mockWs, mockReq); - }); - - it("should set up event handlers after successful upgrade", () => { - const mockReq = {} as IncomingMessage; - const mockSocket = {} as Socket; - const mockHead = Buffer.alloc(0); - - const mockWs = createMockWebSocket(); - - const mockHandleUpgrade = jest.fn((req, socket, head, cb) => { - cb(mockWs); - }); - const mockEmit = jest.fn((event: string, ws: WebSocket) => { - if (event === "connection") { - // Simulate the WebSocketServer connection event handler + handleUpgrade: jest.fn((req, socket, head, cb) => { + cb(mockWs); const connectionHandler = (mockWss.on as jest.Mock).mock.calls.find( ([eventName]: [string]) => eventName === "connection", )?.[1]; - connectionHandler?.(ws); - } - }); - - const mockWss = { - handleUpgrade: mockHandleUpgrade, - emit: mockEmit, - on: jest.fn(), + if (connectionHandler) { + connectionHandler(mockWs); + } + }), + emit: jest.fn(), + on: jest.fn() as jest.Mock, close: jest.fn(), } as unknown as WebSocketServer; jest.mocked(WebSocketServer).mockImplementation(() => mockWss); const onOpenMock = jest.fn(); - const onCloseMock = jest.fn(); - const onErrorMock = jest.fn(); - - synapse = new Synapse(); // Recreate synapse with mocked WSS + synapse = new Synapse(); synapse.onOpen(onOpenMock); - synapse.onClose(onCloseMock); - synapse.onError(onErrorMock); - - synapse.handleUpgrade(mockReq, mockSocket, mockHead); - - // The onOpen callback should be triggered when the connection is established - expect(onOpenMock).toHaveBeenCalled(); - - mockWs.onclose!({} as WebSocket.CloseEvent); - expect(onCloseMock).toHaveBeenCalled(); - mockWs.onerror!({} as WebSocket.ErrorEvent); - expect(onErrorMock).toHaveBeenCalledWith( - new Error( - "WebSocket error: Unknown error. Connection to localhost:3000 failed.", - ), + synapse.handleUpgrade( + {} as IncomingMessage, + {} as Socket, + Buffer.alloc(0), ); + expect(onOpenMock).toHaveBeenCalled(); }); }); }); From 67020f6579e9dfc752260b3eb89b5ade52f9f688 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 3 Apr 2025 16:00:24 +0000 Subject: [PATCH 22/42] chore: update class name from codestyle to code --- README.md | 8 +++---- package.json | 7 +++--- src/index.ts | 2 +- src/services/{codestyle.ts => code.ts} | 2 +- src/services/terminal.ts | 24 ++----------------- .../{codestyle.test.ts => code.test.ts} | 16 ++++++------- 6 files changed, 20 insertions(+), 39 deletions(-) rename src/services/{codestyle.ts => code.ts} (99%) rename tests/services/{codestyle.test.ts => code.test.ts} (89%) diff --git a/README.md b/README.md index 53b7e57..cdb3137 100644 --- a/README.md +++ b/README.md @@ -137,17 +137,17 @@ await git.push(); ```typescript // Lint and format code -import { Synapse, CodeStyle } from "@appwrite.io/synapse"; +import { Synapse, Code } from "@appwrite.io/synapse"; const synapse = new Synapse(); -const codeStyle = new CodeStyle(synapse); +const code = new Code(synapse); // Format code with specific options const code = `function hello(name) { return "Hello, " + name; }`; -const formatResult = await codeStyle.format(code, { +const formatResult = await code.format(code, { language: "javascript", indent: 2, singleQuote: true, @@ -157,7 +157,7 @@ const formatResult = await codeStyle.format(code, { console.log("Formatted code:", formatResult.data); // Lint code for potential issues -const lintResult = await codeStyle.lint(code, { +const lintResult = await code.lint(code, { language: "javascript", rules: { semi: "error", diff --git a/package.json b/package.json index 145d231..73059f9 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,11 @@ }, "license": "MIT", "dependencies": { - "node-pty": "^1.0.0", - "ws": "^8.18.1", "eslint": "^9.23.0", - "prettier": "^3.5.3" + "micro": "^10.0.1", + "node-pty": "^1.0.0", + "prettier": "^3.5.3", + "ws": "^8.18.1" }, "devDependencies": { "@types/jest": "^29.5.14", diff --git a/src/index.ts b/src/index.ts index 2b2ce5f..98f9b3e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -export { CodeStyle } from "./services/codestyle"; +export { Code } from "./services/code"; export { Filesystem } from "./services/filesystem"; export { Git } from "./services/git"; export { System } from "./services/system"; diff --git a/src/services/codestyle.ts b/src/services/code.ts similarity index 99% rename from src/services/codestyle.ts rename to src/services/code.ts index e6f52a9..70cb935 100644 --- a/src/services/codestyle.ts +++ b/src/services/code.ts @@ -36,7 +36,7 @@ export interface LintResult { error?: string; } -export class CodeStyle { +export class Code { private synapse: Synapse; /** diff --git a/src/services/terminal.ts b/src/services/terminal.ts index 47d32b6..ba3f0c1 100644 --- a/src/services/terminal.ts +++ b/src/services/terminal.ts @@ -153,24 +153,6 @@ export class Terminal { } } - /** - * Cleans terminal output data by removing ANSI escape sequences, carriage returns, and extra whitespace - * Also filters out the echoed command from the output - * @param data - The raw terminal output data - * @returns The cleaned data string - */ - private cleanData(data: string): string { - let cleaned = data - .replace(/\u001b\[[0-9;?]*[a-zA-Z]/g, "") // Remove ANSI escape sequences - .replace(/\r/g, ""); // Remove carriage returns - - if (this.lastCommand && cleaned.trim().startsWith(this.lastCommand)) { - cleaned = cleaned.slice(this.lastCommand.length); - } - - return cleaned.trim(); - } - /** * Sets the callback for when data is received from the terminal * @param callback - The callback to set @@ -188,16 +170,14 @@ export class Terminal { data: string, messageId: string | null, ) => { - callback(success, this.cleanData(data), messageId); + callback(success, data, messageId); }; // If there was an initialization error, notify the callback immediately if (this.initializationError && callback) { callback( false, - this.cleanData( - `Terminal initialization failed: ${this.initializationError.message}`, - ), + `Terminal initialization failed: ${this.initializationError.message}`, null, ); } diff --git a/tests/services/codestyle.test.ts b/tests/services/code.test.ts similarity index 89% rename from tests/services/codestyle.test.ts rename to tests/services/code.test.ts index 775d0d7..665a246 100644 --- a/tests/services/codestyle.test.ts +++ b/tests/services/code.test.ts @@ -1,21 +1,21 @@ import { beforeEach, describe, expect, jest, test } from "@jest/globals"; import { ESLint } from "eslint"; import { format } from "prettier"; -import { CodeStyle } from "../../src/services/codestyle"; +import { Code } from "../../src/services/code"; import { Synapse } from "../../src/synapse"; jest.mock("prettier"); jest.mock("eslint"); jest.mock("../../src/synapse"); -describe("CodeStyle", () => { - let codeStyle: CodeStyle; +describe("Code", () => { + let code: Code; let mockSynapse: jest.Mocked; beforeEach(() => { jest.clearAllMocks(); mockSynapse = new Synapse() as jest.Mocked; - codeStyle = new CodeStyle(mockSynapse); + code = new Code(mockSynapse); }); describe("format", () => { @@ -27,7 +27,7 @@ describe("CodeStyle", () => { expected, ); - const result = await codeStyle.format(input, { language: "javascript" }); + const result = await code.format(input, { language: "javascript" }); expect(result).toEqual({ success: true, @@ -43,7 +43,7 @@ describe("CodeStyle", () => { expected, ); - const result = await codeStyle.format(input, { + const result = await code.format(input, { language: "typescript", indent: 4, useTabs: true, @@ -92,7 +92,7 @@ describe("CodeStyle", () => { })); (ESLint as unknown) = MockESLint; - const result = await codeStyle.lint(input, { language: "javascript" }); + const result = await code.lint(input, { language: "javascript" }); expect(result).toEqual({ success: true, @@ -143,7 +143,7 @@ describe("CodeStyle", () => { })); (ESLint as unknown) = MockESLint; - const result = await codeStyle.lint(input, { language: "javascript" }); + const result = await code.lint(input, { language: "javascript" }); expect(result).toEqual({ success: true, From a5e6f67f8fc76ee51c633f94244674c16d124563 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 3 Apr 2025 16:10:56 +0000 Subject: [PATCH 23/42] chore: fix createCommand --- src/services/terminal.ts | 84 ++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 46 deletions(-) diff --git a/src/services/terminal.ts b/src/services/terminal.ts index ba3f0c1..7eeaff7 100644 --- a/src/services/terminal.ts +++ b/src/services/terminal.ts @@ -9,12 +9,17 @@ export type TerminalOptions = { workdir?: string; }; +export type TerminalData = { + success: boolean; + data?: string; + error?: string; +}; + export class Terminal { private synapse: Synapse; private term: pty.IPty | null = null; - private onDataCallback: - | ((success: boolean, data: string, messageId: string | null) => void) - | null = null; + private onDataCallback: ((success: boolean, data: string) => void) | null = + null; private isAlive: boolean = false; private initializationError: Error | null = null; private lastCommand: string = ""; @@ -48,7 +53,7 @@ export class Terminal { this.term.onData((data: string) => { if (this.onDataCallback) { - this.onDataCallback(true, data, null); + this.onDataCallback(true, data); } }); @@ -60,7 +65,6 @@ export class Terminal { this.onDataCallback( false, `Terminal process exited with code ${e?.exitCode ?? "unknown"}`, - null, ); } }); @@ -77,7 +81,6 @@ export class Terminal { this.onDataCallback( false, `Terminal initialization failed: ${errorMessage}`, - null, ); } } @@ -106,28 +109,21 @@ export class Terminal { * @param cols - The number of columns * @param rows - The number of rows */ - updateSize( - cols: number, - rows: number, - messageId: string | null = null, - ): void { + updateSize(cols: number, rows: number): TerminalData { try { this.checkTerminal(); this.term?.resize(Math.max(cols, 1), Math.max(rows, 1)); - if (this.onDataCallback) { - this.onDataCallback( - true, - "Terminal size updated successfully", - messageId, - ); - } + return { + success: true, + data: "Terminal size updated successfully", + }; } catch (error) { console.error("Failed to update terminal size:", error); - if (this.onDataCallback) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error occurred"; - this.onDataCallback(false, errorMessage, messageId); - } + return { + success: false, + error: + error instanceof Error ? error.message : "Unknown error occurred", + }; } } @@ -135,21 +131,28 @@ export class Terminal { * Writes a command to the terminal * @param command - The command to write */ - createCommand(command: string, messageId: string | null = null): void { + createCommand(command: string): TerminalData { try { this.checkTerminal(); this.lastCommand = command.trim(); - this.term?.write(command); - if (this.onDataCallback) { - this.onDataCallback(true, "Command executed successfully", messageId); + + // Ensure command ends with newline + if (!command.endsWith("\n")) { + command += "\n"; } + + this.term?.write(command); + return { + success: true, + data: "Command executed successfully", + }; } catch (error) { console.error("Failed to execute command:", error); - if (this.onDataCallback) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error occurred"; - this.onDataCallback(false, errorMessage, messageId); - } + return { + success: false, + error: + error instanceof Error ? error.message : "Unknown error occurred", + }; } } @@ -157,20 +160,10 @@ export class Terminal { * Sets the callback for when data is received from the terminal * @param callback - The callback to set */ - onData( - callback: ( - success: boolean, - data: string, - messageId: string | null, - ) => void, - ): void { + onData(callback: (success: boolean, data: string) => void): void { // Wrap the callback to clean the data before sending - this.onDataCallback = ( - success: boolean, - data: string, - messageId: string | null, - ) => { - callback(success, data, messageId); + this.onDataCallback = (success: boolean, data: string) => { + callback(success, data); }; // If there was an initialization error, notify the callback immediately @@ -178,7 +171,6 @@ export class Terminal { callback( false, `Terminal initialization failed: ${this.initializationError.message}`, - null, ); } } From 8a4be7f52ffa94fece4dbf001496297cf7701852 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 3 Apr 2025 16:42:05 +0000 Subject: [PATCH 24/42] chore: remove sending of command in terminal response --- src/services/terminal.ts | 59 ++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/src/services/terminal.ts b/src/services/terminal.ts index 7eeaff7..9b69bac 100644 --- a/src/services/terminal.ts +++ b/src/services/terminal.ts @@ -9,12 +9,6 @@ export type TerminalOptions = { workdir?: string; }; -export type TerminalData = { - success: boolean; - data?: string; - error?: string; -}; - export class Terminal { private synapse: Synapse; private term: pty.IPty | null = null; @@ -22,7 +16,23 @@ export class Terminal { null; private isAlive: boolean = false; private initializationError: Error | null = null; - private lastCommand: string = ""; + private lastCommand: string | null = null; + + /** + * Strips ANSI escape sequences and control characters from a string + * @param str - The string to clean + * @returns The cleaned string + */ + private stripAnsi(str: string): string { + return str + .replace( + /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, + "", + ) + .replace(/\r/g, "") // Remove carriage returns + .replace(/\u001b\[?[0-9;]*[A-Za-z]/g, "") // Remove cursor movements + .replace(/\u001b\[\?[0-9;]*[A-Za-z]/g, ""); // Remove terminal mode commands + } /** * Creates a new Terminal instance @@ -109,21 +119,12 @@ export class Terminal { * @param cols - The number of columns * @param rows - The number of rows */ - updateSize(cols: number, rows: number): TerminalData { + updateSize(cols: number, rows: number): void { try { this.checkTerminal(); this.term?.resize(Math.max(cols, 1), Math.max(rows, 1)); - return { - success: true, - data: "Terminal size updated successfully", - }; } catch (error) { console.error("Failed to update terminal size:", error); - return { - success: false, - error: - error instanceof Error ? error.message : "Unknown error occurred", - }; } } @@ -131,28 +132,19 @@ export class Terminal { * Writes a command to the terminal * @param command - The command to write */ - createCommand(command: string): TerminalData { + createCommand(command: string): void { try { this.checkTerminal(); - this.lastCommand = command.trim(); // Ensure command ends with newline if (!command.endsWith("\n")) { command += "\n"; } + this.lastCommand = command; this.term?.write(command); - return { - success: true, - data: "Command executed successfully", - }; } catch (error) { console.error("Failed to execute command:", error); - return { - success: false, - error: - error instanceof Error ? error.message : "Unknown error occurred", - }; } } @@ -161,12 +153,19 @@ export class Terminal { * @param callback - The callback to set */ onData(callback: (success: boolean, data: string) => void): void { - // Wrap the callback to clean the data before sending this.onDataCallback = (success: boolean, data: string) => { + const cleanData = this.stripAnsi(data); + const cleanCommand = this.lastCommand + ? this.stripAnsi(this.lastCommand) + : null; + + if (cleanCommand && cleanData === cleanCommand) { + return; + } + callback(success, data); }; - // If there was an initialization error, notify the callback immediately if (this.initializationError && callback) { callback( false, From 17de6d14091b658186d61c3aa0f090b1a3813944 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 3 Apr 2025 17:03:39 +0000 Subject: [PATCH 25/42] chore: return ansii as well --- src/services/terminal.ts | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/src/services/terminal.ts b/src/services/terminal.ts index 9b69bac..1c99056 100644 --- a/src/services/terminal.ts +++ b/src/services/terminal.ts @@ -16,23 +16,6 @@ export class Terminal { null; private isAlive: boolean = false; private initializationError: Error | null = null; - private lastCommand: string | null = null; - - /** - * Strips ANSI escape sequences and control characters from a string - * @param str - The string to clean - * @returns The cleaned string - */ - private stripAnsi(str: string): string { - return str - .replace( - /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, - "", - ) - .replace(/\r/g, "") // Remove carriage returns - .replace(/\u001b\[?[0-9;]*[A-Za-z]/g, "") // Remove cursor movements - .replace(/\u001b\[\?[0-9;]*[A-Za-z]/g, ""); // Remove terminal mode commands - } /** * Creates a new Terminal instance @@ -141,7 +124,6 @@ export class Terminal { command += "\n"; } - this.lastCommand = command; this.term?.write(command); } catch (error) { console.error("Failed to execute command:", error); @@ -154,15 +136,6 @@ export class Terminal { */ onData(callback: (success: boolean, data: string) => void): void { this.onDataCallback = (success: boolean, data: string) => { - const cleanData = this.stripAnsi(data); - const cleanCommand = this.lastCommand - ? this.stripAnsi(this.lastCommand) - : null; - - if (cleanCommand && cleanData === cleanCommand) { - return; - } - callback(success, data); }; From 1f3c10b7d49d683e8f228e682a128fc5bb4313f6 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 3 Apr 2025 17:29:19 +0000 Subject: [PATCH 26/42] chore: add one central working dir --- src/services/code.ts | 15 +++++++ src/services/filesystem.ts | 88 +++++++++++--------------------------- src/services/git.ts | 11 ----- src/services/system.ts | 11 +++-- src/services/terminal.ts | 50 ++++++++++++++++------ 5 files changed, 86 insertions(+), 89 deletions(-) diff --git a/src/services/code.ts b/src/services/code.ts index 70cb935..1e3ec12 100644 --- a/src/services/code.ts +++ b/src/services/code.ts @@ -47,6 +47,11 @@ export class Code { this.synapse = synapse; } + private log(message: string): void { + const timestamp = new Date().toISOString(); + console.log(`[Code][${timestamp}] ${message}`); + } + private getParserForLanguage(language: string): string { const languageMap: Record = { javascript: "babel", @@ -99,6 +104,8 @@ export class Code { }; } + this.log(`Formatting code with language: ${options.language}`); + const prettierOptions = this.toPrettierOptions(options.language, options); const formattedCode = await format(code, prettierOptions); @@ -107,6 +114,9 @@ export class Code { data: formattedCode, }; } catch (error) { + this.log( + `Formatting failed: ${error instanceof Error ? error.message : "Unknown error"}`, + ); return { success: false, error: `Formatting failed: ${error instanceof Error ? error.message : "Unknown error"}`, @@ -136,6 +146,8 @@ export class Code { }; } + this.log(`Linting code with language: ${options.language}`); + const eslintOptions = { overrideConfig: { languageOptions: { @@ -165,6 +177,9 @@ export class Code { }, }; } catch (error) { + this.log( + `Linting failed: ${error instanceof Error ? error.message : "Unknown error"}`, + ); return { success: false, error: `Linting failed: ${error instanceof Error ? error.message : "Unknown error"}`, diff --git a/src/services/filesystem.ts b/src/services/filesystem.ts index cb09559..8ded5f9 100644 --- a/src/services/filesystem.ts +++ b/src/services/filesystem.ts @@ -15,20 +15,18 @@ export type FileOperationResult = { export class Filesystem { private synapse: Synapse; - private workingDir: string; /** * Creates a new Filesystem instance * @param synapse - The Synapse instance to use - * @param workingDir - The working directory to use */ - constructor(synapse: Synapse, workingDir: string = process.cwd()) { + constructor(synapse: Synapse) { this.synapse = synapse; - this.workingDir = workingDir; } - private log(method: string, message: string): void { - this.synapse.logger(`[${method}] ${message}`); + private log(message: string): void { + const timestamp = new Date().toISOString(); + console.log(`[Filesystem][${timestamp}] ${message}`); } /** @@ -43,22 +41,16 @@ export class Filesystem { content: string = "", ): Promise { try { - this.log("createFile", `Creating file at path: ${filePath}`); - const fullPath = path.join(this.workingDir, filePath); - + this.log(`Creating file at path: ${filePath}`); + const fullPath = path.join(this.synapse.workDir, filePath); const dirPath = path.dirname(filePath); - this.log("createFile", `Ensuring directory exists: ${dirPath}`); await this.createFolder(dirPath); - - this.log("createFile", "Writing file content..."); await fs.writeFile(fullPath, content); - this.log("createFile", "File created successfully"); return { success: true }; } catch (error) { this.log( - "createFile", `Error: ${error instanceof Error ? error.message : String(error)}`, ); return { @@ -76,16 +68,14 @@ export class Filesystem { */ async getFile(filePath: string): Promise { try { - this.log("getFile", `Reading file at path: ${filePath}`); - const fullPath = path.join(this.workingDir, filePath); + this.log(`Reading file at path: ${filePath}`); + const fullPath = path.join(this.synapse.workDir, filePath); const data = await fs.readFile(fullPath, "utf-8"); - this.log("getFile", "File read successfully"); return { success: true, data }; } catch (error) { this.log( - "getFile", `Error: ${error instanceof Error ? error.message : String(error)}`, ); return { @@ -107,22 +97,16 @@ export class Filesystem { content: string, ): Promise { try { - this.log("updateFile", `Updating file at path: ${filePath}`); - const fullPath = path.join(this.workingDir, filePath); - + this.log(`Updating file at path: ${filePath}`); + const fullPath = path.join(this.synapse.workDir, filePath); const dirPath = path.dirname(filePath); - this.log("updateFile", `Ensuring directory exists: ${dirPath}`); await this.createFolder(dirPath); - - this.log("updateFile", "Writing file content..."); await fs.writeFile(fullPath, content); - this.log("updateFile", "File updated successfully"); return { success: true }; } catch (error) { this.log( - "updateFile", `Error: ${error instanceof Error ? error.message : String(error)}`, ); return { @@ -144,17 +128,15 @@ export class Filesystem { newPath: string, ): Promise { try { - this.log("updateFilePath", `Moving file from ${oldPath} to ${newPath}`); - const fullOldPath = path.join(this.workingDir, oldPath); - const fullNewPath = path.join(this.workingDir, newPath); + this.log(`Moving file from ${oldPath} to ${newPath}`); + const fullOldPath = path.join(this.synapse.workDir, oldPath); + const fullNewPath = path.join(this.synapse.workDir, newPath); await fs.rename(fullOldPath, fullNewPath); - this.log("updateFilePath", "File moved successfully"); return { success: true }; } catch (error) { this.log( - "updateFilePath", `Error: ${error instanceof Error ? error.message : String(error)}`, ); return { @@ -172,16 +154,14 @@ export class Filesystem { */ async deleteFile(filePath: string): Promise { try { - this.log("deleteFile", `Deleting file at path: ${filePath}`); - const fullPath = path.join(this.workingDir, filePath); + this.log(`Deleting file at path: ${filePath}`); + const fullPath = path.join(this.synapse.workDir, filePath); await fs.unlink(fullPath); - this.log("deleteFile", "File deleted successfully"); return { success: true }; } catch (error) { this.log( - "deleteFile", `Error: ${error instanceof Error ? error.message : String(error)}`, ); return { @@ -199,16 +179,14 @@ export class Filesystem { */ async createFolder(dirPath: string): Promise { try { - this.log("createFolder", `Creating directory at path: ${dirPath}`); - const fullPath = path.join(this.workingDir, dirPath); + this.log(`Creating directory at path: ${dirPath}`); + const fullPath = path.join(this.synapse.workDir, dirPath); await fs.mkdir(fullPath, { recursive: true }); - this.log("createFolder", "Directory created successfully"); return { success: true }; } catch (error) { this.log( - "createFolder", `Error: ${error instanceof Error ? error.message : String(error)}`, ); return { @@ -226,8 +204,8 @@ export class Filesystem { */ async getFolder(dirPath: string): Promise { try { - this.log("getFolder", `Reading directory at path: ${dirPath}`); - const fullPath = path.join(this.workingDir, dirPath); + this.log(`Reading directory at path: ${dirPath}`); + const fullPath = path.join(this.synapse.workDir, dirPath); const items = await fs.readdir(fullPath, { withFileTypes: true }); const data: FileItem[] = items.map((item) => ({ @@ -235,14 +213,9 @@ export class Filesystem { isDirectory: item.isDirectory(), })); - this.log( - "getFolder", - `Directory read successfully, found ${items.length} items`, - ); return { success: true, data }; } catch (error) { this.log( - "getFolder", `Error: ${error instanceof Error ? error.message : String(error)}`, ); throw error; @@ -261,19 +234,17 @@ export class Filesystem { name: string, ): Promise { try { - this.log("updateFolderName", `Renaming folder at ${dirPath} to ${name}`); - const fullPath = path.join(this.workingDir, dirPath); + this.log(`Renaming folder at ${dirPath} to ${name}`); + const fullPath = path.join(this.synapse.workDir, dirPath); const dir = path.dirname(fullPath); const newPath = path.join(dir, name); await fs.rename(fullPath, newPath); - this.log("updateFolderName", "Folder renamed successfully"); return { success: true }; } catch (error) { this.log( - "updateFolderName", `Error: ${error instanceof Error ? error.message : String(error)}`, ); return { @@ -295,20 +266,15 @@ export class Filesystem { newPath: string, ): Promise { try { - this.log( - "updateFolderPath", - `Moving folder from ${oldPath} to ${newPath}`, - ); - const fullOldPath = path.join(this.workingDir, oldPath); - const fullNewPath = path.join(this.workingDir, newPath); + this.log(`Moving folder from ${oldPath} to ${newPath}`); + const fullOldPath = path.join(this.synapse.workDir, oldPath); + const fullNewPath = path.join(this.synapse.workDir, newPath); await fs.rename(fullOldPath, fullNewPath); - this.log("updateFolderPath", "Folder moved successfully"); return { success: true }; } catch (error) { this.log( - "updateFolderPath", `Error: ${error instanceof Error ? error.message : String(error)}`, ); return { @@ -326,16 +292,14 @@ export class Filesystem { */ async deleteFolder(dirPath: string): Promise { try { - this.log("deleteFolder", `Deleting folder at path: ${dirPath}`); - const fullPath = path.join(this.workingDir, dirPath); + this.log(`Deleting folder at path: ${dirPath}`); + const fullPath = path.join(this.synapse.workDir, dirPath); await fs.rm(fullPath, { recursive: true, force: true }); - this.log("deleteFolder", "Folder deleted successfully"); return { success: true }; } catch (error) { this.log( - "deleteFolder", `Error: ${error instanceof Error ? error.message : String(error)}`, ); return { diff --git a/src/services/git.ts b/src/services/git.ts index 0afd8cd..9fc3c70 100644 --- a/src/services/git.ts +++ b/src/services/git.ts @@ -23,18 +23,10 @@ export class Git { this.workingDir = workingDir; } - /** - * Check if an error is a NodeJS.ErrnoException - */ private isErrnoException(error: unknown): error is NodeJS.ErrnoException { return error instanceof Error && "code" in error; } - /** - * Executes a git command and returns the output as a Promise - * @param args - The arguments to pass to the git command - * @returns The output of the git command - */ private async execute(args: string[]): Promise { return new Promise((resolve) => { const git = spawn("git", args, { cwd: this.workingDir }); @@ -69,9 +61,6 @@ export class Git { }); } - /** - * Check if the current directory is already a git repository - */ private async isGitRepository(): Promise { try { const gitDir = path.join(this.workingDir, ".git"); diff --git a/src/services/system.ts b/src/services/system.ts index e567f07..45f6505 100644 --- a/src/services/system.ts +++ b/src/services/system.ts @@ -33,6 +33,11 @@ export class System { this.synapse = synapse; } + private log(message: string): void { + const timestamp = new Date().toISOString(); + console.log(`[System][${timestamp}] ${message}`); + } + private calculateCPUUsage(startUsage: CPUTimes, endUsage: CPUTimes): number { const userDiff = endUsage.user - startUsage.user; const systemDiff = endUsage.system - startUsage.system; @@ -67,7 +72,7 @@ export class System { */ async getUsage(measurementInterval: number = 3000): Promise { try { - this.synapse.logger("Starting system usage measurement"); + this.log("Starting system usage measurement"); const startMeasurements = this.getCPUUsage(); await new Promise((resolve) => setTimeout(resolve, measurementInterval)); @@ -84,7 +89,7 @@ export class System { const [loadAvg1m, loadAvg5m, loadAvg15m] = os.loadavg(); - this.synapse.logger("System usage measurement completed"); + this.log("System usage measurement completed"); return { success: true, @@ -104,7 +109,7 @@ export class System { }, }; } catch (error) { - this.synapse.logger( + this.log( `Error in getUsage: ${error instanceof Error ? error.message : String(error)}`, ); return { diff --git a/src/services/terminal.ts b/src/services/terminal.ts index 1c99056..c745f8f 100644 --- a/src/services/terminal.ts +++ b/src/services/terminal.ts @@ -6,7 +6,6 @@ export type TerminalOptions = { shell: string; cols?: number; rows?: number; - workdir?: string; }; export class Terminal { @@ -16,6 +15,7 @@ export class Terminal { null; private isAlive: boolean = false; private initializationError: Error | null = null; + private lastCommand: string | null = null; /** * Creates a new Terminal instance @@ -28,7 +28,6 @@ export class Terminal { shell: os.platform() === "win32" ? "powershell.exe" : "bash", cols: 80, rows: 24, - workdir: process.cwd(), }, ) { this.synapse = synapse; @@ -38,7 +37,7 @@ export class Terminal { name: "xterm-color", cols: terminalOptions.cols, rows: terminalOptions.rows, - cwd: terminalOptions.workdir, + cwd: this.synapse.workDir, env: process.env, }); @@ -79,24 +78,36 @@ export class Terminal { } } - /** - * Get initialization error if any - * @returns The initialization error or null - */ - getInitializationError(): Error | null { - return this.initializationError; + private log(message: string): void { + const timestamp = new Date().toISOString(); + console.log(`[Terminal][${timestamp}] ${message}`); + } + + private stripAnsi(str: string): string { + return str + .replace( + /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, + "", + ) // Remove ANSI escape sequences + .replace(/\r/g, "") // Remove carriage returns + .replace(/\u001b\[?[0-9;]*[A-Za-z]/g, "") // Remove cursor movements + .replace(/\u001b\[\?[0-9;]*[A-Za-z]/g, ""); // Remove terminal mode commands } - /** - * Checks if the terminal is alive and ready - * @throws Error if terminal is not alive - */ private checkTerminal(): void { if (!this.isAlive || !this.term) { throw new Error("Terminal is not alive or has been terminated"); } } + /** + * Get initialization error if any + * @returns The initialization error or null + */ + getInitializationError(): Error | null { + return this.initializationError; + } + /** * Resizes the terminal * @param cols - The number of columns @@ -118,6 +129,9 @@ export class Terminal { createCommand(command: string): void { try { this.checkTerminal(); + this.log(`Writing command: ${command}`); + + this.lastCommand = command; // Ensure command ends with newline if (!command.endsWith("\n")) { @@ -136,6 +150,15 @@ export class Terminal { */ onData(callback: (success: boolean, data: string) => void): void { this.onDataCallback = (success: boolean, data: string) => { + const cleanData = this.stripAnsi(data); + const cleanCommand = this.lastCommand + ? this.stripAnsi(this.lastCommand) + : null; + + if (cleanCommand && cleanData === cleanCommand) { + return; + } + callback(success, data); }; @@ -152,6 +175,7 @@ export class Terminal { */ kill(): void { if (this.isAlive && this.term) { + this.log("Killing terminal"); this.term.kill(); this.isAlive = false; this.term = null; From 1d3a4c1e5940480ebf2bc6dbdba91d51944b8123 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 3 Apr 2025 17:31:50 +0000 Subject: [PATCH 27/42] fix: changes --- src/synapse.ts | 31 +++++++++++++++++-------------- tests/services/filesystem.test.ts | 3 ++- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/synapse.ts b/src/synapse.ts index 4e04e89..342639a 100644 --- a/src/synapse.ts +++ b/src/synapse.ts @@ -30,19 +30,30 @@ class Synapse { private lastPath: string | null = null; private reconnectTimeout: NodeJS.Timeout | null = null; - logger: Logger = console.log; private host: string; private port: number; - constructor(host: string = "localhost", port: number = 3000) { + public workDir: string; + + constructor( + host: string = "localhost", + port: number = 3000, + workDir: string = process.cwd(), + ) { this.host = host; this.port = port; + this.workDir = workDir; this.wss = new WebSocketServer({ noServer: true }); this.wss.on("connection", (ws: WebSocket) => { this.setupWebSocket(ws); }); } + private log(message: string): void { + const timestamp = new Date().toISOString(); + console.log(`[Synapse][${timestamp}] ${message}`); + } + private setupWebSocket(ws: WebSocket): void { this.ws = ws; this.reconnectAttempts = 0; @@ -75,17 +86,17 @@ class Synapse { this.reconnectAttempts++; this.reconnectTimeout = setTimeout(() => { - this.logger( + this.log( `Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`, ); this.connect(this.lastPath || "/") .then(() => { this.isReconnecting = false; - this.logger("Reconnection successful"); + this.log("Reconnection successful"); }) .catch((error) => { this.isReconnecting = false; - this.logger(`Reconnection failed: ${error.message}`); + this.log(`Reconnection failed: ${error.message}`); if (this.reconnectAttempts < this.maxReconnectAttempts) { this.attemptReconnect(); } @@ -122,7 +133,7 @@ class Synapse { } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown parsing error"; - this.logger( + this.log( `Message parsing error: ${errorMessage}. Raw message: ${event.data}`, ); } @@ -181,14 +192,6 @@ class Synapse { }); } - /** - * Sets the logger function for the Synapse instance - * @param logger - The logger function to use - */ - setLogger(logger: Logger): void { - this.logger = logger; - } - /** * Sends a message to the WebSocket server * @param type - The type of message to send diff --git a/tests/services/filesystem.test.ts b/tests/services/filesystem.test.ts index c489e0a..9631771 100644 --- a/tests/services/filesystem.test.ts +++ b/tests/services/filesystem.test.ts @@ -11,9 +11,10 @@ describe("Filesystem", () => { beforeEach(() => { mockSynapse = jest.mocked({ logger: jest.fn(), + workDir: "/test", } as unknown as Synapse); - filesystem = new Filesystem(mockSynapse, "/test"); + filesystem = new Filesystem(mockSynapse); }); describe("createFile", () => { From 84519e3f0428eefcce8eed6a93562de948c6938f Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 3 Apr 2025 17:37:37 +0000 Subject: [PATCH 28/42] chore: remove last data hiding logic --- src/services/terminal.ts | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/services/terminal.ts b/src/services/terminal.ts index c745f8f..4227a73 100644 --- a/src/services/terminal.ts +++ b/src/services/terminal.ts @@ -15,7 +15,6 @@ export class Terminal { null; private isAlive: boolean = false; private initializationError: Error | null = null; - private lastCommand: string | null = null; /** * Creates a new Terminal instance @@ -83,17 +82,6 @@ export class Terminal { console.log(`[Terminal][${timestamp}] ${message}`); } - private stripAnsi(str: string): string { - return str - .replace( - /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, - "", - ) // Remove ANSI escape sequences - .replace(/\r/g, "") // Remove carriage returns - .replace(/\u001b\[?[0-9;]*[A-Za-z]/g, "") // Remove cursor movements - .replace(/\u001b\[\?[0-9;]*[A-Za-z]/g, ""); // Remove terminal mode commands - } - private checkTerminal(): void { if (!this.isAlive || !this.term) { throw new Error("Terminal is not alive or has been terminated"); @@ -131,8 +119,6 @@ export class Terminal { this.checkTerminal(); this.log(`Writing command: ${command}`); - this.lastCommand = command; - // Ensure command ends with newline if (!command.endsWith("\n")) { command += "\n"; @@ -150,15 +136,6 @@ export class Terminal { */ onData(callback: (success: boolean, data: string) => void): void { this.onDataCallback = (success: boolean, data: string) => { - const cleanData = this.stripAnsi(data); - const cleanCommand = this.lastCommand - ? this.stripAnsi(this.lastCommand) - : null; - - if (cleanCommand && cleanData === cleanCommand) { - return; - } - callback(success, data); }; From 9cb9aae3d00568cfdea6d8a776484b63ac39ae56 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 4 Apr 2025 11:14:18 +0000 Subject: [PATCH 29/42] chore: initialized micro server inside synapse --- package.json | 2 ++ src/synapse.ts | 70 ++++++++++++++++++++++++++++++++++++++++++- tests/synapse.test.ts | 16 +++++++++- 3 files changed, 86 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 73059f9..b42adf1 100644 --- a/package.json +++ b/package.json @@ -31,12 +31,14 @@ "devDependencies": { "@types/jest": "^29.5.14", "@types/node": "^22.13.13", + "@types/node-fetch": "^2.6.11", "@types/ws": "^8.18.0", "@typescript-eslint/eslint-plugin": "^8.28.0", "@typescript-eslint/parser": "^8.28.0", "esbuild": "^0.25.2", "esbuild-plugin-file-path-extensions": "^2.1.4", "jest": "^29.7.0", + "node-fetch": "^2.6.7", "ts-jest": "^29.3.0", "tsup": "^8.4.0", "typescript": "^5.8.2" diff --git a/src/synapse.ts b/src/synapse.ts index 342639a..968b406 100644 --- a/src/synapse.ts +++ b/src/synapse.ts @@ -1,4 +1,5 @@ -import { IncomingMessage } from "http"; +import { IncomingMessage, Server, ServerResponse, createServer } from "http"; +import { send, serve } from "micro"; import { Socket } from "net"; import WebSocket, { WebSocketServer } from "ws"; @@ -29,6 +30,7 @@ class Synapse { private reconnectInterval = 3000; private lastPath: string | null = null; private reconnectTimeout: NodeJS.Timeout | null = null; + private server: Server | null = null; private host: string; private port: number; @@ -43,10 +45,30 @@ class Synapse { this.host = host; this.port = port; this.workDir = workDir; + this.wss = new WebSocketServer({ noServer: true }); this.wss.on("connection", (ws: WebSocket) => { this.setupWebSocket(ws); }); + + const handler = serve(async (req: IncomingMessage, res: ServerResponse) => { + this.log(`HTTP Request received: ${req.method} ${req.url}`); + + if (req.headers.upgrade?.toLowerCase() === "websocket" && req.socket) { + this.handleUpgrade(req, req.socket, Buffer.alloc(0)); + return; + } + + return send(res, 404, "Not found"); + }); + + this.server = createServer(handler); + + if (this.server) { + this.server.on("connection", (socket: Socket) => { + this.log(`New connection from ${socket.remoteAddress}`); + }); + } } private log(message: string): void { @@ -292,6 +314,49 @@ class Synapse { return this.ws && this.ws.readyState === WebSocket.OPEN; } + /** + * Starts the HTTP/WebSocket server + * @returns Promise that resolves when the server is listening + */ + public listen(): Promise { + return new Promise((resolve, reject) => { + if (!this.server) { + reject(new Error("Server not initialized")); + return; + } + + this.server + .listen(this.port, this.host, () => { + this.log(`Server running on ${this.host}:${this.port}`); + resolve(); + }) + .on("error", (error) => { + reject(error); + }); + }); + } + + /** + * Stops the HTTP/WebSocket server and closes all connections + */ + public close(): Promise { + return new Promise((resolve, reject) => { + if (!this.server) { + resolve(); + return; + } + + this.server.close((error) => { + if (error) { + reject(error); + return; + } + this.server = null; + resolve(); + }); + }); + } + /** * Closes the WebSocket connection */ @@ -301,6 +366,9 @@ class Synapse { this.ws.close(); this.ws = null; } + this.close().catch((error) => { + this.log(`Error closing server: ${error.message}`); + }); this.wss.close(); } } diff --git a/tests/synapse.test.ts b/tests/synapse.test.ts index 99139ae..0b31313 100644 --- a/tests/synapse.test.ts +++ b/tests/synapse.test.ts @@ -1,5 +1,6 @@ import { IncomingMessage } from "http"; import { Socket } from "net"; +import fetch from "node-fetch"; import WebSocket, { WebSocketServer } from "ws"; import { Synapse } from "../src/synapse"; @@ -32,7 +33,7 @@ describe("Synapse", () => { jest.restoreAllMocks(); }); - describe("connect", () => { + describe("webSocket connection", () => { it("should connect successfully", async () => { const mockWs = createMockWebSocket(); (WebSocket as unknown as jest.Mock).mockImplementation(() => mockWs); @@ -61,6 +62,19 @@ describe("Synapse", () => { }); }); + describe("http server connection", () => { + it("should respond with 404 for regular HTTP requests", async () => { + const synapse = new Synapse("localhost", 8081); + await synapse.listen(); + + const response = await fetch("http://localhost:8081"); + expect(response.status).toBe(404); + expect(await response.text()).toBe("Not found"); + + await synapse.close(); + }); + }); + describe("message handling", () => { let mockWs: ReturnType; From 977cc4c1a2bf208333b49b93d8b2835aea56c198 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 4 Apr 2025 13:09:35 +0000 Subject: [PATCH 30/42] chore: add http server handler --- src/synapse.ts | 130 ++++++++++++++++++++++++++++++++++++------ tests/synapse.test.ts | 56 ++++++++++++++++-- 2 files changed, 164 insertions(+), 22 deletions(-) diff --git a/src/synapse.ts b/src/synapse.ts index 968b406..f768634 100644 --- a/src/synapse.ts +++ b/src/synapse.ts @@ -1,6 +1,7 @@ import { IncomingMessage, Server, ServerResponse, createServer } from "http"; -import { send, serve } from "micro"; +import { json, send, serve } from "micro"; import { Socket } from "net"; +import { URL } from "url"; import WebSocket, { WebSocketServer } from "ws"; export type MessagePayload = { @@ -9,7 +10,7 @@ export type MessagePayload = { [key: string]: string | Record; }; -export type MessageHandler = (message: MessagePayload) => void; +export type MessageHandler = (message: MessagePayload) => Promise; export type ConnectionCallback = () => void; export type ErrorCallback = (error: Error) => void; export type Logger = (message: string) => void; @@ -54,12 +55,92 @@ class Synapse { const handler = serve(async (req: IncomingMessage, res: ServerResponse) => { this.log(`HTTP Request received: ${req.method} ${req.url}`); + // Handle WebSocket upgrade requests if (req.headers.upgrade?.toLowerCase() === "websocket" && req.socket) { this.handleUpgrade(req, req.socket, Buffer.alloc(0)); return; } - return send(res, 404, "Not found"); + try { + // Handle HTTP requests + if (req.url) { + const url = new URL(req.url, `http://${req.headers.host}`); + const path = url.pathname; + + const pathParts = path.split("/").filter((part) => part); + + if (pathParts.length >= 2) { + const service = pathParts[0]; + const operation = pathParts[1]; + + if (this.messageHandlers[service]) { + if (req.method === "POST") { + // For POST requests, get the body data + const body = await json(req); + + const message: MessagePayload = { + type: service, + requestId: Date.now().toString(), + operation: operation, + params: (body || {}) as Record, + }; + + try { + const result = await this.messageHandlers[service](message); + return send(res, 200, result || { success: true }); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + return send(res, 400, { + success: false, + error: errorMessage, + }); + } + } else if (req.method === "GET") { + // For GET requests, use query parameters + const params: Record = {}; + url.searchParams.forEach((value, key) => { + params[key] = value; + }); + + const message: MessagePayload = { + type: service, + requestId: Date.now().toString(), + operation: operation, + params: params, + }; + + try { + const result = await this.messageHandlers[service](message); + return send(res, 200, result || { success: true }); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + return send(res, 400, { + success: false, + error: errorMessage, + }); + } + } else { + return send(res, 405, { + success: false, + error: "Method not allowed", + }); + } + } + } + } + + return send(res, 404, { success: false, error: "Not found" }); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + this.log(`HTTP error: ${errorMessage}`); + return send(res, 500, { + success: false, + error: "Internal server error", + }); + } }); this.server = createServer(handler); @@ -138,7 +219,7 @@ class Synapse { this.reconnectAttempts = this.maxReconnectAttempts; } - private handleMessage(event: WebSocket.MessageEvent): void { + private async handleMessage(event: WebSocket.MessageEvent): Promise { try { const data = event.data as string; @@ -150,7 +231,16 @@ class Synapse { const message: MessagePayload = JSON.parse(data); if (this.messageHandlers[message.type]) { - this.messageHandlers[message.type](message); + const result = await this.messageHandlers[message.type](message); + if (result !== null && this.ws) { + this.ws.send( + JSON.stringify({ + type: `${message.type}Response`, + requestId: message.requestId, + ...result, + }), + ); + } } } catch (error) { const errorMessage = @@ -158,6 +248,22 @@ class Synapse { this.log( `Message parsing error: ${errorMessage}. Raw message: ${event.data}`, ); + + if (this.ws) { + try { + const message = JSON.parse(event.data as string); + this.ws.send( + JSON.stringify({ + type: `${message.type}Response`, + requestId: message.requestId, + success: false, + error: errorMessage, + }), + ); + } catch { + // If we can't parse the original message, we can't send a proper response + } + } } } @@ -241,18 +347,6 @@ class Synapse { }); } - /** - * Sends a command to the terminal for execution - * @param command - The command string to execute - * @returns A promise that resolves with the message payload - */ - sendCommand(command: string): Promise { - return this.send("terminal", { - operation: "createCommand", - params: { command }, - }); - } - /** * Registers a callback for when the WebSocket connection is established * @param callback - Function to be called when connection opens @@ -284,7 +378,7 @@ class Synapse { } /** - * Registers a handler for specific message types received through WebSocket + * Registers a handler for specific message types received through WebSocket and HTTP * @param type - The message type to handle * @param handler - Function to handle messages of the specified type * @returns The Synapse instance for method chaining diff --git a/tests/synapse.test.ts b/tests/synapse.test.ts index 0b31313..7cdbe10 100644 --- a/tests/synapse.test.ts +++ b/tests/synapse.test.ts @@ -63,15 +63,63 @@ describe("Synapse", () => { }); describe("http server connection", () => { - it("should respond with 404 for regular HTTP requests", async () => { - const synapse = new Synapse("localhost", 8081); + let synapse: Synapse; + + beforeAll(async () => { + synapse = new Synapse("localhost", 8081); await synapse.listen(); + }); + afterAll(async () => { + await synapse.close(); + }); + + it("should respond with 404 for regular HTTP requests", async () => { const response = await fetch("http://localhost:8081"); expect(response.status).toBe(404); - expect(await response.text()).toBe("Not found"); + expect(await response.text()).toBe( + '{"success":false,"error":"Not found"}', + ); + }); - await synapse.close(); + it("should handle POST requests successfully", async () => { + const handler = jest.fn().mockResolvedValue({ result: "success" }); + synapse.onMessageType("test", handler); + + const response = await fetch("http://localhost:8081/test/operation", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ data: "test" }), + }); + + expect(response.status).toBe(200); + const result = await response.json(); + expect(result).toEqual({ result: "success" }); + expect(handler).toHaveBeenCalledWith({ + type: "test", + requestId: expect.any(String), + operation: "operation", + params: { data: "test" }, + }); + }); + + it("should handle GET requests with query parameters", async () => { + const handler = jest.fn().mockResolvedValue({ result: "success" }); + synapse.onMessageType("test", handler); + + const response = await fetch( + "http://localhost:8081/test/operation?param=value", + ); + + expect(response.status).toBe(200); + const result = await response.json(); + expect(result).toEqual({ result: "success" }); + expect(handler).toHaveBeenCalledWith({ + type: "test", + requestId: expect.any(String), + operation: "operation", + params: { param: "value" }, + }); }); }); From 4d60a07011dd3f90c44c0740e4eb52849841cf67 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 4 Apr 2025 13:31:02 +0000 Subject: [PATCH 31/42] Revert "chore: add http server handler" This reverts commit 977cc4c1a2bf208333b49b93d8b2835aea56c198. --- src/synapse.ts | 130 ++++++------------------------------------ tests/synapse.test.ts | 56 ++---------------- 2 files changed, 22 insertions(+), 164 deletions(-) diff --git a/src/synapse.ts b/src/synapse.ts index f768634..968b406 100644 --- a/src/synapse.ts +++ b/src/synapse.ts @@ -1,7 +1,6 @@ import { IncomingMessage, Server, ServerResponse, createServer } from "http"; -import { json, send, serve } from "micro"; +import { send, serve } from "micro"; import { Socket } from "net"; -import { URL } from "url"; import WebSocket, { WebSocketServer } from "ws"; export type MessagePayload = { @@ -10,7 +9,7 @@ export type MessagePayload = { [key: string]: string | Record; }; -export type MessageHandler = (message: MessagePayload) => Promise; +export type MessageHandler = (message: MessagePayload) => void; export type ConnectionCallback = () => void; export type ErrorCallback = (error: Error) => void; export type Logger = (message: string) => void; @@ -55,92 +54,12 @@ class Synapse { const handler = serve(async (req: IncomingMessage, res: ServerResponse) => { this.log(`HTTP Request received: ${req.method} ${req.url}`); - // Handle WebSocket upgrade requests if (req.headers.upgrade?.toLowerCase() === "websocket" && req.socket) { this.handleUpgrade(req, req.socket, Buffer.alloc(0)); return; } - try { - // Handle HTTP requests - if (req.url) { - const url = new URL(req.url, `http://${req.headers.host}`); - const path = url.pathname; - - const pathParts = path.split("/").filter((part) => part); - - if (pathParts.length >= 2) { - const service = pathParts[0]; - const operation = pathParts[1]; - - if (this.messageHandlers[service]) { - if (req.method === "POST") { - // For POST requests, get the body data - const body = await json(req); - - const message: MessagePayload = { - type: service, - requestId: Date.now().toString(), - operation: operation, - params: (body || {}) as Record, - }; - - try { - const result = await this.messageHandlers[service](message); - return send(res, 200, result || { success: true }); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error"; - return send(res, 400, { - success: false, - error: errorMessage, - }); - } - } else if (req.method === "GET") { - // For GET requests, use query parameters - const params: Record = {}; - url.searchParams.forEach((value, key) => { - params[key] = value; - }); - - const message: MessagePayload = { - type: service, - requestId: Date.now().toString(), - operation: operation, - params: params, - }; - - try { - const result = await this.messageHandlers[service](message); - return send(res, 200, result || { success: true }); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error"; - return send(res, 400, { - success: false, - error: errorMessage, - }); - } - } else { - return send(res, 405, { - success: false, - error: "Method not allowed", - }); - } - } - } - } - - return send(res, 404, { success: false, error: "Not found" }); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error"; - this.log(`HTTP error: ${errorMessage}`); - return send(res, 500, { - success: false, - error: "Internal server error", - }); - } + return send(res, 404, "Not found"); }); this.server = createServer(handler); @@ -219,7 +138,7 @@ class Synapse { this.reconnectAttempts = this.maxReconnectAttempts; } - private async handleMessage(event: WebSocket.MessageEvent): Promise { + private handleMessage(event: WebSocket.MessageEvent): void { try { const data = event.data as string; @@ -231,16 +150,7 @@ class Synapse { const message: MessagePayload = JSON.parse(data); if (this.messageHandlers[message.type]) { - const result = await this.messageHandlers[message.type](message); - if (result !== null && this.ws) { - this.ws.send( - JSON.stringify({ - type: `${message.type}Response`, - requestId: message.requestId, - ...result, - }), - ); - } + this.messageHandlers[message.type](message); } } catch (error) { const errorMessage = @@ -248,22 +158,6 @@ class Synapse { this.log( `Message parsing error: ${errorMessage}. Raw message: ${event.data}`, ); - - if (this.ws) { - try { - const message = JSON.parse(event.data as string); - this.ws.send( - JSON.stringify({ - type: `${message.type}Response`, - requestId: message.requestId, - success: false, - error: errorMessage, - }), - ); - } catch { - // If we can't parse the original message, we can't send a proper response - } - } } } @@ -347,6 +241,18 @@ class Synapse { }); } + /** + * Sends a command to the terminal for execution + * @param command - The command string to execute + * @returns A promise that resolves with the message payload + */ + sendCommand(command: string): Promise { + return this.send("terminal", { + operation: "createCommand", + params: { command }, + }); + } + /** * Registers a callback for when the WebSocket connection is established * @param callback - Function to be called when connection opens @@ -378,7 +284,7 @@ class Synapse { } /** - * Registers a handler for specific message types received through WebSocket and HTTP + * Registers a handler for specific message types received through WebSocket * @param type - The message type to handle * @param handler - Function to handle messages of the specified type * @returns The Synapse instance for method chaining diff --git a/tests/synapse.test.ts b/tests/synapse.test.ts index 7cdbe10..0b31313 100644 --- a/tests/synapse.test.ts +++ b/tests/synapse.test.ts @@ -63,63 +63,15 @@ describe("Synapse", () => { }); describe("http server connection", () => { - let synapse: Synapse; - - beforeAll(async () => { - synapse = new Synapse("localhost", 8081); + it("should respond with 404 for regular HTTP requests", async () => { + const synapse = new Synapse("localhost", 8081); await synapse.listen(); - }); - afterAll(async () => { - await synapse.close(); - }); - - it("should respond with 404 for regular HTTP requests", async () => { const response = await fetch("http://localhost:8081"); expect(response.status).toBe(404); - expect(await response.text()).toBe( - '{"success":false,"error":"Not found"}', - ); - }); - - it("should handle POST requests successfully", async () => { - const handler = jest.fn().mockResolvedValue({ result: "success" }); - synapse.onMessageType("test", handler); - - const response = await fetch("http://localhost:8081/test/operation", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ data: "test" }), - }); + expect(await response.text()).toBe("Not found"); - expect(response.status).toBe(200); - const result = await response.json(); - expect(result).toEqual({ result: "success" }); - expect(handler).toHaveBeenCalledWith({ - type: "test", - requestId: expect.any(String), - operation: "operation", - params: { data: "test" }, - }); - }); - - it("should handle GET requests with query parameters", async () => { - const handler = jest.fn().mockResolvedValue({ result: "success" }); - synapse.onMessageType("test", handler); - - const response = await fetch( - "http://localhost:8081/test/operation?param=value", - ); - - expect(response.status).toBe(200); - const result = await response.json(); - expect(result).toEqual({ result: "success" }); - expect(handler).toHaveBeenCalledWith({ - type: "test", - requestId: expect.any(String), - operation: "operation", - params: { param: "value" }, - }); + await synapse.close(); }); }); From dfd15b9b2678079cd92b207af0013ce0d90d1491 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sat, 5 Apr 2025 18:28:15 +0000 Subject: [PATCH 32/42] chore: add workdir creation if not exists --- src/synapse.ts | 6 +++++- tests/services/terminal.test.ts | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/synapse.ts b/src/synapse.ts index 968b406..c367f6c 100644 --- a/src/synapse.ts +++ b/src/synapse.ts @@ -1,8 +1,8 @@ +import fs from "fs"; import { IncomingMessage, Server, ServerResponse, createServer } from "http"; import { send, serve } from "micro"; import { Socket } from "net"; import WebSocket, { WebSocketServer } from "ws"; - export type MessagePayload = { type: string; requestId: string; @@ -44,6 +44,10 @@ class Synapse { ) { this.host = host; this.port = port; + + if (!fs.existsSync(workDir)) { + fs.mkdirSync(workDir, { recursive: true }); + } this.workDir = workDir; this.wss = new WebSocketServer({ noServer: true }); diff --git a/tests/services/terminal.test.ts b/tests/services/terminal.test.ts index df17a24..8082b57 100644 --- a/tests/services/terminal.test.ts +++ b/tests/services/terminal.test.ts @@ -75,7 +75,6 @@ describe("Terminal", () => { shell: "zsh", cols: 100, rows: 30, - workdir: "/custom/path", }; const customTerminal = new Terminal(mockSynapse, customOptions); From 2bbe0c1a99eb6c8929a74f984b79887e8a2897c5 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 7 Apr 2025 11:23:00 +0000 Subject: [PATCH 33/42] chore: adjust code --- src/synapse.ts | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/synapse.ts b/src/synapse.ts index c367f6c..5a163af 100644 --- a/src/synapse.ts +++ b/src/synapse.ts @@ -130,27 +130,9 @@ class Synapse { }, this.reconnectInterval); } - /** - * Cancels the reconnection process - */ - public cancelReconnect(): void { - if (this.reconnectTimeout) { - clearTimeout(this.reconnectTimeout); - this.reconnectTimeout = null; - } - this.isReconnecting = false; - this.reconnectAttempts = this.maxReconnectAttempts; - } - private handleMessage(event: WebSocket.MessageEvent): void { try { const data = event.data as string; - - if (data === "ping" && this.ws) { - this.ws.send("pong"); - return; - } - const message: MessagePayload = JSON.parse(data); if (this.messageHandlers[message.type]) { @@ -169,6 +151,28 @@ class Synapse { return `ws://${this.host}:${this.port}${path}`; } + /** + * Sets the working directory for the Synapse instance + * @param workDir - The path to the working directory + * @returns void + */ + setWorkDir(workDir: string): void { + this.workDir = workDir; + } + + /** + * Cancels the reconnection process + * @returns void + */ + cancelReconnect(): void { + if (this.reconnectTimeout) { + clearTimeout(this.reconnectTimeout); + this.reconnectTimeout = null; + } + this.isReconnecting = false; + this.reconnectAttempts = this.maxReconnectAttempts; + } + /** * Establishes a WebSocket connection to the specified URL and initializes the terminal * @param path - The WebSocket endpoint path (e.g. '/' or '/terminal') @@ -322,7 +326,7 @@ class Synapse { * Starts the HTTP/WebSocket server * @returns Promise that resolves when the server is listening */ - public listen(): Promise { + listen(): Promise { return new Promise((resolve, reject) => { if (!this.server) { reject(new Error("Server not initialized")); @@ -343,7 +347,7 @@ class Synapse { /** * Stops the HTTP/WebSocket server and closes all connections */ - public close(): Promise { + close(): Promise { return new Promise((resolve, reject) => { if (!this.server) { resolve(); From b8b9b117491affc8dcbb54e47a00e0841789d795 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 7 Apr 2025 13:22:09 +0000 Subject: [PATCH 34/42] chore: remove HTTP server logic --- src/services/terminal.ts | 16 +++++ src/synapse.ts | 100 ++++++++++---------------------- tests/services/terminal.test.ts | 5 ++ tests/synapse.test.ts | 14 ----- 4 files changed, 52 insertions(+), 83 deletions(-) diff --git a/src/services/terminal.ts b/src/services/terminal.ts index 4227a73..e639639 100644 --- a/src/services/terminal.ts +++ b/src/services/terminal.ts @@ -30,6 +30,7 @@ export class Terminal { }, ) { this.synapse = synapse; + this.synapse.registerTerminal(this); try { this.term = pty.spawn(terminalOptions.shell, [], { @@ -147,6 +148,20 @@ export class Terminal { } } + /** + * Updates the working directory of the terminal + * @param workDir - The new working directory + */ + updateWorkDir(workDir: string): void { + try { + this.checkTerminal(); + // Send cd command to change directory + this.createCommand(`cd "${workDir}"`); + } catch (error) { + console.error("Failed to update working directory:", error); + } + } + /** * Kills the terminal */ @@ -156,6 +171,7 @@ export class Terminal { this.term.kill(); this.isAlive = false; this.term = null; + this.synapse.unregisterTerminal(this); } } diff --git a/src/synapse.ts b/src/synapse.ts index 5a163af..be8b3a0 100644 --- a/src/synapse.ts +++ b/src/synapse.ts @@ -1,8 +1,9 @@ import fs from "fs"; -import { IncomingMessage, Server, ServerResponse, createServer } from "http"; -import { send, serve } from "micro"; +import { IncomingMessage } from "http"; import { Socket } from "net"; import WebSocket, { WebSocketServer } from "ws"; +import { Terminal } from "./services/terminal"; + export type MessagePayload = { type: string; requestId: string; @@ -24,13 +25,14 @@ class Synapse { onError: (() => {}) as ErrorCallback, }; + private terminals: Set = new Set(); + private isReconnecting = false; private maxReconnectAttempts = 5; private reconnectAttempts = 0; private reconnectInterval = 3000; private lastPath: string | null = null; private reconnectTimeout: NodeJS.Timeout | null = null; - private server: Server | null = null; private host: string; private port: number; @@ -54,25 +56,6 @@ class Synapse { this.wss.on("connection", (ws: WebSocket) => { this.setupWebSocket(ws); }); - - const handler = serve(async (req: IncomingMessage, res: ServerResponse) => { - this.log(`HTTP Request received: ${req.method} ${req.url}`); - - if (req.headers.upgrade?.toLowerCase() === "websocket" && req.socket) { - this.handleUpgrade(req, req.socket, Buffer.alloc(0)); - return; - } - - return send(res, 404, "Not found"); - }); - - this.server = createServer(handler); - - if (this.server) { - this.server.on("connection", (socket: Socket) => { - this.log(`New connection from ${socket.remoteAddress}`); - }); - } } private log(message: string): void { @@ -151,13 +134,38 @@ class Synapse { return `ws://${this.host}:${this.port}${path}`; } + /** + * Registers a terminal instance with Synapse + * @param terminal - The terminal instance to register + */ + registerTerminal(terminal: Terminal): void { + this.terminals.add(terminal); + } + + /** + * Unregisters a terminal instance from Synapse + * @param terminal - The terminal instance to unregister + */ + unregisterTerminal(terminal: Terminal): void { + this.terminals.delete(terminal); + } + /** * Sets the working directory for the Synapse instance * @param workDir - The path to the working directory * @returns void */ - setWorkDir(workDir: string): void { + updateWorkDir(workDir: string): { success: boolean; data: string } { this.workDir = workDir; + this.terminals.forEach((terminal) => { + if (terminal.isTerminalAlive()) { + terminal.updateWorkDir(workDir); + } + }); + return { + success: true, + data: "Work directory updated successfully", + }; } /** @@ -322,49 +330,6 @@ class Synapse { return this.ws && this.ws.readyState === WebSocket.OPEN; } - /** - * Starts the HTTP/WebSocket server - * @returns Promise that resolves when the server is listening - */ - listen(): Promise { - return new Promise((resolve, reject) => { - if (!this.server) { - reject(new Error("Server not initialized")); - return; - } - - this.server - .listen(this.port, this.host, () => { - this.log(`Server running on ${this.host}:${this.port}`); - resolve(); - }) - .on("error", (error) => { - reject(error); - }); - }); - } - - /** - * Stops the HTTP/WebSocket server and closes all connections - */ - close(): Promise { - return new Promise((resolve, reject) => { - if (!this.server) { - resolve(); - return; - } - - this.server.close((error) => { - if (error) { - reject(error); - return; - } - this.server = null; - resolve(); - }); - }); - } - /** * Closes the WebSocket connection */ @@ -374,9 +339,6 @@ class Synapse { this.ws.close(); this.ws = null; } - this.close().catch((error) => { - this.log(`Error closing server: ${error.message}`); - }); this.wss.close(); } } diff --git a/tests/services/terminal.test.ts b/tests/services/terminal.test.ts index 8082b57..e2b9e8b 100644 --- a/tests/services/terminal.test.ts +++ b/tests/services/terminal.test.ts @@ -62,6 +62,11 @@ describe("Terminal", () => { terminal = new Terminal(mockSynapse); }); + it("should update working directory", () => { + terminal.updateWorkDir("/new/path"); + expect(mockPty.write).toHaveBeenCalledWith('cd "/new/path"\n'); + }); + it("should not operate when terminal is dead", () => { terminal.kill(); terminal.updateSize(80, 24); diff --git a/tests/synapse.test.ts b/tests/synapse.test.ts index 0b31313..cf35e14 100644 --- a/tests/synapse.test.ts +++ b/tests/synapse.test.ts @@ -1,6 +1,5 @@ import { IncomingMessage } from "http"; import { Socket } from "net"; -import fetch from "node-fetch"; import WebSocket, { WebSocketServer } from "ws"; import { Synapse } from "../src/synapse"; @@ -62,19 +61,6 @@ describe("Synapse", () => { }); }); - describe("http server connection", () => { - it("should respond with 404 for regular HTTP requests", async () => { - const synapse = new Synapse("localhost", 8081); - await synapse.listen(); - - const response = await fetch("http://localhost:8081"); - expect(response.status).toBe(404); - expect(await response.text()).toBe("Not found"); - - await synapse.close(); - }); - }); - describe("message handling", () => { let mockWs: ReturnType; From 068801dcc96322202810d0093a84b634810f104d Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 7 Apr 2025 13:31:51 +0000 Subject: [PATCH 35/42] chore: add back ping pong --- src/synapse.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/synapse.ts b/src/synapse.ts index be8b3a0..f2d5cf5 100644 --- a/src/synapse.ts +++ b/src/synapse.ts @@ -116,6 +116,12 @@ class Synapse { private handleMessage(event: WebSocket.MessageEvent): void { try { const data = event.data as string; + + if (data === "ping" && this.ws) { + this.ws.send("pong"); + return; + } + const message: MessagePayload = JSON.parse(data); if (this.messageHandlers[message.type]) { From b10c0415765e606ded495bc497b7df7b00cdee13 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 7 Apr 2025 13:50:52 +0000 Subject: [PATCH 36/42] chore: added synapse.workDir to git service --- src/services/git.ts | 11 ++++------- tests/services/git.test.ts | 40 +++++++++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/services/git.ts b/src/services/git.ts index 9fc3c70..b9b3bb4 100644 --- a/src/services/git.ts +++ b/src/services/git.ts @@ -11,16 +11,13 @@ export type GitOperationResult = { export class Git { private synapse: Synapse; - private workingDir: string; /** * Creates a new Git instance * @param synapse - The Synapse instance to use - * @param workingDir - The working directory to use */ - constructor(synapse: Synapse, workingDir: string = process.cwd()) { + constructor(synapse: Synapse) { this.synapse = synapse; - this.workingDir = workingDir; } private isErrnoException(error: unknown): error is NodeJS.ErrnoException { @@ -29,7 +26,7 @@ export class Git { private async execute(args: string[]): Promise { return new Promise((resolve) => { - const git = spawn("git", args, { cwd: this.workingDir }); + const git = spawn("git", args, { cwd: this.synapse.workDir }); let output = ""; let errorOutput = ""; @@ -63,7 +60,7 @@ export class Git { private async isGitRepository(): Promise { try { - const gitDir = path.join(this.workingDir, ".git"); + const gitDir = path.join(this.synapse.workDir, ".git"); return fs.existsSync(gitDir) && fs.statSync(gitDir).isDirectory(); } catch (error: unknown) { if (this.isErrnoException(error)) { @@ -89,7 +86,7 @@ export class Git { // Check if we have write permissions in the current directory try { - await fs.promises.access(this.workingDir, fs.constants.W_OK); + await fs.promises.access(this.synapse.workDir, fs.constants.W_OK); } catch (error: unknown) { if (this.isErrnoException(error)) { return { diff --git a/tests/services/git.test.ts b/tests/services/git.test.ts index 6050c84..783912c 100644 --- a/tests/services/git.test.ts +++ b/tests/services/git.test.ts @@ -31,7 +31,14 @@ describe("Git Service", () => { jest.spyOn(process, "cwd").mockReturnValue(mockWorkingDir); (fs.existsSync as jest.Mock).mockReturnValue(false); (fs.statSync as jest.Mock).mockReturnValue({ isDirectory: () => true }); - mockSynapse = {} as jest.Mocked; + + mockSynapse = { + workDir: mockWorkingDir, + updateWorkDir: jest.fn((dir: string) => { + mockSynapse.workDir = dir; + return { success: true, data: "Work directory updated successfully" }; + }), + } as unknown as jest.Mocked; mockSpawn = spawn as jest.Mock; git = new Git(mockSynapse); }); @@ -82,11 +89,42 @@ describe("Git Service", () => { it("error - repository exists", async () => { (fs.existsSync as jest.Mock).mockReturnValue(true); + (fs.existsSync as jest.Mock).mockImplementation(function ( + this: unknown, + ...args: unknown[] + ): boolean { + const path = args[0] as string; + return path.includes(".git"); + }); expect(await git.init()).toEqual({ success: false, error: "Git repository already exists in this directory", }); }); + + it("success - change workDir", async () => { + // First ensure no git repo exists + (fs.existsSync as jest.Mock).mockImplementation(function ( + this: unknown, + ...args: unknown[] + ): boolean { + return false; + }); + setupMockProcess("Initialized empty Git repository"); + + // Change workDir + mockSynapse.updateWorkDir("/workspace/another-project"); + + // Now init should succeed + const secondInit = await git.init(); + expect(secondInit).toEqual({ + success: true, + data: "Initialized empty Git repository", + }); + + // Verify workDir was changed + expect(mockSynapse.workDir).toBe("/workspace/another-project"); + }); }); describe("addRemote", () => { From 4b85fccfb3325ea7765c422facc8f5a48886ccff Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 7 Apr 2025 19:17:40 +0000 Subject: [PATCH 37/42] chore: update dependencies --- package.json | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index b42adf1..dfe1036 100644 --- a/package.json +++ b/package.json @@ -22,26 +22,25 @@ }, "license": "MIT", "dependencies": { - "eslint": "^9.23.0", - "micro": "^10.0.1", + "eslint": "^9.24.0", "node-pty": "^1.0.0", "prettier": "^3.5.3", "ws": "^8.18.1" }, "devDependencies": { "@types/jest": "^29.5.14", - "@types/node": "^22.13.13", - "@types/node-fetch": "^2.6.11", - "@types/ws": "^8.18.0", - "@typescript-eslint/eslint-plugin": "^8.28.0", - "@typescript-eslint/parser": "^8.28.0", + "@types/node": "^22.14.0", + "@types/node-fetch": "^2.6.12", + "@types/ws": "^8.18.1", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", "esbuild": "^0.25.2", "esbuild-plugin-file-path-extensions": "^2.1.4", "jest": "^29.7.0", - "node-fetch": "^2.6.7", - "ts-jest": "^29.3.0", + "node-fetch": "^3.3.2", + "ts-jest": "^29.3.1", "tsup": "^8.4.0", - "typescript": "^5.8.2" + "typescript": "^5.8.3" }, "bugs": { "url": "https://github.com/appwrite/synapse/issues" From de903785b0b7d413261a0344a557d1e2ca1335fb Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 10 Apr 2025 13:39:57 +0000 Subject: [PATCH 38/42] chore: better error catching for filesystem service --- src/services/filesystem.ts | 97 ++++++++++++++++++++++--------- tests/services/filesystem.test.ts | 38 ++++++++++-- 2 files changed, 102 insertions(+), 33 deletions(-) diff --git a/src/services/filesystem.ts b/src/services/filesystem.ts index 8ded5f9..0bbf1a0 100644 --- a/src/services/filesystem.ts +++ b/src/services/filesystem.ts @@ -1,3 +1,4 @@ +import { constants as fsConstants } from "fs"; import * as fs from "fs/promises"; import * as path from "path"; import { Synapse } from "../synapse"; @@ -30,33 +31,65 @@ export class Filesystem { } /** - * Creates a new file at the specified path with optional content + * Creates a new file at the specified path with optional content. + * Fails if the file already exists. * @param filePath - The full path where the file should be created * @param content - Optional content to write to the file (defaults to empty string) * @returns Promise indicating success or failure - * @throws Error if file creation fails + * @throws Error if file creation fails for reasons other than existence */ async createFile( filePath: string, content: string = "", ): Promise { - try { - this.log(`Creating file at path: ${filePath}`); - const fullPath = path.join(this.synapse.workDir, filePath); - const dirPath = path.dirname(filePath); - - await this.createFolder(dirPath); - await fs.writeFile(fullPath, content); + const fullPath = path.join(this.synapse.workDir, filePath); - return { success: true }; - } catch (error) { - this.log( - `Error: ${error instanceof Error ? error.message : String(error)}`, - ); - return { - success: false, - error: error instanceof Error ? error.message : String(error), - }; + try { + await fs.access(fullPath, fsConstants.F_OK); + const errorMsg = `File already exists at path: ${filePath}`; + this.log(`Error: ${errorMsg}`); + + return { success: false, error: errorMsg }; // file already exists + } catch (accessError: any) { + if (accessError?.code === "ENOENT") { + try { + const dirPath = path.dirname(filePath); + const folderResult = await this.createFolder(dirPath); + if (!folderResult.success) { + this.log( + `Failed to create parent directory for ${filePath}: ${folderResult.error}`, + ); + + return { success: false, error: folderResult.error }; // failed to create parent directory + } + await fs.writeFile(fullPath, content, { flag: "wx" }); + + return { success: true, data: "File created successfully" }; // file created successfully + } catch (writeError: any) { + const errorMsg = + writeError instanceof Error + ? writeError.message + : String(writeError); + this.log(`Error during file write: ${errorMsg}`); + if (writeError?.code === "EEXIST") { + // file already exists + return { + success: false, + error: `File already exists at path: ${filePath}`, + }; + } + + return { success: false, error: errorMsg }; // failed to write file + } + } else { + const errorMsg = + accessError instanceof Error + ? accessError.message + : String(accessError); + this.log(`Error accessing path ${filePath}: ${errorMsg}`); + + return { success: false, error: errorMsg }; // failed to access path + } } } @@ -68,7 +101,6 @@ export class Filesystem { */ async getFile(filePath: string): Promise { try { - this.log(`Reading file at path: ${filePath}`); const fullPath = path.join(this.synapse.workDir, filePath); const data = await fs.readFile(fullPath, "utf-8"); @@ -178,20 +210,26 @@ export class Filesystem { * @throws Error if directory creation fails */ async createFolder(dirPath: string): Promise { - try { - this.log(`Creating directory at path: ${dirPath}`); - const fullPath = path.join(this.synapse.workDir, dirPath); + if (dirPath === "." || dirPath === "" || dirPath === "/") { + // Skip creation for root or relative '.' path + return { success: true }; + } + const fullPath = path.join(this.synapse.workDir, dirPath); + + try { await fs.mkdir(fullPath, { recursive: true }); return { success: true }; - } catch (error) { + } catch (error: any) { + const errorMsg = error instanceof Error ? error.message : String(error); this.log( - `Error: ${error instanceof Error ? error.message : String(error)}`, + `Error creating directory at path ${fullPath}: ${errorMsg} (Code: ${error?.code})`, ); + return { success: false, - error: error instanceof Error ? error.message : String(error), + error: errorMsg, }; } } @@ -216,9 +254,12 @@ export class Filesystem { return { success: true, data }; } catch (error) { this.log( - `Error: ${error instanceof Error ? error.message : String(error)}`, + `Error reading directory ${dirPath}: ${error instanceof Error ? error.message : String(error)}`, ); - throw error; + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; } } @@ -295,7 +336,7 @@ export class Filesystem { this.log(`Deleting folder at path: ${dirPath}`); const fullPath = path.join(this.synapse.workDir, dirPath); - await fs.rm(fullPath, { recursive: true, force: true }); + await fs.rm(fullPath, { recursive: true }); return { success: true }; } catch (error) { diff --git a/tests/services/filesystem.test.ts b/tests/services/filesystem.test.ts index 9631771..efd4214 100644 --- a/tests/services/filesystem.test.ts +++ b/tests/services/filesystem.test.ts @@ -15,6 +15,7 @@ describe("Filesystem", () => { } as unknown as Synapse); filesystem = new Filesystem(mockSynapse); + jest.clearAllMocks(); }); describe("createFile", () => { @@ -22,25 +23,52 @@ describe("Filesystem", () => { const filePath = "/file.txt"; const content = "test content"; - (fs.mkdir as jest.Mock).mockResolvedValue(undefined); + // Mock access to indicate file does NOT exist initially + const accessError = new Error("ENOENT") as NodeJS.ErrnoException; + accessError.code = "ENOENT"; + (fs.access as jest.Mock).mockRejectedValue(accessError); + (fs.mkdir as jest.Mock).mockResolvedValue(undefined); // Assuming createFolder is called (fs.writeFile as jest.Mock).mockResolvedValue(undefined); const result = await filesystem.createFile(filePath, content); - expect(result).toEqual({ success: true }); + expect(result).toEqual({ + success: true, + data: "File created successfully", + }); + }); + + it("should return error if file already exists", async () => { + const filePath = "/existing.txt"; + const content = "test content"; + + // Mock access to indicate file DOES exist + (fs.access as jest.Mock).mockResolvedValue(undefined); + + const result = await filesystem.createFile(filePath, content); + expect(fs.writeFile).not.toHaveBeenCalled(); // Should not attempt to write + expect(result).toEqual({ + success: false, + error: `File already exists at path: ${filePath}`, + }); }); - it("should handle file creation errors", async () => { + it("should handle file creation errors during write", async () => { const filePath = "/test/error.txt"; const content = "test content"; + // Mock access to indicate file does NOT exist initially + const accessError = new Error("ENOENT") as NodeJS.ErrnoException; + accessError.code = "ENOENT"; + (fs.access as jest.Mock).mockRejectedValue(accessError); + (fs.mkdir as jest.Mock).mockResolvedValue(undefined); // Mock directory creation (fs.writeFile as jest.Mock).mockRejectedValue( - new Error("Failed to create file"), + new Error("Failed to write file"), ); const result = await filesystem.createFile(filePath, content); expect(result).toEqual({ success: false, - error: "Failed to create file", + error: "Failed to write file", }); }); }); From 7e1eef08c61acfc7d58f479a5cc9f9e6758c5184 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 10 Apr 2025 13:48:24 +0000 Subject: [PATCH 39/42] chore: add logic to update workdir correctly --- src/synapse.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/synapse.ts b/src/synapse.ts index f2d5cf5..5540c3d 100644 --- a/src/synapse.ts +++ b/src/synapse.ts @@ -162,6 +162,22 @@ class Synapse { * @returns void */ updateWorkDir(workDir: string): { success: boolean; data: string } { + if (!fs.existsSync(workDir)) { + try { + fs.mkdirSync(workDir, { recursive: true }); + } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : "Unknown error creating directory"; + this.log(`Failed to create work directory: ${errorMessage}`); + return { + success: false, + data: `Failed to create work directory: ${errorMessage}`, + }; + } + } + this.workDir = workDir; this.terminals.forEach((terminal) => { if (terminal.isTerminalAlive()) { From f6b7c303f1705eb17d0a98afb7cf0d41cb70cadc Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 10 Apr 2025 14:20:40 +0000 Subject: [PATCH 40/42] log full path for debugging --- src/services/filesystem.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/filesystem.ts b/src/services/filesystem.ts index 0bbf1a0..49f2d03 100644 --- a/src/services/filesystem.ts +++ b/src/services/filesystem.ts @@ -46,10 +46,10 @@ export class Filesystem { try { await fs.access(fullPath, fsConstants.F_OK); - const errorMsg = `File already exists at path: ${filePath}`; - this.log(`Error: ${errorMsg}`); + const errorMsg = `File already exists at path: `; + this.log(`Error: ${errorMsg} ${fullPath}`); - return { success: false, error: errorMsg }; // file already exists + return { success: false, error: `${errorMsg} ${filePath}` }; // file already exists } catch (accessError: any) { if (accessError?.code === "ENOENT") { try { From 1f92cd5c16c1ebb320a901914c2492aaa4a4551f Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 14 Apr 2025 04:36:48 +0000 Subject: [PATCH 41/42] chore: fix lint issues --- package.json | 7 ++++--- src/services/filesystem.ts | 14 +++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index dfe1036..3eca4f4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "name": "@appwrite.io/synapse", - "version": "0.2.1", + "version": "0.3.0", "description": "Operating system gateway for remote serverless environments", + "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ @@ -29,7 +30,7 @@ }, "devDependencies": { "@types/jest": "^29.5.14", - "@types/node": "^22.14.0", + "@types/node": "^22.14.1", "@types/node-fetch": "^2.6.12", "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^8.29.1", @@ -38,7 +39,7 @@ "esbuild-plugin-file-path-extensions": "^2.1.4", "jest": "^29.7.0", "node-fetch": "^3.3.2", - "ts-jest": "^29.3.1", + "ts-jest": "^29.3.2", "tsup": "^8.4.0", "typescript": "^5.8.3" }, diff --git a/src/services/filesystem.ts b/src/services/filesystem.ts index 49f2d03..2375ec7 100644 --- a/src/services/filesystem.ts +++ b/src/services/filesystem.ts @@ -46,12 +46,12 @@ export class Filesystem { try { await fs.access(fullPath, fsConstants.F_OK); - const errorMsg = `File already exists at path: `; + const errorMsg = `File already exists at path:`; this.log(`Error: ${errorMsg} ${fullPath}`); return { success: false, error: `${errorMsg} ${filePath}` }; // file already exists - } catch (accessError: any) { - if (accessError?.code === "ENOENT") { + } catch (accessError: unknown) { + if ((accessError as NodeJS.ErrnoException)?.code === "ENOENT") { try { const dirPath = path.dirname(filePath); const folderResult = await this.createFolder(dirPath); @@ -65,13 +65,13 @@ export class Filesystem { await fs.writeFile(fullPath, content, { flag: "wx" }); return { success: true, data: "File created successfully" }; // file created successfully - } catch (writeError: any) { + } catch (writeError: unknown) { const errorMsg = writeError instanceof Error ? writeError.message : String(writeError); this.log(`Error during file write: ${errorMsg}`); - if (writeError?.code === "EEXIST") { + if ((writeError as NodeJS.ErrnoException)?.code === "EEXIST") { // file already exists return { success: false, @@ -221,10 +221,10 @@ export class Filesystem { await fs.mkdir(fullPath, { recursive: true }); return { success: true }; - } catch (error: any) { + } catch (error: unknown) { const errorMsg = error instanceof Error ? error.message : String(error); this.log( - `Error creating directory at path ${fullPath}: ${errorMsg} (Code: ${error?.code})`, + `Error creating directory at path ${fullPath}: ${errorMsg} (Code: ${(error as NodeJS.ErrnoException)?.code})`, ); return { From b7fe28bfc0031dfcdd004447127e8848e1aa98d7 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 14 Apr 2025 05:17:51 +0000 Subject: [PATCH 42/42] chore: remove type module --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 3eca4f4..5271183 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,6 @@ "name": "@appwrite.io/synapse", "version": "0.3.0", "description": "Operating system gateway for remote serverless environments", - "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", "files": [