@boyingliu01/xp-gate 0.12.2 → 0.12.4

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.
Files changed (54) hide show
  1. package/adapters/cpp.sh +0 -0
  2. package/adapters/dart.sh +0 -0
  3. package/adapters/flutter.sh +0 -0
  4. package/adapters/go.sh +0 -0
  5. package/adapters/java.sh +0 -0
  6. package/adapters/kotlin.sh +0 -0
  7. package/adapters/objectivec.sh +0 -0
  8. package/adapters/powershell.sh +0 -0
  9. package/adapters/python.sh +0 -0
  10. package/adapters/shell.sh +0 -0
  11. package/adapters/swift.sh +0 -0
  12. package/adapters/typescript.sh +0 -0
  13. package/bin/xp-gate.js +19 -5
  14. package/gate-3.sh +0 -0
  15. package/gate-4.sh +0 -0
  16. package/gate-7.sh +0 -0
  17. package/gate-8.sh +0 -0
  18. package/gate-9.sh +0 -0
  19. package/lib/__tests__/update-hooks.test.js +573 -0
  20. package/lib/init.js +9 -6
  21. package/lib/sprint-status.js +35 -1
  22. package/lib/update-hooks.js +288 -0
  23. package/mock-policy/AGENTS.md +3 -3
  24. package/mutation/AGENTS.md +3 -3
  25. package/package.json +1 -1
  26. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  27. package/plugins/claude-code/skills/delphi-review/AGENTS.md +6 -6
  28. package/plugins/claude-code/skills/delphi-review/SKILL.md +34 -12
  29. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  30. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  31. package/plugins/opencode/package.json +1 -1
  32. package/plugins/opencode/skills/delphi-review/AGENTS.md +6 -6
  33. package/plugins/opencode/skills/delphi-review/SKILL.md +34 -12
  34. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  35. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  36. package/plugins/qoder/plugin.json +1 -1
  37. package/plugins/qoder/skills/delphi-review/AGENTS.md +6 -6
  38. package/plugins/qoder/skills/delphi-review/SKILL.md +198 -264
  39. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  40. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  41. package/principles/AGENTS.md +3 -3
  42. package/skills/delphi-review/AGENTS.md +6 -6
  43. package/skills/delphi-review/SKILL.md +34 -12
  44. package/skills/sprint-flow/AGENTS.md +3 -3
  45. package/skills/test-specification-alignment/AGENTS.md +3 -3
  46. package/lib/__tests__/next-sprint.test.js +0 -231
  47. package/lib/next-sprint.js +0 -129
  48. package/lib/shared-phase-constants.d.ts +0 -17
  49. package/lib/shared-phase-constants.js +0 -77
  50. package/plugins/opencode/__tests__/tui-plugin.test.ts +0 -543
  51. package/plugins/opencode/__tests__/xp-gate-e2e-upgrade.test.ts +0 -562
  52. package/plugins/opencode/__tests__/xp-gate-update.test.ts +0 -518
  53. package/plugins/opencode/tui-plugin.ts +0 -414
  54. package/plugins/opencode/tui-plugin.tsx +0 -306
@@ -1,518 +0,0 @@
1
- /**
2
- * XP-Gate auto-update tests
3
- *
4
- * Tests for:
5
- * - semverLt: version comparison
6
- * - checkXpGateUpdate: xp-gate npm registry check + cache + auto-upgrade
7
- * - chat.message integration
8
- * - Legacy checkPluginUpdate modified to detect BOTH packages
9
- */
10
-
11
- import { describe, it, before, after } from "node:test"
12
- import assert from "node:assert/strict"
13
- import { randomUUID } from "node:crypto"
14
- import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs"
15
- import { join } from "node:path"
16
- import { tmpdir, homedir } from "node:os"
17
- import { execSync, spawn } from "node:child_process"
18
-
19
- // ── Pure function: semverLt ──
20
-
21
- function semverLt(a: string, b: string): boolean {
22
- const pa = a.replace(/^v/, "").split(".").map(Number)
23
- const pb = b.replace(/^v/, "").split(".").map(Number)
24
- for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
25
- const na = pa[i] ?? 0
26
- const nb = pb[i] ?? 0
27
- if (na !== nb) return na < nb
28
- }
29
- return false
30
- }
31
-
32
- // ── XP-GATE specific: these are the new functions we need to implement ──
33
-
34
- const xpGateCacheFile = () => join(homedir(), ".xp-gate", "xp-gate-version-check.json")
35
- const XP_GATE_NPM_PKG = "@boyingliu01/xp-gate"
36
- const XP_GATE_REGISTRY_URL = `https://registry.npmjs.org/-/package/${encodeURIComponent(XP_GATE_NPM_PKG)}/dist-tags`
37
- const CACHE_TTL_MS = 86_400_000 // 24h
38
-
39
- type XpGateCache = {
40
- ts: number
41
- localVersion: string
42
- remoteVersion: string
43
- status?: "current" | "upgraded"
44
- }
45
-
46
- /**
47
- * Read version from installed xp-gate npm package.
48
- * Returns null if not installed.
49
- */
50
- /**
51
- * Get local xp-gate version. Can be overridden in tests via getLocalVersionOverride.
52
- */
53
- let getLocalVersionOverride: (() => string | null) | null = null
54
-
55
- function getLocalXpGateVersion(): string | null {
56
- if (getLocalVersionOverride) return getLocalVersionOverride()
57
- try {
58
- const pkgPath = join(
59
- execSync("npm root -g", { encoding: "utf8" }).trim(),
60
- XP_GATE_NPM_PKG,
61
- "package.json"
62
- )
63
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"))
64
- return pkg.version || null
65
- } catch {
66
- return null
67
- }
68
- }
69
-
70
- /**
71
- * Read cache file, returns null if absent / stale.
72
- */
73
- function readXpGateCache(): XpGateCache | null {
74
- try {
75
- if (!existsSync(xpGateCacheFile())) return null
76
- const raw = readFileSync(xpGateCacheFile(), "utf8")
77
- const data: XpGateCache = JSON.parse(raw)
78
- if (Date.now() - data.ts < CACHE_TTL_MS && data.remoteVersion) {
79
- return data
80
- }
81
- return null // stale
82
- } catch {
83
- return null
84
- }
85
- }
86
-
87
- /**
88
- * Write cache file.
89
- */
90
- function writeXpGateCache(data: XpGateCache): void {
91
- try {
92
- mkdirSync(join(homedir(), ".xp-gate"), { recursive: true })
93
- const tmp = xpGateCacheFile() + ".tmp." + process.pid
94
- writeFileSync(tmp, JSON.stringify(data), "utf8")
95
- try { rmSync(xpGateCacheFile()) } catch {}
96
- const orig = readFileSync(tmp, "utf8")
97
- writeFileSync(xpGateCacheFile(), orig, "utf8")
98
- rmSync(tmp)
99
- } catch {
100
- // silent
101
- }
102
- }
103
-
104
- /**
105
- * Fetch latest version from npm registry. Returns null on failure.
106
- */
107
- async function fetchNpmLatestVersion(url: string, timeoutMs = 5000): Promise<string | null> {
108
- const controller = new AbortController()
109
- const timer = setTimeout(() => controller.abort(), timeoutMs)
110
- try {
111
- const response = await fetch(url, { signal: controller.signal })
112
- if (!response.ok) return null
113
- const data: Record<string, unknown> = await response.json()
114
- const latest = data.latest
115
- return typeof latest === "string" ? latest : null
116
- } catch {
117
- return null
118
- } finally {
119
- clearTimeout(timer)
120
- }
121
- }
122
-
123
- type UpgradeResult = {
124
- action: "noop" | "upgraded" | "error"
125
- localVersion: string | null
126
- remoteVersion: string | null
127
- error?: string
128
- }
129
-
130
- /**
131
- * Check xp-gate version and auto-upgrade if outdated.
132
- *
133
- * - Respects daily cache
134
- * - Fires npm install -g in background
135
- * - Returns result for user notification
136
- */
137
- async function checkXpGateUpdate(): Promise<UpgradeResult> {
138
- // 1. Check cache — must validate localVersion still matches
139
- const cached = readXpGateCache()
140
- const localVersion = getLocalXpGateVersion()
141
-
142
- if (cached && cached.status === "current" && cached.localVersion && localVersion && cached.localVersion === localVersion) {
143
- return { action: "noop", localVersion, remoteVersion: cached.remoteVersion }
144
- }
145
-
146
- // 2. No valid cache or local changed — check remote
147
- if (!localVersion) {
148
- return { action: "noop", localVersion: null, remoteVersion: null }
149
- }
150
-
151
- // 3. Fetch remote
152
- const remoteVersion = await fetchNpmLatestVersion(XP_GATE_REGISTRY_URL)
153
- if (!remoteVersion) {
154
- return { action: "noop", localVersion, remoteVersion: null }
155
- }
156
-
157
- // 4. Compare
158
- if (!semverLt(localVersion, remoteVersion)) {
159
- writeXpGateCache({ ts: Date.now(), localVersion, remoteVersion, status: "current" })
160
- return { action: "noop", localVersion, remoteVersion }
161
- }
162
-
163
- // 5. Outdated — auto upgrade (awaited spawn)
164
- writeXpGateCache({ ts: Date.now(), localVersion, remoteVersion })
165
- try {
166
- const exitCode = await new Promise<number | null>((resolve, reject) => {
167
- const child = spawn("npm", ["install", "-g", `${XP_GATE_NPM_PKG}@${remoteVersion}`], {
168
- stdio: "pipe",
169
- timeout: 120_000,
170
- })
171
- child.on("close", (code) => resolve(code))
172
- child.on("error", (err) => reject(err))
173
- })
174
- if (exitCode === 0) {
175
- writeXpGateCache({ ts: Date.now(), localVersion: remoteVersion, remoteVersion, status: "current" })
176
- return { action: "upgraded", localVersion, remoteVersion }
177
- }
178
- return { action: "error", localVersion, remoteVersion, error: `npm install exited with code ${exitCode}` }
179
- } catch (err) {
180
- const msg = err instanceof Error ? err.message : String(err)
181
- return { action: "error", localVersion, remoteVersion, error: msg }
182
- }
183
- }
184
-
185
- /**
186
- * Read xp-gate config.json for opt-out settings.
187
- * Returns null if config doesn't exist or is malformed.
188
- */
189
- function readXpGateConfig(): { autoUpgrade?: boolean } | null {
190
- const cfgPath = join(homedir(), ".xp-gate", "config.json")
191
- try {
192
- if (!existsSync(cfgPath)) return null
193
- const raw = readFileSync(cfgPath, "utf8")
194
- return JSON.parse(raw)
195
- } catch {
196
- return null
197
- }
198
- }
199
-
200
- // ── Tests ──
201
-
202
- void describe("semverLt", () => {
203
- void it("returns true when local < remote", () => {
204
- assert.equal(semverLt("0.9.1", "0.9.2"), true)
205
- })
206
-
207
- void it("returns false when local > remote", () => {
208
- assert.equal(semverLt("0.9.3", "0.9.2"), false)
209
- })
210
-
211
- void it("returns false when versions equal", () => {
212
- assert.equal(semverLt("0.9.2", "0.9.2"), false)
213
- })
214
-
215
- void it("handles 'v' prefix", () => {
216
- assert.equal(semverLt("v0.9.1", "v0.9.2"), true)
217
- })
218
-
219
- void it("handles mixed prefix", () => {
220
- assert.equal(semverLt("v0.9.1", "0.9.2"), true)
221
- })
222
-
223
- void it("handles different segment counts", () => {
224
- assert.equal(semverLt("0.9", "0.9.2"), true)
225
- })
226
-
227
- void it("handles major version bumps", () => {
228
- assert.equal(semverLt("0.9.2", "1.0.0"), true)
229
- })
230
-
231
- void it("returns false for identical versions with v prefix", () => {
232
- assert.equal(semverLt("v1.0.0", "1.0.0"), false)
233
- })
234
- })
235
-
236
- void describe("checkXpGateUpdate — cache & upgrade", () => {
237
- const fakeHome = join(tmpdir(), "xp-gate-upd-test-" + randomUUID())
238
- const origHome = process.env.HOME
239
-
240
- before(() => {
241
- process.env.HOME = fakeHome
242
- mkdirSync(join(fakeHome, ".xp-gate"), { recursive: true })
243
- })
244
-
245
- after(() => {
246
- process.env.HOME = origHome
247
- rmSync(fakeHome, { recursive: true, force: true })
248
- })
249
-
250
- void it("returns noop when no xp-gate installed locally", async () => {
251
- // Test isolation limitation: npm root -g returns the REAL global prefix
252
- // regardless of HOME, so if xp-gate IS installed globally, this test
253
- // will detect it and try to upgrade. This is acceptable — the key
254
- // behavior is that it doesn't crash.
255
- const result = await checkXpGateUpdate()
256
- assert.ok(["noop", "upgraded", "error"].includes(result.action))
257
- })
258
-
259
- void it("returns noop when cache says current AND local version matches", async () => {
260
- const cachePath = join(fakeHome, ".xp-gate", "xp-gate-version-check.json")
261
- writeFileSync(cachePath, JSON.stringify({
262
- ts: Date.now(),
263
- localVersion: "0.9.2",
264
- remoteVersion: "0.9.2",
265
- status: "current",
266
- }))
267
-
268
- // Override local version to match cache so "current" check passes
269
- getLocalVersionOverride = () => "0.9.2"
270
-
271
- const result = await checkXpGateUpdate()
272
- getLocalVersionOverride = null
273
-
274
- // If cache matches local, action should be "noop" (cache hit).
275
- // However, fetchNpmLatestVersion is a real network call that cannot
276
- // be mocked here — if npm registry returns a different version,
277
- // the cache-localVersion match still prevents re-check.
278
- assert.equal(result.action, "noop")
279
- assert.equal(result.localVersion, "0.9.2")
280
- })
281
-
282
- void it("ignores cache when local version changed (manual upgrade detected)", async () => {
283
- const cachePath = join(fakeHome, ".xp-gate", "xp-gate-version-check.json")
284
- writeFileSync(cachePath, JSON.stringify({
285
- ts: Date.now(),
286
- localVersion: "0.9.1", // old cached version
287
- remoteVersion: "0.9.1",
288
- status: "current",
289
- }))
290
-
291
- // Simulate user manually upgraded npm package to 0.10.8
292
- getLocalVersionOverride = () => "0.10.8"
293
-
294
- const result = await checkXpGateUpdate()
295
- getLocalVersionOverride = null
296
-
297
- // Cache must be ignored because cached localVersion (0.9.1) ≠ actual (0.10.8)
298
- // Falls through to network check. We got past the cache shortcut — that's the pass condition.
299
- assert.ok(["noop", "upgraded", "error"].includes(result.action))
300
- })
301
-
302
- void it("handles cache expiry correctly", async () => {
303
- const cachePath = join(fakeHome, ".xp-gate", "xp-gate-version-check.json")
304
- writeFileSync(cachePath, JSON.stringify({
305
- ts: Date.now() - 86_400_000 - 3600_000, // 25h old
306
- localVersion: "0.9.1",
307
- remoteVersion: "0.9.2",
308
- }))
309
-
310
- const result = await checkXpGateUpdate()
311
- // Stale cache should be ignored — should check npm registry
312
- // If network check fails, returns noop with localVersion if found
313
- assert.equal(result.action, "noop")
314
- })
315
- })
316
-
317
-
318
- // ── UPG-002: spawn-based upgrade tests ──
319
-
320
- void describe("checkXpGateUpdate — spawn (UPG-002)", () => {
321
- const fakeHome = join(tmpdir(), "xp-gate-upd-spawn-" + randomUUID())
322
- const origHome = process.env.HOME
323
-
324
- before(() => {
325
- process.env.HOME = fakeHome
326
- mkdirSync(join(fakeHome, ".xp-gate"), { recursive: true })
327
- })
328
-
329
- after(() => {
330
- process.env.HOME = origHome
331
- rmSync(fakeHome, { recursive: true, force: true })
332
- })
333
-
334
- void it("returns upgraded with spawn-based npm install", async () => {
335
- const cachePath = join(fakeHome, ".xp-gate", "xp-gate-version-check.json")
336
- writeFileSync(cachePath, JSON.stringify({
337
- ts: Date.now() - 86_400_000 - 3600_000, // stale
338
- localVersion: "0.9.1",
339
- remoteVersion: "0.9.2",
340
- }))
341
-
342
- const result = await checkXpGateUpdate()
343
- assert.ok(["noop", "upgraded", "error"].includes(result.action))
344
- })
345
- })
346
-
347
- // ── UPG-003: readXpGateConfig isolated tests ──
348
-
349
- void describe("readXpGateConfig", () => {
350
- const fakeHome = join(tmpdir(), "xp-gate-upd-cfg-" + randomUUID())
351
- const origHome = process.env.HOME
352
-
353
- before(() => {
354
- process.env.HOME = fakeHome
355
- mkdirSync(join(fakeHome, ".xp-gate"), { recursive: true })
356
- })
357
-
358
- after(() => {
359
- process.env.HOME = origHome
360
- rmSync(fakeHome, { recursive: true, force: true })
361
- })
362
-
363
- void it("returns null when no config.json exists", () => {
364
- assert.equal(readXpGateConfig(), null)
365
- })
366
-
367
- void it("returns parsed config when config.json exists with autoUpgrade false", () => {
368
- const cfgPath = join(fakeHome, ".xp-gate", "config.json")
369
- writeFileSync(cfgPath, JSON.stringify({ autoUpgrade: false }))
370
- const cfg = readXpGateConfig()
371
- assert.notEqual(cfg, null)
372
- assert.equal(cfg!.autoUpgrade, false)
373
- })
374
-
375
- void it("returns parsed config when config.json exists with autoUpgrade true", () => {
376
- const cfgPath = join(fakeHome, ".xp-gate", "config.json")
377
- writeFileSync(cfgPath, JSON.stringify({ autoUpgrade: true }))
378
- const cfg = readXpGateConfig()
379
- assert.notEqual(cfg, null)
380
- assert.equal(cfg!.autoUpgrade, true)
381
- })
382
-
383
- void it("returns null when config.json is malformed", () => {
384
- const cfgPath = join(fakeHome, ".xp-gate", "config.json")
385
- writeFileSync(cfgPath, "not-json")
386
- assert.equal(readXpGateConfig(), null)
387
- })
388
- })
389
-
390
- // ── UPG-003: opt-out config integration tests ──
391
-
392
- void describe("checkXpGateUpdate — opt-out config integration (UPG-003)", () => {
393
- const fakeHome = join(tmpdir(), "xp-gate-upd-cfg-int-" + randomUUID())
394
- const origHome = process.env.HOME
395
-
396
- before(() => {
397
- process.env.HOME = fakeHome
398
- mkdirSync(join(fakeHome, ".xp-gate"), { recursive: true })
399
- })
400
-
401
- after(() => {
402
- process.env.HOME = origHome
403
- rmSync(fakeHome, { recursive: true, force: true })
404
- })
405
-
406
- void it("handles config.json with autoUpgrade false (no crash, graceful noop)", async () => {
407
- const cfgPath = join(fakeHome, ".xp-gate", "config.json")
408
- writeFileSync(cfgPath, JSON.stringify({ autoUpgrade: false }))
409
-
410
- const result = await checkXpGateUpdate()
411
- // Should not crash — returns noop because local install not found in fake home
412
- assert.ok(["noop", "upgraded", "error"].includes(result.action))
413
- })
414
- })
415
-
416
- // ── UPG-004: Fire-and-forget spawn → cache write test ──
417
-
418
- void describe("checkXpGateUpdate — spawn completes and writes cache (UPG-004)", () => {
419
- const fakeHome = join(tmpdir(), "xp-gate-upd-spawn-cache-" + randomUUID())
420
- const origHome = process.env.HOME
421
-
422
- before(() => {
423
- process.env.HOME = fakeHome
424
- mkdirSync(join(fakeHome, ".xp-gate"), { recursive: true })
425
- })
426
-
427
- after(() => {
428
- process.env.HOME = origHome
429
- rmSync(fakeHome, { recursive: true, force: true })
430
- })
431
-
432
- void it("should write cache with status:current after spawn-based upgrade completes", async () => {
433
- // WARNING: This test ACTUALLY spawns npm install -g.
434
- // It's skipped in CI environments.
435
- if (process.env.CI) {
436
- console.log("SKIP: UPG-004 spawn test disabled in CI")
437
- return
438
- }
439
-
440
- // Simulate: local is old, remote is newer → should trigger spawn
441
- const cachePath = join(fakeHome, ".xp-gate", "xp-gate-version-check.json")
442
- writeFileSync(cachePath, JSON.stringify({
443
- ts: Date.now() - 86_400_000 - 3600_000, // stale cache
444
- localVersion: "0.9.0",
445
- remoteVersion: "0.9.1",
446
- }))
447
-
448
- // Override local version to force upgrade path
449
- getLocalVersionOverride = () => "0.9.0"
450
-
451
- const result = await checkXpGateUpdate()
452
- getLocalVersionOverride = null
453
-
454
- // After fix: spawn is awaited, so cache is already written when function returns
455
- assert.equal(result.action, "upgraded")
456
-
457
- // Cache should have status:current immediately (no delay needed)
458
- const finalCache = readXpGateCache()
459
- if (finalCache) {
460
- assert.equal(finalCache.status, "current",
461
- "UPG-004 FAIL: spawn did not write status:current. The upgrade promise " +
462
- "must be awaited so the cache reflects the completed install.")
463
- }
464
- })
465
- })
466
-
467
- // ── UPG-005: runBackgroundUpdates awaits checkXpGateUpdate ──
468
-
469
- /**
470
- * Simulates runBackgroundUpdates to verify it properly awaits the upgrade.
471
- * This test validates that the chat.message hook doesn't lose the upgrade.
472
- */
473
- void describe("runBackgroundUpdates — await verification (UPG-005)", () => {
474
- const fakeHome = join(tmpdir(), "xp-gate-upd-await-" + randomUUID())
475
- const origHome = process.env.HOME
476
-
477
- before(() => {
478
- process.env.HOME = fakeHome
479
- mkdirSync(join(fakeHome, ".xp-gate"), { recursive: true })
480
- })
481
-
482
- after(() => {
483
- process.env.HOME = origHome
484
- rmSync(fakeHome, { recursive: true, force: true })
485
- })
486
-
487
- void it("checkXpGateUpdate resolves its promise BEFORE returning result", async () => {
488
- // If spawn is fire-and-forget, the function returns before spawn completes.
489
- // This test measures: does the returned promise resolve before or after spawn?
490
- if (process.env.CI) {
491
- console.log("SKIP: UPG-005 spawn timing test disabled in CI")
492
- return
493
- }
494
-
495
- const cachePath = join(fakeHome, ".xp-gate", "xp-gate-version-check.json")
496
- writeFileSync(cachePath, JSON.stringify({
497
- ts: Date.now() - 86_400_000 - 3600_000,
498
- localVersion: "0.9.0",
499
- remoteVersion: "0.9.1",
500
- }))
501
-
502
- getLocalVersionOverride = () => "0.9.0"
503
-
504
- const startTime = Date.now()
505
- const result = await checkXpGateUpdate()
506
- const elapsed = Date.now() - startTime
507
- getLocalVersionOverride = null
508
-
509
- assert.equal(result.action, "upgraded")
510
-
511
- // After fix: spawn is properly awaited, so elapsed >= 1000ms
512
- console.log(`UPG-005: checkXpGateUpdate() returned in ${elapsed}ms`)
513
- assert.ok(elapsed > 1000,
514
- `UPG-005 FAIL: resolve took only ${elapsed}ms — spawn is NOT awaited. ` +
515
- "The chat.message hook must await the upgrade promise so " +
516
- "npm install completes and cache gets status:current.")
517
- })
518
- })