From 040cea2e8e3d226e45bca26484b2b26d607a1683 Mon Sep 17 00:00:00 2001 From: Exelo Date: Thu, 11 Jun 2026 11:00:04 +0900 Subject: [PATCH] feat: add github sync --- .../action/repositories/project.repository.ts | 4 ++ src/lib/client/project/save-handler.ts | 20 ++++++++ src/lib/components/Menu/MenuBar.svelte | 11 ++++- .../server/actions/project/complete.action.ts | 4 +- .../server/actions/project/gateway.action.ts | 8 +++ src/lib/server/actions/project/load.action.ts | 12 +++-- src/lib/server/api/types/project.type.ts | 2 +- src/lib/server/git/git.ts | 49 ++++++++----------- .../server/session/project/project.type.ts | 2 +- .../server/utils/request-handler/handler.ts | 2 +- src/routes/actions/project/+page.server.ts | 6 ++- 11 files changed, 80 insertions(+), 40 deletions(-) diff --git a/src/lib/client/action/repositories/project.repository.ts b/src/lib/client/action/repositories/project.repository.ts index 959b514..47243d3 100644 --- a/src/lib/client/action/repositories/project.repository.ts +++ b/src/lib/client/action/repositories/project.repository.ts @@ -33,4 +33,8 @@ export class ProjectRepository extends BaseRepository { getGatewayProjects(): Promise { return this.run(`/actions/project?/get-gateway-projects`); } + + syncGatewayProject(): Promise { + return this.run(`/actions/project?/sync-gateway-project`); + } } diff --git a/src/lib/client/project/save-handler.ts b/src/lib/client/project/save-handler.ts index f1ae48a..6dbb7ad 100644 --- a/src/lib/client/project/save-handler.ts +++ b/src/lib/client/project/save-handler.ts @@ -1,5 +1,6 @@ import { type Writable, get, writable } from 'svelte/store'; +import { getConfig } from '$lib/client/config'; import type { Project } from '$lib/client/project'; import type { Save } from '@utils/types'; @@ -15,6 +16,7 @@ export class SaveHandler { private _readyToSync = true; private _syncTimer?: Timer; private _needSync: Writable = writable(false); + private _syncGatewayEnable?: boolean = true; constructor(project: Project) { this._project = project; @@ -28,6 +30,9 @@ export class SaveHandler { this._save.subscribe(() => { this.syncToServer(); }); + if (getConfig().mode === 'online') { + this.initSyncGateway(); + } } async fetchFromServer() { @@ -102,4 +107,19 @@ export class SaveHandler { void this.syncToServer(); }); } + + private initSyncGateway() { + setInterval( + async () => { + if (this._syncGatewayEnable) { + await this._project.actions.project.syncGatewayProject(); + } + }, + 1000 * 60 * 5, + ); + window.addEventListener('beforeunload', (e) => { + const res = this._project.actions.project.syncGatewayProject(); + if (!res) e.preventDefault(); + }); + } } diff --git a/src/lib/components/Menu/MenuBar.svelte b/src/lib/components/Menu/MenuBar.svelte index 8dbf8be..7a6d20a 100644 --- a/src/lib/components/Menu/MenuBar.svelte +++ b/src/lib/components/Menu/MenuBar.svelte @@ -4,8 +4,11 @@ import { resolve } from '$app/paths'; import { goto } from '$app/navigation'; import type { Snippet } from 'svelte'; - import { ProjectLoader } from '$lib/client/project'; + import { ProjectLoader, useProject } from '$lib/client/project'; import { PUBLIC_DOCS_URL, PUBLIC_LANDING_URL } from '$env/static/public'; + import { getConfig } from '$lib/client/config'; + + const { actions } = useProject(); let fileInput: HTMLInputElement; @@ -38,13 +41,17 @@ input.value = ''; } + const handleSave = async () => { + if (getConfig().mode === 'online') await actions.project.syncGatewayProject(); + }; + const nullFunction = () => {}; const elements: Menu[] = [ { name: 'File', items: [ - { name: 'Save', icon: 'i-solar-cloud-download-bold-duotone', onClick: nullFunction }, + { name: 'Save', icon: 'i-solar-cloud-download-bold-duotone', onClick: handleSave }, { snippet: fileImportSnippet, icon: 'i-solar-download-bold-duotone', diff --git a/src/lib/server/actions/project/complete.action.ts b/src/lib/server/actions/project/complete.action.ts index aeb34b0..daec4eb 100644 --- a/src/lib/server/actions/project/complete.action.ts +++ b/src/lib/server/actions/project/complete.action.ts @@ -1,5 +1,5 @@ import { Expose } from 'class-transformer'; -import { IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; +import { IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { resolveSessionFunctions } from '$lib/server/actions/project/load.action'; import { loadProject } from '$lib/server/project'; @@ -8,7 +8,7 @@ import { useActionHandler } from '@utils-server/request-handler'; export class CompleteProjectBody { @Expose() - @IsUUID(8) + @IsString() @IsNotEmpty() gatewayId!: string; diff --git a/src/lib/server/actions/project/gateway.action.ts b/src/lib/server/actions/project/gateway.action.ts index c3ffc12..81b2df4 100644 --- a/src/lib/server/actions/project/gateway.action.ts +++ b/src/lib/server/actions/project/gateway.action.ts @@ -16,3 +16,11 @@ export const getGatewayProjectsAction = useActionHandler( }, { onlineOnly: true, projectOptional: true }, ); + +export const syncGatewayProjectAction = useActionHandler( + async ({ git }) => { + console.log('Syncing gateway project'); + return await git.push(); + }, + { onlineOnly: true }, +); diff --git a/src/lib/server/actions/project/load.action.ts b/src/lib/server/actions/project/load.action.ts index 29d339e..6c10e47 100644 --- a/src/lib/server/actions/project/load.action.ts +++ b/src/lib/server/actions/project/load.action.ts @@ -2,6 +2,7 @@ import { Expose } from 'class-transformer'; import { IsOptional, IsString } from 'class-validator'; import { join } from 'path'; +import { Git } from '$lib/server/git'; import { loadProject } from '$lib/server/project'; import { loadProjectFromId } from '$lib/server/project/load-project'; import type { SessionProject } from '$lib/server/session'; @@ -34,18 +35,21 @@ export class LoadProjectBody { const resolveSessionFromGatewayId = async ( gatewayId: string, - { api, git, context }: Handler, + { api, context }: Handler, ): Promise => { if (!context.online) throw new Exception('Bad Request', 'Cannot load project from gatewayId while offline', 400); const project = await api.projects.getProject(gatewayId); - const basePath = await git.clone(project.gatewayProjectRegistryUrl, { - sshKey: project.gatewayProjectRegistryMetadata.sshKey, + + const git = new Git({ + ...context, + project: { path: '', gateway: { id: gatewayId, token: project.token } }, }); + const basePath = await git.clone(project.gatewayProjectRegistryUrl); return { path: join(basePath, project.gatewayProjectRegistryMetadata.dir ?? ''), - gateway: { id: gatewayId, sshKey: project.gatewayProjectRegistryMetadata.sshKey }, + gateway: { id: gatewayId, token: project.token }, }; }; diff --git a/src/lib/server/api/types/project.type.ts b/src/lib/server/api/types/project.type.ts index f842348..9facd64 100644 --- a/src/lib/server/api/types/project.type.ts +++ b/src/lib/server/api/types/project.type.ts @@ -6,6 +6,6 @@ export interface ApiProject { gatewayProjectRegistryUrl: string; gatewayProjectRegistryMetadata: { dir: string | null; - sshKey: string; }; + token: string; } diff --git a/src/lib/server/git/git.ts b/src/lib/server/git/git.ts index 303f126..18bffe4 100644 --- a/src/lib/server/git/git.ts +++ b/src/lib/server/git/git.ts @@ -4,50 +4,43 @@ import { resolve } from 'path'; import { env } from '$env/dynamic/private'; +import type { Context } from '@utils-server/request-handler'; import { generateKey } from '@utils-server/string'; export class Git { private readonly _rootPath: string; + private readonly _token: string | null; + private readonly _path: string; - constructor() { + constructor(context: Context) { this._rootPath = resolve(env.FS_ROOT ?? ''); + this._token = context.project.gateway?.token ?? null; + this._path = context.project.path; } - async clone(url: string, options?: { sshKey?: string }): Promise { + async clone(url: string): Promise { const path = await this.resolvePath(url); if (existsSync(path)) return path; - await this.runCommand('clone', [url, path], { ...options }); + if (this._token) + url = url.replace('https://github.com/', `https://oauth2:${this._token}@github.com/`); + await this.runCommand('clone', [url, path]); return path; } - private async runCommand( - command: string, - params: string[], - options?: { path?: string; sshKey?: string }, - ) { - let sshPath: string | undefined; - if (options?.sshKey) { - sshPath = await this.createSshKeyFile(options.sshKey); - } - - const cwd = resolve(this._rootPath, options?.path ?? ''); - const sshEnv = sshPath ? { GIT_SSH_COMMAND: `ssh -i ${sshPath}` } : {}; - - try { - await $`git ${command} ${params}`.cwd(cwd).env({ ...process.env, ...sshEnv }); - } finally { - if (sshPath) await this.deleteSshKeyFile(sshPath); - } + async push() { + await this.runCommand('add', ['--all'], { path: this._path }); + const res = await this.runCommand('status', [], { path: this._path }); + if (res.stdout.includes('nothing to commit')) return; + await this.runCommand('commit', ['-m', `nanoforged at ${new Date().toISOString()}`], { + path: this._path, + }); + await this.runCommand('push', ['-u', 'origin', 'main'], { path: this._path }); } - private async createSshKeyFile(sshKey: string): Promise { - const path = `/tmp/nanoforge/${generateKey()}`; - await Bun.file(path).write(sshKey); - return path; - } + private runCommand(command: string, params: string[], options?: { path?: string }) { + const cwd = resolve(this._rootPath, options?.path ?? ''); - private async deleteSshKeyFile(path: string): Promise { - await Bun.file(path).delete(); + return $`git ${command} ${params}`.cwd(cwd).env({ ...process.env }); } private async resolvePath(url: string) { diff --git a/src/lib/server/session/project/project.type.ts b/src/lib/server/session/project/project.type.ts index 6d0a5cb..a970c5b 100644 --- a/src/lib/server/session/project/project.type.ts +++ b/src/lib/server/session/project/project.type.ts @@ -2,6 +2,6 @@ export interface SessionProject { path: string; gateway?: { id: string; - sshKey: string; + token: string; }; } diff --git a/src/lib/server/utils/request-handler/handler.ts b/src/lib/server/utils/request-handler/handler.ts index 6660f00..7238e26 100644 --- a/src/lib/server/utils/request-handler/handler.ts +++ b/src/lib/server/utils/request-handler/handler.ts @@ -63,7 +63,7 @@ export class Handler { } get git(): Git { - if (!this._gitCache) this._gitCache = new Git(); + if (!this._gitCache) this._gitCache = new Git(this._context); return this._gitCache; } diff --git a/src/routes/actions/project/+page.server.ts b/src/routes/actions/project/+page.server.ts index 35186f1..af35f1b 100644 --- a/src/routes/actions/project/+page.server.ts +++ b/src/routes/actions/project/+page.server.ts @@ -1,5 +1,8 @@ import { completeProjectAction } from '$lib/server/actions/project/complete.action'; -import { getGatewayProjectsAction } from '$lib/server/actions/project/gateway.action'; +import { + getGatewayProjectsAction, + syncGatewayProjectAction, +} from '$lib/server/actions/project/gateway.action'; import { getInfoProjectAction, setInfoProjectAction, @@ -14,4 +17,5 @@ export const actions = { 'get-info': getInfoProjectAction, 'set-info': setInfoProjectAction, 'get-gateway-projects': getGatewayProjectsAction, + 'sync-gateway-project': syncGatewayProjectAction, };