akm-opencode 0.9.11202609031834 → 0.9.11202609031957
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 +47 -14
- package/package.json +1 -1
- package/shared/vendor-semver.ts +18 -0
package/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import os from "node:os"
|
|
|
11
11
|
import path from "node:path"
|
|
12
12
|
import { fileURLToPath } from "node:url"
|
|
13
13
|
import { filterAndRankCuratedItems, renderCuratedItems } from "./shared/curate-render"
|
|
14
|
+
import { compareSemver } from "./shared/vendor-semver"
|
|
14
15
|
import { classifyFeedbackSignal, createExplicitCorrectionRegex, createRetrospectiveFeedbackRegex, createRetrospectiveNegativeRegex, shouldSubmitAutomaticFeedback } from "./shared/feedback-signals"
|
|
15
16
|
import { appendMemoryEvent, getEventLogPath, type AkmMemoryEvent } from "./shared/memory-events"
|
|
16
17
|
import { AKM_VERSION_RANGE, satisfiesAkmVersionRange } from "./shared/akm-version"
|
|
@@ -2367,7 +2368,7 @@ type AkmResolutionTrail = Array<{
|
|
|
2367
2368
|
command: string
|
|
2368
2369
|
source: "bundled" | "path" | "local_build"
|
|
2369
2370
|
version: string | null
|
|
2370
|
-
outcome: "selected" | "version_out_of_range" | "missing" | "probe_failed"
|
|
2371
|
+
outcome: "selected" | "superseded" | "version_out_of_range" | "missing" | "probe_failed"
|
|
2371
2372
|
failureReason: string | null
|
|
2372
2373
|
}>
|
|
2373
2374
|
|
|
@@ -2375,17 +2376,29 @@ let lastAkmResolutionTrail: AkmResolutionTrail = []
|
|
|
2375
2376
|
|
|
2376
2377
|
function getResolvedAkmDetails(): { command: string; argsPrefix: string[]; displayCommand: string; version: string; source: "bundled" | "path" | "local_build" } | null {
|
|
2377
2378
|
const candidates: Array<{ command: string; argsPrefix: string[]; displayCommand: string; source: "bundled" | "path" | "local_build" }> = []
|
|
2378
|
-
// Resolution
|
|
2379
|
+
// Resolution: probe every candidate, then take the NEWEST compatible one.
|
|
2380
|
+
//
|
|
2381
|
+
// This used to be first-compatible-wins with PATH ahead of the bundled dep,
|
|
2382
|
+
// on the reasoning that the user's own akm wrote their config and has its
|
|
2383
|
+
// native deps built. That reasoning is sound about which candidates are
|
|
2384
|
+
// ELIGIBLE; it is not a reason to stop at the first one. Because the floor is
|
|
2385
|
+
// a range (^0.9.8) and not an exact pin, first-match silently bound a plugin
|
|
2386
|
+
// that depends on a much newer akm-cli to whatever ancient-but-in-range `akm`
|
|
2387
|
+
// happened to sit earliest on PATH — for as long as that stayed true. That is
|
|
2388
|
+
// the failure mode behind akm-plugins#106: a plugin driving a CLI it was not
|
|
2389
|
+
// built against, indefinitely, with nothing reporting the mismatch.
|
|
2390
|
+
//
|
|
2391
|
+
// Picking the newest compatible candidate keeps every prior eligibility rule
|
|
2392
|
+
// (a config-incompatible or unbuilt candidate still fails its `--version`
|
|
2393
|
+
// probe and is skipped, so the probe remains the compatibility gate) while
|
|
2394
|
+
// removing the arbitrary dependence on PATH order. When the user's own akm is
|
|
2395
|
+
// current — the normal case — it is still selected, because it ties with or
|
|
2396
|
+
// beats the bundled copy and ties resolve in candidate order.
|
|
2397
|
+
//
|
|
2398
|
+
// Candidate order therefore now only breaks ties:
|
|
2379
2399
|
// 1. AKM_LOCAL_BUILD_CLI — explicit dev override
|
|
2380
|
-
// 2. PATH / user installs —
|
|
2381
|
-
//
|
|
2382
|
-
// 3. bundled akm-cli — last-resort fallback for users with no akm
|
|
2383
|
-
// The bundled CLI is deliberately LAST: preferring it over the user's own
|
|
2384
|
-
// install would ignore a newer user akm that understands a newer config and
|
|
2385
|
-
// route through a bundled copy whose native postinstalls may be unbuilt. A
|
|
2386
|
-
// config-INCOMPATIBLE candidate fails its `--version` probe — older akm builds
|
|
2387
|
-
// validate config on every invocation and exit non-zero — so it is skipped
|
|
2388
|
-
// silently and the version probe doubles as a config-compatibility gate.
|
|
2400
|
+
// 2. PATH / user installs — preferred at equal version
|
|
2401
|
+
// 3. bundled akm-cli — the dependency npm resolved for this plugin
|
|
2389
2402
|
const localBuild = getLocalBuildAkmCommand()
|
|
2390
2403
|
if (localBuild) candidates.push({ ...localBuild, source: "local_build" })
|
|
2391
2404
|
for (const command of getPathAkmCandidates()) {
|
|
@@ -2400,6 +2413,7 @@ function getResolvedAkmDetails(): { command: string; argsPrefix: string[]; displ
|
|
|
2400
2413
|
|
|
2401
2414
|
const trail: AkmResolutionTrail = []
|
|
2402
2415
|
const seen = new Set<string>()
|
|
2416
|
+
const eligible: Array<{ candidate: (typeof candidates)[number]; version: string }> = []
|
|
2403
2417
|
for (const candidate of candidates) {
|
|
2404
2418
|
const cacheKey = `${candidate.command}::${candidate.argsPrefix.join(" ")}`
|
|
2405
2419
|
if (!candidate.command || seen.has(cacheKey)) continue
|
|
@@ -2417,11 +2431,27 @@ function getResolvedAkmDetails(): { command: string; argsPrefix: string[]; displ
|
|
|
2417
2431
|
trail.push({ command: candidate.displayCommand, source: candidate.source, version: probe.version, outcome: "version_out_of_range", failureReason: null })
|
|
2418
2432
|
continue
|
|
2419
2433
|
}
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2434
|
+
eligible.push({ candidate, version: probe.version })
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
// Newest compatible wins; candidate order breaks ties, so an up-to-date user
|
|
2438
|
+
// install still beats the bundled copy at equal version.
|
|
2439
|
+
let best: { candidate: (typeof candidates)[number]; version: string } | null = null
|
|
2440
|
+
for (const entry of eligible) {
|
|
2441
|
+
if (!best || compareSemver(entry.version, best.version) > 0) best = entry
|
|
2442
|
+
}
|
|
2443
|
+
for (const entry of eligible) {
|
|
2444
|
+
const isSelected = best !== null && entry.candidate === best.candidate
|
|
2445
|
+
trail.push({
|
|
2446
|
+
command: entry.candidate.displayCommand,
|
|
2447
|
+
source: entry.candidate.source,
|
|
2448
|
+
version: entry.version,
|
|
2449
|
+
outcome: isSelected ? "selected" : "superseded",
|
|
2450
|
+
failureReason: null,
|
|
2451
|
+
})
|
|
2423
2452
|
}
|
|
2424
2453
|
lastAkmResolutionTrail = trail
|
|
2454
|
+
if (best) return { ...best.candidate, version: best.version }
|
|
2425
2455
|
return null
|
|
2426
2456
|
}
|
|
2427
2457
|
|
|
@@ -3670,6 +3700,9 @@ export const AkmPlugin = Object.assign(akmPlugin, {
|
|
|
3670
3700
|
// file that re-imports here would leak into tests/opencode-plugin.test.ts.
|
|
3671
3701
|
// tests/opencode-curate-floor.test.ts therefore drives these two seams from
|
|
3672
3702
|
// a subprocess instead, which shares no module registry with anything.
|
|
3703
|
+
// #106 — lets a test observe WHICH akm the resolver selected (and at what
|
|
3704
|
+
// version), which is the whole behaviour the newest-compatible rule changes.
|
|
3705
|
+
__resolvedAkmDetailsForTests: getResolvedAkmDetails,
|
|
3673
3706
|
__buildCurateArgsForTests: buildCurateArgs,
|
|
3674
3707
|
__renderCuratedJsonResponseForTests: renderCuratedJsonResponse,
|
|
3675
3708
|
__resetWriteGateForTests,
|
package/package.json
CHANGED
package/shared/vendor-semver.ts
CHANGED
|
@@ -76,6 +76,24 @@ function parseCaretRange(token: string): CaretRange | null {
|
|
|
76
76
|
return { min, maxExclusive, allowPrereleases: min.prerelease.length > 0 }
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Compare two versions by semver precedence: negative when `a < b`, positive
|
|
81
|
+
* when `a > b`, 0 when equal. Unparseable input sorts LOWEST, so a candidate
|
|
82
|
+
* whose `--version` could not be read never wins a comparison.
|
|
83
|
+
*
|
|
84
|
+
* Exists so callers can pick the NEWEST compatible candidate rather than the
|
|
85
|
+
* first one that happens to satisfy a range — a range is an eligibility test,
|
|
86
|
+
* not an ordering.
|
|
87
|
+
*/
|
|
88
|
+
export function compareSemver(a: string, b: string): number {
|
|
89
|
+
const pa = parse(a)
|
|
90
|
+
const pb = parse(b)
|
|
91
|
+
if (!pa && !pb) return 0
|
|
92
|
+
if (!pa) return -1
|
|
93
|
+
if (!pb) return 1
|
|
94
|
+
return cmp(pa, pb)
|
|
95
|
+
}
|
|
96
|
+
|
|
79
97
|
export function satisfies(version: string, range: string): boolean {
|
|
80
98
|
const v = parse(version)
|
|
81
99
|
if (!v) return false
|