Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
B
bit-pm
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
燕伟桐
bit-pm
Commits
05a97d31
提交
05a97d31
authored
4月 15, 2025
作者:
Will Chen
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
github repo creation flow
上级
7ad83a2b
隐藏空白字符变更
内嵌
并排
正在显示
12 个修改的文件
包含
288 行增加
和
10 行删除
+288
-10
0000_nebulous_proemial_gods.sql
drizzle/0000_nebulous_proemial_gods.sql
+3
-1
0000_snapshot.json
drizzle/meta/0000_snapshot.json
+15
-1
_journal.json
drizzle/meta/_journal.json
+2
-2
GitHubConnector.tsx
src/components/GitHubConnector.tsx
+123
-3
index.ts
src/db/index.ts
+12
-0
schema.ts
src/db/schema.ts
+2
-0
github_handlers.ts
src/ipc/handlers/github_handlers.ts
+91
-1
ipc_client.ts
src/ipc/ipc_client.ts
+34
-0
ipc_types.ts
src/ipc/ipc_types.ts
+2
-0
app-details.tsx
src/pages/app-details.tsx
+1
-1
paths.ts
src/paths/paths.ts
+1
-1
preload.ts
src/preload.ts
+2
-0
没有找到文件。
drizzle/0000_
mighty_stark_industrie
s.sql
→
drizzle/0000_
nebulous_proemial_god
s.sql
浏览文件 @
05a97d31
...
...
@@ -3,7 +3,9 @@ CREATE TABLE `apps` (
`name`
text
NOT
NULL
,
`path`
text
NOT
NULL
,
`created_at`
integer
DEFAULT
(
unixepoch
())
NOT
NULL
,
`updated_at`
integer
DEFAULT
(
unixepoch
())
NOT
NULL
`updated_at`
integer
DEFAULT
(
unixepoch
())
NOT
NULL
,
`github_org`
text
,
`github_repo`
text
);
--> statement-breakpoint
CREATE
TABLE
`chats`
(
...
...
drizzle/meta/0000_snapshot.json
浏览文件 @
05a97d31
{
"version"
:
"6"
,
"dialect"
:
"sqlite"
,
"id"
:
"
8551dcbc-91d8-4b82-afc4-9c4b6684de3a
"
,
"id"
:
"
1a0ffcb3-606d-4b03-81b7-7c585555a548
"
,
"prevId"
:
"00000000-0000-0000-0000-000000000000"
,
"tables"
:
{
"apps"
:
{
...
...
@@ -43,6 +43,20 @@
"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
}
},
"indexes"
:
{},
...
...
drizzle/meta/_journal.json
浏览文件 @
05a97d31
...
...
@@ -5,8 +5,8 @@
{
"idx"
:
0
,
"version"
:
"6"
,
"when"
:
1744
233051865
,
"tag"
:
"0000_
mighty_stark_industrie
s"
,
"when"
:
1744
692127560
,
"tag"
:
"0000_
nebulous_proemial_god
s"
,
"breakpoints"
:
true
}
]
...
...
src/components/GitHubConnector.tsx
浏览文件 @
05a97d31
...
...
@@ -3,13 +3,16 @@ import { Button } from "@/components/ui/button";
import
{
Github
}
from
"lucide-react"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
import
{
useSettings
}
from
"@/hooks/useSettings"
;
import
{
useLoadApp
}
from
"@/hooks/useLoadApp"
;
interface
GitHubConnectorProps
{
appId
:
number
|
null
;
folderName
:
string
;
}
export
function
GitHubConnector
({
appId
}:
GitHubConnectorProps
)
{
export
function
GitHubConnector
({
appId
,
folderName
}:
GitHubConnectorProps
)
{
// --- GitHub Device Flow State ---
const
{
app
,
refreshApp
}
=
useLoadApp
(
appId
);
const
{
settings
,
refreshSettings
}
=
useSettings
();
const
[
githubUserCode
,
setGithubUserCode
]
=
useState
<
string
|
null
>
(
null
);
const
[
githubVerificationUri
,
setGithubVerificationUri
]
=
useState
<
...
...
@@ -106,10 +109,127 @@ export function GitHubConnector({ appId }: GitHubConnectorProps) {
};
},
[
appId
]);
// Re-run effect if appId changes
// --- Create 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)
// 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
handleRepoNameBlur
=
async
()
=>
{
setRepoCheckError
(
null
);
setRepoAvailable
(
null
);
if
(
!
repoName
)
return
;
setIsCheckingRepo
(
true
);
try
{
const
result
=
await
IpcClient
.
getInstance
().
checkGithubRepoAvailable
(
githubOrg
,
repoName
);
setRepoAvailable
(
result
.
available
);
if
(
!
result
.
available
)
{
setRepoCheckError
(
result
.
error
||
"Repository name is not available."
);
}
}
catch
(
err
:
any
)
{
setRepoCheckError
(
err
.
message
||
"Failed to check repo availability."
);
}
finally
{
setIsCheckingRepo
(
false
);
}
};
const
handleCreateRepo
=
async
(
e
:
React
.
FormEvent
)
=>
{
e
.
preventDefault
();
setCreateRepoError
(
null
);
setIsCreatingRepo
(
true
);
setCreateRepoSuccess
(
false
);
try
{
const
result
=
await
IpcClient
.
getInstance
().
createGithubRepo
(
githubOrg
,
repoName
,
appId
!
);
if
(
result
.
success
)
{
setCreateRepoSuccess
(
true
);
setRepoCheckError
(
null
);
refreshApp
();
}
else
{
setCreateRepoError
(
result
.
error
||
"Failed to create repository."
);
}
}
catch
(
err
:
any
)
{
setCreateRepoError
(
err
.
message
||
"Failed to create repository."
);
}
finally
{
setIsCreatingRepo
(
false
);
}
};
if
(
app
?.
githubOrg
&&
app
?.
githubRepo
)
{
return
(
<
div
className=
"mt-4 w-full border border-gray-200 rounded-md p-4"
>
<
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
>
</
div
>
);
}
if
(
settings
?.
githubSettings
.
secrets
)
{
return
(
<
div
className=
"mt-4 w-full"
>
<
p
>
Connected to GitHub!
</
p
>
<
div
className=
"mt-4 w-full border border-gray-200 rounded-md p-4"
>
<
p
>
Set up your GitHub repo
</
p
>
<
form
className=
"mt-4 space-y-2"
onSubmit=
{
handleCreateRepo
}
>
<
label
className=
"block text-sm font-medium"
>
Repository Name
</
label
>
<
input
className=
"w-full border rounded px-2 py-1"
value=
{
repoName
}
onChange=
{
(
e
)
=>
{
setRepoName
(
e
.
target
.
value
);
setRepoAvailable
(
null
);
setRepoCheckError
(
null
);
}
}
onBlur=
{
handleRepoNameBlur
}
disabled=
{
isCreatingRepo
}
/>
{
isCheckingRepo
&&
(
<
p
className=
"text-xs text-gray-500"
>
Checking availability...
</
p
>
)
}
{
repoAvailable
===
true
&&
(
<
p
className=
"text-xs text-green-600"
>
Repository name is available!
</
p
>
)
}
{
repoAvailable
===
false
&&
(
<
p
className=
"text-xs text-red-600"
>
{
repoCheckError
}
</
p
>
)
}
<
Button
type=
"submit"
disabled=
{
isCreatingRepo
||
repoAvailable
===
false
||
!
repoName
}
>
{
isCreatingRepo
?
"Creating..."
:
"Create Repo"
}
</
Button
>
</
form
>
{
createRepoError
&&
(
<
p
className=
"text-red-600 mt-2"
>
{
createRepoError
}
</
p
>
)
}
{
createRepoSuccess
&&
(
<
p
className=
"text-green-600 mt-2"
>
Repository created and linked!
</
p
>
)
}
</
div
>
);
}
...
...
src/db/index.ts
浏览文件 @
05a97d31
...
...
@@ -8,6 +8,7 @@ import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import
path
from
"node:path"
;
import
fs
from
"node:fs"
;
import
{
getDyadAppPath
,
getUserDataPath
}
from
"../paths/paths"
;
import
{
eq
}
from
"drizzle-orm"
;
// Database connection factory
let
_db
:
ReturnType
<
typeof
drizzle
>
|
null
=
null
;
...
...
@@ -91,3 +92,14 @@ try {
export
const
db
=
_db
as
any
as
BetterSQLite3Database
<
typeof
schema
>
&
{
$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
));
}
src/db/schema.ts
浏览文件 @
05a97d31
...
...
@@ -12,6 +12,8 @@ export const apps = sqliteTable("apps", {
updatedAt
:
integer
(
"updated_at"
,
{
mode
:
"timestamp"
})
.
notNull
()
.
default
(
sql
`(unixepoch())`
),
githubOrg
:
text
(
"github_org"
),
githubRepo
:
text
(
"github_repo"
),
});
export
const
chats
=
sqliteTable
(
"chats"
,
{
...
...
src/ipc/handlers/github_handlers.ts
浏览文件 @
05a97d31
...
...
@@ -5,7 +5,8 @@ import {
IpcMainInvokeEvent
,
}
from
"electron"
;
import
fetch
from
"node-fetch"
;
// Use node-fetch for making HTTP requests in main process
import
{
writeSettings
}
from
"../../main/settings"
;
import
{
writeSettings
,
readSettings
}
from
"../../main/settings"
;
import
{
updateAppGithubRepo
}
from
"../../db/index"
;
// --- GitHub Device Flow Constants ---
// TODO: Fetch this securely, e.g., from environment variables or a config file
...
...
@@ -275,8 +276,97 @@ function handleStartGithubFlow(
// event.sender.send('github:flow-cancelled', { message: 'GitHub flow cancelled.' });
// }
// --- GitHub Repo Availability Handler ---
async
function
handleIsRepoAvailable
(
event
:
IpcMainInvokeEvent
,
{
org
,
repo
}:
{
org
:
string
;
repo
:
string
}
)
{
try
{
// Get access token from settings
const
settings
=
readSettings
();
const
accessToken
=
settings
.
githubSettings
?.
secrets
?.
accessToken
;
if
(
!
accessToken
)
{
return
{
available
:
false
,
error
:
"Not authenticated with GitHub."
};
}
// If org is empty, use the authenticated user
const
owner
=
org
||
(
await
fetch
(
"https://api.github.com/user"
,
{
headers
:
{
Authorization
:
`Bearer
${
accessToken
}
`
},
})
.
then
((
r
)
=>
r
.
json
())
.
then
((
u
)
=>
u
.
login
));
// Check if repo exists
const
url
=
`https://api.github.com/repos/
${
owner
}
/
${
repo
}
`
;
const
res
=
await
fetch
(
url
,
{
headers
:
{
Authorization
:
`Bearer
${
accessToken
}
`
},
});
if
(
res
.
status
===
404
)
{
return
{
available
:
true
};
}
else
if
(
res
.
ok
)
{
return
{
available
:
false
,
error
:
"Repository already exists."
};
}
else
{
const
data
=
await
res
.
json
();
return
{
available
:
false
,
error
:
data
.
message
||
"Unknown error"
};
}
}
catch
(
err
:
any
)
{
return
{
available
:
false
,
error
:
err
.
message
||
"Unknown error"
};
}
}
// --- GitHub Create Repo Handler ---
async
function
handleCreateRepo
(
event
:
IpcMainInvokeEvent
,
{
org
,
repo
,
appId
}:
{
org
:
string
;
repo
:
string
;
appId
:
number
}
)
{
try
{
// Get access token from settings
const
settings
=
readSettings
();
const
accessToken
=
settings
.
githubSettings
?.
secrets
?.
accessToken
;
if
(
!
accessToken
)
{
return
{
success
:
false
,
error
:
"Not authenticated with GitHub."
};
}
// If org is empty, create for the authenticated user
let
owner
=
org
;
if
(
!
owner
)
{
const
userRes
=
await
fetch
(
"https://api.github.com/user"
,
{
headers
:
{
Authorization
:
`Bearer
${
accessToken
}
`
},
});
const
user
=
await
userRes
.
json
();
owner
=
user
.
login
;
}
// Create repo
const
createUrl
=
org
?
`https://api.github.com/orgs/
${
owner
}
/repos`
:
`https://api.github.com/user/repos`
;
const
res
=
await
fetch
(
createUrl
,
{
method
:
"POST"
,
headers
:
{
Authorization
:
`Bearer
${
accessToken
}
`
,
"Content-Type"
:
"application/json"
,
Accept
:
"application/vnd.github+json"
,
},
body
:
JSON
.
stringify
({
name
:
repo
,
private
:
true
,
}),
});
if
(
!
res
.
ok
)
{
const
data
=
await
res
.
json
();
return
{
success
:
false
,
error
:
data
.
message
||
"Failed to create repo"
};
}
// Store org and repo in the app's DB row (apps table)
await
updateAppGithubRepo
(
appId
,
owner
,
repo
);
return
{
success
:
true
};
}
catch
(
err
:
any
)
{
return
{
success
:
false
,
error
:
err
.
message
||
"Unknown error"
};
}
}
// --- Registration ---
export
function
registerGithubHandlers
()
{
ipcMain
.
handle
(
"github:start-flow"
,
handleStartGithubFlow
);
// ipcMain.on('github:cancel-flow', handleCancelGithubFlow); // Uncomment if you add cancellation
ipcMain
.
handle
(
"github:is-repo-available"
,
handleIsRepoAvailable
);
ipcMain
.
handle
(
"github:create-repo"
,
handleCreateRepo
);
}
src/ipc/ipc_client.ts
浏览文件 @
05a97d31
...
...
@@ -571,6 +571,40 @@ export class IpcClient {
// }
// --- End GitHub Device Flow ---
// --- GitHub Repo Management ---
public
async
checkGithubRepoAvailable
(
org
:
string
,
repo
:
string
):
Promise
<
{
available
:
boolean
;
error
?:
string
}
>
{
try
{
const
result
=
await
this
.
ipcRenderer
.
invoke
(
"github:is-repo-available"
,
{
org
,
repo
,
});
return
result
;
}
catch
(
error
:
any
)
{
return
{
available
:
false
,
error
:
error
.
message
||
"Unknown error"
};
}
}
public
async
createGithubRepo
(
org
:
string
,
repo
:
string
,
appId
:
number
):
Promise
<
{
success
:
boolean
;
error
?:
string
}
>
{
try
{
const
result
=
await
this
.
ipcRenderer
.
invoke
(
"github:create-repo"
,
{
org
,
repo
,
appId
,
});
return
result
;
}
catch
(
error
:
any
)
{
return
{
success
:
false
,
error
:
error
.
message
||
"Unknown error"
};
}
}
// --- End GitHub Repo Management ---
// Example methods for listening to events (if needed)
// public on(channel: string, func: (...args: any[]) => void): void {
}
src/ipc/ipc_types.ts
浏览文件 @
05a97d31
...
...
@@ -50,6 +50,8 @@ export interface App {
files
:
string
[];
createdAt
:
Date
;
updatedAt
:
Date
;
githubOrg
:
string
|
null
;
githubRepo
:
string
|
null
;
}
export
interface
Version
{
...
...
src/pages/app-details.tsx
浏览文件 @
05a97d31
...
...
@@ -238,7 +238,7 @@ export default function AppDetailsPage() {
Open in Chat
<
MessageCircle
className=
"h-5 w-5"
/>
</
Button
>
<
GitHubConnector
appId=
{
appId
}
/>
<
GitHubConnector
appId=
{
appId
}
folderName=
{
selectedApp
.
path
}
/>
</
div
>
{
/* Rename Dialog */
}
...
...
src/paths/paths.ts
浏览文件 @
05a97d31
...
...
@@ -15,7 +15,7 @@ export function getUserDataPath(): string {
const
electron
=
getElectron
();
// When running in Electron and app is ready
if
(
process
.
env
.
NODE_ENV
!==
"development"
)
{
if
(
process
.
env
.
NODE_ENV
!==
"development"
&&
electron
)
{
return
electron
!
.
app
.
getPath
(
"userData"
);
}
...
...
src/preload.ts
浏览文件 @
05a97d31
...
...
@@ -33,6 +33,8 @@ const validInvokeChannels = [
"reset-all"
,
"nodejs-status"
,
"github:start-flow"
,
"github:is-repo-available"
,
"github:create-repo"
,
]
as
const
;
// Add valid receive channels
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论