GitHub workflows (#428)
Fixes #348 Fixes #274 Fixes #149 - Connect to existing repos - Push to other branches on GitHub besides main - Allows force push (with confirmation) dialog --------- Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
This commit is contained in:
1
drizzle/0007_dapper_overlord.sql
Normal file
1
drizzle/0007_dapper_overlord.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `apps` ADD `github_branch` text;
|
||||||
384
drizzle/meta/0007_snapshot.json
Normal file
384
drizzle/meta/0007_snapshot.json
Normal file
@@ -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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,6 +50,13 @@
|
|||||||
"when": 1749515724373,
|
"when": 1749515724373,
|
||||||
"tag": "0006_mushy_squirrel_girl",
|
"tag": "0006_mushy_squirrel_girl",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 7,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1750186036000,
|
||||||
|
"tag": "0007_dapper_overlord",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
145
e2e-tests/github.spec.ts
Normal file
145
e2e-tests/github.spec.ts
Normal file
@@ -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",
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 {
|
export class PageObject {
|
||||||
private userDataDir: string;
|
private userDataDir: string;
|
||||||
|
public githubConnector: GitHubConnector;
|
||||||
constructor(
|
constructor(
|
||||||
public electronApp: ElectronApplication,
|
public electronApp: ElectronApplication,
|
||||||
public page: Page,
|
public page: Page,
|
||||||
{ userDataDir }: { userDataDir: string },
|
{ userDataDir }: { userDataDir: string },
|
||||||
) {
|
) {
|
||||||
this.userDataDir = userDataDir;
|
this.userDataDir = userDataDir;
|
||||||
|
this.githubConnector = new GitHubConnector(this.page);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async baseSetup() {
|
||||||
|
await this.githubConnector.clearPushEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
async setUp({
|
async setUp({
|
||||||
autoApprove = false,
|
autoApprove = false,
|
||||||
nativeGit = false,
|
nativeGit = false,
|
||||||
}: { autoApprove?: boolean; nativeGit?: boolean } = {}) {
|
}: { autoApprove?: boolean; nativeGit?: boolean } = {}) {
|
||||||
|
await this.baseSetup();
|
||||||
await this.goToSettingsTab();
|
await this.goToSettingsTab();
|
||||||
if (autoApprove) {
|
if (autoApprove) {
|
||||||
await this.toggleAutoApprove();
|
await this.toggleAutoApprove();
|
||||||
@@ -93,16 +221,8 @@ export class PageObject {
|
|||||||
await this.selectTestModel();
|
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 } = {}) {
|
async setUpDyadPro({ autoApprove = false }: { autoApprove?: boolean } = {}) {
|
||||||
|
await this.baseSetup();
|
||||||
await this.goToSettingsTab();
|
await this.goToSettingsTab();
|
||||||
if (autoApprove) {
|
if (autoApprove) {
|
||||||
await this.toggleAutoApprove();
|
await this.toggleAutoApprove();
|
||||||
@@ -112,7 +232,6 @@ export class PageObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setUpDyadProvider() {
|
async setUpDyadProvider() {
|
||||||
// await page.getByRole('link', { name: 'Settings' }).click();
|
|
||||||
await this.page
|
await this.page
|
||||||
.locator("div")
|
.locator("div")
|
||||||
.filter({ hasText: /^DyadNeeds Setup$/ })
|
.filter({ hasText: /^DyadNeeds Setup$/ })
|
||||||
@@ -123,12 +242,15 @@ export class PageObject {
|
|||||||
.getByRole("textbox", { name: "Set Dyad API Key" })
|
.getByRole("textbox", { name: "Set Dyad API Key" })
|
||||||
.fill("testdyadkey");
|
.fill("testdyadkey");
|
||||||
await this.page.getByRole("button", { name: "Save Key" }).click();
|
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();
|
async importApp(appDir: string) {
|
||||||
// await page.getByRole('switch', { name: 'Turbo Edits' }).click();
|
await this.page.getByRole("button", { name: "Import App" }).click();
|
||||||
// await page.locator('div').filter({ hasText: /^Import App$/ }).click();
|
await eph.stubDialog(this.electronApp, "showOpenDialog", {
|
||||||
// await page.getByRole('button', { name: 'Select Folder' }).press('Escape');
|
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() {
|
async openContextFilesPicker() {
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
- paragraph: "Connected to GitHub Repo:"
|
||||||
|
- text: testuser/existing-app
|
||||||
|
- paragraph: "Branch: main"
|
||||||
|
- button "Sync to GitHub"
|
||||||
|
- button "Disconnect from repo"
|
||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -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!
|
||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -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!
|
||||||
@@ -1,19 +1,281 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
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 { IpcClient } from "@/ipc/ipc_client";
|
||||||
import { useSettings } from "@/hooks/useSettings";
|
import { useSettings } from "@/hooks/useSettings";
|
||||||
import { useLoadApp } from "@/hooks/useLoadApp";
|
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 {
|
interface GitHubConnectorProps {
|
||||||
appId: number | null;
|
appId: number | null;
|
||||||
folderName: string;
|
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<string | null>(null);
|
||||||
|
const [syncSuccess, setSyncSuccess] = useState<boolean>(false);
|
||||||
|
const [showForceDialog, setShowForceDialog] = useState(false);
|
||||||
|
const [isDisconnecting, setIsDisconnecting] = useState(false);
|
||||||
|
const [disconnectError, setDisconnectError] = useState<string | null>(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 (
|
||||||
|
<div
|
||||||
|
className="mt-4 w-full border border-gray-200 rounded-md p-4"
|
||||||
|
data-testid="github-connected-repo"
|
||||||
|
>
|
||||||
|
<p>Connected to GitHub Repo:</p>
|
||||||
|
<a
|
||||||
|
onClick={(e) => {
|
||||||
|
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}
|
||||||
|
</a>
|
||||||
|
{app.githubBranch && (
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-300 mt-1">
|
||||||
|
Branch: <span className="font-mono">{app.githubBranch}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="mt-2 flex gap-2">
|
||||||
|
<Button onClick={() => handleSyncToGithub(false)} disabled={isSyncing}>
|
||||||
|
{isSyncing ? (
|
||||||
|
<>
|
||||||
|
<svg
|
||||||
|
className="animate-spin h-5 w-5 mr-2 inline"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
style={{ display: "inline" }}
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
Syncing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Sync to GitHub"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleDisconnectRepo}
|
||||||
|
disabled={isDisconnecting}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{isDisconnecting ? "Disconnecting..." : "Disconnect from repo"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{syncError && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-red-600">
|
||||||
|
{syncError}{" "}
|
||||||
|
<a
|
||||||
|
onClick={(e) => {
|
||||||
|
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
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
{(syncError.includes("rejected") ||
|
||||||
|
syncError.includes("non-fast-forward")) && (
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowForceDialog(true)}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="mt-2 text-orange-600 border-orange-600 hover:bg-orange-50"
|
||||||
|
>
|
||||||
|
<AlertTriangle className="h-4 w-4 mr-2" />
|
||||||
|
Force Push (Dangerous)
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{syncSuccess && (
|
||||||
|
<p className="text-green-600 mt-2">Successfully pushed to GitHub!</p>
|
||||||
|
)}
|
||||||
|
{disconnectError && (
|
||||||
|
<p className="text-red-600 mt-2">{disconnectError}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Force Push Warning Dialog */}
|
||||||
|
<Dialog open={showForceDialog} onOpenChange={setShowForceDialog}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<AlertTriangle className="h-5 w-5 text-orange-500" />
|
||||||
|
Force Push Warning
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p>
|
||||||
|
You are about to perform a <strong>force push</strong> to your
|
||||||
|
GitHub repository.
|
||||||
|
</p>
|
||||||
|
<div className="bg-orange-50 dark:bg-orange-900/20 p-3 rounded-md border border-orange-200 dark:border-orange-800">
|
||||||
|
<p className="text-sm text-orange-800 dark:text-orange-200">
|
||||||
|
<strong>
|
||||||
|
This is dangerous and non-reversible and will:
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
<ul className="text-sm text-orange-700 dark:text-orange-300 list-disc list-inside mt-2 space-y-1">
|
||||||
|
<li>Overwrite the remote repository history</li>
|
||||||
|
<li>
|
||||||
|
Permanently delete commits that exist on the remote but
|
||||||
|
not locally
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm">
|
||||||
|
Only proceed if you're certain this is what you want to do.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setShowForceDialog(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => handleSyncToGithub(true)}
|
||||||
|
disabled={isSyncing}
|
||||||
|
>
|
||||||
|
{isSyncing ? "Force Pushing..." : "Force Push"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UnconnectedGitHubConnector({
|
||||||
|
appId,
|
||||||
|
folderName,
|
||||||
|
settings,
|
||||||
|
refreshSettings,
|
||||||
|
refreshApp,
|
||||||
|
}: UnconnectedGitHubConnectorProps) {
|
||||||
|
// --- Collapsible State ---
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
// --- GitHub Device Flow State ---
|
// --- GitHub Device Flow State ---
|
||||||
const { app, refreshApp } = useLoadApp(appId);
|
|
||||||
const { settings, refreshSettings } = useSettings();
|
|
||||||
const [githubUserCode, setGithubUserCode] = useState<string | null>(null);
|
const [githubUserCode, setGithubUserCode] = useState<string | null>(null);
|
||||||
const [githubVerificationUri, setGithubVerificationUri] = useState<
|
const [githubVerificationUri, setGithubVerificationUri] = useState<
|
||||||
string | null
|
string | null
|
||||||
@@ -24,7 +286,37 @@ export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) {
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [codeCopied, setCodeCopied] = useState(false);
|
const [codeCopied, setCodeCopied] = useState(false);
|
||||||
// --- ---
|
|
||||||
|
// --- Repo Setup State ---
|
||||||
|
const [repoSetupMode, setRepoSetupMode] = useState<"create" | "existing">(
|
||||||
|
"create",
|
||||||
|
);
|
||||||
|
const [availableRepos, setAvailableRepos] = useState<GitHubRepo[]>([]);
|
||||||
|
const [isLoadingRepos, setIsLoadingRepos] = useState(false);
|
||||||
|
const [selectedRepo, setSelectedRepo] = useState<string>("");
|
||||||
|
const [availableBranches, setAvailableBranches] = useState<GitHubBranch[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const [isLoadingBranches, setIsLoadingBranches] = useState(false);
|
||||||
|
const [selectedBranch, setSelectedBranch] = useState<string>("main");
|
||||||
|
const [branchInputMode, setBranchInputMode] = useState<"select" | "custom">(
|
||||||
|
"select",
|
||||||
|
);
|
||||||
|
const [customBranchName, setCustomBranchName] = useState<string>("");
|
||||||
|
|
||||||
|
// Create new repo state
|
||||||
|
const [repoName, setRepoName] = useState(folderName);
|
||||||
|
const [repoAvailable, setRepoAvailable] = useState<boolean | null>(null);
|
||||||
|
const [repoCheckError, setRepoCheckError] = useState<string | null>(null);
|
||||||
|
const [isCheckingRepo, setIsCheckingRepo] = useState(false);
|
||||||
|
const [isCreatingRepo, setIsCreatingRepo] = useState(false);
|
||||||
|
const [createRepoError, setCreateRepoError] = useState<string | null>(null);
|
||||||
|
const [createRepoSuccess, setCreateRepoSuccess] = useState<boolean>(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<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const handleConnectToGithub = async () => {
|
const handleConnectToGithub = async () => {
|
||||||
if (!appId) return;
|
if (!appId) return;
|
||||||
@@ -78,7 +370,7 @@ export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) {
|
|||||||
setGithubError(null);
|
setGithubError(null);
|
||||||
setIsConnectingToGithub(false);
|
setIsConnectingToGithub(false);
|
||||||
refreshSettings();
|
refreshSettings();
|
||||||
// TODO: Maybe update parent UI to show "Connected" state or trigger next action
|
setIsExpanded(true);
|
||||||
});
|
});
|
||||||
cleanupFunctions.push(removeSuccessListener);
|
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
|
// Cleanup function to remove all listeners when component unmounts or appId changes
|
||||||
return () => {
|
return () => {
|
||||||
cleanupFunctions.forEach((cleanup) => cleanup());
|
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
|
// Reset state when appId changes or component unmounts
|
||||||
setGithubUserCode(null);
|
setGithubUserCode(null);
|
||||||
setGithubVerificationUri(null);
|
setGithubVerificationUri(null);
|
||||||
@@ -110,23 +399,58 @@ export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) {
|
|||||||
};
|
};
|
||||||
}, [appId]); // Re-run effect if appId changes
|
}, [appId]); // Re-run effect if appId changes
|
||||||
|
|
||||||
// --- Create Repo State ---
|
// Load available repos when GitHub is connected
|
||||||
const [repoName, setRepoName] = useState(folderName);
|
useEffect(() => {
|
||||||
const [repoAvailable, setRepoAvailable] = useState<boolean | null>(null);
|
if (settings?.githubAccessToken && repoSetupMode === "existing") {
|
||||||
const [repoCheckError, setRepoCheckError] = useState<string | null>(null);
|
loadAvailableRepos();
|
||||||
const [isCheckingRepo, setIsCheckingRepo] = useState(false);
|
}
|
||||||
const [isCreatingRepo, setIsCreatingRepo] = useState(false);
|
}, [settings?.githubAccessToken, repoSetupMode]);
|
||||||
const [createRepoError, setCreateRepoError] = useState<string | null>(null);
|
|
||||||
const [createRepoSuccess, setCreateRepoSuccess] = useState<boolean>(false);
|
|
||||||
// --- Sync to GitHub State ---
|
|
||||||
const [isSyncing, setIsSyncing] = useState(false);
|
|
||||||
const [syncError, setSyncError] = useState<string | null>(null);
|
|
||||||
const [syncSuccess, setSyncSuccess] = useState<boolean>(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)
|
|
||||||
|
|
||||||
const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(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(
|
const checkRepoAvailability = useCallback(
|
||||||
async (name: string) => {
|
async (name: string) => {
|
||||||
@@ -166,48 +490,49 @@ export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) {
|
|||||||
[checkRepoAvailability],
|
[checkRepoAvailability],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCreateRepo = async (e: React.FormEvent) => {
|
const handleSetupRepo = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (!appId) return;
|
||||||
|
|
||||||
setCreateRepoError(null);
|
setCreateRepoError(null);
|
||||||
setIsCreatingRepo(true);
|
setIsCreatingRepo(true);
|
||||||
setCreateRepoSuccess(false);
|
setCreateRepoSuccess(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (repoSetupMode === "create") {
|
||||||
await IpcClient.getInstance().createGithubRepo(
|
await IpcClient.getInstance().createGithubRepo(
|
||||||
githubOrg,
|
githubOrg,
|
||||||
repoName,
|
repoName,
|
||||||
appId!,
|
appId,
|
||||||
|
selectedBranch,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
const [owner, repo] = selectedRepo.split("/");
|
||||||
|
const branchToUse =
|
||||||
|
branchInputMode === "custom" ? customBranchName : selectedBranch;
|
||||||
|
await IpcClient.getInstance().connectToExistingGithubRepo(
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
branchToUse,
|
||||||
|
appId,
|
||||||
|
);
|
||||||
|
}
|
||||||
setCreateRepoSuccess(true);
|
setCreateRepoSuccess(true);
|
||||||
setRepoCheckError(null);
|
setRepoCheckError(null);
|
||||||
refreshApp();
|
refreshApp();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setCreateRepoError(err.message || "Failed to create repository.");
|
setCreateRepoError(
|
||||||
|
err.message ||
|
||||||
|
`Failed to ${repoSetupMode === "create" ? "create" : "connect to"} repository.`,
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsCreatingRepo(false);
|
setIsCreatingRepo(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const [isDisconnecting, setIsDisconnecting] = useState(false);
|
|
||||||
const [disconnectError, setDisconnectError] = useState<string | null>(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) {
|
if (!settings?.githubAccessToken) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-1 w-full">
|
<div className="mt-1 w-full" data-testid="github-unconnected-repo">
|
||||||
{" "}
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleConnectToGithub}
|
onClick={handleConnectToGithub}
|
||||||
className="cursor-pointer w-full py-5 flex justify-center items-center gap-2"
|
className="cursor-pointer w-full py-5 flex justify-center items-center gap-2"
|
||||||
@@ -310,114 +635,82 @@ export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (app?.githubOrg && app?.githubRepo) {
|
|
||||||
const handleSyncToGithub = async () => {
|
|
||||||
setIsSyncing(true);
|
|
||||||
setSyncError(null);
|
|
||||||
setSyncSuccess(false);
|
|
||||||
try {
|
|
||||||
const result = await IpcClient.getInstance().syncGithubRepo(appId!);
|
|
||||||
if (result.success) {
|
|
||||||
setSyncSuccess(true);
|
|
||||||
} else {
|
|
||||||
setSyncError(result.error || "Failed to sync to GitHub.");
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
setSyncError(err.message || "Failed to sync to GitHub.");
|
|
||||||
} finally {
|
|
||||||
setIsSyncing(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 w-full border border-gray-200 rounded-md p-4">
|
<div
|
||||||
<p>Connected to GitHub Repo:</p>
|
className="mt-4 w-full border border-gray-200 rounded-md"
|
||||||
<a
|
data-testid="github-setup-repo"
|
||||||
onClick={(e) => {
|
|
||||||
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}
|
{/* Collapsible Header */}
|
||||||
</a>
|
<button
|
||||||
<div className="mt-2 flex gap-2">
|
type="button"
|
||||||
<Button onClick={handleSyncToGithub} disabled={isSyncing}>
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
{isSyncing ? (
|
className={`cursor-pointer w-full p-4 text-left transition-colors rounded-md flex items-center justify-between ${
|
||||||
<>
|
!isExpanded ? "hover:bg-gray-50 dark:hover:bg-gray-800/50" : ""
|
||||||
<svg
|
}`}
|
||||||
className="animate-spin h-5 w-5 mr-2 inline"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
style={{ display: "inline" }}
|
|
||||||
>
|
>
|
||||||
<circle
|
<span className="font-medium">Set up your GitHub repo</span>
|
||||||
className="opacity-25"
|
{isExpanded ? (
|
||||||
cx="12"
|
<ChevronDown className="h-4 w-4 text-gray-500" />
|
||||||
cy="12"
|
|
||||||
r="10"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="4"
|
|
||||||
></circle>
|
|
||||||
<path
|
|
||||||
className="opacity-75"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
Syncing...
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
"Sync to GitHub"
|
<ChevronRight className="h-4 w-4 text-gray-500" />
|
||||||
)}
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Collapsible Content */}
|
||||||
|
<div
|
||||||
|
className={`overflow-hidden transition-all duration-300 ease-in-out ${
|
||||||
|
isExpanded ? "max-h-[800px] opacity-100" : "max-h-0 opacity-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="p-4 pt-0 space-y-4">
|
||||||
|
{/* Mode Selection */}
|
||||||
|
<div>
|
||||||
|
<div className="flex rounded-md border border-gray-200 dark:border-gray-700">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={repoSetupMode === "create" ? "default" : "ghost"}
|
||||||
|
className={`flex-1 rounded-none rounded-l-md border-0 ${
|
||||||
|
repoSetupMode === "create"
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
setRepoSetupMode("create");
|
||||||
|
setCreateRepoError(null);
|
||||||
|
setCreateRepoSuccess(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create new repo
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleDisconnectRepo}
|
type="button"
|
||||||
disabled={isDisconnecting}
|
variant={repoSetupMode === "existing" ? "default" : "ghost"}
|
||||||
variant="outline"
|
className={`flex-1 rounded-none rounded-r-md border-0 border-l border-gray-200 dark:border-gray-700 ${
|
||||||
|
repoSetupMode === "existing"
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
setRepoSetupMode("existing");
|
||||||
|
setCreateRepoError(null);
|
||||||
|
setCreateRepoSuccess(false);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{isDisconnecting ? "Disconnecting..." : "Disconnect from repo"}
|
Connect to existing repo
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{syncError && (
|
|
||||||
<p className="text-red-600 mt-2">
|
|
||||||
{syncError}{" "}
|
|
||||||
<a
|
|
||||||
onClick={(e) => {
|
|
||||||
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
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{syncSuccess && (
|
|
||||||
<p className="text-green-600 mt-2">Successfully pushed to GitHub!</p>
|
|
||||||
)}
|
|
||||||
{disconnectError && (
|
|
||||||
<p className="text-red-600 mt-2">{disconnectError}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
} else {
|
<form className="space-y-4" onSubmit={handleSetupRepo}>
|
||||||
return (
|
{repoSetupMode === "create" ? (
|
||||||
<div className="mt-4 w-full border border-gray-200 rounded-md p-4">
|
<>
|
||||||
<p>Set up your GitHub repo</p>
|
<div>
|
||||||
<form className="mt-4 space-y-2" onSubmit={handleCreateRepo}>
|
<Label className="block text-sm font-medium">
|
||||||
<label className="block text-sm font-medium">Repository Name</label>
|
Repository Name
|
||||||
<input
|
</Label>
|
||||||
className="w-full border rounded px-2 py-1"
|
<Input
|
||||||
|
data-testid="github-create-repo-name-input"
|
||||||
|
className="w-full mt-1"
|
||||||
value={repoName}
|
value={repoName}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newValue = e.target.value;
|
const newValue = e.target.value;
|
||||||
@@ -429,30 +722,184 @@ export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) {
|
|||||||
disabled={isCreatingRepo}
|
disabled={isCreatingRepo}
|
||||||
/>
|
/>
|
||||||
{isCheckingRepo && (
|
{isCheckingRepo && (
|
||||||
<p className="text-xs text-gray-500">Checking availability...</p>
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Checking availability...
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
{repoAvailable === true && (
|
{repoAvailable === true && (
|
||||||
<p className="text-xs text-green-600">
|
<p className="text-xs text-green-600 mt-1">
|
||||||
Repository name is available!
|
Repository name is available!
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{repoAvailable === false && (
|
{repoAvailable === false && (
|
||||||
<p className="text-xs text-red-600">{repoCheckError}</p>
|
<p className="text-xs text-red-600 mt-1">
|
||||||
|
{repoCheckError}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Label className="block text-sm font-medium">
|
||||||
|
Select Repository
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedRepo}
|
||||||
|
onValueChange={setSelectedRepo}
|
||||||
|
disabled={isLoadingRepos}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
className="w-full mt-1"
|
||||||
|
data-testid="github-repo-select"
|
||||||
|
>
|
||||||
|
<SelectValue
|
||||||
|
placeholder={
|
||||||
|
isLoadingRepos
|
||||||
|
? "Loading repositories..."
|
||||||
|
: "Select a repository"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{availableRepos.map((repo) => (
|
||||||
|
<SelectItem key={repo.full_name} value={repo.full_name}>
|
||||||
|
{repo.full_name} {repo.private && "(private)"}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Branch Selection */}
|
||||||
|
<div>
|
||||||
|
<Label className="block text-sm font-medium">Branch</Label>
|
||||||
|
{repoSetupMode === "existing" && selectedRepo ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Select
|
||||||
|
value={
|
||||||
|
branchInputMode === "select" ? selectedBranch : "custom"
|
||||||
|
}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
if (value === "custom") {
|
||||||
|
setBranchInputMode("custom");
|
||||||
|
setCustomBranchName("");
|
||||||
|
} else {
|
||||||
|
setBranchInputMode("select");
|
||||||
|
setSelectedBranch(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={isLoadingBranches}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
className="w-full mt-1"
|
||||||
|
data-testid="github-branch-select"
|
||||||
|
>
|
||||||
|
<SelectValue
|
||||||
|
placeholder={
|
||||||
|
isLoadingBranches
|
||||||
|
? "Loading branches..."
|
||||||
|
: "Select a branch"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{availableBranches.map((branch) => (
|
||||||
|
<SelectItem key={branch.name} value={branch.name}>
|
||||||
|
{branch.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectItem value="custom">
|
||||||
|
<span className="font-medium">
|
||||||
|
✏️ Type custom branch name
|
||||||
|
</span>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{branchInputMode === "custom" && (
|
||||||
|
<Input
|
||||||
|
data-testid="github-custom-branch-input"
|
||||||
|
className="w-full"
|
||||||
|
value={customBranchName}
|
||||||
|
onChange={(e) => setCustomBranchName(e.target.value)}
|
||||||
|
placeholder="Enter branch name (e.g., feature/new-feature)"
|
||||||
|
disabled={isCreatingRepo}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
className="w-full mt-1"
|
||||||
|
value={selectedBranch}
|
||||||
|
onChange={(e) => setSelectedBranch(e.target.value)}
|
||||||
|
placeholder="main"
|
||||||
|
disabled={isCreatingRepo}
|
||||||
|
data-testid="github-new-repo-branch-input"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isCreatingRepo || repoAvailable === false || !repoName}
|
disabled={
|
||||||
|
isCreatingRepo ||
|
||||||
|
(repoSetupMode === "create" &&
|
||||||
|
(repoAvailable === false || !repoName)) ||
|
||||||
|
(repoSetupMode === "existing" &&
|
||||||
|
(!selectedRepo ||
|
||||||
|
!selectedBranch ||
|
||||||
|
(branchInputMode === "custom" && !customBranchName.trim())))
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{isCreatingRepo ? "Creating..." : "Create Repo"}
|
{isCreatingRepo
|
||||||
|
? repoSetupMode === "create"
|
||||||
|
? "Creating..."
|
||||||
|
: "Connecting..."
|
||||||
|
: repoSetupMode === "create"
|
||||||
|
? "Create Repo"
|
||||||
|
: "Connect to Repo"}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{createRepoError && (
|
{createRepoError && (
|
||||||
<p className="text-red-600 mt-2">{createRepoError}</p>
|
<p className="text-red-600 mt-2">{createRepoError}</p>
|
||||||
)}
|
)}
|
||||||
{createRepoSuccess && (
|
{createRepoSuccess && (
|
||||||
<p className="text-green-600 mt-2">Repository created and linked!</p>
|
<p className="text-green-600 mt-2">
|
||||||
|
{repoSetupMode === "create"
|
||||||
|
? "Repository created and linked!"
|
||||||
|
: "Connected to repository!"}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) {
|
||||||
|
const { app, refreshApp } = useLoadApp(appId);
|
||||||
|
const { settings, refreshSettings } = useSettings();
|
||||||
|
|
||||||
|
if (app?.githubOrg && app?.githubRepo && appId) {
|
||||||
|
return (
|
||||||
|
<ConnectedGitHubConnector
|
||||||
|
appId={appId}
|
||||||
|
app={app}
|
||||||
|
refreshApp={refreshApp}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<UnconnectedGitHubConnector
|
||||||
|
appId={appId}
|
||||||
|
folderName={folderName}
|
||||||
|
settings={settings}
|
||||||
|
refreshSettings={refreshSettings}
|
||||||
|
refreshApp={refreshApp}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { getDyadAppPath, getUserDataPath } from "../paths/paths";
|
import { getDyadAppPath, getUserDataPath } from "../paths/paths";
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import log from "electron-log";
|
import log from "electron-log";
|
||||||
|
|
||||||
const logger = log.scope("db");
|
const logger = log.scope("db");
|
||||||
@@ -87,14 +87,3 @@ try {
|
|||||||
export const db = _db as any as BetterSQLite3Database<typeof schema> & {
|
export const db = _db as any as BetterSQLite3Database<typeof schema> & {
|
||||||
$client: Database.Database;
|
$client: Database.Database;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function updateAppGithubRepo(
|
|
||||||
appId: number,
|
|
||||||
org: string,
|
|
||||||
repo: string,
|
|
||||||
): Promise<void> {
|
|
||||||
await db
|
|
||||||
.update(schema.apps)
|
|
||||||
.set({ githubOrg: org, githubRepo: repo })
|
|
||||||
.where(eq(schema.apps.id, appId));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const apps = sqliteTable("apps", {
|
|||||||
.default(sql`(unixepoch())`),
|
.default(sql`(unixepoch())`),
|
||||||
githubOrg: text("github_org"),
|
githubOrg: text("github_org"),
|
||||||
githubRepo: text("github_repo"),
|
githubRepo: text("github_repo"),
|
||||||
|
githubBranch: text("github_branch"),
|
||||||
supabaseProjectId: text("supabase_project_id"),
|
supabaseProjectId: text("supabase_project_id"),
|
||||||
chatContext: text("chat_context", { mode: "json" }),
|
chatContext: text("chat_context", { mode: "json" }),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { ipcMain, BrowserWindow, IpcMainInvokeEvent } from "electron";
|
import { ipcMain, BrowserWindow, IpcMainInvokeEvent } from "electron";
|
||||||
import fetch from "node-fetch"; // Use node-fetch for making HTTP requests in main process
|
import fetch from "node-fetch"; // Use node-fetch for making HTTP requests in main process
|
||||||
import { writeSettings, readSettings } from "../../main/settings";
|
import { writeSettings, readSettings } from "../../main/settings";
|
||||||
import { updateAppGithubRepo } from "../../db/index";
|
|
||||||
import git from "isomorphic-git";
|
import git from "isomorphic-git";
|
||||||
import http from "isomorphic-git/http/node";
|
import http from "isomorphic-git/http/node";
|
||||||
|
import * as schema from "../../db/schema";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { getDyadAppPath } from "../../paths/paths";
|
import { getDyadAppPath } from "../../paths/paths";
|
||||||
import { db } from "../../db";
|
import { db } from "../../db";
|
||||||
@@ -12,14 +11,31 @@ import { apps } from "../../db/schema";
|
|||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { GithubUser } from "../../lib/schemas";
|
import { GithubUser } from "../../lib/schemas";
|
||||||
import log from "electron-log";
|
import log from "electron-log";
|
||||||
|
import { IS_TEST_BUILD } from "../utils/test_utils";
|
||||||
|
|
||||||
const logger = log.scope("github_handlers");
|
const logger = log.scope("github_handlers");
|
||||||
|
|
||||||
// --- GitHub Device Flow Constants ---
|
// --- GitHub Device Flow Constants ---
|
||||||
// TODO: Fetch this securely, e.g., from environment variables or a config file
|
// 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_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
|
const GITHUB_SCOPES = "repo,user,workflow"; // Define the scopes needed
|
||||||
|
|
||||||
// --- State Management (Simple in-memory, consider alternatives for robustness) ---
|
// --- State Management (Simple in-memory, consider alternatives for robustness) ---
|
||||||
@@ -48,7 +64,7 @@ export async function getGithubUser(): Promise<GithubUser | null> {
|
|||||||
try {
|
try {
|
||||||
const accessToken = settings.githubAccessToken?.value;
|
const accessToken = settings.githubAccessToken?.value;
|
||||||
if (!accessToken) return null;
|
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}` },
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
});
|
});
|
||||||
if (!res.ok) return null;
|
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 ---
|
// --- GitHub Repo Availability Handler ---
|
||||||
async function handleIsRepoAvailable(
|
async function handleIsRepoAvailable(
|
||||||
event: IpcMainInvokeEvent,
|
event: IpcMainInvokeEvent,
|
||||||
@@ -296,13 +396,13 @@ async function handleIsRepoAvailable(
|
|||||||
// If org is empty, use the authenticated user
|
// If org is empty, use the authenticated user
|
||||||
const owner =
|
const owner =
|
||||||
org ||
|
org ||
|
||||||
(await fetch("https://api.github.com/user", {
|
(await fetch(`${GITHUB_API_BASE}/user`, {
|
||||||
headers: { Authorization: `Bearer ${accessToken}` },
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
})
|
})
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((u) => u.login));
|
.then((u) => u.login));
|
||||||
// Check if repo exists
|
// 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, {
|
const res = await fetch(url, {
|
||||||
headers: { Authorization: `Bearer ${accessToken}` },
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
});
|
});
|
||||||
@@ -322,7 +422,12 @@ async function handleIsRepoAvailable(
|
|||||||
// --- GitHub Create Repo Handler ---
|
// --- GitHub Create Repo Handler ---
|
||||||
async function handleCreateRepo(
|
async function handleCreateRepo(
|
||||||
event: IpcMainInvokeEvent,
|
event: IpcMainInvokeEvent,
|
||||||
{ org, repo, appId }: { org: string; repo: string; appId: number },
|
{
|
||||||
|
org,
|
||||||
|
repo,
|
||||||
|
appId,
|
||||||
|
branch,
|
||||||
|
}: { org: string; repo: string; appId: number; branch?: string },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Get access token from settings
|
// Get access token from settings
|
||||||
const settings = readSettings();
|
const settings = readSettings();
|
||||||
@@ -333,7 +438,7 @@ async function handleCreateRepo(
|
|||||||
// If org is empty, create for the authenticated user
|
// If org is empty, create for the authenticated user
|
||||||
let owner = org;
|
let owner = org;
|
||||||
if (!owner) {
|
if (!owner) {
|
||||||
const userRes = await fetch("https://api.github.com/user", {
|
const userRes = await fetch(`${GITHUB_API_BASE}/user`, {
|
||||||
headers: { Authorization: `Bearer ${accessToken}` },
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
});
|
});
|
||||||
const user = await userRes.json();
|
const user = await userRes.json();
|
||||||
@@ -341,8 +446,8 @@ async function handleCreateRepo(
|
|||||||
}
|
}
|
||||||
// Create repo
|
// Create repo
|
||||||
const createUrl = org
|
const createUrl = org
|
||||||
? `https://api.github.com/orgs/${owner}/repos`
|
? `${GITHUB_API_BASE}/orgs/${owner}/repos`
|
||||||
: `https://api.github.com/user/repos`;
|
: `${GITHUB_API_BASE}/user/repos`;
|
||||||
const res = await fetch(createUrl, {
|
const res = await fetch(createUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -395,14 +500,58 @@ async function handleCreateRepo(
|
|||||||
|
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
// Store org and repo in the app's DB row (apps table)
|
// Store org, repo, and branch in the app's DB row (apps table)
|
||||||
await updateAppGithubRepo(appId, owner, repo);
|
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<void> {
|
||||||
|
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 ---
|
// --- GitHub Push Handler ---
|
||||||
async function handlePushToGithub(
|
async function handlePushToGithub(
|
||||||
event: IpcMainInvokeEvent,
|
event: IpcMainInvokeEvent,
|
||||||
{ appId }: { appId: number },
|
{ appId, force }: { appId: number; force?: boolean },
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
// Get access token from settings
|
// Get access token from settings
|
||||||
@@ -417,8 +566,12 @@ async function handlePushToGithub(
|
|||||||
return { success: false, error: "App is not linked to a GitHub repo." };
|
return { success: false, error: "App is not linked to a GitHub repo." };
|
||||||
}
|
}
|
||||||
const appPath = getDyadAppPath(app.path);
|
const appPath = getDyadAppPath(app.path);
|
||||||
|
const branch = app.githubBranch || "main";
|
||||||
|
|
||||||
// Set up remote URL with token
|
// 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
|
// Set or update remote URL using git config
|
||||||
await git.setConfig({
|
await git.setConfig({
|
||||||
fs,
|
fs,
|
||||||
@@ -433,11 +586,12 @@ async function handlePushToGithub(
|
|||||||
dir: appPath,
|
dir: appPath,
|
||||||
remote: "origin",
|
remote: "origin",
|
||||||
ref: "main",
|
ref: "main",
|
||||||
|
remoteRef: branch,
|
||||||
onAuth: () => ({
|
onAuth: () => ({
|
||||||
username: accessToken,
|
username: accessToken,
|
||||||
password: "x-oauth-basic",
|
password: "x-oauth-basic",
|
||||||
}),
|
}),
|
||||||
force: false,
|
force: !!force,
|
||||||
});
|
});
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -463,12 +617,13 @@ async function handleDisconnectGithubRepo(
|
|||||||
throw new Error("App not found");
|
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
|
await db
|
||||||
.update(apps)
|
.update(apps)
|
||||||
.set({
|
.set({
|
||||||
githubRepo: null,
|
githubRepo: null,
|
||||||
githubOrg: null,
|
githubOrg: null,
|
||||||
|
githubBranch: null,
|
||||||
})
|
})
|
||||||
.where(eq(apps.id, appId));
|
.where(eq(apps.id, appId));
|
||||||
}
|
}
|
||||||
@@ -476,10 +631,44 @@ async function handleDisconnectGithubRepo(
|
|||||||
// --- Registration ---
|
// --- Registration ---
|
||||||
export function registerGithubHandlers() {
|
export function registerGithubHandlers() {
|
||||||
ipcMain.handle("github:start-flow", handleStartGithubFlow);
|
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:is-repo-available", handleIsRepoAvailable);
|
||||||
ipcMain.handle("github:create-repo", handleCreateRepo);
|
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:push", handlePushToGithub);
|
||||||
ipcMain.handle("github:disconnect", (event, args: { appId: number }) =>
|
ipcMain.handle("github:disconnect", (event, args: { appId: number }) =>
|
||||||
handleDisconnectGithubRepo(event, args),
|
handleDisconnectGithubRepo(event, args),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateAppGithubRepo({
|
||||||
|
appId,
|
||||||
|
org,
|
||||||
|
repo,
|
||||||
|
branch,
|
||||||
|
}: {
|
||||||
|
appId: number;
|
||||||
|
org?: string;
|
||||||
|
repo: string;
|
||||||
|
branch?: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
await db
|
||||||
|
.update(schema.apps)
|
||||||
|
.set({
|
||||||
|
githubOrg: org,
|
||||||
|
githubRepo: repo,
|
||||||
|
githubBranch: branch || "main",
|
||||||
|
})
|
||||||
|
.where(eq(schema.apps.id, appId));
|
||||||
|
}
|
||||||
|
|||||||
@@ -554,6 +554,36 @@ export class IpcClient {
|
|||||||
// --- End GitHub Device Flow ---
|
// --- End GitHub Device Flow ---
|
||||||
|
|
||||||
// --- GitHub Repo Management ---
|
// --- 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<void> {
|
||||||
|
await this.ipcRenderer.invoke("github:connect-existing-repo", {
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
branch,
|
||||||
|
appId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async checkGithubRepoAvailable(
|
public async checkGithubRepoAvailable(
|
||||||
org: string,
|
org: string,
|
||||||
repo: string,
|
repo: string,
|
||||||
@@ -568,25 +598,25 @@ export class IpcClient {
|
|||||||
org: string,
|
org: string,
|
||||||
repo: string,
|
repo: string,
|
||||||
appId: number,
|
appId: number,
|
||||||
|
branch?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.ipcRenderer.invoke("github:create-repo", {
|
await this.ipcRenderer.invoke("github:create-repo", {
|
||||||
org,
|
org,
|
||||||
repo,
|
repo,
|
||||||
appId,
|
appId,
|
||||||
|
branch,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync (push) local repo to GitHub
|
// Sync (push) local repo to GitHub
|
||||||
public async syncGithubRepo(
|
public async syncGithubRepo(
|
||||||
appId: number,
|
appId: number,
|
||||||
|
force?: boolean,
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
try {
|
return this.ipcRenderer.invoke("github:push", {
|
||||||
const result = await this.ipcRenderer.invoke("github:push", { appId });
|
appId,
|
||||||
return result as { success: boolean; error?: string };
|
force,
|
||||||
} catch (error) {
|
});
|
||||||
showError(error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async disconnectGithubRepo(appId: number): Promise<void> {
|
public async disconnectGithubRepo(appId: number): Promise<void> {
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export interface App {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
githubOrg: string | null;
|
githubOrg: string | null;
|
||||||
githubRepo: string | null;
|
githubRepo: string | null;
|
||||||
|
githubBranch: string | null;
|
||||||
supabaseProjectId: string | null;
|
supabaseProjectId: string | null;
|
||||||
supabaseProjectName: string | null;
|
supabaseProjectName: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,8 +46,11 @@ const validInvokeChannels = [
|
|||||||
"nodejs-status",
|
"nodejs-status",
|
||||||
"install-node",
|
"install-node",
|
||||||
"github:start-flow",
|
"github:start-flow",
|
||||||
|
"github:list-repos",
|
||||||
|
"github:get-repo-branches",
|
||||||
"github:is-repo-available",
|
"github:is-repo-available",
|
||||||
"github:create-repo",
|
"github:create-repo",
|
||||||
|
"github:connect-existing-repo",
|
||||||
"github:push",
|
"github:push",
|
||||||
"github:disconnect",
|
"github:disconnect",
|
||||||
"get-app-version",
|
"get-app-version",
|
||||||
|
|||||||
417
testing/fake-llm-server/githubHandler.ts
Normal file
417
testing/fake-llm-server/githubHandler.ts
Normal file
@@ -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,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,19 @@ import express from "express";
|
|||||||
import { createServer } from "http";
|
import { createServer } from "http";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import { createChatCompletionHandler } from "./chatCompletionHandler";
|
import { createChatCompletionHandler } from "./chatCompletionHandler";
|
||||||
|
import {
|
||||||
|
handleDeviceCode,
|
||||||
|
handleAccessToken,
|
||||||
|
handleUser,
|
||||||
|
handleUserEmails,
|
||||||
|
handleUserRepos,
|
||||||
|
handleRepo,
|
||||||
|
handleRepoBranches,
|
||||||
|
handleOrgRepos,
|
||||||
|
handleGitPush,
|
||||||
|
handleGetPushEvents,
|
||||||
|
handleClearPushEvents,
|
||||||
|
} from "./githubHandler";
|
||||||
|
|
||||||
// Create Express app
|
// Create Express app
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -179,6 +192,29 @@ app.get("/lmstudio/api/v0/models", (req, res) => {
|
|||||||
// Default test provider handler:
|
// Default test provider handler:
|
||||||
app.post("/v1/chat/completions", createChatCompletionHandler("."));
|
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
|
// Start the server
|
||||||
const server = createServer(app);
|
const server = createServer(app);
|
||||||
server.listen(PORT, () => {
|
server.listen(PORT, () => {
|
||||||
|
|||||||
629
testing/fake-llm-server/package-lock.json
generated
629
testing/fake-llm-server/package-lock.json
generated
@@ -17,6 +17,7 @@
|
|||||||
"@types/cors": "^2.8.18",
|
"@types/cors": "^2.8.18",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/node": "^20.17.46",
|
"@types/node": "^20.17.46",
|
||||||
|
"git-http-mock-server": "^2.0.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
@@ -201,6 +202,42 @@
|
|||||||
"node": ">=0.4.0"
|
"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": {
|
"node_modules/arg": {
|
||||||
"version": "4.1.3",
|
"version": "4.1.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -210,6 +247,73 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.20.3",
|
"version": "1.20.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -232,6 +336,24 @@
|
|||||||
"npm": "1.2.8000 || >= 1.4.16"
|
"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": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -264,6 +386,45 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/content-disposition": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -308,6 +469,26 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/debug": {
|
||||||
"version": "2.6.9",
|
"version": "2.6.9",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -338,6 +519,19 @@
|
|||||||
"node": ">=0.3.1"
|
"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": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -395,6 +589,16 @@
|
|||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/etag": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -462,6 +666,19 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -476,6 +693,25 @@
|
|||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -516,6 +752,70 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/gopd": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -526,6 +826,23 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/has-symbols": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -546,6 +863,32 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/http-errors": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -570,6 +913,25 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/inherits": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
@@ -581,6 +943,16 @@
|
|||||||
"node": ">= 0.10"
|
"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": {
|
"node_modules/make-error": {
|
||||||
"version": "1.3.6",
|
"version": "1.3.6",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -614,6 +986,16 @@
|
|||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/mime": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -641,6 +1023,39 @@
|
|||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/ms": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
@@ -679,6 +1094,16 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -686,10 +1111,43 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "0.1.12",
|
"version": "0.1.12",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -756,6 +1214,16 @@
|
|||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/send": {
|
||||||
"version": "0.19.0",
|
"version": "0.19.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -870,6 +1338,61 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -884,6 +1407,52 @@
|
|||||||
"emitter-component": "^1.1.1"
|
"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": {
|
"node_modules/toidentifier": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -891,6 +1460,16 @@
|
|||||||
"node": ">=0.6"
|
"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": {
|
"node_modules/ts-node": {
|
||||||
"version": "10.9.2",
|
"version": "10.9.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -956,11 +1535,47 @@
|
|||||||
"node": ">=14.17"
|
"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": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.19.8",
|
"version": "6.19.8",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/unpipe": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -987,6 +1602,20 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/yn": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"@types/cors": "^2.8.18",
|
"@types/cors": "^2.8.18",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/node": "^20.17.46",
|
"@types/node": "^20.17.46",
|
||||||
|
"git-http-mock-server": "^2.0.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user