@adea-ai/plugins 3.0.10 → 4.0.1
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/README.md +18 -11
- package/dist/agent-plugins-catalog.d.ts +2 -3
- package/dist/agent-plugins-catalog.js +2 -10
- package/dist/agent-plugins-catalog.js.map +1 -1
- package/dist/catalog-store.d.ts +81 -0
- package/dist/catalog-store.js +487 -0
- package/dist/catalog-store.js.map +1 -0
- package/dist/consumer-index.d.ts +1 -1
- package/dist/consumer-index.js +17 -7
- package/dist/consumer-index.js.map +1 -1
- package/dist/display-name.js +3 -2
- package/dist/display-name.js.map +1 -1
- package/dist/harness.d.ts +50 -9
- package/dist/harness.js +190 -147
- package/dist/harness.js.map +1 -1
- package/dist/icon-mirror.d.ts +2 -2
- package/dist/icon-mirror.js +38 -25
- package/dist/icon-mirror.js.map +1 -1
- package/dist/icons.js +5 -27
- package/dist/icons.js.map +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.js +103 -88
- package/dist/index.js.map +1 -1
- package/dist/publication.d.ts +8 -11
- package/dist/publication.js +8 -11
- package/dist/publication.js.map +1 -1
- package/dist/schema.d.ts +0 -40
- package/dist/schema.js +0 -36
- package/dist/schema.js.map +1 -1
- package/dist/site-icons.d.ts +2 -0
- package/dist/site-icons.js +46 -9
- package/dist/site-icons.js.map +1 -1
- package/dist/source-digest.d.ts +2 -0
- package/dist/source-digest.js +12 -0
- package/dist/source-digest.js.map +1 -0
- package/dist/sources.js +3 -3
- package/dist/sources.js.map +1 -1
- package/native/build-provenance.txt +5 -5
- package/native/staged-reader-win32-arm64.node +0 -0
- package/native/staged-reader-win32-x64.node +0 -0
- package/package.json +4 -9
- package/src/agent-plugins-catalog.ts +3 -10
- package/src/artifacts.test.ts +80 -0
- package/src/catalog-installation.test.ts +1 -1
- package/src/catalog-store.ts +546 -0
- package/src/consumer-index.test.ts +125 -0
- package/src/consumer-index.ts +23 -8
- package/src/default-head.test.ts +143 -0
- package/src/display-name.test.ts +8 -0
- package/src/display-name.ts +3 -2
- package/src/{agent-plugins-harness.test.ts → harness.test.ts} +1 -1
- package/src/harness.ts +249 -170
- package/src/icon-mirror.ts +36 -23
- package/src/icons.test.ts +5 -0
- package/src/icons.ts +6 -29
- package/src/index.test.ts +272 -40
- package/src/index.ts +131 -94
- package/src/manifest-retry.test.ts +49 -0
- package/src/portable-catalog.test.ts +8 -5
- package/src/publication.ts +8 -11
- package/src/schema.ts +0 -40
- package/src/site-icon-fallback.test.ts +36 -19
- package/src/site-icons.test.ts +104 -0
- package/src/site-icons.ts +47 -9
- package/src/source-config-sync.test.ts +84 -0
- package/src/source-digest.test.ts +101 -0
- package/src/source-digest.ts +14 -0
- package/src/sources-json.test.ts +53 -0
- package/src/sources.ts +3 -3
- package/src/upstream-headers.test.ts +31 -7
- package/src/write-artifacts.test.ts +375 -0
- package/dist/agent-plugins-harness.d.ts +0 -54
- package/dist/agent-plugins-harness.js +0 -212
- package/dist/agent-plugins-harness.js.map +0 -1
- package/src/agent-plugins-harness.ts +0 -281
package/src/consumer-index.ts
CHANGED
|
@@ -194,7 +194,11 @@ export interface ConsumerCategory {
|
|
|
194
194
|
export interface LeadingDiagnostic {
|
|
195
195
|
readonly category: string
|
|
196
196
|
readonly name: string
|
|
197
|
-
readonly reason:
|
|
197
|
+
readonly reason:
|
|
198
|
+
| 'UNKNOWN_CATEGORY'
|
|
199
|
+
| 'PRODUCT_NOT_FOUND'
|
|
200
|
+
| 'PRODUCT_NOT_IN_CATEGORY'
|
|
201
|
+
| 'PRODUCT_AMBIGUOUS'
|
|
198
202
|
}
|
|
199
203
|
|
|
200
204
|
/** Brand-mark coverage, so an incomplete set is visible rather than implied. */
|
|
@@ -257,10 +261,10 @@ function selectCanonical(
|
|
|
257
261
|
const index = preference.sourcePreference.indexOf(plugin.sourceId)
|
|
258
262
|
return index === -1 ? preference.sourcePreference.length : index
|
|
259
263
|
}
|
|
260
|
-
// A variant with no resolved release repository proves nothing about its
|
|
264
|
+
// A variant with no resolved current-release repository proves nothing about its
|
|
261
265
|
// origin, so it is treated as marketplace-shipped rather than vendor-published.
|
|
262
266
|
const vendorPublished = (plugin: Plugin): boolean => {
|
|
263
|
-
const repository = plugin
|
|
267
|
+
const repository = currentRelease(plugin)?.resolvedRepositoryUrl
|
|
264
268
|
return repository !== undefined && repository !== sourceRepository.get(plugin.sourceId)
|
|
265
269
|
}
|
|
266
270
|
return [...variants].toSorted(
|
|
@@ -417,7 +421,8 @@ export function buildConsumerIndex(input: {
|
|
|
417
421
|
sourcePreference: [...DEFAULT_SOURCE_PREFERENCE],
|
|
418
422
|
}
|
|
419
423
|
const leading = input.leading ?? {}
|
|
420
|
-
const topCount = input.topCount
|
|
424
|
+
const topCount = input.topCount === undefined ? DEFAULT_TOP_COUNT : input.topCount
|
|
425
|
+
if (!Number.isSafeInteger(topCount) || topCount < 0) throw new Error('TOP_COUNT_INVALID')
|
|
421
426
|
const sourceRepository = new Map(
|
|
422
427
|
input.catalog.sources.map((source) => [source.sourceId, source.repositoryUrl])
|
|
423
428
|
)
|
|
@@ -503,18 +508,28 @@ export function buildConsumerIndex(input: {
|
|
|
503
508
|
const curatedByCategory = new Map<string, string[]>()
|
|
504
509
|
for (const category of categoryNames) {
|
|
505
510
|
const members = input.catalog.plugins.filter((plugin) => plugin.categories.includes(category))
|
|
511
|
+
const categoryProducts = new Set(members.map((plugin) => plugin.productGroupingKey))
|
|
506
512
|
const curated: string[] = []
|
|
507
513
|
for (const name of leading[category] ?? []) {
|
|
508
|
-
const matches =
|
|
509
|
-
|
|
514
|
+
const matches = input.catalog.plugins.filter((plugin) => plugin.upstreamPluginName === name)
|
|
515
|
+
const keys = new Set(
|
|
516
|
+
matches
|
|
517
|
+
.map((plugin) => plugin.productGroupingKey)
|
|
518
|
+
.filter((key) => categoryProducts.has(key))
|
|
519
|
+
)
|
|
520
|
+
if (keys.size === 0) {
|
|
510
521
|
diagnostics.push({
|
|
511
522
|
category,
|
|
512
523
|
name,
|
|
513
|
-
reason:
|
|
524
|
+
reason: matches.length > 0 ? 'PRODUCT_NOT_IN_CATEGORY' : 'PRODUCT_NOT_FOUND',
|
|
514
525
|
})
|
|
515
526
|
continue
|
|
516
527
|
}
|
|
517
|
-
|
|
528
|
+
if (keys.size > 1) {
|
|
529
|
+
diagnostics.push({ category, name, reason: 'PRODUCT_AMBIGUOUS' })
|
|
530
|
+
continue
|
|
531
|
+
}
|
|
532
|
+
const key = keys.values().next().value!
|
|
518
533
|
if (!curated.includes(key)) curated.push(key)
|
|
519
534
|
}
|
|
520
535
|
curatedByCategory.set(category, curated)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { chmod, mkdir, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { expect, test } from 'bun:test'
|
|
5
|
+
import { buildCatalog, digest, synchronize } from './index.js'
|
|
6
|
+
import { parseMarketplaceManifest } from './sources.js'
|
|
7
|
+
import { withPortableFixture } from '../test-support/portable-fixture.js'
|
|
8
|
+
|
|
9
|
+
test.skipIf(process.platform === 'win32')(
|
|
10
|
+
'resolves unspecified external refs from the remote default branch',
|
|
11
|
+
() =>
|
|
12
|
+
withPortableFixture(async (input, root) => {
|
|
13
|
+
const repo = join(root, 'repository')
|
|
14
|
+
const bin = join(root, 'bin')
|
|
15
|
+
const originalPath = process.env.PATH
|
|
16
|
+
const originalGit = process.env.ADEA_TEST_REAL_GIT
|
|
17
|
+
const originalRepo = process.env.ADEA_TEST_HEAD_REPO
|
|
18
|
+
const originalFetch = globalThis.fetch
|
|
19
|
+
const realGit = execFileSync('which', ['git'], { encoding: 'utf8' }).trim()
|
|
20
|
+
const git = (...args: string[]) =>
|
|
21
|
+
execFileSync(realGit, ['-C', repo, ...args], { encoding: 'utf8' }).trim()
|
|
22
|
+
try {
|
|
23
|
+
await mkdir(repo)
|
|
24
|
+
await mkdir(bin)
|
|
25
|
+
git('init', '-q', '-b', 'legacy')
|
|
26
|
+
git(
|
|
27
|
+
'-c',
|
|
28
|
+
'user.name=Fixture',
|
|
29
|
+
'-c',
|
|
30
|
+
'user.email=fixture@example.invalid',
|
|
31
|
+
'commit',
|
|
32
|
+
'--allow-empty',
|
|
33
|
+
'-qm',
|
|
34
|
+
'main commit'
|
|
35
|
+
)
|
|
36
|
+
git('branch', 'main')
|
|
37
|
+
const mainCommit = git('rev-parse', 'refs/heads/main')
|
|
38
|
+
git(
|
|
39
|
+
'-c',
|
|
40
|
+
'user.name=Fixture',
|
|
41
|
+
'-c',
|
|
42
|
+
'user.email=fixture@example.invalid',
|
|
43
|
+
'commit',
|
|
44
|
+
'--allow-empty',
|
|
45
|
+
'-qm',
|
|
46
|
+
'default commit'
|
|
47
|
+
)
|
|
48
|
+
const defaultCommit = git('rev-parse', 'HEAD')
|
|
49
|
+
expect(defaultCommit).not.toBe(mainCommit)
|
|
50
|
+
const sourceSha = '5'.repeat(40)
|
|
51
|
+
const source = {
|
|
52
|
+
...input.sources[0]!,
|
|
53
|
+
repositoryUrl: 'https://github.com/acme/marketplace',
|
|
54
|
+
}
|
|
55
|
+
const script = join(bin, 'git')
|
|
56
|
+
await writeFile(
|
|
57
|
+
script,
|
|
58
|
+
`#!/bin/sh\nif [ "$2" = "${source.repositoryUrl}" ]; then\n printf '%s\\t%s\\n' '${sourceSha}' "$3"\nelse\n shift 2\n exec "$ADEA_TEST_REAL_GIT" ls-remote "$ADEA_TEST_HEAD_REPO" "$@"\nfi\n`
|
|
59
|
+
)
|
|
60
|
+
await chmod(script, 0o700)
|
|
61
|
+
process.env.ADEA_TEST_REAL_GIT = realGit
|
|
62
|
+
process.env.ADEA_TEST_HEAD_REPO = repo
|
|
63
|
+
process.env.PATH = `${bin}:${originalPath ?? ''}`
|
|
64
|
+
const url = 'https://github.com/acme/remote-plugin'
|
|
65
|
+
const manifest = JSON.stringify({
|
|
66
|
+
plugins: [
|
|
67
|
+
{ name: 'object-source', source: { source: 'git', url } },
|
|
68
|
+
{ name: 'url-source', source: url },
|
|
69
|
+
{ name: 'subdir-source', source: { source: 'git-subdir', url, path: 'plugins/demo' } },
|
|
70
|
+
{ name: 'explicit-main', source: { source: 'git', url, ref: 'main' } },
|
|
71
|
+
],
|
|
72
|
+
})
|
|
73
|
+
globalThis.fetch = (async (_url: unknown) => new Response(manifest)) as typeof fetch
|
|
74
|
+
const options = {
|
|
75
|
+
...input,
|
|
76
|
+
sources: [source],
|
|
77
|
+
mode: 'live' as const,
|
|
78
|
+
snapshotLoader: {
|
|
79
|
+
load: async () => ({
|
|
80
|
+
files: new Map([['plugin.json', new TextEncoder().encode('{"name":"Remote"}')]]),
|
|
81
|
+
symlinks: [],
|
|
82
|
+
}),
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
const result = await synchronize(options)
|
|
86
|
+
const pins = Object.fromEntries(
|
|
87
|
+
result.lock!.sources[0]!.pluginPins.map((pin) => [pin.pluginName, pin.resolvedCommitSha])
|
|
88
|
+
)
|
|
89
|
+
expect(pins).toEqual({
|
|
90
|
+
'object-source': defaultCommit,
|
|
91
|
+
'url-source': defaultCommit,
|
|
92
|
+
'subdir-source': defaultCommit,
|
|
93
|
+
'explicit-main': mainCommit,
|
|
94
|
+
})
|
|
95
|
+
const direct = await buildCatalog({
|
|
96
|
+
resolvedSources: [
|
|
97
|
+
{
|
|
98
|
+
config: source,
|
|
99
|
+
commitSha: sourceSha,
|
|
100
|
+
manifestText: manifest,
|
|
101
|
+
parsed: parseMarketplaceManifest(JSON.parse(manifest), source),
|
|
102
|
+
pluginPins: [],
|
|
103
|
+
retrievedAt: '2026-01-01T00:00:00.000Z',
|
|
104
|
+
manifestDigest: digest(manifest),
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
categoryMap: input.categoryMap,
|
|
108
|
+
productAliases: input.productAliases,
|
|
109
|
+
policy: input.policy,
|
|
110
|
+
snapshotLoader: options.snapshotLoader,
|
|
111
|
+
resolveExternalRefs: true,
|
|
112
|
+
})
|
|
113
|
+
expect(
|
|
114
|
+
direct.plugins.find((plugin) => plugin.upstreamPluginName === 'url-source')
|
|
115
|
+
?.availableReleases[0]?.resolvedCommitSha
|
|
116
|
+
).toBe(defaultCommit)
|
|
117
|
+
git('symbolic-ref', 'HEAD', 'refs/heads/main')
|
|
118
|
+
const replay = await synchronize({ ...options, fromLock: result.lock! })
|
|
119
|
+
expect(
|
|
120
|
+
replay.lock!.sources[0]!.pluginPins.find((pin) => pin.pluginName === 'url-source')
|
|
121
|
+
?.resolvedCommitSha
|
|
122
|
+
).toBe(defaultCommit)
|
|
123
|
+
const refreshed = await synchronize({ ...options, existingLock: result.lock! })
|
|
124
|
+
expect(refreshed.changed).toBe(true)
|
|
125
|
+
expect(
|
|
126
|
+
refreshed.lock!.sources[0]!.pluginPins.find((pin) => pin.pluginName === 'url-source')
|
|
127
|
+
?.resolvedCommitSha
|
|
128
|
+
).toBe(mainCommit)
|
|
129
|
+
git('branch', 'x/HEAD', 'main')
|
|
130
|
+
git('symbolic-ref', 'HEAD', 'refs/heads/missing')
|
|
131
|
+
expect(git('ls-remote', repo, 'HEAD')).toContain('refs/heads/x/HEAD')
|
|
132
|
+
await expect(synchronize(options)).rejects.toThrow('GIT_REF_NOT_FOUND')
|
|
133
|
+
} finally {
|
|
134
|
+
globalThis.fetch = originalFetch
|
|
135
|
+
if (originalPath === undefined) delete process.env.PATH
|
|
136
|
+
else process.env.PATH = originalPath
|
|
137
|
+
if (originalGit === undefined) delete process.env.ADEA_TEST_REAL_GIT
|
|
138
|
+
else process.env.ADEA_TEST_REAL_GIT = originalGit
|
|
139
|
+
if (originalRepo === undefined) delete process.env.ADEA_TEST_HEAD_REPO
|
|
140
|
+
else process.env.ADEA_TEST_HEAD_REPO = originalRepo
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
)
|
package/src/display-name.test.ts
CHANGED
|
@@ -49,6 +49,14 @@ describe('plugin display names', () => {
|
|
|
49
49
|
)
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
+
test('treats object property names as ordinary display-name text', () => {
|
|
53
|
+
expect(normalizeDisplayName('constructor')).toBe('Constructor')
|
|
54
|
+
expect(normalizeDisplayName('constructor-tools')).toBe('Constructor Tools')
|
|
55
|
+
expect(normalizeDisplayName('toString')).toBe('toString')
|
|
56
|
+
expect(normalizeDisplayName('valueOf')).toBe('valueOf')
|
|
57
|
+
expect(normalizeDisplayName('__proto__')).toBe('Proto')
|
|
58
|
+
})
|
|
59
|
+
|
|
52
60
|
test('leaves a name it cannot improve alone', () => {
|
|
53
61
|
expect(normalizeDisplayName('')).toBe('')
|
|
54
62
|
expect(normalizeDisplayName(' ')).toBe(' ')
|
package/src/display-name.ts
CHANGED
|
@@ -139,14 +139,15 @@ const FULL_NAMES: Readonly<Record<string, string>> = {
|
|
|
139
139
|
*/
|
|
140
140
|
export function normalizeDisplayName(value: string): string {
|
|
141
141
|
const trimmed = value.trim()
|
|
142
|
-
const
|
|
142
|
+
const fullNameKey = trimmed.toLowerCase()
|
|
143
|
+
const known = Object.hasOwn(FULL_NAMES, fullNameKey) ? FULL_NAMES[fullNameKey] : undefined
|
|
143
144
|
if (known !== undefined) return known
|
|
144
145
|
const tokens = trimmed.split(/[-_\s]+/u).filter(Boolean)
|
|
145
146
|
if (tokens.length === 0) return value
|
|
146
147
|
return tokens
|
|
147
148
|
.map((token, index) => {
|
|
148
149
|
const key = token.toLowerCase()
|
|
149
|
-
const brand = BRAND_TOKENS[key]
|
|
150
|
+
const brand = Object.hasOwn(BRAND_TOKENS, key) ? BRAND_TOKENS[key] : undefined
|
|
150
151
|
if (brand !== undefined) return brand
|
|
151
152
|
// Any uppercase means the vendor already chose a casing.
|
|
152
153
|
if (/\p{Lu}/u.test(token)) return token
|