@boyingliu01/opencode-plugin 0.10.9 → 0.10.11

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 CHANGED
@@ -55,7 +55,7 @@ async function fetchNpmLatestVersion(url: string): Promise<string | null> {
55
55
  }
56
56
  }
57
57
 
58
- function readCache(file: string): { ts: number; remoteVersion: string; status?: string } | null {
58
+ function readCache(file: string): { ts: number; remoteVersion: string; localVersion?: string; status?: string } | null {
59
59
  try {
60
60
  if (!existsSync(file)) return null
61
61
  const raw = readFileSync(file, "utf8")
@@ -100,11 +100,16 @@ function getLocalXpGateVersion(): string | null {
100
100
 
101
101
  async function checkXpGateUpdate(): Promise<UpgradeResult> {
102
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 }
103
+ const localVersion = getLocalXpGateVersion()
104
+
105
+ // If cache exists and local version hasn't changed AND status is "current",
106
+ // the cache is still valid — skip network check
107
+ if (cached?.status === "current" && cached.remoteVersion && localVersion && cached.localVersion === localVersion) {
108
+ return { action: "noop", localVersion, remoteVersion: cached.remoteVersion }
105
109
  }
106
110
 
107
- const localVersion = getLocalXpGateVersion()
111
+ // If we have a valid cache but local version changed (e.g., manual upgrade)
112
+ // or cache has no "current" status, we need to re-check remote
108
113
  if (!localVersion) return { action: "noop", localVersion: null, remoteVersion: null }
109
114
 
110
115
  const remoteVersion = await fetchNpmLatestVersion(XP_GATE_REGISTRY_URL)
@@ -124,17 +129,19 @@ async function checkXpGateUpdate(): Promise<UpgradeResult> {
124
129
 
125
130
  writeCache(XP_GATE_CACHE_FILE, { ts: Date.now(), localVersion, remoteVersion })
126
131
  try {
127
- const child = spawn("npm", ["install", "-g", `${XP_GATE_NPM_PKG}@${remoteVersion}`], {
128
- stdio: "pipe",
129
- timeout: 120_000,
130
- })
131
- child.on("close", (code) => {
132
- if (code === 0) {
133
- writeCache(XP_GATE_CACHE_FILE, { ts: Date.now(), localVersion: remoteVersion, remoteVersion, status: "current" })
134
- }
132
+ const exitCode = await new Promise<number | null>((resolve, reject) => {
133
+ const child = spawn("npm", ["install", "-g", `${XP_GATE_NPM_PKG}@${remoteVersion}`], {
134
+ stdio: "pipe",
135
+ timeout: 120_000,
136
+ })
137
+ child.on("close", (code) => resolve(code))
138
+ child.on("error", (err) => reject(err))
135
139
  })
136
- child.on("error", () => { /* empty — cache won't get status:current, so next check retries */ })
137
- return { action: "upgraded", localVersion, remoteVersion }
140
+ if (exitCode === 0) {
141
+ writeCache(XP_GATE_CACHE_FILE, { ts: Date.now(), localVersion: remoteVersion, remoteVersion, status: "current" })
142
+ return { action: "upgraded", localVersion, remoteVersion }
143
+ }
144
+ return { action: "error", localVersion, remoteVersion, error: `npm install exited with code ${exitCode}` }
138
145
  } catch (err) {
139
146
  const msg = err instanceof Error ? err.message : String(err)
140
147
  return { action: "error", localVersion, remoteVersion, error: msg }
@@ -174,6 +181,22 @@ async function checkPluginUpdate(pluginDir: string): Promise<void> {
174
181
  }
175
182
  }
176
183
 
184
+ // ── TUI upgrade notice ──
185
+
186
+ const UPGRADE_NOTICE_FILE = join(homedir(), ".xp-gate", "upgrade-notice.json")
187
+
188
+ type UpgradeNotice = {
189
+ kind: "upgraded" | "outdated" | "error"
190
+ localVersion: string | null
191
+ remoteVersion: string | null
192
+ message: string
193
+ ts: number
194
+ }
195
+
196
+ function writeUpgradeNotice(notice: UpgradeNotice): void {
197
+ writeCache(UPGRADE_NOTICE_FILE, notice)
198
+ }
199
+
177
200
  // ── Combined background check (runs once on first chat.message) ──
178
201
 
179
202
  async function runBackgroundUpdates(pluginDir: string): Promise<string | null> {
@@ -181,10 +204,19 @@ async function runBackgroundUpdates(pluginDir: string): Promise<string | null> {
181
204
  await checkPluginUpdate(pluginDir)
182
205
 
183
206
  if (result.action === "upgraded") {
184
- return `[XP-Gate] Auto-upgraded from v${result.localVersion} to v${result.remoteVersion}`
207
+ const msg = `[XP-Gate] Auto-upgraded from v${result.localVersion} to v${result.remoteVersion}`
208
+ writeUpgradeNotice({ kind: "upgraded", localVersion: result.localVersion, remoteVersion: result.remoteVersion, message: msg, ts: Date.now() })
209
+ return msg
185
210
  }
186
211
  if (result.action === "error") {
187
- return `[XP-Gate] Upgrade check: v${result.remoteVersion} available (auto-upgrade failed: ${result.error})`
212
+ const msg = `[XP-Gate] Upgrade check: v${result.remoteVersion} available (auto-upgrade failed: ${result.error})`
213
+ writeUpgradeNotice({ kind: "error", localVersion: result.localVersion, remoteVersion: result.remoteVersion, message: msg, ts: Date.now() })
214
+ return msg
215
+ }
216
+ if (result.remoteVersion && result.localVersion && semverLt(result.localVersion, result.remoteVersion)) {
217
+ const msg = `[XP-Gate] New version v${result.remoteVersion} available (you have v${result.localVersion})`
218
+ writeUpgradeNotice({ kind: "outdated", localVersion: result.localVersion, remoteVersion: result.remoteVersion, message: msg, ts: Date.now() })
219
+ return msg
188
220
  }
189
221
  return null
190
222
  }
@@ -289,11 +321,9 @@ export const XpGatePlugin = async (input: OpenCodePluginInput) => {
289
321
  "chat.message": async (_input: { message: string }) => {
290
322
  if (!checked) {
291
323
  checked = true
292
- runBackgroundUpdates(directory).then((msg) => {
293
- if (msg) process.stderr.write(`${msg}\n`)
294
- })
324
+ const msg = await runBackgroundUpdates(directory).catch(() => null)
325
+ if (msg) process.stderr.write(`${msg}\n`)
295
326
  }
296
- return { action: "continue" }
297
327
  },
298
328
  }
299
329
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/opencode-plugin",
3
- "version": "0.10.9",
3
+ "version": "0.10.11",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "XP-Gate quality gates + AI workflow skills + Sprint Flow TUI sidebar for OpenCode",
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-06-23
4
- **Commit:** 34d5784
4
+ **Commit:** bac916f
5
5
  **Branch:** main
6
- **Version:** 0.10.8.0
6
+ **Version:** 0.10.11.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, 3 experts from ≥2 providers, domestic models only). Supports design + code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-06-23
4
- **Commit:** 34d5784
4
+ **Commit:** bac916f
5
5
  **Branch:** main
6
- **Version:** 0.10.8.0
6
+ **Version:** 0.10.11.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **11-phase** development pipeline: ISOLATE → AUTO-ESTIMATE → THINK → PLAN → BUILD → REVIEW → USER ACCEPTANCE → FEEDBACK → SHIP → LAND → CLEANUP. Phase 2 default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE in Phase 1: design must pass Delphi review (≥90% consensus) before any coding.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-06-23
4
- **Commit:** 34d5784
4
+ **Commit:** bac916f
5
5
  **Branch:** main
6
- **Version:** 0.10.8.0
6
+ **Version:** 0.10.11.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
package/tui-plugin.ts CHANGED
@@ -17,6 +17,7 @@
17
17
 
18
18
  import { existsSync, readFileSync, readdirSync } from "node:fs"
19
19
  import { join, dirname, resolve, parse } from "node:path"
20
+ import { homedir } from "node:os"
20
21
  import type { TuiPlugin, TuiSlotPlugin, TuiSlotProps } from "@opencode-ai/plugin/tui"
21
22
 
22
23
  // ── Phase constants (inlined from ../../src/npm-package/lib/shared-phase-constants.js) ──
@@ -174,7 +175,7 @@ function discoverActiveSprints(dir: string): DiscoveredSprint[] {
174
175
 
175
176
  // ── Cache (module-level, 5s TTL) ──
176
177
 
177
- let _cache: { data: DiscoveredSprint[]; ts: number; dir: string } | null = null;
178
+ let _cache: { data: DiscoveredSprint[]; upgradeNotice: string | null; ts: number; dir: string } | null = null;
178
179
  const CACHE_TTL_MS = 5_000;
179
180
 
180
181
  // ── Helpers ──
@@ -328,6 +329,45 @@ function renderMultiSprintSidebar(sprints: DiscoveredSprint[]): string | null {
328
329
  return blocks.join("\n---\n");
329
330
  }
330
331
 
332
+ function renderContent(sprints: DiscoveredSprint[], upgradeNotice: string | null): string | null {
333
+ const sprintContent = renderMultiSprintSidebar(sprints)
334
+ if (upgradeNotice && sprintContent) return `${upgradeNotice}\n---\n${sprintContent}`
335
+ if (upgradeNotice) return upgradeNotice
336
+ return sprintContent
337
+ }
338
+
339
+ // ── Upgrade notice ──
340
+
341
+ const UPGRADE_NOTICE_FILE = join(homedir(), ".xp-gate", "upgrade-notice.json")
342
+ const UPGRADE_NOTICE_TTL_MS = 86_400_000 // 24h
343
+
344
+ type UpgradeNotice = {
345
+ kind: string
346
+ localVersion: string | null
347
+ remoteVersion: string | null
348
+ message: string
349
+ ts: number
350
+ }
351
+
352
+ function readUpgradeNotice(): UpgradeNotice | null {
353
+ try {
354
+ if (!existsSync(UPGRADE_NOTICE_FILE)) return null
355
+ const raw = readFileSync(UPGRADE_NOTICE_FILE, "utf8")
356
+ const data = JSON.parse(raw) as UpgradeNotice
357
+ if (Date.now() - data.ts < UPGRADE_NOTICE_TTL_MS && data.message) return data
358
+ return null
359
+ } catch {
360
+ return null
361
+ }
362
+ }
363
+
364
+ function renderUpgradeNotice(): string | null {
365
+ const notice = readUpgradeNotice()
366
+ if (!notice) return null
367
+ const icon = notice.kind === "upgraded" ? "✓" : notice.kind === "outdated" ? "↑" : "⚠"
368
+ return `${icon} ${notice.message}`
369
+ }
370
+
331
371
  // ── TUI Slot Plugin ──
332
372
 
333
373
  const tuiPlugin: TuiSlotPlugin = {
@@ -338,12 +378,13 @@ const tuiPlugin: TuiSlotPlugin = {
338
378
 
339
379
  // Use cache if still valid for current directory
340
380
  if (_cache && _cache.dir === dir && now - _cache.ts < CACHE_TTL_MS) {
341
- return renderMultiSprintSidebar(_cache.data);
381
+ return renderContent(_cache.data, _cache.upgradeNotice);
342
382
  }
343
383
 
344
384
  const sprints = discoverActiveSprints(dir);
345
- _cache = { data: sprints, ts: now, dir };
346
- return renderMultiSprintSidebar(sprints);
385
+ const upgradeNotice = renderUpgradeNotice()
386
+ _cache = { data: sprints, ts: now, dir, upgradeNotice };
387
+ return renderContent(sprints, upgradeNotice);
347
388
  },
348
389
  },
349
390
  }