feat(airi-plugin-vscode): publish workflow

This commit is contained in:
Neko Ayaka
2025-12-02 02:46:15 +08:00
parent 48c7c1f6b9
commit 86d62a91c2
15 changed files with 325 additions and 57 deletions
+8
View File
@@ -0,0 +1,8 @@
.github/**
.vscode/**
.vscode-test/**
scripts/**
src/**
test/**
tsconfig.json
tsdown.config.ts
+3
View File
@@ -0,0 +1,3 @@
# AIRI VSCode Plugin
> Official VSCode extension for AIRI, streaming your current working at stuff back to AIRI.
+24 -13
View File
@@ -1,14 +1,21 @@
{
"publisher": "airi",
"publisher": "proj-airi",
"name": "@proj-airi/airi-plugin-vscode",
"displayName": "AIRI",
"version": "0.8.0-alpha.6",
"private": true,
"description": "VSCode extension that shares your coding context with AIRI",
"repository": {
"type": "git",
"url": "https://github.com/moeru-ai/airi.git",
"directory": "plugins/airi-plugin-vscode"
},
"categories": [
"Other"
],
"main": "./dist/extension.js",
"preview": true,
"main": "./dist/extension.cjs",
"icon": "res/logo.jpg",
"engines": {
"vscode": "^1.106.1"
},
@@ -18,32 +25,32 @@
"contributes": {
"commands": [
{
"command": "airi.enable",
"command": "airi-vscode.enable",
"title": "AIRI: Enable"
},
{
"command": "airi.disable",
"command": "airi-vscode.disable",
"title": "AIRI: Disable"
},
{
"command": "airi.status",
"command": "airi-vscode.status",
"title": "AIRI: Show Status"
}
],
"configuration": {
"title": "AIRI",
"properties": {
"airi.enabled": {
"airi-vscode.enabled": {
"type": "boolean",
"default": true,
"description": "Enable AIRI companion"
"description": "Enable or disable the AIRI extension"
},
"airi.contextLines": {
"airi-vscode.contextLines": {
"type": "number",
"default": 5,
"description": "Number of context lines to send (before and after current line)"
},
"airi.sendInterval": {
"airi-vscode.sendInterval": {
"type": "number",
"default": 3000,
"description": "Interval in milliseconds to send updates (0 for real-time)"
@@ -52,9 +59,11 @@
}
},
"scripts": {
"vscode:prepublish": "pnpm run build",
"build": "tsdown",
"prepare": "pnpm run update",
"update": "tsx ./scripts/vscode-ext-gen.ts",
"dev": "tsdown --watch",
"build": "tsdown",
"publish": "tsx ./scripts/publish.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
@@ -62,6 +71,8 @@
"@proj-airi/server-sdk": "workspace:*"
},
"devDependencies": {
"@types/vscode": "^1.106.1"
"@types/vscode": "^1.106.1",
"tinyexec": "^1.0.2",
"vscode-ext-gen": "^1.4.0"
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

@@ -0,0 +1,9 @@
// https://github.com/unocss/unocss/blob/dba521e377887ed2b3b38dc86f36d4f292230ac8/packages-integrations/vscode/scripts/dev.ts
import { packageJSONForVSCode } from './shared'
async function run() {
await packageJSONForVSCode('airi-vscode')
}
run()
@@ -0,0 +1,58 @@
// https://github.com/unocss/unocss/blob/dba521e377887ed2b3b38dc86f36d4f292230ac8/packages-integrations/vscode/scripts/publish.ts
import process from 'node:process'
import { copyFile, readFile, rm, writeFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { exec } from 'tinyexec'
import { packageJSONForVSCode } from './shared'
const dir = typeof __dirname === 'string' ? __dirname : dirname(fileURLToPath(import.meta.url))
const root = dirname(dir)
async function publish() {
const { restore, isPreview } = await packageJSONForVSCode('airi-vscode')
const pkgPath = join(root, 'package.json')
const rawJSON = await readFile(pkgPath, 'utf-8')
const pkg = JSON.parse(rawJSON)
if (isPreview)
pkg.preview = true
else
delete pkg.preview
await writeFile(pkgPath, JSON.stringify(pkg, null, 2), 'utf-8')
await rm(join(root, 'LICENSE'), { force: true }).catch(() => {})
await copyFile(join(root, '..', '..', 'LICENSE'), join(root, 'LICENSE'))
try {
{
// eslint-disable-next-line no-console
console.log('\nPublish to VSCE...\n')
const execPublish = exec('pnpx', ['@vscode/vsce', 'publish', '--no-dependencies', '-p', process.env.VSCE_TOKEN!, ...[(isPreview ? '--pre-release' : '')]], { nodeOptions: { cwd: root } })
for await (const line of execPublish) {
// eslint-disable-next-line no-console
console.log(line)
}
}
{
// eslint-disable-next-line no-console
console.log('\nPublish to OVSE...\n')
const execPublish = exec('pnpx', ['ovsx', 'publish', '--no-dependencies', '-p', process.env.OVSX_TOKEN!, ...[(isPreview ? '--pre-release' : '')]], { nodeOptions: { cwd: root } })
for await (const line of execPublish) {
// eslint-disable-next-line no-console
console.log(line)
}
}
}
finally {
await restore()
}
}
publish()
@@ -0,0 +1,93 @@
// https://github.com/unocss/unocss/blob/dba521e377887ed2b3b38dc86f36d4f292230ac8/packages-integrations/vscode/scripts/dev.ts
import { readFile, writeFile } from 'node:fs/promises'
export async function packageJSONForVSCode(name: string) {
const json = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf-8'))
const originalName = json.name
const originalVersion = json.version
if (json.name !== name) {
json.name = name
await writeFile(new URL('../package.json', import.meta.url), JSON.stringify(json, null, 2), 'utf-8')
}
const numericVersion = encodeNumericVersion(originalVersion)
if (json.version !== numericVersion.version) {
json.version = numericVersion.version
await writeFile(new URL('../package.json', import.meta.url), JSON.stringify(json, null, 2), 'utf-8')
}
return {
originalName,
originalVersion,
name,
version: numericVersion.version,
isPreview: numericVersion.preview,
restore: async () => {
json.name = originalName
json.version = originalVersion
await writeFile(new URL('../package.json', import.meta.url), JSON.stringify(json, null, 2), 'utf-8')
},
}
}
// NOTICE: VSCE rejects prerelease identifiers, so we encode stage+sequence into a numeric-only patch bucket:
// encodedPatch = patch*10000 + stageBucket + sequence.
// stageBucket: alpha=1000, beta=2000, rc=3000, stable=9000.
// Examples:
// 0.8.0-alpha.6 -> 0.8.(0*10000+1000+6)=0.8.1006 (preview=true)
// 0.8.0-beta.1 -> 0.8.2001 (preview=true)
// 0.8.0 -> 0.8.(0*10000+9000)=0.8.9000 (preview=false)
// This keeps ordering: alpha < beta < rc < stable. Unknown prerelease tags default to alpha.
export function encodeNumericVersion(version: string) {
const match = version.match(/^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(-(?<pre>[0-9A-Z.-]+))?$/i)
if (!match || !match.groups)
throw new Error(`Invalid semver: ${version}`)
const major = Number.parseInt(match.groups.major, 10)
const minor = Number.parseInt(match.groups.minor, 10)
const patch = Number.parseInt(match.groups.patch, 10)
const prerelease = match.groups.pre
const multiplier = 10_000
const stageBuckets = {
alpha: 1_000,
beta: 2_000,
rc: 3_000,
stable: 9_000,
} as const
const { stage, sequence } = parsePrerelease(prerelease)
const maxSequence = (multiplier - 1) - stageBuckets[stage]
if (sequence > maxSequence) {
throw new Error(`Prerelease sequence overflow for ${stage}: ${sequence} exceeds limit ${maxSequence}`)
}
if (sequence < 0) {
throw new Error(`Prerelease sequence must be non-negative: ${sequence}`)
}
const encodedPatch = (patch * multiplier) + (stageBuckets[stage] ?? stageBuckets.alpha) + sequence
const encoded = `${major}.${minor}.${encodedPatch}`
return {
version: encoded,
preview: stage !== 'stable',
}
}
function parsePrerelease(prerelease?: string) {
if (!prerelease) {
return { stage: 'stable' as const, sequence: 0 }
}
const [stageRaw, sequenceRaw] = prerelease.split('.')
const stage = stageRaw === 'beta' || stageRaw === 'rc' || stageRaw === 'alpha' ? stageRaw : 'alpha'
const sequenceParsed = Number.parseInt(sequenceRaw ?? '', 10)
const sequence = Number.isFinite(sequenceParsed) ? sequenceParsed : 0
return { stage, sequence }
}
@@ -0,0 +1,19 @@
import { dirname } from 'node:path'
import { exec } from 'tinyexec'
import { packageJSONForVSCode } from './shared'
async function run() {
const { restore, name } = await packageJSONForVSCode('airi-vscode')
const execGen = exec('pnpm', ['-F', name, 'exec', 'vscode-ext-gen', '--scope=unocss', '--output', 'src/generated/meta.ts'], { nodeOptions: { cwd: dirname(new URL('../', import.meta.url).pathname) } })
for await (const line of execGen) {
// eslint-disable-next-line no-console
console.log(line)
}
await restore()
}
run()
@@ -1,20 +1,20 @@
import type { AiriEvent } from './types'
import type { Events } from './types'
import { useLogger } from '@guiiai/logg'
import { Client } from '@proj-airi/server-sdk'
import { Client as ServerClient } from '@proj-airi/server-sdk'
/**
* Airi Channel Server Client
*/
export class AiriClient {
private client: Client<AiriEvent> | null = null
export class Client {
private client: ServerClient<Events> | null = null
/**
* Connect to Channel Server
*/
async connect(): Promise<boolean> {
try {
this.client = new Client({ name: 'proj-airi:plugin-vscode' })
this.client = new ServerClient({ name: 'proj-airi:plugin-vscode' })
useLogger().log('Airi companion connected to Channel Server')
return true
@@ -39,7 +39,7 @@ export class AiriClient {
/**
* Send event to Airi
*/
sendEvent(event: AiriEvent): void {
sendEvent(event: Events): void {
if (!this.client) {
useLogger().warn('Cannot send event: not connected to Airi Channel Server')
return
+23 -23
View File
@@ -2,10 +2,10 @@ import type * as vscode from 'vscode'
import { initLogger, LoggerFormat, LoggerLevel, useLogger } from '@guiiai/logg'
import { AiriClient } from './airi-client'
import { Client } from './airi-client'
import { ContextCollector } from './context-collector'
let airiClient: AiriClient
let client: Client
let contextCollector: ContextCollector
let updateTimer: NodeJS.Timeout | null = null
let isEnabled = true
@@ -19,48 +19,48 @@ export async function activate(context: vscode.ExtensionContext) {
const { window, workspace, commands } = await import('vscode')
useLogger().log('Airi Companion is activating...')
useLogger().log('AIRI is activating...')
// Get the configuration
const config = workspace.getConfiguration('airi.companion')
const config = workspace.getConfiguration('airi-vscode')
isEnabled = config.get<boolean>('enabled', true)
const contextLines = config.get<number>('contextLines', 5)
const sendInterval = config.get<number>('sendInterval', 3000)
// Initialize
airiClient = new AiriClient()
client = new Client()
contextCollector = new ContextCollector(contextLines)
// Connect to Airi Channel Server
if (isEnabled) {
const connected = await airiClient.connect()
const connected = await client.connect()
if (connected) {
window.showInformationMessage('Airi Companion connected!')
window.showInformationMessage('AIRI server channel connected!')
}
else {
window.showWarningMessage('Airi Companion failed to connect to server')
window.showWarningMessage('AIRI server channel connection failed!')
}
}
// Register commands
context.subscriptions.push(
commands.registerCommand('airi.companion.enable', async () => {
commands.registerCommand('airi-vscode.enable', async () => {
isEnabled = true
await airiClient.connect()
await client.connect()
await registerListeners(sendInterval)
window.showInformationMessage('Airi Companion enabled')
window.showInformationMessage('AIRI enabled!')
}),
commands.registerCommand('airi.companion.disable', () => {
commands.registerCommand('airi-vscode.disable', () => {
isEnabled = false
unregisterListeners()
airiClient.disconnect()
window.showInformationMessage('Airi Companion disabled')
client.disconnect()
window.showInformationMessage('AIRI disabled!')
}),
commands.registerCommand('airi.companion.status', () => {
const status = isEnabled && airiClient ? 'Connected' : 'Disconnected'
window.showInformationMessage(`Airi Companion Status: ${status}`)
commands.registerCommand('airi-vscode.status', () => {
const status = isEnabled && client ? 'Connected' : 'Disconnected'
window.showInformationMessage(`AIRI server channel status: ${status}.`)
}),
)
@@ -69,7 +69,7 @@ export async function activate(context: vscode.ExtensionContext) {
await registerListeners(sendInterval)
}
useLogger().log('Airi Companion activated successfully')
useLogger().log('AIRI activated successfully')
}
/**
@@ -87,7 +87,7 @@ async function registerListeners(sendInterval: number) {
if (editor && editor.document === document) {
const ctx = await contextCollector.collect(editor)
if (ctx) {
airiClient.sendEvent({
client.sendEvent({
type: 'coding:save',
data: ctx,
})
@@ -102,7 +102,7 @@ async function registerListeners(sendInterval: number) {
if (editor) {
const ctx = await contextCollector.collect(editor)
if (ctx) {
airiClient.sendEvent({
client.sendEvent({
type: 'coding:switch-file',
data: ctx,
})
@@ -143,7 +143,7 @@ function startMonitoring(interval: number) {
const ctx = await contextCollector.collect(editor)
if (ctx) {
airiClient.sendEvent({
client.sendEvent({
type: 'coding:context',
data: ctx,
})
@@ -166,6 +166,6 @@ function stopMonitoring() {
*/
export function deactivate() {
unregisterListeners()
airiClient?.disconnect()
useLogger().log('Airi Companion deactivated')
client?.disconnect()
useLogger().log('AIRI deactivated!')
}
+1 -1
View File
@@ -42,7 +42,7 @@ export interface CodingContext {
/**
* Event types sent to Airi
*/
export interface AiriEvent {
export interface Events {
type: 'coding:context' | 'coding:save' | 'coding:switch-file'
data: CodingContext
}