@boyingliu01/opencode-plugin 0.9.0 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +158 -75
- package/package.json +14 -3
- package/skills/admin-template-guidelines/SKILL.md +6 -797
- package/skills/admin-template-guidelines/references/rule-1.md +91 -0
- package/skills/admin-template-guidelines/references/rule-2.md +155 -0
- package/skills/admin-template-guidelines/references/rule-3.md +70 -0
- package/skills/admin-template-guidelines/references/rule-4.md +154 -0
- package/skills/admin-template-guidelines/references/rule-5.md +126 -0
- package/skills/admin-template-guidelines/references/rule-6.md +180 -0
- package/skills/delphi-review/AGENTS.md +2 -2
- package/skills/delphi-review/SKILL.md +59 -361
- package/skills/delphi-review/references/orchestrator-dispatch.md +64 -0
- package/skills/delphi-review/references/round-templates.md +67 -0
- package/skills/ralph-loop/SKILL.md +87 -429
- package/skills/ralph-loop/references/components.md +113 -0
- package/skills/ralph-loop/references/workflow-details.md +99 -0
- package/skills/sprint-flow/AGENTS.md +2 -2
- package/skills/test-specification-alignment/AGENTS.md +2 -2
- package/skills/test-specification-alignment/SKILL.md +97 -599
- package/tsconfig.json +1 -1
- package/tui-plugin.ts +188 -0
- package/skills/ralph-loop/references/components/memory.md +0 -55
- package/skills/ralph-loop/references/components/middleware.md +0 -54
- package/skills/ralph-loop/references/components/skill-invocations.md +0 -39
- package/skills/ralph-loop/references/components/system-prompt.md +0 -24
- package/skills/ralph-loop/references/components/tool-descriptions.md +0 -32
package/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin"
|
|
2
2
|
import { z } from "zod"
|
|
3
|
-
import { exec } from "child_process"
|
|
4
|
-
import { promisify } from "util"
|
|
5
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"
|
|
3
|
+
import { exec, execSync } from "node:child_process"
|
|
4
|
+
import { promisify } from "node:util"
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from "node:fs"
|
|
6
6
|
import { join } from "node:path"
|
|
7
7
|
import { homedir } from "node:os"
|
|
8
8
|
|
|
@@ -13,6 +13,158 @@ interface OpenCodePluginInput {
|
|
|
13
13
|
$: (strings: TemplateStringsArray, ...values: unknown[]) => Promise<{ text(): Promise<string> }>
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
// ── Constants ──
|
|
17
|
+
|
|
18
|
+
const CACHE_TTL_MS = 86_400_000 // 24h
|
|
19
|
+
const FETCH_TIMEOUT_MS = 5_000
|
|
20
|
+
|
|
21
|
+
const XP_GATE_NPM_PKG = "@boyingliu01/xp-gate"
|
|
22
|
+
const XP_GATE_CACHE_FILE = join(homedir(), ".xp-gate", "xp-gate-version-check.json")
|
|
23
|
+
const XP_GATE_REGISTRY_URL = `https://registry.npmjs.org/-/package/${encodeURIComponent(XP_GATE_NPM_PKG)}/dist-tags`
|
|
24
|
+
|
|
25
|
+
const OPENCODE_PLUGIN_REGISTRY = "https://registry.npmjs.org/-/package/@boyingliu01%2Fopencode-plugin/dist-tags"
|
|
26
|
+
const OPENCODE_CACHE_FILE = join(homedir(), ".xp-gate", "opencode-plugin-version-check.json")
|
|
27
|
+
|
|
28
|
+
let checked = false
|
|
29
|
+
|
|
30
|
+
// ── Utilities ──
|
|
31
|
+
|
|
32
|
+
function semverLt(a: string, b: string): boolean {
|
|
33
|
+
const pa = a.replace(/^v/, "").split(".").map(Number)
|
|
34
|
+
const pb = b.replace(/^v/, "").split(".").map(Number)
|
|
35
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
36
|
+
const na = pa[i] ?? 0
|
|
37
|
+
const nb = pb[i] ?? 0
|
|
38
|
+
if (na !== nb) return na < nb
|
|
39
|
+
}
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function fetchNpmLatestVersion(url: string): Promise<string | null> {
|
|
44
|
+
const controller = new AbortController()
|
|
45
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetch(url, { signal: controller.signal })
|
|
48
|
+
if (!response.ok) return null
|
|
49
|
+
const data: Record<string, unknown> = await response.json()
|
|
50
|
+
return typeof data.latest === "string" ? data.latest : null
|
|
51
|
+
} catch {
|
|
52
|
+
return null
|
|
53
|
+
} finally {
|
|
54
|
+
clearTimeout(timer)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readCache(file: string): { ts: number; remoteVersion: string; status?: string } | null {
|
|
59
|
+
try {
|
|
60
|
+
if (!existsSync(file)) return null
|
|
61
|
+
const raw = readFileSync(file, "utf8")
|
|
62
|
+
const data = JSON.parse(raw)
|
|
63
|
+
if (Date.now() - data.ts < CACHE_TTL_MS && data.remoteVersion) return data
|
|
64
|
+
return null
|
|
65
|
+
} catch {
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function writeCache(file: string, data: object): void {
|
|
71
|
+
try {
|
|
72
|
+
mkdirSync(join(homedir(), ".xp-gate"), { recursive: true })
|
|
73
|
+
writeFileSync(file + ".tmp", JSON.stringify(data), "utf8")
|
|
74
|
+
try { rmSync(file) } catch {}
|
|
75
|
+
writeFileSync(file, readFileSync(file + ".tmp", "utf8"), "utf8")
|
|
76
|
+
try { rmSync(file + ".tmp") } catch {}
|
|
77
|
+
} catch {
|
|
78
|
+
// silent
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── XP-Gate npm package auto-update ──
|
|
83
|
+
|
|
84
|
+
type UpgradeResult = {
|
|
85
|
+
action: "noop" | "upgraded" | "error"
|
|
86
|
+
localVersion: string | null
|
|
87
|
+
remoteVersion: string | null
|
|
88
|
+
error?: string
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getLocalXpGateVersion(): string | null {
|
|
92
|
+
try {
|
|
93
|
+
const globalRoot = execSync("npm root -g", { encoding: "utf8" }).trim()
|
|
94
|
+
const pkg = JSON.parse(readFileSync(join(globalRoot, XP_GATE_NPM_PKG, "package.json"), "utf8"))
|
|
95
|
+
return pkg.version || null
|
|
96
|
+
} catch {
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function checkXpGateUpdate(): Promise<UpgradeResult> {
|
|
102
|
+
const cached = readCache(XP_GATE_CACHE_FILE)
|
|
103
|
+
if (cached?.status === "current" && cached.remoteVersion) {
|
|
104
|
+
return { action: "noop", localVersion: cached.remoteVersion, remoteVersion: cached.remoteVersion }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const localVersion = getLocalXpGateVersion()
|
|
108
|
+
if (!localVersion) return { action: "noop", localVersion: null, remoteVersion: null }
|
|
109
|
+
|
|
110
|
+
const remoteVersion = await fetchNpmLatestVersion(XP_GATE_REGISTRY_URL)
|
|
111
|
+
if (!remoteVersion) return { action: "noop", localVersion, remoteVersion: null }
|
|
112
|
+
|
|
113
|
+
if (!semverLt(localVersion, remoteVersion)) {
|
|
114
|
+
writeCache(XP_GATE_CACHE_FILE, { ts: Date.now(), localVersion, remoteVersion, status: "current" })
|
|
115
|
+
return { action: "noop", localVersion, remoteVersion }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
writeCache(XP_GATE_CACHE_FILE, { ts: Date.now(), localVersion, remoteVersion })
|
|
119
|
+
try {
|
|
120
|
+
execSync(`npm install -g ${XP_GATE_NPM_PKG}@${remoteVersion}`, { stdio: "pipe", timeout: 120_000 })
|
|
121
|
+
writeCache(XP_GATE_CACHE_FILE, { ts: Date.now(), localVersion: remoteVersion, remoteVersion, status: "current" })
|
|
122
|
+
return { action: "upgraded", localVersion, remoteVersion }
|
|
123
|
+
} catch (err) {
|
|
124
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
125
|
+
return { action: "error", localVersion, remoteVersion, error: msg }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── OpenCode plugin version check (notification only) ──
|
|
130
|
+
|
|
131
|
+
async function checkPluginUpdate(pluginDir: string): Promise<void> {
|
|
132
|
+
const cached = readCache(OPENCODE_CACHE_FILE)
|
|
133
|
+
if (cached?.status === "current" && cached.remoteVersion) return
|
|
134
|
+
|
|
135
|
+
let localVersion = ""
|
|
136
|
+
try {
|
|
137
|
+
const pkg = JSON.parse(readFileSync(join(pluginDir, "package.json"), "utf8"))
|
|
138
|
+
localVersion = pkg.version || ""
|
|
139
|
+
} catch {
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const remoteVersion = await fetchNpmLatestVersion(OPENCODE_PLUGIN_REGISTRY)
|
|
144
|
+
if (!remoteVersion) return
|
|
145
|
+
|
|
146
|
+
if (remoteVersion && localVersion && semverLt(localVersion, remoteVersion)) {
|
|
147
|
+
writeCache(OPENCODE_CACHE_FILE, { ts: Date.now(), localVersion, remoteVersion })
|
|
148
|
+
} else if (remoteVersion && localVersion) {
|
|
149
|
+
writeCache(OPENCODE_CACHE_FILE, { ts: Date.now(), localVersion, remoteVersion, status: "current" })
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── Combined background check (runs once on first chat.message) ──
|
|
154
|
+
|
|
155
|
+
async function runBackgroundUpdates(pluginDir: string): Promise<string | null> {
|
|
156
|
+
const result = await checkXpGateUpdate()
|
|
157
|
+
await checkPluginUpdate(pluginDir)
|
|
158
|
+
|
|
159
|
+
if (result.action === "upgraded") {
|
|
160
|
+
return `[XP-Gate] Auto-upgraded from v${result.localVersion} to v${result.remoteVersion}`
|
|
161
|
+
}
|
|
162
|
+
if (result.action === "error") {
|
|
163
|
+
return `[XP-Gate] Upgrade check: v${result.remoteVersion} available (auto-upgrade failed: ${result.error})`
|
|
164
|
+
}
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
|
|
16
168
|
async function runCmd(cmd: string, cwd: string): Promise<string> {
|
|
17
169
|
try {
|
|
18
170
|
const { stdout } = await execAsync(cmd, { cwd, timeout: 30000 })
|
|
@@ -49,77 +201,6 @@ async function getUpgradeSuggestion(cwd: string): Promise<string> {
|
|
|
49
201
|
}
|
|
50
202
|
}
|
|
51
203
|
|
|
52
|
-
// ── Auto-update check for opencode-plugin ──
|
|
53
|
-
|
|
54
|
-
const CACHE_TTL_MS = 86_400_000
|
|
55
|
-
const NPM_REGISTRY_URL = "https://registry.npmjs.org/-/package/@boyingliu01%2Fopencode-plugin/dist-tags"
|
|
56
|
-
const FETCH_TIMEOUT_MS = 5_000
|
|
57
|
-
const CACHE_FILE = join(homedir(), ".xp-gate", "opencode-plugin-version-check.json")
|
|
58
|
-
|
|
59
|
-
let checked = false
|
|
60
|
-
let checkInFlight: Promise<void> | null = null
|
|
61
|
-
|
|
62
|
-
function semverLt(a: string, b: string): boolean {
|
|
63
|
-
const pa = a.replace(/^v/, "").split(".").map(Number)
|
|
64
|
-
const pb = b.replace(/^v/, "").split(".").map(Number)
|
|
65
|
-
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
66
|
-
const na = pa[i] ?? 0
|
|
67
|
-
const nb = pb[i] ?? 0
|
|
68
|
-
if (na !== nb) return na < nb
|
|
69
|
-
}
|
|
70
|
-
return false
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function checkPluginUpdate(pluginDir: string): Promise<void> {
|
|
74
|
-
if (checkInFlight) return
|
|
75
|
-
|
|
76
|
-
checkInFlight = (async () => {
|
|
77
|
-
try {
|
|
78
|
-
mkdirSync(join(homedir(), ".xp-gate"), { recursive: true })
|
|
79
|
-
|
|
80
|
-
if (existsSync(CACHE_FILE)) {
|
|
81
|
-
const cached = JSON.parse(readFileSync(CACHE_FILE, "utf8"))
|
|
82
|
-
if (Date.now() - cached.ts < CACHE_TTL_MS) return
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
let localVersion = ""
|
|
86
|
-
try {
|
|
87
|
-
const pkg = JSON.parse(readFileSync(join(pluginDir, "package.json"), "utf8"))
|
|
88
|
-
localVersion = pkg.version || ""
|
|
89
|
-
} catch {
|
|
90
|
-
return
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const controller = new AbortController()
|
|
94
|
-
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
|
|
95
|
-
try {
|
|
96
|
-
const response = await fetch(NPM_REGISTRY_URL, { signal: controller.signal })
|
|
97
|
-
if (!response.ok) return
|
|
98
|
-
const data: Record<string, unknown> = await response.json()
|
|
99
|
-
const remoteVersion = String(data.latest || "")
|
|
100
|
-
|
|
101
|
-
if (remoteVersion && localVersion && semverLt(localVersion, remoteVersion)) {
|
|
102
|
-
writeFileSync(CACHE_FILE, JSON.stringify({ ts: Date.now(), localVersion, remoteVersion }))
|
|
103
|
-
process.stderr.write(
|
|
104
|
-
`[XP-Gate] New opencode-plugin version v${remoteVersion} available (you have v${localVersion})\n` +
|
|
105
|
-
`[XP-Gate] Update with: cd ~/.config/opencode && npm update @boyingliu01/opencode-plugin\n`
|
|
106
|
-
)
|
|
107
|
-
} else if (remoteVersion && localVersion) {
|
|
108
|
-
// Cache "up to date" to avoid re-fetching every session
|
|
109
|
-
writeFileSync(CACHE_FILE, JSON.stringify({ ts: Date.now(), localVersion, remoteVersion, status: "current" }))
|
|
110
|
-
}
|
|
111
|
-
} finally {
|
|
112
|
-
clearTimeout(timer)
|
|
113
|
-
}
|
|
114
|
-
} catch {
|
|
115
|
-
// All errors silently ignored
|
|
116
|
-
}
|
|
117
|
-
})()
|
|
118
|
-
|
|
119
|
-
await checkInFlight
|
|
120
|
-
checkInFlight = null
|
|
121
|
-
}
|
|
122
|
-
|
|
123
204
|
// ── Plugin definition ──
|
|
124
205
|
|
|
125
206
|
export const XpGatePlugin = async (input: OpenCodePluginInput) => {
|
|
@@ -184,7 +265,9 @@ export const XpGatePlugin = async (input: OpenCodePluginInput) => {
|
|
|
184
265
|
"chat.message": async (_input: { message: string }) => {
|
|
185
266
|
if (!checked) {
|
|
186
267
|
checked = true
|
|
187
|
-
|
|
268
|
+
runBackgroundUpdates(directory).then((msg) => {
|
|
269
|
+
if (msg) process.stderr.write(`${msg}\n`)
|
|
270
|
+
})
|
|
188
271
|
}
|
|
189
272
|
return { action: "continue" }
|
|
190
273
|
},
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boyingliu01/opencode-plugin",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
|
-
"description": "XP-Gate quality gates + AI workflow skills for OpenCode",
|
|
6
|
+
"description": "XP-Gate quality gates + AI workflow skills + Sprint Flow TUI sidebar for OpenCode",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/boyingliu01/xp-gate",
|
|
@@ -13,8 +13,19 @@
|
|
|
13
13
|
"registry": "https://registry.npmjs.org",
|
|
14
14
|
"access": "public"
|
|
15
15
|
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"import": "./index.ts",
|
|
19
|
+
"types": "./index.ts"
|
|
20
|
+
},
|
|
21
|
+
"./tui": {
|
|
22
|
+
"import": "./tui-plugin.ts",
|
|
23
|
+
"types": "./tui-plugin.ts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
16
26
|
"files": [
|
|
17
27
|
"index.ts",
|
|
28
|
+
"tui-plugin.ts",
|
|
18
29
|
"skills/",
|
|
19
30
|
"tsconfig.json",
|
|
20
31
|
"README.md"
|
|
@@ -24,7 +35,7 @@
|
|
|
24
35
|
"check": "tsc --noEmit"
|
|
25
36
|
},
|
|
26
37
|
"dependencies": {
|
|
27
|
-
"@opencode-ai/plugin": "^1.
|
|
38
|
+
"@opencode-ai/plugin": "^1.17.8"
|
|
28
39
|
},
|
|
29
40
|
"devDependencies": {
|
|
30
41
|
"typescript": "^5.4.0"
|