diff --git a/drizzle/0007_dapper_overlord.sql b/drizzle/0007_dapper_overlord.sql new file mode 100644 index 0000000..f6bf75f --- /dev/null +++ b/drizzle/0007_dapper_overlord.sql @@ -0,0 +1 @@ +ALTER TABLE `apps` ADD `github_branch` text; \ No newline at end of file diff --git a/drizzle/meta/0007_snapshot.json b/drizzle/meta/0007_snapshot.json new file mode 100644 index 0000000..521a492 --- /dev/null +++ b/drizzle/meta/0007_snapshot.json @@ -0,0 +1,384 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "035de440-2d81-4a70-8068-ad4702c9fe32", + "prevId": "164b6b9d-8df1-41f0-b3d2-5fe479312bdc", + "tables": { + "apps": { + "name": "apps", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "github_org": { + "name": "github_org", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_repo": { + "name": "github_repo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_branch": { + "name": "github_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supabase_project_id": { + "name": "supabase_project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "chat_context": { + "name": "chat_context", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chats": { + "name": "chats", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "app_id": { + "name": "app_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initial_commit_hash": { + "name": "initial_commit_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "chats_app_id_apps_id_fk": { + "name": "chats_app_id_apps_id_fk", + "tableFrom": "chats", + "tableTo": "apps", + "columnsFrom": [ + "app_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "language_model_providers": { + "name": "language_model_providers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "api_base_url": { + "name": "api_base_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "env_var_name": { + "name": "env_var_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "language_models": { + "name": "language_models", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "api_name": { + "name": "api_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "builtin_provider_id": { + "name": "builtin_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_provider_id": { + "name": "custom_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_output_tokens": { + "name": "max_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "context_window": { + "name": "context_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "language_models_custom_provider_id_language_model_providers_id_fk": { + "name": "language_models_custom_provider_id_language_model_providers_id_fk", + "tableFrom": "language_models", + "tableTo": "language_model_providers", + "columnsFrom": [ + "custom_provider_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "chat_id": { + "name": "chat_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "approval_state": { + "name": "approval_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commit_hash": { + "name": "commit_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "messages_chat_id_chats_id_fk": { + "name": "messages_chat_id_chats_id_fk", + "tableFrom": "messages", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 524c5f6..a8b283e 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -50,6 +50,13 @@ "when": 1749515724373, "tag": "0006_mushy_squirrel_girl", "breakpoints": true + }, + { + "idx": 7, + "version": "6", + "when": 1750186036000, + "tag": "0007_dapper_overlord", + "breakpoints": true } ] } \ No newline at end of file diff --git a/e2e-tests/github.spec.ts b/e2e-tests/github.spec.ts new file mode 100644 index 0000000..f1e8a4b --- /dev/null +++ b/e2e-tests/github.spec.ts @@ -0,0 +1,145 @@ +import { expect } from "@playwright/test"; +import { test } from "./helpers/test_helper"; + +test("should connect to GitHub using device flow", async ({ po }) => { + await po.setUp(); + await po.sendPrompt("tc=basic"); + + await po.getTitleBarAppNameButton().click(); + await po.githubConnector.connect(); + + // Wait for device flow to start and show the code + await expect(po.page.locator("text=FAKE-CODE")).toBeVisible(); + + // Verify the verification URI is displayed + await expect( + po.page.locator("text=https://github.com/login/device"), + ).toBeVisible(); + + // Verify the "Set up your GitHub repo" section appears + await expect(po.githubConnector.getSetupYourGitHubRepoButton()).toBeVisible(); +}); + +test("create and sync to new repo", async ({ po }) => { + await po.setUp(); + await po.sendPrompt("tc=basic"); + + await po.getTitleBarAppNameButton().click(); + await po.githubConnector.connect(); + + // Verify "Create new repo" is selected by default + await expect(po.githubConnector.getCreateNewRepoModeButton()).toHaveClass( + /bg-primary/, + ); + + await po.githubConnector.fillCreateRepoName("test-new-repo"); + + // Wait for availability check + await po.page.waitForSelector("text=Repository name is available!", { + timeout: 5000, + }); + + // Click create repo button + await po.githubConnector.clickCreateRepoButton(); + + // Snapshot post-creation state + await po.githubConnector.snapshotConnectedRepo(); + + // Sync: capture success message + await po.githubConnector.clickSyncToGithubButton(); + + await po.githubConnector.snapshotConnectedRepo(); + // Verify the push was received for the default branch (main) + await po.githubConnector.verifyPushEvent({ + repo: "test-new-repo", + branch: "main", + operation: "create", + }); +}); + +test("create and sync to new repo - custom branch", async ({ po }) => { + await po.setUp(); + await po.sendPrompt("tc=basic"); + + await po.getTitleBarAppNameButton().click(); + await po.githubConnector.connect(); + + await po.githubConnector.fillCreateRepoName("test-new-repo"); + await po.githubConnector.fillNewRepoBranchName("new-branch"); + + // Click create repo button + await po.githubConnector.clickCreateRepoButton(); + + // Sync to GitHub + await po.githubConnector.clickSyncToGithubButton(); + + // Snapshot post-creation state + await po.githubConnector.snapshotConnectedRepo(); + + // Verify the push was received for the correct custom branch + await po.githubConnector.verifyPushEvent({ + repo: "test-new-repo", + branch: "new-branch", + operation: "create", + }); +}); + +test("disconnect from repo", async ({ po }) => { + await po.setUp(); + await po.sendPrompt("tc=basic"); + + await po.getTitleBarAppNameButton().click(); + await po.githubConnector.connect(); + + await po.githubConnector.fillCreateRepoName("test-new-repo"); + await po.githubConnector.clickCreateRepoButton(); + + await po.githubConnector.clickDisconnectRepoButton(); + await po.githubConnector.getSetupYourGitHubRepoButton().click(); + // Make this deterministic + await po.githubConnector.fillCreateRepoName("[scrubbed]"); + await po.githubConnector.snapshotSetupRepo(); +}); + +test("create and sync to existing repo", async ({ po }) => { + await po.setUp(); + await po.sendPrompt("tc=basic"); + + await po.getTitleBarAppNameButton().click(); + await po.githubConnector.connect(); + + await po.githubConnector.getConnectToExistingRepoModeButton().click(); + + await po.githubConnector.selectRepo("testuser/existing-app"); + await po.githubConnector.selectBranch("main"); + await po.githubConnector.clickConnectToRepoButton(); + + await po.githubConnector.snapshotConnectedRepo(); +}); + +test("create and sync to existing repo - custom branch", async ({ po }) => { + // Clear any previous push events + await po.githubConnector.clearPushEvents(); + + await po.setUp(); + await po.sendPrompt("tc=basic"); + + await po.getTitleBarAppNameButton().click(); + await po.githubConnector.connect(); + + await po.githubConnector.getConnectToExistingRepoModeButton().click(); + + await po.githubConnector.selectRepo("testuser/existing-app"); + await po.githubConnector.selectCustomBranch("new-branch"); + await po.githubConnector.clickConnectToRepoButton(); + + // Sync to GitHub to trigger a push + await po.githubConnector.clickSyncToGithubButton(); + await po.githubConnector.snapshotConnectedRepo(); + // Verify the push was received for the correct custom branch + await po.githubConnector.verifyPushEvent({ + repo: "existing-app", + branch: "new-branch", + operation: "create", + }); +}); diff --git a/e2e-tests/helpers/test_helper.ts b/e2e-tests/helpers/test_helper.ts index 3bc5f83..2783465 100644 --- a/e2e-tests/helpers/test_helper.ts +++ b/e2e-tests/helpers/test_helper.ts @@ -64,21 +64,149 @@ class ProModesDialog { } } +class GitHubConnector { + constructor(public page: Page) {} + + async connect() { + await this.page.getByRole("button", { name: "Connect to GitHub" }).click(); + } + + getSetupYourGitHubRepoButton() { + return this.page.getByText("Set up your GitHub repo"); + } + + getCreateNewRepoModeButton() { + return this.page.getByRole("button", { name: "Create new repo" }); + } + + getConnectToExistingRepoModeButton() { + return this.page.getByRole("button", { name: "Connect to existing repo" }); + } + + async clickCreateRepoButton() { + await this.page.getByRole("button", { name: "Create Repo" }).click(); + } + + async fillCreateRepoName(name: string) { + await this.page.getByTestId("github-create-repo-name-input").fill(name); + } + + async fillNewRepoBranchName(name: string) { + await this.page.getByTestId("github-new-repo-branch-input").fill(name); + } + + async selectRepo(repo: string) { + await this.page.getByTestId("github-repo-select").click(); + await this.page.getByRole("option", { name: repo }).click(); + } + + async selectBranch(branch: string) { + await this.page.getByTestId("github-branch-select").click(); + await this.page.getByRole("option", { name: branch }).click(); + } + + async selectCustomBranch(branch: string) { + await this.page.getByTestId("github-branch-select").click(); + await this.page + .getByRole("option", { name: "✏️ Type custom branch name" }) + .click(); + await this.page.getByTestId("github-custom-branch-input").click(); + await this.page.getByTestId("github-custom-branch-input").fill(branch); + } + + async clickConnectToRepoButton() { + await this.page.getByRole("button", { name: "Connect to repo" }).click(); + } + + async snapshotConnectedRepo() { + await expect( + this.page.getByTestId("github-connected-repo"), + ).toMatchAriaSnapshot(); + } + + async snapshotSetupRepo() { + await expect( + this.page.getByTestId("github-setup-repo"), + ).toMatchAriaSnapshot(); + } + + async snapshotUnconnectedRepo() { + await expect( + this.page.getByTestId("github-unconnected-repo"), + ).toMatchAriaSnapshot(); + } + + async clickSyncToGithubButton() { + await this.page.getByRole("button", { name: "Sync to GitHub" }).click(); + } + + async clickDisconnectRepoButton() { + await this.page + .getByRole("button", { name: "Disconnect from repo" }) + .click(); + } + + async clearPushEvents() { + const response = await this.page.request.post( + "http://localhost:3500/github/api/test/clear-push-events", + ); + return await response.json(); + } + + async getPushEvents(repo?: string) { + const url = repo + ? `http://localhost:3500/github/api/test/push-events?repo=${repo}` + : "http://localhost:3500/github/api/test/push-events"; + const response = await this.page.request.get(url); + return await response.json(); + } + + async verifyPushEvent(expectedEvent: { + repo: string; + branch: string; + operation?: "push" | "create" | "delete"; + }) { + const pushEvents = await this.getPushEvents(expectedEvent.repo); + const matchingEvent = pushEvents.find( + (event: any) => + event.repo === expectedEvent.repo && + event.branch === expectedEvent.branch && + (!expectedEvent.operation || + event.operation === expectedEvent.operation), + ); + + if (!matchingEvent) { + throw new Error( + `Expected push event not found. Expected: ${JSON.stringify(expectedEvent)}. ` + + `Actual events: ${JSON.stringify(pushEvents)}`, + ); + } + + return matchingEvent; + } +} + export class PageObject { private userDataDir: string; - + public githubConnector: GitHubConnector; constructor( public electronApp: ElectronApplication, public page: Page, { userDataDir }: { userDataDir: string }, ) { this.userDataDir = userDataDir; + this.githubConnector = new GitHubConnector(this.page); + } + + private async baseSetup() { + await this.githubConnector.clearPushEvents(); } async setUp({ autoApprove = false, nativeGit = false, }: { autoApprove?: boolean; nativeGit?: boolean } = {}) { + await this.baseSetup(); await this.goToSettingsTab(); if (autoApprove) { await this.toggleAutoApprove(); @@ -93,16 +221,8 @@ export class PageObject { await this.selectTestModel(); } - async importApp(appDir: string) { - await this.page.getByRole("button", { name: "Import App" }).click(); - await eph.stubDialog(this.electronApp, "showOpenDialog", { - filePaths: [path.join(__dirname, "..", "fixtures", "import-app", appDir)], - }); - await this.page.getByRole("button", { name: "Select Folder" }).click(); - await this.page.getByRole("button", { name: "Import" }).click(); - } - async setUpDyadPro({ autoApprove = false }: { autoApprove?: boolean } = {}) { + await this.baseSetup(); await this.goToSettingsTab(); if (autoApprove) { await this.toggleAutoApprove(); @@ -112,7 +232,6 @@ export class PageObject { } async setUpDyadProvider() { - // await page.getByRole('link', { name: 'Settings' }).click(); await this.page .locator("div") .filter({ hasText: /^DyadNeeds Setup$/ }) @@ -123,12 +242,15 @@ export class PageObject { .getByRole("textbox", { name: "Set Dyad API Key" }) .fill("testdyadkey"); await this.page.getByRole("button", { name: "Save Key" }).click(); - // await page.getByRole('link', { name: 'Apps' }).click(); - // await page.getByTestId('home-chat-input-container').getByRole('button', { name: 'Pro' }).click(); - // await page.getByRole('switch', { name: 'Turbo Edits' }).click(); - // await page.getByRole('switch', { name: 'Turbo Edits' }).click(); - // await page.locator('div').filter({ hasText: /^Import App$/ }).click(); - // await page.getByRole('button', { name: 'Select Folder' }).press('Escape'); + } + + async importApp(appDir: string) { + await this.page.getByRole("button", { name: "Import App" }).click(); + await eph.stubDialog(this.electronApp, "showOpenDialog", { + filePaths: [path.join(__dirname, "..", "fixtures", "import-app", appDir)], + }); + await this.page.getByRole("button", { name: "Select Folder" }).click(); + await this.page.getByRole("button", { name: "Import" }).click(); } async openContextFilesPicker() { diff --git a/e2e-tests/snapshots/github.spec.ts_create-and-sync-to-existing-repo---custom-branch-1.aria.yml b/e2e-tests/snapshots/github.spec.ts_create-and-sync-to-existing-repo---custom-branch-1.aria.yml new file mode 100644 index 0000000..dcee27f --- /dev/null +++ b/e2e-tests/snapshots/github.spec.ts_create-and-sync-to-existing-repo---custom-branch-1.aria.yml @@ -0,0 +1,5 @@ +- paragraph: "Connected to GitHub Repo:" +- text: testuser/existing-app +- paragraph: "Branch: new-branch" +- button "Sync to GitHub" +- button "Disconnect from repo" \ No newline at end of file diff --git a/e2e-tests/snapshots/github.spec.ts_create-and-sync-to-existing-repo-1.aria.yml b/e2e-tests/snapshots/github.spec.ts_create-and-sync-to-existing-repo-1.aria.yml new file mode 100644 index 0000000..5130b54 --- /dev/null +++ b/e2e-tests/snapshots/github.spec.ts_create-and-sync-to-existing-repo-1.aria.yml @@ -0,0 +1,5 @@ +- paragraph: "Connected to GitHub Repo:" +- text: testuser/existing-app +- paragraph: "Branch: main" +- button "Sync to GitHub" +- button "Disconnect from repo" \ No newline at end of file diff --git a/e2e-tests/snapshots/github.spec.ts_create-and-sync-to-new-repo---custom-branch-1.aria.yml b/e2e-tests/snapshots/github.spec.ts_create-and-sync-to-new-repo---custom-branch-1.aria.yml new file mode 100644 index 0000000..bbd081e --- /dev/null +++ b/e2e-tests/snapshots/github.spec.ts_create-and-sync-to-new-repo---custom-branch-1.aria.yml @@ -0,0 +1,5 @@ +- paragraph: "Connected to GitHub Repo:" +- text: testuser/test-new-repo +- paragraph: "Branch: new-branch" +- button "Sync to GitHub" +- button "Disconnect from repo" \ No newline at end of file diff --git a/e2e-tests/snapshots/github.spec.ts_create-and-sync-to-new-repo-1.aria.yml b/e2e-tests/snapshots/github.spec.ts_create-and-sync-to-new-repo-1.aria.yml new file mode 100644 index 0000000..56d4885 --- /dev/null +++ b/e2e-tests/snapshots/github.spec.ts_create-and-sync-to-new-repo-1.aria.yml @@ -0,0 +1,5 @@ +- paragraph: "Connected to GitHub Repo:" +- text: testuser/test-new-repo +- paragraph: "Branch: main" +- button "Sync to GitHub" +- button "Disconnect from repo" \ No newline at end of file diff --git a/e2e-tests/snapshots/github.spec.ts_create-and-sync-to-new-repo-2.aria.yml b/e2e-tests/snapshots/github.spec.ts_create-and-sync-to-new-repo-2.aria.yml new file mode 100644 index 0000000..a442f1d --- /dev/null +++ b/e2e-tests/snapshots/github.spec.ts_create-and-sync-to-new-repo-2.aria.yml @@ -0,0 +1,6 @@ +- paragraph: "Connected to GitHub Repo:" +- text: testuser/test-new-repo +- paragraph: "Branch: main" +- button "Sync to GitHub" +- button "Disconnect from repo" +- paragraph: Successfully pushed to GitHub! \ No newline at end of file diff --git a/e2e-tests/snapshots/github.spec.ts_disconnect-from-repo-1.aria.yml b/e2e-tests/snapshots/github.spec.ts_disconnect-from-repo-1.aria.yml new file mode 100644 index 0000000..3890030 --- /dev/null +++ b/e2e-tests/snapshots/github.spec.ts_disconnect-from-repo-1.aria.yml @@ -0,0 +1,10 @@ +- button "Set up your GitHub repo": + - img +- button "Create new repo" +- button "Connect to existing repo" +- text: Repository Name +- textbox: "[scrubbed]" +- paragraph: Repository name is available! +- text: Branch +- textbox "main" +- button "Create Repo" \ No newline at end of file diff --git a/e2e-tests/snapshots/github.spec.ts_should-create-a-new-GitHub-repository-1.aria.yml b/e2e-tests/snapshots/github.spec.ts_should-create-a-new-GitHub-repository-1.aria.yml new file mode 100644 index 0000000..56d4885 --- /dev/null +++ b/e2e-tests/snapshots/github.spec.ts_should-create-a-new-GitHub-repository-1.aria.yml @@ -0,0 +1,5 @@ +- paragraph: "Connected to GitHub Repo:" +- text: testuser/test-new-repo +- paragraph: "Branch: main" +- button "Sync to GitHub" +- button "Disconnect from repo" \ No newline at end of file diff --git a/e2e-tests/snapshots/github.spec.ts_should-create-a-new-GitHub-repository-2.aria.yml b/e2e-tests/snapshots/github.spec.ts_should-create-a-new-GitHub-repository-2.aria.yml new file mode 100644 index 0000000..a442f1d --- /dev/null +++ b/e2e-tests/snapshots/github.spec.ts_should-create-a-new-GitHub-repository-2.aria.yml @@ -0,0 +1,6 @@ +- paragraph: "Connected to GitHub Repo:" +- text: testuser/test-new-repo +- paragraph: "Branch: main" +- button "Sync to GitHub" +- button "Disconnect from repo" +- paragraph: Successfully pushed to GitHub! \ No newline at end of file diff --git a/src/components/GitHubConnector.tsx b/src/components/GitHubConnector.tsx index a1e349d..1969f14 100644 --- a/src/components/GitHubConnector.tsx +++ b/src/components/GitHubConnector.tsx @@ -1,19 +1,281 @@ import { useState, useEffect, useCallback, useRef } from "react"; import { Button } from "@/components/ui/button"; -import { Github, Clipboard, Check } from "lucide-react"; +import { + Github, + Clipboard, + Check, + AlertTriangle, + ChevronDown, + ChevronRight, +} from "lucide-react"; import { IpcClient } from "@/ipc/ipc_client"; import { useSettings } from "@/hooks/useSettings"; import { useLoadApp } from "@/hooks/useLoadApp"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; interface GitHubConnectorProps { appId: number | null; folderName: string; } -export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) { +interface GitHubRepo { + name: string; + full_name: string; + private: boolean; +} + +interface GitHubBranch { + name: string; + commit: { sha: string }; +} + +interface ConnectedGitHubConnectorProps { + appId: number; + app: any; + refreshApp: () => void; +} + +interface UnconnectedGitHubConnectorProps { + appId: number | null; + folderName: string; + settings: any; + refreshSettings: () => void; + refreshApp: () => void; +} + +function ConnectedGitHubConnector({ + appId, + app, + refreshApp, +}: ConnectedGitHubConnectorProps) { + const [isSyncing, setIsSyncing] = useState(false); + const [syncError, setSyncError] = useState(null); + const [syncSuccess, setSyncSuccess] = useState(false); + const [showForceDialog, setShowForceDialog] = useState(false); + const [isDisconnecting, setIsDisconnecting] = useState(false); + const [disconnectError, setDisconnectError] = useState(null); + + const handleDisconnectRepo = async () => { + setIsDisconnecting(true); + setDisconnectError(null); + try { + await IpcClient.getInstance().disconnectGithubRepo(appId); + refreshApp(); + } catch (err: any) { + setDisconnectError(err.message || "Failed to disconnect repository."); + } finally { + setIsDisconnecting(false); + } + }; + + const handleSyncToGithub = async (force: boolean = false) => { + setIsSyncing(true); + setSyncError(null); + setSyncSuccess(false); + setShowForceDialog(false); + + try { + const result = await IpcClient.getInstance().syncGithubRepo(appId, force); + if (result.success) { + setSyncSuccess(true); + } else { + setSyncError(result.error || "Failed to sync to GitHub."); + // If it's a push rejection error, show the force dialog + if ( + result.error?.includes("rejected") || + result.error?.includes("non-fast-forward") + ) { + // Don't show force dialog immediately, let user see the error first + } + } + } catch (err: any) { + setSyncError(err.message || "Failed to sync to GitHub."); + } finally { + setIsSyncing(false); + } + }; + + return ( +
+

Connected to GitHub Repo:

+ { + e.preventDefault(); + IpcClient.getInstance().openExternalUrl( + `https://github.com/${app.githubOrg}/${app.githubRepo}`, + ); + }} + className="cursor-pointer text-blue-600 hover:underline dark:text-blue-400" + target="_blank" + rel="noopener noreferrer" + > + {app.githubOrg}/{app.githubRepo} + + {app.githubBranch && ( +

+ Branch: {app.githubBranch} +

+ )} +
+ + +
+ {syncError && ( +
+

+ {syncError}{" "} + { + e.preventDefault(); + IpcClient.getInstance().openExternalUrl( + "https://www.dyad.sh/docs/integrations/github#troubleshooting", + ); + }} + className="cursor-pointer text-blue-600 hover:underline dark:text-blue-400" + target="_blank" + rel="noopener noreferrer" + > + See troubleshooting guide + +

+ {(syncError.includes("rejected") || + syncError.includes("non-fast-forward")) && ( + + )} +
+ )} + {syncSuccess && ( +

Successfully pushed to GitHub!

+ )} + {disconnectError && ( +

{disconnectError}

+ )} + + {/* Force Push Warning Dialog */} + + + + + + Force Push Warning + + +
+

+ You are about to perform a force push to your + GitHub repository. +

+
+

+ + This is dangerous and non-reversible and will: + +

+
    +
  • Overwrite the remote repository history
  • +
  • + Permanently delete commits that exist on the remote but + not locally +
  • +
+
+

+ Only proceed if you're certain this is what you want to do. +

+
+
+
+ + + + +
+
+
+ ); +} + +function UnconnectedGitHubConnector({ + appId, + folderName, + settings, + refreshSettings, + refreshApp, +}: UnconnectedGitHubConnectorProps) { + // --- Collapsible State --- + const [isExpanded, setIsExpanded] = useState(false); + // --- GitHub Device Flow State --- - const { app, refreshApp } = useLoadApp(appId); - const { settings, refreshSettings } = useSettings(); const [githubUserCode, setGithubUserCode] = useState(null); const [githubVerificationUri, setGithubVerificationUri] = useState< string | null @@ -24,7 +286,37 @@ export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) { null, ); const [codeCopied, setCodeCopied] = useState(false); - // --- --- + + // --- Repo Setup State --- + const [repoSetupMode, setRepoSetupMode] = useState<"create" | "existing">( + "create", + ); + const [availableRepos, setAvailableRepos] = useState([]); + const [isLoadingRepos, setIsLoadingRepos] = useState(false); + const [selectedRepo, setSelectedRepo] = useState(""); + const [availableBranches, setAvailableBranches] = useState( + [], + ); + const [isLoadingBranches, setIsLoadingBranches] = useState(false); + const [selectedBranch, setSelectedBranch] = useState("main"); + const [branchInputMode, setBranchInputMode] = useState<"select" | "custom">( + "select", + ); + const [customBranchName, setCustomBranchName] = useState(""); + + // Create new repo state + const [repoName, setRepoName] = useState(folderName); + const [repoAvailable, setRepoAvailable] = useState(null); + const [repoCheckError, setRepoCheckError] = useState(null); + const [isCheckingRepo, setIsCheckingRepo] = useState(false); + const [isCreatingRepo, setIsCreatingRepo] = useState(false); + const [createRepoError, setCreateRepoError] = useState(null); + const [createRepoSuccess, setCreateRepoSuccess] = useState(false); + + // Assume org is the authenticated user for now (could add org input later) + const githubOrg = ""; // Use empty string for now (GitHub API will default to the authenticated user) + + const debounceTimeoutRef = useRef(null); const handleConnectToGithub = async () => { if (!appId) return; @@ -78,7 +370,7 @@ export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) { setGithubError(null); setIsConnectingToGithub(false); refreshSettings(); - // TODO: Maybe update parent UI to show "Connected" state or trigger next action + setIsExpanded(true); }); cleanupFunctions.push(removeSuccessListener); @@ -98,9 +390,6 @@ export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) { // Cleanup function to remove all listeners when component unmounts or appId changes return () => { cleanupFunctions.forEach((cleanup) => cleanup()); - // Optional: Send a message to main process to cancel polling if component unmounts - // Only cancel if we were actually connecting for this specific appId - // IpcClient.getInstance().cancelGithubDeviceFlow(appId); // Reset state when appId changes or component unmounts setGithubUserCode(null); setGithubVerificationUri(null); @@ -110,23 +399,58 @@ export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) { }; }, [appId]); // Re-run effect if appId changes - // --- Create Repo State --- - const [repoName, setRepoName] = useState(folderName); - const [repoAvailable, setRepoAvailable] = useState(null); - const [repoCheckError, setRepoCheckError] = useState(null); - const [isCheckingRepo, setIsCheckingRepo] = useState(false); - const [isCreatingRepo, setIsCreatingRepo] = useState(false); - const [createRepoError, setCreateRepoError] = useState(null); - const [createRepoSuccess, setCreateRepoSuccess] = useState(false); - // --- Sync to GitHub State --- - const [isSyncing, setIsSyncing] = useState(false); - const [syncError, setSyncError] = useState(null); - const [syncSuccess, setSyncSuccess] = useState(false); - // Assume org is the authenticated user for now (could add org input later) - // TODO: After device flow, fetch and store the GitHub username/org in settings for use here - const githubOrg = ""; // Use empty string for now (GitHub API will default to the authenticated user) + // Load available repos when GitHub is connected + useEffect(() => { + if (settings?.githubAccessToken && repoSetupMode === "existing") { + loadAvailableRepos(); + } + }, [settings?.githubAccessToken, repoSetupMode]); - const debounceTimeoutRef = useRef(null); + const loadAvailableRepos = async () => { + setIsLoadingRepos(true); + try { + const repos = await IpcClient.getInstance().listGithubRepos(); + setAvailableRepos(repos); + } catch (error) { + console.error("Failed to load GitHub repos:", error); + } finally { + setIsLoadingRepos(false); + } + }; + + // Load branches when a repo is selected + useEffect(() => { + if (selectedRepo && repoSetupMode === "existing") { + loadRepoBranches(); + } + }, [selectedRepo, repoSetupMode]); + + const loadRepoBranches = async () => { + if (!selectedRepo) return; + + setIsLoadingBranches(true); + setBranchInputMode("select"); // Reset to select mode when loading new repo + setCustomBranchName(""); // Clear custom branch name + try { + const [owner, repo] = selectedRepo.split("/"); + const branches = await IpcClient.getInstance().getGithubRepoBranches( + owner, + repo, + ); + setAvailableBranches(branches); + // Default to main if available, otherwise first branch + const defaultBranch = + branches.find((b) => b.name === "main" || b.name === "master") || + branches[0]; + if (defaultBranch) { + setSelectedBranch(defaultBranch.name); + } + } catch (error) { + console.error("Failed to load repo branches:", error); + } finally { + setIsLoadingBranches(false); + } + }; const checkRepoAvailability = useCallback( async (name: string) => { @@ -166,48 +490,49 @@ export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) { [checkRepoAvailability], ); - const handleCreateRepo = async (e: React.FormEvent) => { + const handleSetupRepo = async (e: React.FormEvent) => { e.preventDefault(); + if (!appId) return; + setCreateRepoError(null); setIsCreatingRepo(true); setCreateRepoSuccess(false); + try { - await IpcClient.getInstance().createGithubRepo( - githubOrg, - repoName, - appId!, - ); + if (repoSetupMode === "create") { + await IpcClient.getInstance().createGithubRepo( + githubOrg, + repoName, + appId, + selectedBranch, + ); + } else { + const [owner, repo] = selectedRepo.split("/"); + const branchToUse = + branchInputMode === "custom" ? customBranchName : selectedBranch; + await IpcClient.getInstance().connectToExistingGithubRepo( + owner, + repo, + branchToUse, + appId, + ); + } setCreateRepoSuccess(true); setRepoCheckError(null); refreshApp(); } catch (err: any) { - setCreateRepoError(err.message || "Failed to create repository."); + setCreateRepoError( + err.message || + `Failed to ${repoSetupMode === "create" ? "create" : "connect to"} repository.`, + ); } finally { setIsCreatingRepo(false); } }; - const [isDisconnecting, setIsDisconnecting] = useState(false); - const [disconnectError, setDisconnectError] = useState(null); - - const handleDisconnectRepo = async () => { - if (!appId) return; - setIsDisconnecting(true); - setDisconnectError(null); - try { - await IpcClient.getInstance().disconnectGithubRepo(appId); - refreshApp(); - } catch (err: any) { - setDisconnectError(err.message || "Failed to disconnect repository."); - } finally { - setIsDisconnecting(false); - } - }; - if (!settings?.githubAccessToken) { return ( -
- {" "} +
- return ( -
-

Connected to GitHub Repo:

- { - e.preventDefault(); - IpcClient.getInstance().openExternalUrl( - `https://github.com/${app.githubOrg}/${app.githubRepo}`, - ); - }} - className="cursor-pointer text-blue-600 hover:underline dark:text-blue-400" - target="_blank" - rel="noopener noreferrer" - > - {app.githubOrg}/{app.githubRepo} - -
- + +
+
+ +
+ {repoSetupMode === "create" ? ( <> - - - - - Syncing... +
+ + { + const newValue = e.target.value; + setRepoName(newValue); + setRepoAvailable(null); + setRepoCheckError(null); + debouncedCheckRepoAvailability(newValue); + }} + disabled={isCreatingRepo} + /> + {isCheckingRepo && ( +

+ Checking availability... +

+ )} + {repoAvailable === true && ( +

+ Repository name is available! +

+ )} + {repoAvailable === false && ( +

+ {repoCheckError} +

+ )} +
) : ( - "Sync to GitHub" + <> +
+ + +
+ )} - - -
- {syncError && ( -

- {syncError}{" "} - { - e.preventDefault(); - IpcClient.getInstance().openExternalUrl( - "https://www.dyad.sh/docs/integrations/github#troubleshooting", - ); - }} - className="cursor-pointer text-blue-600 hover:underline dark:text-blue-400" - target="_blank" - rel="noopener noreferrer" + + {/* Branch Selection */} +

+ + + + + {createRepoError && ( +

{createRepoError}

+ )} + {createRepoSuccess && ( +

+ {repoSetupMode === "create" + ? "Repository created and linked!" + : "Connected to repository!"} +

+ )} +
+ + ); +} + +export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) { + const { app, refreshApp } = useLoadApp(appId); + const { settings, refreshSettings } = useSettings(); + + if (app?.githubOrg && app?.githubRepo && appId) { + return ( + ); } else { return ( -
-

Set up your GitHub repo

-
- - { - const newValue = e.target.value; - setRepoName(newValue); - setRepoAvailable(null); - setRepoCheckError(null); - debouncedCheckRepoAvailability(newValue); - }} - disabled={isCreatingRepo} - /> - {isCheckingRepo && ( -

Checking availability...

- )} - {repoAvailable === true && ( -

- Repository name is available! -

- )} - {repoAvailable === false && ( -

{repoCheckError}

- )} - -
- {createRepoError && ( -

{createRepoError}

- )} - {createRepoSuccess && ( -

Repository created and linked!

- )} -
+ ); } } diff --git a/src/db/index.ts b/src/db/index.ts index ca9bcd1..000c3f9 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -8,7 +8,7 @@ import { migrate } from "drizzle-orm/better-sqlite3/migrator"; import path from "node:path"; import fs from "node:fs"; import { getDyadAppPath, getUserDataPath } from "../paths/paths"; -import { eq } from "drizzle-orm"; + import log from "electron-log"; const logger = log.scope("db"); @@ -87,14 +87,3 @@ try { export const db = _db as any as BetterSQLite3Database & { $client: Database.Database; }; - -export async function updateAppGithubRepo( - appId: number, - org: string, - repo: string, -): Promise { - await db - .update(schema.apps) - .set({ githubOrg: org, githubRepo: repo }) - .where(eq(schema.apps.id, appId)); -} diff --git a/src/db/schema.ts b/src/db/schema.ts index a360609..63affa3 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -14,6 +14,7 @@ export const apps = sqliteTable("apps", { .default(sql`(unixepoch())`), githubOrg: text("github_org"), githubRepo: text("github_repo"), + githubBranch: text("github_branch"), supabaseProjectId: text("supabase_project_id"), chatContext: text("chat_context", { mode: "json" }), }); diff --git a/src/ipc/handlers/github_handlers.ts b/src/ipc/handlers/github_handlers.ts index bd32d61..2c0509a 100644 --- a/src/ipc/handlers/github_handlers.ts +++ b/src/ipc/handlers/github_handlers.ts @@ -1,10 +1,9 @@ import { ipcMain, BrowserWindow, IpcMainInvokeEvent } from "electron"; import fetch from "node-fetch"; // Use node-fetch for making HTTP requests in main process import { writeSettings, readSettings } from "../../main/settings"; -import { updateAppGithubRepo } from "../../db/index"; import git from "isomorphic-git"; import http from "isomorphic-git/http/node"; - +import * as schema from "../../db/schema"; import fs from "node:fs"; import { getDyadAppPath } from "../../paths/paths"; import { db } from "../../db"; @@ -12,14 +11,31 @@ import { apps } from "../../db/schema"; import { eq } from "drizzle-orm"; import { GithubUser } from "../../lib/schemas"; import log from "electron-log"; +import { IS_TEST_BUILD } from "../utils/test_utils"; const logger = log.scope("github_handlers"); // --- GitHub Device Flow Constants --- // TODO: Fetch this securely, e.g., from environment variables or a config file const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || "Ov23liWV2HdC0RBLecWx"; -const GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code"; -const GITHUB_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"; + +// Use test server URLs when in test mode + +const TEST_SERVER_BASE = "http://localhost:3500"; + +const GITHUB_DEVICE_CODE_URL = IS_TEST_BUILD + ? `${TEST_SERVER_BASE}/github/login/device/code` + : "https://github.com/login/device/code"; +const GITHUB_ACCESS_TOKEN_URL = IS_TEST_BUILD + ? `${TEST_SERVER_BASE}/github/login/oauth/access_token` + : "https://github.com/login/oauth/access_token"; +const GITHUB_API_BASE = IS_TEST_BUILD + ? `${TEST_SERVER_BASE}/github/api` + : "https://api.github.com"; +const GITHUB_GIT_BASE = IS_TEST_BUILD + ? `${TEST_SERVER_BASE}/github/git` + : "https://github.com"; + const GITHUB_SCOPES = "repo,user,workflow"; // Define the scopes needed // --- State Management (Simple in-memory, consider alternatives for robustness) --- @@ -48,7 +64,7 @@ export async function getGithubUser(): Promise { try { const accessToken = settings.githubAccessToken?.value; if (!accessToken) return null; - const res = await fetch("https://api.github.com/user/emails", { + const res = await fetch(`${GITHUB_API_BASE}/user/emails`, { headers: { Authorization: `Bearer ${accessToken}` }, }); if (!res.ok) return null; @@ -281,6 +297,90 @@ function handleStartGithubFlow( }); } +// --- GitHub List Repos Handler --- +async function handleListGithubRepos(): Promise< + { name: string; full_name: string; private: boolean }[] +> { + try { + // Get access token from settings + const settings = readSettings(); + const accessToken = settings.githubAccessToken?.value; + if (!accessToken) { + throw new Error("Not authenticated with GitHub."); + } + + // Fetch user's repositories + const response = await fetch( + `${GITHUB_API_BASE}/user/repos?per_page=100&sort=updated`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/vnd.github+json", + }, + }, + ); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error( + `GitHub API error: ${errorData.message || response.statusText}`, + ); + } + + const repos = await response.json(); + return repos.map((repo: any) => ({ + name: repo.name, + full_name: repo.full_name, + private: repo.private, + })); + } catch (err: any) { + logger.error("[GitHub Handler] Failed to list repos:", err); + throw new Error(err.message || "Failed to list GitHub repositories."); + } +} + +// --- GitHub Get Repo Branches Handler --- +async function handleGetRepoBranches( + event: IpcMainInvokeEvent, + { owner, repo }: { owner: string; repo: string }, +): Promise<{ name: string; commit: { sha: string } }[]> { + try { + // Get access token from settings + const settings = readSettings(); + const accessToken = settings.githubAccessToken?.value; + if (!accessToken) { + throw new Error("Not authenticated with GitHub."); + } + + // Fetch repository branches + const response = await fetch( + `${GITHUB_API_BASE}/repos/${owner}/${repo}/branches`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/vnd.github+json", + }, + }, + ); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error( + `GitHub API error: ${errorData.message || response.statusText}`, + ); + } + + const branches = await response.json(); + return branches.map((branch: any) => ({ + name: branch.name, + commit: { sha: branch.commit.sha }, + })); + } catch (err: any) { + logger.error("[GitHub Handler] Failed to get repo branches:", err); + throw new Error(err.message || "Failed to get repository branches."); + } +} + // --- GitHub Repo Availability Handler --- async function handleIsRepoAvailable( event: IpcMainInvokeEvent, @@ -296,13 +396,13 @@ async function handleIsRepoAvailable( // If org is empty, use the authenticated user const owner = org || - (await fetch("https://api.github.com/user", { + (await fetch(`${GITHUB_API_BASE}/user`, { headers: { Authorization: `Bearer ${accessToken}` }, }) .then((r) => r.json()) .then((u) => u.login)); // Check if repo exists - const url = `https://api.github.com/repos/${owner}/${repo}`; + const url = `${GITHUB_API_BASE}/repos/${owner}/${repo}`; const res = await fetch(url, { headers: { Authorization: `Bearer ${accessToken}` }, }); @@ -322,7 +422,12 @@ async function handleIsRepoAvailable( // --- GitHub Create Repo Handler --- async function handleCreateRepo( event: IpcMainInvokeEvent, - { org, repo, appId }: { org: string; repo: string; appId: number }, + { + org, + repo, + appId, + branch, + }: { org: string; repo: string; appId: number; branch?: string }, ): Promise { // Get access token from settings const settings = readSettings(); @@ -333,7 +438,7 @@ async function handleCreateRepo( // If org is empty, create for the authenticated user let owner = org; if (!owner) { - const userRes = await fetch("https://api.github.com/user", { + const userRes = await fetch(`${GITHUB_API_BASE}/user`, { headers: { Authorization: `Bearer ${accessToken}` }, }); const user = await userRes.json(); @@ -341,8 +446,8 @@ async function handleCreateRepo( } // Create repo const createUrl = org - ? `https://api.github.com/orgs/${owner}/repos` - : `https://api.github.com/user/repos`; + ? `${GITHUB_API_BASE}/orgs/${owner}/repos` + : `${GITHUB_API_BASE}/user/repos`; const res = await fetch(createUrl, { method: "POST", headers: { @@ -395,14 +500,58 @@ async function handleCreateRepo( throw new Error(errorMessage); } - // Store org and repo in the app's DB row (apps table) - await updateAppGithubRepo(appId, owner, repo); + // Store org, repo, and branch in the app's DB row (apps table) + await updateAppGithubRepo({ appId, org: owner, repo, branch }); +} + +// --- GitHub Connect to Existing Repo Handler --- +async function handleConnectToExistingRepo( + event: IpcMainInvokeEvent, + { + owner, + repo, + branch, + appId, + }: { owner: string; repo: string; branch: string; appId: number }, +): Promise { + try { + // Get access token from settings + const settings = readSettings(); + const accessToken = settings.githubAccessToken?.value; + if (!accessToken) { + throw new Error("Not authenticated with GitHub."); + } + + // Verify the repository exists and user has access + const repoResponse = await fetch( + `${GITHUB_API_BASE}/repos/${owner}/${repo}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/vnd.github+json", + }, + }, + ); + + if (!repoResponse.ok) { + const errorData = await repoResponse.json(); + throw new Error( + `Repository not found or access denied: ${errorData.message}`, + ); + } + + // Store org, repo, and branch in the app's DB row + await updateAppGithubRepo({ appId, org: owner, repo, branch }); + } catch (err: any) { + logger.error("[GitHub Handler] Failed to connect to existing repo:", err); + throw new Error(err.message || "Failed to connect to existing repository."); + } } // --- GitHub Push Handler --- async function handlePushToGithub( event: IpcMainInvokeEvent, - { appId }: { appId: number }, + { appId, force }: { appId: number; force?: boolean }, ) { try { // Get access token from settings @@ -417,8 +566,12 @@ async function handlePushToGithub( return { success: false, error: "App is not linked to a GitHub repo." }; } const appPath = getDyadAppPath(app.path); + const branch = app.githubBranch || "main"; + // Set up remote URL with token - const remoteUrl = `https://${accessToken}:x-oauth-basic@github.com/${app.githubOrg}/${app.githubRepo}.git`; + const remoteUrl = IS_TEST_BUILD + ? `${GITHUB_GIT_BASE}/${app.githubOrg}/${app.githubRepo}.git` + : `https://${accessToken}:x-oauth-basic@github.com/${app.githubOrg}/${app.githubRepo}.git`; // Set or update remote URL using git config await git.setConfig({ fs, @@ -433,11 +586,12 @@ async function handlePushToGithub( dir: appPath, remote: "origin", ref: "main", + remoteRef: branch, onAuth: () => ({ username: accessToken, password: "x-oauth-basic", }), - force: false, + force: !!force, }); return { success: true }; } catch (err: any) { @@ -463,12 +617,13 @@ async function handleDisconnectGithubRepo( throw new Error("App not found"); } - // Update app in database to remove GitHub repo and org + // Update app in database to remove GitHub repo, org, and branch await db .update(apps) .set({ githubRepo: null, githubOrg: null, + githubBranch: null, }) .where(eq(apps.id, appId)); } @@ -476,10 +631,44 @@ async function handleDisconnectGithubRepo( // --- Registration --- export function registerGithubHandlers() { ipcMain.handle("github:start-flow", handleStartGithubFlow); + ipcMain.handle("github:list-repos", handleListGithubRepos); + ipcMain.handle( + "github:get-repo-branches", + (event, args: { owner: string; repo: string }) => + handleGetRepoBranches(event, args), + ); ipcMain.handle("github:is-repo-available", handleIsRepoAvailable); ipcMain.handle("github:create-repo", handleCreateRepo); + ipcMain.handle( + "github:connect-existing-repo", + ( + event, + args: { owner: string; repo: string; branch: string; appId: number }, + ) => handleConnectToExistingRepo(event, args), + ); ipcMain.handle("github:push", handlePushToGithub); ipcMain.handle("github:disconnect", (event, args: { appId: number }) => handleDisconnectGithubRepo(event, args), ); } + +export async function updateAppGithubRepo({ + appId, + org, + repo, + branch, +}: { + appId: number; + org?: string; + repo: string; + branch?: string; +}): Promise { + await db + .update(schema.apps) + .set({ + githubOrg: org, + githubRepo: repo, + githubBranch: branch || "main", + }) + .where(eq(schema.apps.id, appId)); +} diff --git a/src/ipc/ipc_client.ts b/src/ipc/ipc_client.ts index b8641d5..dc4c0e0 100644 --- a/src/ipc/ipc_client.ts +++ b/src/ipc/ipc_client.ts @@ -554,6 +554,36 @@ export class IpcClient { // --- End GitHub Device Flow --- // --- GitHub Repo Management --- + public async listGithubRepos(): Promise< + { name: string; full_name: string; private: boolean }[] + > { + return this.ipcRenderer.invoke("github:list-repos"); + } + + public async getGithubRepoBranches( + owner: string, + repo: string, + ): Promise<{ name: string; commit: { sha: string } }[]> { + return this.ipcRenderer.invoke("github:get-repo-branches", { + owner, + repo, + }); + } + + public async connectToExistingGithubRepo( + owner: string, + repo: string, + branch: string, + appId: number, + ): Promise { + await this.ipcRenderer.invoke("github:connect-existing-repo", { + owner, + repo, + branch, + appId, + }); + } + public async checkGithubRepoAvailable( org: string, repo: string, @@ -568,25 +598,25 @@ export class IpcClient { org: string, repo: string, appId: number, + branch?: string, ): Promise { await this.ipcRenderer.invoke("github:create-repo", { org, repo, appId, + branch, }); } // Sync (push) local repo to GitHub public async syncGithubRepo( appId: number, + force?: boolean, ): Promise<{ success: boolean; error?: string }> { - try { - const result = await this.ipcRenderer.invoke("github:push", { appId }); - return result as { success: boolean; error?: string }; - } catch (error) { - showError(error); - throw error; - } + return this.ipcRenderer.invoke("github:push", { + appId, + force, + }); } public async disconnectGithubRepo(appId: number): Promise { diff --git a/src/ipc/ipc_types.ts b/src/ipc/ipc_types.ts index 3aa7f54..3e784e9 100644 --- a/src/ipc/ipc_types.ts +++ b/src/ipc/ipc_types.ts @@ -70,6 +70,7 @@ export interface App { updatedAt: Date; githubOrg: string | null; githubRepo: string | null; + githubBranch: string | null; supabaseProjectId: string | null; supabaseProjectName: string | null; } diff --git a/src/preload.ts b/src/preload.ts index 379e12b..49a1f87 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -46,8 +46,11 @@ const validInvokeChannels = [ "nodejs-status", "install-node", "github:start-flow", + "github:list-repos", + "github:get-repo-branches", "github:is-repo-available", "github:create-repo", + "github:connect-existing-repo", "github:push", "github:disconnect", "get-app-version", diff --git a/testing/fake-llm-server/githubHandler.ts b/testing/fake-llm-server/githubHandler.ts new file mode 100644 index 0000000..39c4e38 --- /dev/null +++ b/testing/fake-llm-server/githubHandler.ts @@ -0,0 +1,417 @@ +import { Request, Response } from "express"; +import * as path from "path"; +import * as fs from "fs"; +import * as os from "os"; + +const gitHttpMiddlewareFactory = require("git-http-mock-server/middleware"); + +// Push event tracking for tests +interface PushEvent { + timestamp: Date; + repo: string; + branch: string; + operation: "push" | "create" | "delete"; + commitSha?: string; +} + +const pushEvents: PushEvent[] = []; + +// Mock data for testing +const mockAccessToken = "fake_access_token_12345"; +const mockDeviceCode = "fake_device_code_12345"; +const mockUserCode = "FAKE-CODE"; +const mockUser = { + login: "testuser", + id: 12345, + email: "testuser@example.com", +}; + +const mockRepos = [ + { + id: 1, + name: "test-repo-1", + full_name: "testuser/test-repo-1", + private: false, + owner: { login: "testuser" }, + default_branch: "main", + }, + { + id: 2, + name: "test-repo-2", + full_name: "testuser/test-repo-2", + private: true, + owner: { login: "testuser" }, + default_branch: "main", + }, + { + id: 3, + name: "existing-app", + full_name: "testuser/existing-app", + private: false, + owner: { login: "testuser" }, + default_branch: "main", + }, +]; + +const mockBranches = [ + { name: "main", commit: { sha: "abc123" } }, + { name: "develop", commit: { sha: "def456" } }, + { name: "feature/test", commit: { sha: "ghi789" } }, +]; + +// Store device flow state +let deviceFlowState = { + deviceCode: mockDeviceCode, + userCode: mockUserCode, + authorized: false, + pollCount: 0, +}; + +// GitHub Device Flow - Step 1: Get device code +export function handleDeviceCode(req: Request, res: Response) { + console.log("* GitHub Device Code requested"); + + // Reset state for new flow + deviceFlowState = { + deviceCode: mockDeviceCode, + userCode: mockUserCode, + authorized: false, + pollCount: 0, + }; + + res.json({ + device_code: mockDeviceCode, + user_code: mockUserCode, + verification_uri: "https://github.com/login/device", + verification_uri_complete: `https://github.com/login/device?user_code=${mockUserCode}`, + expires_in: 900, + interval: 1, // Short interval for testing + }); +} + +// GitHub Device Flow - Step 2: Poll for access token +export function handleAccessToken(req: Request, res: Response) { + console.log("* GitHub Access Token polling", { + pollCount: deviceFlowState.pollCount, + }); + + const { device_code } = req.body; + + if (device_code !== mockDeviceCode) { + return res.status(400).json({ + error: "invalid_request", + error_description: "Invalid device code", + }); + } + + deviceFlowState.pollCount++; + + // Simulate authorization after 3 polls (for testing) + if (deviceFlowState.pollCount >= 3) { + deviceFlowState.authorized = true; + return res.json({ + access_token: mockAccessToken, + token_type: "bearer", + scope: "repo,user,workflow", + }); + } + + // Return pending status + res.status(400).json({ + error: "authorization_pending", + error_description: "The authorization request is still pending", + }); +} + +// Get authenticated user info +export function handleUser(req: Request, res: Response) { + console.log("* GitHub User info requested"); + + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.includes(mockAccessToken)) { + return res.status(401).json({ + message: "Bad credentials", + }); + } + + res.json(mockUser); +} + +// Get user emails +export function handleUserEmails(req: Request, res: Response) { + console.log("* GitHub User emails requested"); + + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.includes(mockAccessToken)) { + return res.status(401).json({ + message: "Bad credentials", + }); + } + + res.json([ + { + email: "testuser@example.com", + primary: true, + verified: true, + visibility: "public", + }, + ]); +} + +// List user repositories +export function handleUserRepos(req: Request, res: Response) { + console.log("* GitHub User repos requested"); + + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.includes(mockAccessToken)) { + return res.status(401).json({ + message: "Bad credentials", + }); + } + + if (req.method === "GET") { + // List repos + res.json(mockRepos); + } else if (req.method === "POST") { + // Create repo + const { name, private: isPrivate } = req.body; + console.log("* Creating repository:", name); + + // Check if repo already exists + const existingRepo = mockRepos.find((repo) => repo.name === name); + if (existingRepo) { + return res.status(422).json({ + message: "Repository creation failed.", + errors: [ + { + resource: "Repository", + code: "already_exists", + field: "name", + }, + ], + }); + } + + // Create new repo + const newRepo = { + id: mockRepos.length + 1, + name, + full_name: `${mockUser.login}/${name}`, + private: !!isPrivate, + owner: { login: mockUser.login }, + default_branch: "main", + }; + + res.status(201).json(newRepo); + } +} + +// Get repository info +export function handleRepo(req: Request, res: Response) { + console.log("* GitHub Repo info requested"); + + const { owner, repo } = req.params; + const authHeader = req.headers.authorization; + + if (!authHeader || !authHeader.includes(mockAccessToken)) { + return res.status(401).json({ + message: "Bad credentials", + }); + } + + const foundRepo = mockRepos.find((r) => r.full_name === `${owner}/${repo}`); + + if (!foundRepo) { + return res.status(404).json({ + message: "Not Found", + }); + } + + res.json(foundRepo); +} + +// Get repository branches +export function handleRepoBranches(req: Request, res: Response) { + console.log("* GitHub Repo branches requested"); + + const { owner, repo } = req.params; + const authHeader = req.headers.authorization; + + if (!authHeader || !authHeader.includes(mockAccessToken)) { + return res.status(401).json({ + message: "Bad credentials", + }); + } + + const foundRepo = mockRepos.find((r) => r.full_name === `${owner}/${repo}`); + + if (!foundRepo) { + return res.status(404).json({ + message: "Not Found", + }); + } + + res.json(mockBranches); +} + +// Create repository for organization (not implemented in mock) +export function handleOrgRepos(req: Request, res: Response) { + console.log("* GitHub Org repos requested"); + + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.includes(mockAccessToken)) { + return res.status(401).json({ + message: "Bad credentials", + }); + } + + // For simplicity, just redirect to user repos for mock + handleUserRepos(req, res); +} + +// Push event management functions for testing +export function handleGetPushEvents(req: Request, res: Response) { + console.log("* Getting push events"); + const { repo } = req.query; + + const events = repo ? pushEvents.filter((e) => e.repo === repo) : pushEvents; + + res.json(events); +} + +export function handleClearPushEvents(req: Request, res: Response) { + console.log("* Clearing push events"); + pushEvents.length = 0; + res.json({ cleared: true, timestamp: new Date() }); +} + +// Handle Git operations (push, pull, clone, etc.) using git-http-mock-server +export function handleGitPush(req: Request, res: Response, next?: Function) { + console.log("* GitHub Git operation requested:", req.method, req.url); + + // Log request headers to see git operation details + console.log("* Git Headers:", { + "git-protocol": req.headers["git-protocol"], + "content-type": req.headers["content-type"], + "user-agent": req.headers["user-agent"], + }); + + // Create a unique temporary directory for this request + const mockReposRoot = fs.mkdtempSync( + path.join( + os.tmpdir(), + "dyad-git-mock-" + Math.random().toString(36).substring(2, 15), + ), + ); + console.error(`* Created temporary git repos directory: ${mockReposRoot}`); + + // Create git middleware instance for this request + const gitHttpMiddleware = gitHttpMiddlewareFactory({ + root: mockReposRoot, + route: "/github/git", + glob: "*.git", + }); + + // Extract repo name from URL path like /github/git/testuser/test-repo.git + // The middleware expects the repo name as the basename after the route + const urlPath = req.url; + const match = urlPath.match(/\/github\/git\/[^/]+\/([^/.]+)\.git/); + const repoName = match?.[1]; + + if (repoName) { + console.log(`* Git operation for repo: ${repoName}`); + + // Track push events if this is a git-receive-pack (push) operation + if (req.url.includes("/git-receive-pack") && req.method === "POST") { + console.log("* Git PUSH operation detected for repo:", repoName); + + // Collect request body to parse git protocol + let body = ""; + req.on("data", (chunk) => { + body += chunk.toString(); + }); + req.on("end", () => { + try { + // Parse git pack protocol for branch refs + // Git protocol sends refs in format: "old-sha new-sha refs/heads/branch-name" + const lines = body.split("\n"); + lines.forEach((line) => { + // Look for lines containing refs/heads/ + const refMatch = line.match( + /([0-9a-f]{40})\s+([0-9a-f]{40})\s+refs\/heads\/([^\s\u0000]+)/, + ); + if (refMatch) { + const [, oldSha, newSha, branchName] = refMatch; + const isDelete = newSha === "0".repeat(40); + const isCreate = oldSha === "0".repeat(40); + + let operation: "push" | "create" | "delete" = "push"; + if (isDelete) operation = "delete"; + else if (isCreate) operation = "create"; + + pushEvents.push({ + timestamp: new Date(), + repo: repoName, + branch: branchName, + operation, + commitSha: isDelete ? oldSha : newSha, + }); + + console.log( + `* Recorded ${operation} to ${repoName}/${branchName}, commit: ${isDelete ? oldSha : newSha}`, + ); + } + }); + } catch (error) { + console.error("* Error parsing git protocol:", error); + } + }); + } + + // Ensure the bare git repository exists for this repo + const bareRepoPath = path.join(mockReposRoot, `${repoName}.git`); + console.log(`* Creating bare git repository at: ${bareRepoPath}`); + try { + fs.mkdirSync(bareRepoPath, { recursive: true }); + // Initialize as bare repository + const { execSync } = require("child_process"); + execSync(`git init --bare`, { cwd: bareRepoPath }); + console.log( + `* Successfully created bare git repository: ${repoName}.git`, + ); + } catch (error) { + console.error(`* Failed to create bare git repository:`, error); + return res.status(500).json({ + message: "Failed to initialize git repository", + error: error instanceof Error ? error.message : String(error), + }); + } + + // Rewrite the URL to match what the middleware expects + // Change /github/git/testuser/test-repo.git/... to /github/git/test-repo.git/... + const rewrittenUrl = req.url.replace( + /\/github\/git\/[^/]+\//, + "/github/git/", + ); + req.url = rewrittenUrl; + console.log(`* Rewritten URL from ${urlPath} to ${rewrittenUrl}`); + } + + // Use git-http-mock-server middleware to handle the actual git operations + gitHttpMiddleware( + req, + res, + next || + (() => { + // Fallback if middleware doesn't handle the request + console.log( + `* Git middleware did not handle request: ${req.method} ${req.url}`, + ); + res.status(404).json({ + message: "Git operation not supported", + url: req.url, + method: req.method, + }); + }), + ); +} diff --git a/testing/fake-llm-server/index.ts b/testing/fake-llm-server/index.ts index 143fe2a..4404f4a 100644 --- a/testing/fake-llm-server/index.ts +++ b/testing/fake-llm-server/index.ts @@ -2,6 +2,19 @@ import express from "express"; import { createServer } from "http"; import cors from "cors"; import { createChatCompletionHandler } from "./chatCompletionHandler"; +import { + handleDeviceCode, + handleAccessToken, + handleUser, + handleUserEmails, + handleUserRepos, + handleRepo, + handleRepoBranches, + handleOrgRepos, + handleGitPush, + handleGetPushEvents, + handleClearPushEvents, +} from "./githubHandler"; // Create Express app const app = express(); @@ -179,6 +192,29 @@ app.get("/lmstudio/api/v0/models", (req, res) => { // Default test provider handler: app.post("/v1/chat/completions", createChatCompletionHandler(".")); +// GitHub API Mock Endpoints +console.log("Setting up GitHub mock endpoints"); + +// GitHub OAuth Device Flow +app.post("/github/login/device/code", handleDeviceCode); +app.post("/github/login/oauth/access_token", handleAccessToken); + +// GitHub API endpoints +app.get("/github/api/user", handleUser); +app.get("/github/api/user/emails", handleUserEmails); +app.get("/github/api/user/repos", handleUserRepos); +app.post("/github/api/user/repos", handleUserRepos); +app.get("/github/api/repos/:owner/:repo", handleRepo); +app.get("/github/api/repos/:owner/:repo/branches", handleRepoBranches); +app.post("/github/api/orgs/:org/repos", handleOrgRepos); + +// GitHub test endpoints for verifying push operations +app.get("/github/api/test/push-events", handleGetPushEvents); +app.post("/github/api/test/clear-push-events", handleClearPushEvents); + +// GitHub Git endpoints - intercept all paths with /github/git prefix +app.all("/github/git/*", handleGitPush); + // Start the server const server = createServer(app); server.listen(PORT, () => { diff --git a/testing/fake-llm-server/package-lock.json b/testing/fake-llm-server/package-lock.json index ff460fd..1038663 100644 --- a/testing/fake-llm-server/package-lock.json +++ b/testing/fake-llm-server/package-lock.json @@ -17,6 +17,7 @@ "@types/cors": "^2.8.18", "@types/express": "^4.17.21", "@types/node": "^20.17.46", + "git-http-mock-server": "^2.0.0", "ts-node": "^10.9.2", "typescript": "^5.8.3" } @@ -201,6 +202,42 @@ "node": ">=0.4.0" } }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/apache-crypt": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/apache-crypt/-/apache-crypt-1.2.6.tgz", + "integrity": "sha512-072WetlM4blL8PREJVeY+WHiUh1R5VNt2HfceGS8aKqttPHcmqE5pkKuXPz/ULmJOFkc8Hw3kfKl6vy7Qka6DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "unix-crypt-td-js": "^1.1.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/apache-md5": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/apache-md5/-/apache-md5-1.1.8.tgz", + "integrity": "sha512-FCAJojipPn0bXjuEpjOOOMN8FZDkxfWWp4JGN9mifU2IhxvKyXZYqpzPHdnTSUpmPDy+tsslB6Z1g+Vg6nVbYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/arg": { "version": "4.1.3", "dev": true, @@ -210,6 +247,73 @@ "version": "1.1.1", "license": "MIT" }, + "node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "dev": true, + "license": "MIT" + }, "node_modules/body-parser": { "version": "1.20.3", "license": "MIT", @@ -232,6 +336,24 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/bytes": { "version": "3.1.2", "license": "MIT", @@ -264,6 +386,45 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/content-disposition": { "version": "0.5.4", "license": "MIT", @@ -308,6 +469,26 @@ "dev": true, "license": "MIT" }, + "node_modules/crypto-random-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", + "integrity": "sha512-GsVpkFPlycH7/fRR7Dhcmnoii54gV1nz7y4CWyeFS14N+JVBBhY+r8amRHE4BwSYal7BPTDp8isvAlCxyFt3Hg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/daemonize-process": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/daemonize-process/-/daemonize-process-1.0.9.tgz", + "integrity": "sha512-YoB+AmcgHIBDVeyfVWSCV90FNk799zX8Uvn7RJTDCD8Y0EMNbSfIKLG961VgchJme2GHmqpXUuV8Rxe2j2L+bw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4" + } + }, "node_modules/debug": { "version": "2.6.9", "license": "MIT", @@ -338,6 +519,19 @@ "node": ">=0.3.1" } }, + "node_modules/dir-glob": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", + "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "license": "MIT", @@ -395,6 +589,16 @@ "version": "1.0.3", "license": "MIT" }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/etag": { "version": "1.8.1", "license": "MIT", @@ -462,6 +666,19 @@ "node": ">= 0.8" } }, + "node_modules/fixturez": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fixturez/-/fixturez-1.1.0.tgz", + "integrity": "sha512-c4q9eZsAmCzj9gkrEO/YwIRlrHWt/TXQiX9jR9WeLFOqeeV6EyzdiiV28CpSzF6Ip+gyYrSv5UeOHqyzfcNTVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-extra": "^5.0.0", + "globby": "^7.1.1", + "signal-exit": "^3.0.2", + "tempy": "^0.2.1" + } + }, "node_modules/forwarded": { "version": "0.2.0", "license": "MIT", @@ -476,6 +693,25 @@ "node": ">= 0.6" } }, + "node_modules/fs-extra": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", + "integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/function-bind": { "version": "1.1.2", "license": "MIT", @@ -516,6 +752,70 @@ "node": ">= 0.4" } }, + "node_modules/git-http-mock-server": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/git-http-mock-server/-/git-http-mock-server-2.0.0.tgz", + "integrity": "sha512-LOCls7jjuzwfKmUbcFsqj2yIEqExBzv0rA1tL7j1ULhRLAax4U1Bd/rbU9ebtri1ldzgcPD1VAyuhS1pvDC2pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-auth": "^2.0.0", + "buffer-equal-constant-time": "^1.0.1", + "chalk": "^2.4.1", + "daemonize-process": "^1.0.9", + "fixturez": "^1.1.0", + "htpasswd-js": "^1.0.2", + "micro-cors": "^0.1.1", + "minimisted": "^2.0.0", + "ssh-keygen": "^0.4.2", + "ssh2": "^0.6.1", + "tree-kill": "^1.2.0" + }, + "bin": { + "git-http-mock-server": "http-daemon.js", + "git-ssh-mock-server": "ssh-daemon.js" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globby": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", + "integrity": "sha512-yANWAN2DUcBtuus5Cpd+SKROzXHs2iVXFZt/Ykrfz6SAXqacLX25NZpltE+39ceMexYF4TtEadjuSTw8+3wX4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^1.0.1", + "dir-glob": "^2.0.0", + "glob": "^7.1.2", + "ignore": "^3.3.5", + "pify": "^3.0.0", + "slash": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/gopd": { "version": "1.2.0", "license": "MIT", @@ -526,6 +826,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "license": "MIT", @@ -546,6 +863,32 @@ "node": ">= 0.4" } }, + "node_modules/htpasswd-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/htpasswd-js/-/htpasswd-js-1.0.2.tgz", + "integrity": "sha512-KON5L4YKYXk647tmVclKgmHHG5nApjy9K+WiRoScnoWhS63lMoTca1ommUW2XQ3FDW8TtNDIQA7J0WYXICbMAA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "apache-crypt": "^1.2.1", + "apache-md5": "^1.1.2", + "bcryptjs": "^2.4.3", + "fs-extra": "^4.0.2", + "xerror": "^1.1.2" + } + }, + "node_modules/htpasswd-js/node_modules/fs-extra": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, "node_modules/http-errors": { "version": "2.0.0", "license": "MIT", @@ -570,6 +913,25 @@ "node": ">=0.10.0" } }, + "node_modules/ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true, + "license": "MIT" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "license": "ISC" @@ -581,6 +943,16 @@ "node": ">= 0.10" } }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/make-error": { "version": "1.3.6", "dev": true, @@ -614,6 +986,16 @@ "node": ">= 0.6" } }, + "node_modules/micro-cors": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/micro-cors/-/micro-cors-0.1.1.tgz", + "integrity": "sha512-6WqIahA5sbQR1Gjexp1VuWGFDKbZZleJb/gy1khNGk18a6iN1FdTcr3Q8twaxkV5H94RjxIBjirYbWCehpMBFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/mime": { "version": "1.6.0", "license": "MIT", @@ -641,6 +1023,39 @@ "node": ">= 0.6" } }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimisted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz", + "integrity": "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5" + } + }, "node_modules/ms": { "version": "2.0.0", "license": "MIT" @@ -679,6 +1094,16 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "license": "MIT", @@ -686,10 +1111,43 @@ "node": ">= 0.8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-to-regexp": { "version": "0.1.12", "license": "MIT" }, + "node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "license": "MIT", @@ -756,6 +1214,16 @@ "version": "2.1.2", "license": "MIT" }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/send": { "version": "0.19.0", "license": "MIT", @@ -870,6 +1338,61 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ssh-keygen": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/ssh-keygen/-/ssh-keygen-0.4.2.tgz", + "integrity": "sha512-SlEWW3cCtz87jwtCTfxo+tR+SQd4jJXWaBI/D9JVd74b2/N9ZvrWcd9lMFwFv0iMYb4aVAeMderH4AK5ZyW+Nw==", + "dev": true, + "dependencies": { + "underscore": "1.4.x" + }, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/ssh2": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.6.2.tgz", + "integrity": "sha512-DJ+dOhXEEsmNpcQTI0x69FS++JH6qqL/ltEHf01pI1SSLMAcmD+hL4jRwvHjPwynPsmSUbHJ/WIZYzROfqZWjA==", + "dev": true, + "dependencies": { + "ssh2-streams": "~0.2.0" + }, + "engines": { + "node": ">=4.5.0" + } + }, + "node_modules/ssh2-streams": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.2.1.tgz", + "integrity": "sha512-3zCOsmunh1JWgPshfhKmBCL3lUtHPoh+a/cyQ49Ft0Q0aF7xgN06b76L+oKtFi0fgO57FLjFztb1GlJcEZ4a3Q==", + "dev": true, + "dependencies": { + "asn1": "~0.2.0", + "semver": "^5.1.0", + "streamsearch": "~0.1.2" + }, + "engines": { + "node": ">=4.5.0" + } + }, "node_modules/statuses": { "version": "2.0.1", "license": "MIT", @@ -884,6 +1407,52 @@ "emitter-component": "^1.1.1" } }, + "node_modules/streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/temp-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", + "integrity": "sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tempy": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.2.1.tgz", + "integrity": "sha512-LB83o9bfZGrntdqPuRdanIVCPReam9SOZKW0fOy5I9X3A854GGWi0tjCqoXEk84XIEYBc/x9Hq3EFop/H5wJaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "temp-dir": "^1.0.0", + "unique-string": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "license": "MIT", @@ -891,6 +1460,16 @@ "node": ">=0.6" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/ts-node": { "version": "10.9.2", "dev": true, @@ -956,11 +1535,47 @@ "node": ">=14.17" } }, + "node_modules/underscore": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz", + "integrity": "sha512-ZqGrAgaqqZM7LGRzNjLnw5elevWb5M8LEoDMadxIW3OWbcv72wMMgKdwOKpd5Fqxe8choLD8HN3iSj3TUh/giQ==", + "dev": true + }, "node_modules/undici-types": { "version": "6.19.8", "dev": true, "license": "MIT" }, + "node_modules/unique-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha512-ODgiYu03y5g76A1I9Gt0/chLCzQjvzDy7DsZGsLOE/1MrF6wriEskSncj1+/C58Xk/kPZDppSctDybCwOSaGAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crypto-random-string": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unix-crypt-td-js": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/unix-crypt-td-js/-/unix-crypt-td-js-1.1.4.tgz", + "integrity": "sha512-8rMeVYWSIyccIJscb9NdCfZKSRBKYTeVnwmiRYT2ulE3qd1RaDQ0xQDP+rI3ccIWbhu/zuo5cgN8z73belNZgw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/unpipe": { "version": "1.0.0", "license": "MIT", @@ -987,6 +1602,20 @@ "node": ">= 0.8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xerror": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/xerror/-/xerror-1.1.3.tgz", + "integrity": "sha512-2l5hmDymDUIuKT53v/nYxofTMUDQuu5P/Y3qHOjQiih6QUHBCgWpbpL3I8BoE5TVfUVTMmUQ0jdUAimTGc9UIg==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/yn": { "version": "3.1.1", "dev": true, diff --git a/testing/fake-llm-server/package.json b/testing/fake-llm-server/package.json index ab7cc63..60fca9d 100644 --- a/testing/fake-llm-server/package.json +++ b/testing/fake-llm-server/package.json @@ -21,6 +21,7 @@ "@types/cors": "^2.8.18", "@types/express": "^4.17.21", "@types/node": "^20.17.46", + "git-http-mock-server": "^2.0.0", "ts-node": "^10.9.2", "typescript": "^5.8.3" }