Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}/packages/vscode"
],
"outFiles": [
"${workspaceFolder}/packages/vscode/dist/**/*.mjs"
]
}
]
}
1 change: 1 addition & 0 deletions packages/vscode/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
generated-meta.ts
24 changes: 24 additions & 0 deletions packages/vscode/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# ghfs

GitHub issues/PRs as filesystem, for offline view and operations in batch. Designed for human and agents.

## Commands

<!-- commands -->

| Command | Title |
| ----------- | -------------- |
| `ghfs.sync` | ghfs: Sync Now |

<!-- commands -->

## Configs

<!-- configs -->

| Key | Description | Type | Default |
| ------------------------------- | ------------------------------ | --------- | ------- |
| `ghfs.autoSync.enabled` | Enable automatic periodic sync | `boolean` | `true` |
| `ghfs.autoSync.intervalMinutes` | Auto-sync interval in minutes | `number` | `1440` |

<!-- configs -->
88 changes: 88 additions & 0 deletions packages/vscode/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
{
"publisher": "Anthony Fu",
"name": "vscode-ghfs",
"displayName": "ghfs",
"type": "module",
"version": "0.0.3",
"packageManager": "pnpm@10.30.3",
"description": "GitHub issues/PRs as filesystem, for offline view and operations in batch. Designed for human and agents.",
"author": {
"name": "Vida Xie",
"email": "vida_2020@163.com",
"url": "https://github.com/9romise"
},
"license": "MIT",
"funding": "https://github.com/sponsors/antfu",
"homepage": "https://github.com/antfu/ghfs/packages/vscode",
"repository": "https://github.com/antfu/ghfs",
"bugs": "https://github.com/antfu/ghfs/issues",
"keywords": [
"github",
"issues",
"pull requests",
"markdown",
"cli"
],
"categories": [
"Other"
],
"main": "./dist/index.mjs",
"browser": "./dist/index.mjs",
"icon": "res/logo.png",
"extensionKind": [
"workspace"
],
"files": [
"LICENSE.md",
"dist/*",
"res/*"
],
"engines": {
"vscode": "^1.101.0"
},
"activationEvents": [
"workspaceContains:.ghfs"
],
"contributes": {
"commands": [
{
"command": "ghfs.sync",
"title": "Sync Now",
"category": "ghfs"
}
],
"configuration": {
"title": "ghfs",
"properties": {
"ghfs.autoSync.enabled": {
"type": "boolean",
"default": true,
"description": "Enable automatic periodic sync"
},
"ghfs.autoSync.intervalMinutes": {
"type": "number",
"default": 1440,
"minimum": 1,
"description": "Auto-sync interval in minutes"
}
}
}
},
"scripts": {
"dev": "tsdown --watch",
"build": "tsdown",
"postinstall": "npm run update",
"update": "vscode-ext-gen --scope ghfs",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"vscode:prepublish": "pnpm run build",
"publish": "npx @vscode/vsce publish --no-dependencies",
"package": "npx @vscode/vsce package --no-dependencies"
},
"devDependencies": {
"@ghfs/cli": "workspace:*",
"@types/vscode": "1.101.0",
"reactive-vscode": "^1.0.0-beta.2",
"vscode-ext-gen": "1.6.0"
}
}
9 changes: 9 additions & 0 deletions packages/vscode/src/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { authentication } from 'vscode'

export async function readTokenFromVSCode() {
const session = await authentication.getSession('github', ['repo'], {
createIfNone: true,
})

return session.accessToken
}
38 changes: 38 additions & 0 deletions packages/vscode/src/commands/sync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ProgressLocation, window, workspace } from 'vscode'
import { runSync } from '../sync'

export async function sync(cwd?: string): Promise<void> {
if (!cwd) {
const activeEditor = window.activeTextEditor
const folder = activeEditor
? workspace.getWorkspaceFolder(activeEditor.document.uri)
: workspace.workspaceFolders?.[0]

if (!folder) {
window.showWarningMessage('ghfs: Sync failed - workspace folder not found.')
return
}

cwd = folder.uri.path
}

await window.withProgress(
{
location: ProgressLocation.Notification,
title: 'ghfs: Syncing repository...',
cancellable: true,
},
async () => {
try {
const summary = await runSync({ cwd })
window.showInformationMessage(
`ghfs: Sync finished — ${summary.updatedIssues} issues and ${summary.updatedPulls} PRs updated.`,
)
}
catch (error) {
const message = error instanceof Error ? error.message : String(error)
window.showErrorMessage(`ghfs: Sync failed — ${message}`)
}
},
)
}
101 changes: 101 additions & 0 deletions packages/vscode/src/composables/auto-sync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import type { Uri } from 'vscode'
import { computed, shallowRef, useWindowState, watchEffect } from 'reactive-vscode'
import { workspace } from 'vscode'
import { config, logger } from '../meta'
import { runSync } from '../sync'
import { isValidWorkspace } from '../utils/fs'

export function useAutoSync(): void {
const { focused, active } = useWindowState()

const isSyncing = shallowRef(false)
const intervalMs = computed(() => Math.max(1, config.autoSync.intervalMinutes) * 60_000)

const pendingFolders = new Set<string>()
const timers = new Map<string, ReturnType<typeof setTimeout>>()

async function doSync(cwd: string) {
if (isSyncing.value)
return
isSyncing.value = true
try {
await runSync({ cwd })
timers.set(cwd, setTimeout(() => syncIfActive(cwd), intervalMs.value))
}
catch (err) {
logger.error(`[auto-sync] error: `, err)
}
finally {
isSyncing.value = false
}
}

function syncIfActive(cwd: string) {
if (focused.value && active.value) {
pendingFolders.delete(cwd)
doSync(cwd)
}
else {
pendingFolders.add(cwd)
}
}

async function addFolder(cwd: Uri) {
if (timers.has(cwd.path))
return

if (await isValidWorkspace(cwd))
return

syncIfActive(cwd.path)
}

function removeFolder(cwd: string) {
const timer = timers.get(cwd)
if (timer) {
clearInterval(timer)
timers.delete(cwd)
}
pendingFolders.delete(cwd)
}

function clearAllTimers() {
for (const cwd of timers.keys())
removeFolder(cwd)
}

watchEffect(() => {
if (!focused.value || !active.value)
return
for (const cwd of pendingFolders) {
pendingFolders.delete(cwd)
doSync(cwd)
}
})

watchEffect((onCleanup) => {
if (!config.autoSync.enabled) {
clearAllTimers()
return
}

logger.info('[auto-sync] setup')

for (const folder of workspace.workspaceFolders ?? [])
addFolder(folder.uri)

const disposable = workspace.onDidChangeWorkspaceFolders((e) => {
logger.info('[auto-sync] workspace folders changed')
for (const added of e.added)
addFolder(added.uri)
for (const removed of e.removed)
removeFolder(removed.uri.path)
})

onCleanup(() => {
disposable.dispose()
clearAllTimers()
logger.info('[auto-sync] cleanup')
})
})
}
9 changes: 9 additions & 0 deletions packages/vscode/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineExtension, useCommand } from 'reactive-vscode'
import { sync } from './commands/sync'
import { useAutoSync } from './composables/auto-sync'
import { commands } from './generated-meta'

export const { activate, deactivate } = defineExtension(() => {
useCommand(commands.sync, sync)
useAutoSync()
})
7 changes: 7 additions & 0 deletions packages/vscode/src/meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { NestedScopedConfigs } from './generated-meta'
import { defineConfig, defineLogger } from 'reactive-vscode'
import { displayName, scopedConfigs } from './generated-meta'

export const config = defineConfig<NestedScopedConfigs>(scopedConfigs.scope)

export const logger = defineLogger(displayName)
33 changes: 33 additions & 0 deletions packages/vscode/src/sync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { SyncSummary } from '../../../src/sync'
import { resolve } from 'pathe'
import { getExecuteFile, resolveConfig } from '../../../src/config/load'
import { resolveRepo } from '../../../src/config/repo'
import { ensureExecuteArtifacts } from '../../../src/execute/schema'
import { syncRepository } from '../../../src/sync'
import { readTokenFromVSCode } from './auth'
import { logger } from './meta'

export interface RunSyncOptions {
cwd: string
}

// See src/cli/commands/sync.ts
export async function runSync(options: RunSyncOptions): Promise<SyncSummary> {
logger.info(`[sync] run at ${Date.now()}`)
const config = await resolveConfig({ cwd: options.cwd })
await ensureExecuteArtifacts(resolve(config.cwd, getExecuteFile(config)))

const repo = await resolveRepo({
cwd: config.cwd,
configRepo: config.repo,
interactive: false,
})

const token = config.auth.token?.trim() ?? await readTokenFromVSCode()

return syncRepository({
config,
repo: repo.repo,
token,
})
}
19 changes: 19 additions & 0 deletions packages/vscode/src/utils/fs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Uri, workspace } from 'vscode'
import { resolveConfig } from '../../../../src/config'

export async function pathExist(uri: Uri) {
try {
await workspace.fs.stat(uri)
return true
}
catch {
return false
}
}

export async function isValidWorkspace(cwd: Uri) {
const config = await resolveConfig({ cwd: cwd.path })
const folder = Uri.joinPath(cwd, config.directory)

return await pathExist(folder)
}
21 changes: 21 additions & 0 deletions packages/vscode/tsdown.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { defineConfig } from 'tsdown'

export default defineConfig({
external: ['vscode'],
/// keep-sorted
inlineOnly: [
'before-after-hook',
'bottleneck',
'fast-content-type-parse',
'jiti',
'json-with-bigint',
'pathe',
'toad-cache',
'universal-github-app-jwt',
'universal-user-agent',
'yaml',
/octokit/,
/reactive-vscode/,
],
minify: 'dce-only',
})
Loading