@intentic/registry 1.176.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/LICENSE +21 -0
- package/README.md +82 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +77 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +84 -0
- package/dist/registry.js.map +1 -0
- package/dist/source.d.ts +11 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +38 -0
- package/dist/source.js.map +1 -0
- package/package.json +49 -0
- package/src/index.ts +2 -0
- package/src/registry.test.ts +68 -0
- package/src/registry.ts +161 -0
- package/src/source.test.ts +60 -0
- package/src/source.ts +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Artur Kurowski
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# @intentic/registry
|
|
2
|
+
|
|
3
|
+
The **extension-registry file format** — what a registry repo contains, how its two files join, and the order
|
|
4
|
+
the result is shown in. Published to npm, and depended on by three otherwise-unrelated consumers, which is the
|
|
5
|
+
whole reason it is a package: the daemon that clones a registry, the site that builds the public gallery, and
|
|
6
|
+
the [scanner](../../_tools/registry-scan) that writes the files. One zod schema instead of three that drift.
|
|
7
|
+
|
|
8
|
+
See [Publish & the marketplace](https://intentic.dev/docs/extensions/publish/) for the author-facing version
|
|
9
|
+
of everything below.
|
|
10
|
+
|
|
11
|
+
## The shape of it
|
|
12
|
+
|
|
13
|
+
intentic hosts no extension code, builds none, and signs none. **A registry is a git repo of pointers** —
|
|
14
|
+
each entry names somebody else's repository at a commit, and installing follows that pointer from the owner's
|
|
15
|
+
sandbox straight to the author's git host. Listing costs a pull request; delisting removes a pointer and
|
|
16
|
+
deletes nothing.
|
|
17
|
+
|
|
18
|
+
The file is `.claude-plugin/marketplace.json`, Claude Code's plugin-marketplace format, deliberately.
|
|
19
|
+
`kind` and `trust` are intentic's own fields and Claude Code ignores what it doesn't recognise, so one repo
|
|
20
|
+
lists a team's agent plugins and its intentic extensions together.
|
|
21
|
+
|
|
22
|
+
## Two files, and why
|
|
23
|
+
|
|
24
|
+
| File | Written by | Holds |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| `.claude-plugin/marketplace.json` | humans, via pull request | every decision — what is listed, and at which commit and trust level |
|
|
27
|
+
| `.claude-plugin/registry.generated.json` | the nightly scanner | only facts read back off the source host: stars, last push |
|
|
28
|
+
|
|
29
|
+
Keeping the derived data out of the curated file is load-bearing, not tidiness. Star counts in the hand-edited
|
|
30
|
+
file would make every nightly refresh a merge conflict against every open listing pull request, and would bury
|
|
31
|
+
the decision under churn in the review diff. A registry with no generated file is a registry with no stars,
|
|
32
|
+
which renders fine — most registries are a dozen internal extensions in a private repo and run no scanner.
|
|
33
|
+
|
|
34
|
+
[`resolveRegistry`](src/registry.ts) joins them by entry name into `RegistryEntry`, which is also the daemon's
|
|
35
|
+
browse wire shape, so the app's list and the website's gallery are the same rows in the same order.
|
|
36
|
+
|
|
37
|
+
## Trust, and what each state claims
|
|
38
|
+
|
|
39
|
+
`listed` is the honest default: the pointer resolves, the manifest parses, the publisher owns the source repo,
|
|
40
|
+
and **nobody read the code**. `verified` means a human read the source at that commit. `blocked` means
|
|
41
|
+
known-malicious or known-broken, with the reason alongside.
|
|
42
|
+
|
|
43
|
+
A blocked entry stays in the file. Deleting the row hides it from people browsing and tells the people who
|
|
44
|
+
already installed it nothing, which is backwards — they are the ones at risk. Absent on a third-party registry
|
|
45
|
+
resolves to `listed`, because a registry that doesn't use the field hasn't asserted anything.
|
|
46
|
+
|
|
47
|
+
## The mark
|
|
48
|
+
|
|
49
|
+
A row carries the two display tiers the manifest declares — `logo` (a simple-icons slug) and `icon` (a glyph
|
|
50
|
+
from the app's own set) — and the [scanner](../../_tools/registry-scan) copies whichever is set into the listing it
|
|
51
|
+
proposes, exactly as it copies the version. A row with neither is drawn as the extension's initials.
|
|
52
|
+
|
|
53
|
+
They ride the **curated** file, which looks wrong for a copied value until you ask what the two files are for:
|
|
54
|
+
the mark is part of how a listing presents itself, so it belongs in the row a human reviews and can correct.
|
|
55
|
+
It also has to be here to be worth anything — the gallery and the app's browse list render this row and have
|
|
56
|
+
no access to the manifest, because the whole point of browsing is that the code has not been cloned yet.
|
|
57
|
+
|
|
58
|
+
## The order
|
|
59
|
+
|
|
60
|
+
[`compareEntries`](src/registry.ts): verified first, then stars, then most-recently-pushed, then name. Stars
|
|
61
|
+
are the obvious sort and the wrong one alone — every listing sits at nought to three of them for months, so a
|
|
62
|
+
pure star sort is a random order wearing a merit badge, and it is the most purchasable number on GitHub.
|
|
63
|
+
Recency is what actually does the ordering early on. Stars stay visible; they just don't get to be the
|
|
64
|
+
ranking.
|
|
65
|
+
|
|
66
|
+
## Identity
|
|
67
|
+
|
|
68
|
+
An entry's `name` is `publisher.name` from the manifest — [`extensionIdOf`](../extension-api/src/manifest.ts),
|
|
69
|
+
the same identity the app installs under. It is derived, never declared by the registry, so a registry entry
|
|
70
|
+
cannot rename or spoof an extension, and a repo that copies somebody else's manifest collides with their
|
|
71
|
+
listing instead of shadowing it.
|
|
72
|
+
|
|
73
|
+
[`isShaPinned`](src/source.ts) is the other half: an extension install requires a full 40-character commit sha,
|
|
74
|
+
because extension code runs trusted in the owner's browser and a branch name is a promise the upstream can
|
|
75
|
+
break with a force-push. A registry entry without one still lists and still reads; it just can't be installed
|
|
76
|
+
in a click.
|
|
77
|
+
|
|
78
|
+
## Key files
|
|
79
|
+
|
|
80
|
+
- [src/registry.ts](src/registry.ts) — the file format: what a registry repo contains.
|
|
81
|
+
- [src/source.ts](src/source.ts) — how a sha-pinned pointer names an extension.
|
|
82
|
+
- [src/index.ts](src/index.ts) — the public surface.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const REGISTRY_FILE = ".claude-plugin/marketplace.json";
|
|
3
|
+
export declare const REGISTRY_FACTS_FILE = ".claude-plugin/registry.generated.json";
|
|
4
|
+
export declare const OFFICIAL_REGISTRY_URL = "https://github.com/intentic/registry";
|
|
5
|
+
export declare const REGISTRY_TOPIC = "intentic-extension";
|
|
6
|
+
export declare const RegistryTrustSchema: z.ZodEnum<{
|
|
7
|
+
blocked: "blocked";
|
|
8
|
+
listed: "listed";
|
|
9
|
+
verified: "verified";
|
|
10
|
+
}>;
|
|
11
|
+
export type RegistryTrust = z.infer<typeof RegistryTrustSchema>;
|
|
12
|
+
export declare const RegistryFileSchema: z.ZodObject<{
|
|
13
|
+
name: z.ZodString;
|
|
14
|
+
metadata: z.ZodOptional<z.ZodObject<{
|
|
15
|
+
pluginRoot: z.ZodOptional<z.ZodString>;
|
|
16
|
+
}, z.core.$strip>>;
|
|
17
|
+
plugins: z.ZodArray<z.ZodObject<{
|
|
18
|
+
name: z.ZodString;
|
|
19
|
+
description: z.ZodOptional<z.ZodString>;
|
|
20
|
+
version: z.ZodOptional<z.ZodString>;
|
|
21
|
+
kind: z.ZodOptional<z.ZodEnum<{
|
|
22
|
+
extension: "extension";
|
|
23
|
+
plugin: "plugin";
|
|
24
|
+
}>>;
|
|
25
|
+
trust: z.ZodOptional<z.ZodEnum<{
|
|
26
|
+
blocked: "blocked";
|
|
27
|
+
listed: "listed";
|
|
28
|
+
verified: "verified";
|
|
29
|
+
}>>;
|
|
30
|
+
trustReason: z.ZodOptional<z.ZodString>;
|
|
31
|
+
category: z.ZodOptional<z.ZodString>;
|
|
32
|
+
logo: z.ZodOptional<z.ZodString>;
|
|
33
|
+
icon: z.ZodOptional<z.ZodString>;
|
|
34
|
+
homepage: z.ZodOptional<z.ZodURL>;
|
|
35
|
+
source: z.ZodUnknown;
|
|
36
|
+
}, z.core.$strip>>;
|
|
37
|
+
}, z.core.$strip>;
|
|
38
|
+
export type RegistryFile = z.infer<typeof RegistryFileSchema>;
|
|
39
|
+
export declare const RegistryFactsSchema: z.ZodObject<{
|
|
40
|
+
scannedAt: z.ZodString;
|
|
41
|
+
entries: z.ZodArray<z.ZodObject<{
|
|
42
|
+
name: z.ZodString;
|
|
43
|
+
stars: z.ZodOptional<z.ZodNumber>;
|
|
44
|
+
pushedAt: z.ZodOptional<z.ZodString>;
|
|
45
|
+
}, z.core.$strip>>;
|
|
46
|
+
}, z.core.$strip>;
|
|
47
|
+
export type RegistryFacts = z.infer<typeof RegistryFactsSchema>;
|
|
48
|
+
export declare const RegistryEntrySchema: z.ZodObject<{
|
|
49
|
+
name: z.ZodString;
|
|
50
|
+
description: z.ZodOptional<z.ZodString>;
|
|
51
|
+
version: z.ZodOptional<z.ZodString>;
|
|
52
|
+
kind: z.ZodEnum<{
|
|
53
|
+
extension: "extension";
|
|
54
|
+
plugin: "plugin";
|
|
55
|
+
}>;
|
|
56
|
+
trust: z.ZodEnum<{
|
|
57
|
+
blocked: "blocked";
|
|
58
|
+
listed: "listed";
|
|
59
|
+
verified: "verified";
|
|
60
|
+
}>;
|
|
61
|
+
trustReason: z.ZodOptional<z.ZodString>;
|
|
62
|
+
category: z.ZodOptional<z.ZodString>;
|
|
63
|
+
logo: z.ZodOptional<z.ZodString>;
|
|
64
|
+
icon: z.ZodOptional<z.ZodString>;
|
|
65
|
+
homepage: z.ZodOptional<z.ZodString>;
|
|
66
|
+
install: z.ZodOptional<z.ZodObject<{
|
|
67
|
+
url: z.ZodString;
|
|
68
|
+
ref: z.ZodOptional<z.ZodString>;
|
|
69
|
+
path: z.ZodOptional<z.ZodString>;
|
|
70
|
+
}, z.core.$strip>>;
|
|
71
|
+
stars: z.ZodOptional<z.ZodNumber>;
|
|
72
|
+
pushedAt: z.ZodOptional<z.ZodString>;
|
|
73
|
+
}, z.core.$strip>;
|
|
74
|
+
export type RegistryEntry = z.infer<typeof RegistryEntrySchema>;
|
|
75
|
+
export declare const resolveRegistry: (file: RegistryFile, facts: RegistryFacts | undefined, registryUrl: string) => RegistryEntry[];
|
|
76
|
+
export declare const compareEntries: (a: RegistryEntry, b: RegistryEntry) => number;
|
|
77
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAmBxB,eAAO,MAAM,aAAa,oCAAoC,CAAC;AAC/D,eAAO,MAAM,mBAAmB,2CAA2C,CAAC;AAI5E,eAAO,MAAM,qBAAqB,yCAAyC,CAAC;AAI5E,eAAO,MAAM,cAAc,uBAAuB,CAAC;AAanD,eAAO,MAAM,mBAAmB;;;;EAA4C,CAAC;AAC7E,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AA6BhE,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;iBAI7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAa9D,eAAO,MAAM,mBAAmB;;;;;;;iBAG9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAIhE,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;iBAiB9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAIhE,eAAO,MAAM,eAAe,SAAU,YAAY,SAAS,aAAa,GAAG,SAAS,eAAe,MAAM,KAAG,aAAa,EAqBxH,CAAC;AASF,eAAO,MAAM,cAAc,MAAO,aAAa,KAAK,aAAa,KAAG,MAWnE,CAAC"}
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { RegistryInstallSchema, resolveSource } from "./source.js";
|
|
3
|
+
export const REGISTRY_FILE = ".claude-plugin/marketplace.json";
|
|
4
|
+
export const REGISTRY_FACTS_FILE = ".claude-plugin/registry.generated.json";
|
|
5
|
+
export const OFFICIAL_REGISTRY_URL = "https://github.com/intentic/registry";
|
|
6
|
+
export const REGISTRY_TOPIC = "intentic-extension";
|
|
7
|
+
export const RegistryTrustSchema = z.enum(["verified", "listed", "blocked"]);
|
|
8
|
+
const RegistryFileEntrySchema = z.object({
|
|
9
|
+
name: z.string(),
|
|
10
|
+
description: z.string().optional(),
|
|
11
|
+
version: z.string().optional(),
|
|
12
|
+
kind: z.enum(["plugin", "extension"]).optional(),
|
|
13
|
+
trust: RegistryTrustSchema.optional(),
|
|
14
|
+
trustReason: z.string().optional(),
|
|
15
|
+
category: z.string().optional(),
|
|
16
|
+
logo: z.string().optional(),
|
|
17
|
+
icon: z.string().optional(),
|
|
18
|
+
homepage: z.url().optional(),
|
|
19
|
+
source: z.unknown(),
|
|
20
|
+
});
|
|
21
|
+
export const RegistryFileSchema = z.object({
|
|
22
|
+
name: z.string(),
|
|
23
|
+
metadata: z.object({ pluginRoot: z.string().optional() }).optional(),
|
|
24
|
+
plugins: z.array(RegistryFileEntrySchema),
|
|
25
|
+
});
|
|
26
|
+
const RegistryFactsEntrySchema = z.object({
|
|
27
|
+
name: z.string(),
|
|
28
|
+
stars: z.number().int().nonnegative().optional(),
|
|
29
|
+
pushedAt: z.string().optional(),
|
|
30
|
+
});
|
|
31
|
+
export const RegistryFactsSchema = z.object({
|
|
32
|
+
scannedAt: z.string(),
|
|
33
|
+
entries: z.array(RegistryFactsEntrySchema),
|
|
34
|
+
});
|
|
35
|
+
export const RegistryEntrySchema = z.object({
|
|
36
|
+
name: z.string(),
|
|
37
|
+
description: z.string().optional(),
|
|
38
|
+
version: z.string().optional(),
|
|
39
|
+
kind: z.enum(["plugin", "extension"]),
|
|
40
|
+
trust: RegistryTrustSchema,
|
|
41
|
+
trustReason: z.string().optional(),
|
|
42
|
+
category: z.string().optional(),
|
|
43
|
+
logo: z.string().optional(),
|
|
44
|
+
icon: z.string().optional(),
|
|
45
|
+
homepage: z.string().optional(),
|
|
46
|
+
install: RegistryInstallSchema.optional(),
|
|
47
|
+
stars: z.number().int().nonnegative().optional(),
|
|
48
|
+
pushedAt: z.string().optional(),
|
|
49
|
+
});
|
|
50
|
+
export const resolveRegistry = (file, facts, registryUrl) => {
|
|
51
|
+
const byName = new Map(facts?.entries.map((entry) => [entry.name, entry]) ?? []);
|
|
52
|
+
return file.plugins.map((plugin) => {
|
|
53
|
+
const upstream = byName.get(plugin.name);
|
|
54
|
+
const install = resolveSource(plugin.source, registryUrl, file.metadata?.pluginRoot);
|
|
55
|
+
return {
|
|
56
|
+
name: plugin.name,
|
|
57
|
+
kind: plugin.kind ?? "plugin",
|
|
58
|
+
trust: plugin.trust ?? "listed",
|
|
59
|
+
...(plugin.description !== undefined ? { description: plugin.description } : {}),
|
|
60
|
+
...(plugin.version !== undefined ? { version: plugin.version } : {}),
|
|
61
|
+
...(plugin.trustReason !== undefined ? { trustReason: plugin.trustReason } : {}),
|
|
62
|
+
...(plugin.category !== undefined ? { category: plugin.category } : {}),
|
|
63
|
+
...(plugin.logo !== undefined ? { logo: plugin.logo } : {}),
|
|
64
|
+
...(plugin.icon !== undefined ? { icon: plugin.icon } : {}),
|
|
65
|
+
...(plugin.homepage !== undefined ? { homepage: plugin.homepage } : {}),
|
|
66
|
+
...(install !== undefined ? { install } : {}),
|
|
67
|
+
...(upstream?.stars !== undefined ? { stars: upstream.stars } : {}),
|
|
68
|
+
...(upstream?.pushedAt !== undefined ? { pushedAt: upstream.pushedAt } : {}),
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
export const compareEntries = (a, b) => {
|
|
73
|
+
if ((a.trust === "verified") !== (b.trust === "verified")) {
|
|
74
|
+
return a.trust === "verified" ? -1 : 1;
|
|
75
|
+
}
|
|
76
|
+
if ((a.stars ?? 0) !== (b.stars ?? 0)) {
|
|
77
|
+
return (b.stars ?? 0) - (a.stars ?? 0);
|
|
78
|
+
}
|
|
79
|
+
if ((a.pushedAt ?? "") !== (b.pushedAt ?? "")) {
|
|
80
|
+
return (b.pushedAt ?? "").localeCompare(a.pushedAt ?? "");
|
|
81
|
+
}
|
|
82
|
+
return a.name.localeCompare(b.name);
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
|
package/dist/source.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const RegistryInstallSchema: z.ZodObject<{
|
|
3
|
+
url: z.ZodString;
|
|
4
|
+
ref: z.ZodOptional<z.ZodString>;
|
|
5
|
+
path: z.ZodOptional<z.ZodString>;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
export type RegistryInstall = z.infer<typeof RegistryInstallSchema>;
|
|
8
|
+
export declare const resolveSource: (source: unknown, registryUrl: string, pluginRoot: string | undefined) => RegistryInstall | undefined;
|
|
9
|
+
export declare const isShaPinned: (install: RegistryInstall | undefined) => boolean;
|
|
10
|
+
export declare const githubRepoOf: (install: RegistryInstall | undefined) => string | undefined;
|
|
11
|
+
//# sourceMappingURL=source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../src/source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAWxB,eAAO,MAAM,qBAAqB;;;;iBAIhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAMpE,eAAO,MAAM,aAAa,WAAY,OAAO,eAAe,MAAM,cAAc,MAAM,GAAG,SAAS,KAAG,eAAe,GAAG,SAsBtH,CAAC;AASF,eAAO,MAAM,WAAW,YAAa,eAAe,GAAG,SAAS,KAAG,OAAmE,CAAC;AAIvI,eAAO,MAAM,YAAY,YAAa,eAAe,GAAG,SAAS,KAAG,MAAM,GAAG,SAM5E,CAAC"}
|
package/dist/source.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const RegistryInstallSchema = z.object({
|
|
3
|
+
url: z.string(),
|
|
4
|
+
ref: z.string().optional(),
|
|
5
|
+
path: z.string().optional(),
|
|
6
|
+
});
|
|
7
|
+
export const resolveSource = (source, registryUrl, pluginRoot) => {
|
|
8
|
+
if (typeof source === "string") {
|
|
9
|
+
const relative = source.replace(/^\.\//, "");
|
|
10
|
+
const root = pluginRoot?.replace(/^\.\//, "").replace(/\/$/, "");
|
|
11
|
+
return { url: registryUrl, path: root !== undefined && root !== "" ? `${root}/${relative}` : relative };
|
|
12
|
+
}
|
|
13
|
+
if (typeof source !== "object" || source === null) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const s = source;
|
|
17
|
+
const ref = s.sha ?? s.ref;
|
|
18
|
+
if (s.source === "github" && typeof s.repo === "string") {
|
|
19
|
+
return { url: `https://github.com/${s.repo}.git`, ...(ref !== undefined ? { ref } : {}) };
|
|
20
|
+
}
|
|
21
|
+
if (s.source === "url" && typeof s.url === "string") {
|
|
22
|
+
return { url: s.url, ...(ref !== undefined ? { ref } : {}) };
|
|
23
|
+
}
|
|
24
|
+
if (s.source === "git-subdir" && typeof s.url === "string" && typeof s.path === "string") {
|
|
25
|
+
return { url: s.url, path: s.path, ...(ref !== undefined ? { ref } : {}) };
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
};
|
|
29
|
+
const FULL_SHA = /^[0-9a-f]{40}$/;
|
|
30
|
+
export const isShaPinned = (install) => install?.ref !== undefined && FULL_SHA.test(install.ref);
|
|
31
|
+
export const githubRepoOf = (install) => {
|
|
32
|
+
if (install === undefined) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const match = /^https:\/\/github\.com\/([^/]+)\/(.+?)(?:\.git)?$/.exec(install.url);
|
|
36
|
+
return match === null ? undefined : `${match[1]}/${match[2]}`;
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source.js","sourceRoot":"","sources":["../src/source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAWxB,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1C,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;IACf,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC1B,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC9B,CAAC,CAAC;AAOH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,MAAe,EAAE,WAAmB,EAAE,UAA8B,EAA+B,EAAE;IAC/H,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC7B,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QAC7C,MAAM,IAAI,GAAG,UAAU,EAAE,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACjE,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,QAAQ,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC5G,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QAChD,OAAO,SAAS,CAAC;IACrB,CAAC;IACD,MAAM,CAAC,GAAG,MAAqG,CAAC;IAEhH,MAAM,GAAG,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC;IAC3B,IAAI,CAAC,CAAC,MAAM,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtD,OAAO,EAAE,GAAG,EAAE,sBAAsB,CAAC,CAAC,IAAI,MAAM,EAAE,GAAG,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;IAC9F,CAAC;IACD,IAAI,CAAC,CAAC,MAAM,KAAK,KAAK,IAAI,OAAO,CAAC,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;QAClD,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;IACjE,CAAC;IACD,IAAI,CAAC,CAAC,MAAM,KAAK,YAAY,IAAI,OAAO,CAAC,CAAC,GAAG,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACvF,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;IAC/E,CAAC;IACD,OAAO,SAAS,CAAC;AACrB,CAAC,CAAC;AAEF,MAAM,QAAQ,GAAG,gBAAgB,CAAC;AAOlC,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,OAAoC,EAAW,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,SAAS,IAAI,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;AAIvI,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,OAAoC,EAAsB,EAAE;IACrF,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,SAAS,CAAC;IACrB,CAAC;IACD,MAAM,KAAK,GAAG,mDAAmD,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACpF,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAClE,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@intentic/registry",
|
|
3
|
+
"version": "1.176.3",
|
|
4
|
+
"description": "The extension-registry file format — a git repo of sha-pinned pointers, shared by the daemon, the site gallery and the scanner",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/intentic/intentic.git",
|
|
10
|
+
"directory": "_sandbox/registry"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"src"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public",
|
|
18
|
+
"registry": "https://registry.npmjs.org/"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": {
|
|
24
|
+
"@intentic/src": "./src/index.ts",
|
|
25
|
+
"default": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"require": {
|
|
28
|
+
"@intentic/src": "./src/index.ts",
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"tslib": "2.8.1",
|
|
35
|
+
"zod": "4.4.3"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
39
|
+
"vitest": "4.1.10",
|
|
40
|
+
"@intentic/tsconfig": "0.0.0"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsgo",
|
|
44
|
+
"typecheck": "tsgo --noEmit -p tsconfig.test.json",
|
|
45
|
+
"watch": "tsgo --build --watch",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"test:watch": "vitest"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { compareEntries, type RegistryEntry, RegistryFactsSchema, RegistryFileSchema, resolveRegistry } from "./registry.js";
|
|
3
|
+
|
|
4
|
+
const SHA = "9f2c1ab0d4e5f60718293a4b5c6d7e8f90a1b2c3";
|
|
5
|
+
const REGISTRY = "https://github.com/intentic/registry";
|
|
6
|
+
|
|
7
|
+
const file = RegistryFileSchema.parse({
|
|
8
|
+
name: `intentic`,
|
|
9
|
+
plugins: [
|
|
10
|
+
{ name: `incidents`, kind: `extension`, trust: `verified`, source: { source: `github`, repo: `acme/incidents`, sha: SHA } },
|
|
11
|
+
{ name: `linear`, kind: `extension`, source: { source: `github`, repo: `acme/linear`, sha: SHA } },
|
|
12
|
+
{ name: `evil`, kind: `extension`, trust: `blocked`, trustReason: `exfiltrates workspace secrets`, source: { source: `github`, repo: `bad/evil` } },
|
|
13
|
+
],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe(`resolveRegistry`, () => {
|
|
17
|
+
it(`joins upstream facts onto the curated entries by name`, () => {
|
|
18
|
+
const facts = RegistryFactsSchema.parse({
|
|
19
|
+
scannedAt: `2026-08-01T00:00:00.000Z`,
|
|
20
|
+
entries: [{ name: `linear`, stars: 42, pushedAt: `2026-07-30T12:00:00Z` }],
|
|
21
|
+
});
|
|
22
|
+
const entries = resolveRegistry(file, facts, REGISTRY);
|
|
23
|
+
expect(entries.find((entry) => entry.name === `linear`)).toMatchObject({ stars: 42, pushedAt: `2026-07-30T12:00:00Z` });
|
|
24
|
+
// A curated entry the scanner has no facts for is still a complete row.
|
|
25
|
+
expect(entries.find((entry) => entry.name === `incidents`)?.stars).toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Most registries are six internal extensions in a private repo and run no scanner at all.
|
|
29
|
+
it(`resolves with no generated file at all`, () => {
|
|
30
|
+
expect(resolveRegistry(file, undefined, REGISTRY)).toHaveLength(3);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it(`defaults an unstated trust to listed and an unstated kind to plugin`, () => {
|
|
34
|
+
const entries = resolveRegistry(RegistryFileSchema.parse({ name: `x`, plugins: [{ name: `a`, source: `./a` }] }), undefined, REGISTRY);
|
|
35
|
+
expect(entries[0]).toMatchObject({ trust: `listed`, kind: `plugin` });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Blocked rows must survive the resolve — deleting them is what hides a warning from the people who
|
|
39
|
+
// already installed the thing.
|
|
40
|
+
it(`keeps a blocked entry, with its reason`, () => {
|
|
41
|
+
const blocked = resolveRegistry(file, undefined, REGISTRY).find((entry) => entry.name === `evil`);
|
|
42
|
+
expect(blocked).toMatchObject({ trust: `blocked`, trustReason: `exfiltrates workspace secrets` });
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe(`compareEntries`, () => {
|
|
47
|
+
const entry = (over: Partial<RegistryEntry>): RegistryEntry => ({ name: `x`, kind: `extension`, trust: `listed`, ...over });
|
|
48
|
+
|
|
49
|
+
it(`puts verified first even when it has fewer stars`, () => {
|
|
50
|
+
const sorted = [entry({ name: `popular`, stars: 900 }), entry({ name: `checked`, trust: `verified`, stars: 1 })].toSorted(compareEntries);
|
|
51
|
+
expect(sorted.map((e) => e.name)).toEqual([`checked`, `popular`]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it(`ranks by stars within a trust tier`, () => {
|
|
55
|
+
const sorted = [entry({ name: `few`, stars: 3 }), entry({ name: `many`, stars: 30 })].toSorted(compareEntries);
|
|
56
|
+
expect(sorted.map((e) => e.name)).toEqual([`many`, `few`]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// The common case at launch: everything on zero stars, so recency is what actually orders the page.
|
|
60
|
+
it(`breaks star ties on recency, then on name`, () => {
|
|
61
|
+
const sorted = [
|
|
62
|
+
entry({ name: `stale`, pushedAt: `2026-01-01T00:00:00Z` }),
|
|
63
|
+
entry({ name: `fresh`, pushedAt: `2026-07-31T00:00:00Z` }),
|
|
64
|
+
entry({ name: `undated` }),
|
|
65
|
+
].toSorted(compareEntries);
|
|
66
|
+
expect(sorted.map((e) => e.name)).toEqual([`fresh`, `stale`, `undated`]);
|
|
67
|
+
});
|
|
68
|
+
});
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type RegistryInstall, RegistryInstallSchema, resolveSource } from "./source.js";
|
|
3
|
+
|
|
4
|
+
/* THE EXTENSION REGISTRY: a git repo of pointers, and the two files in it.
|
|
5
|
+
*
|
|
6
|
+
* intentic hosts no extension code, builds none, and signs none. A registry is a repository whose
|
|
7
|
+
* `.claude-plugin/marketplace.json` lists pointers to other people's repositories at a commit — so listing
|
|
8
|
+
* costs a pull request, delisting deletes nothing, and anybody can run their own registry against a private
|
|
9
|
+
* one. That file is Claude Code's plugin-marketplace format deliberately: `kind` and `trust` are intentic's
|
|
10
|
+
* own fields and Claude Code ignores what it doesn't know, so one repo serves both consumers.
|
|
11
|
+
*
|
|
12
|
+
* The split into TWO files is the load-bearing part. `marketplace.json` is hand-edited and is the only place a
|
|
13
|
+
* human decision is recorded; `registry.generated.json` is bot-written and holds nothing but facts read back
|
|
14
|
+
* off the source host. Keeping star counts out of the curated file is what stops every nightly refresh from
|
|
15
|
+
* being a merge conflict against an open pull request — and it keeps the review diff to the thing being
|
|
16
|
+
* decided. A registry that carries no generated file is a registry with no stars, which renders fine. */
|
|
17
|
+
|
|
18
|
+
// Repo-relative, and the same two strings for the daemon (which clones), the scanner (which commits) and the
|
|
19
|
+
// site build (which fetches them raw).
|
|
20
|
+
export const REGISTRY_FILE = ".claude-plugin/marketplace.json";
|
|
21
|
+
export const REGISTRY_FACTS_FILE = ".claude-plugin/registry.generated.json";
|
|
22
|
+
|
|
23
|
+
// The default the Extensions tab browses and the gallery is built from. A default, not a gate: the browse
|
|
24
|
+
// field stays editable, so a company points it at an internal registry and never touches this one.
|
|
25
|
+
export const OFFICIAL_REGISTRY_URL = "https://github.com/intentic/registry";
|
|
26
|
+
|
|
27
|
+
// The GitHub topic an author adds to be found. This is discovery only — appearing in the scan gets you a pull
|
|
28
|
+
// request against the registry, never a listing.
|
|
29
|
+
export const REGISTRY_TOPIC = "intentic-extension";
|
|
30
|
+
|
|
31
|
+
/* WHAT A LISTING CLAIMS, and precisely what it doesn't.
|
|
32
|
+
*
|
|
33
|
+
* - `listed` the pointer resolves, the manifest parses, the publisher owns the source repo. NO claim about
|
|
34
|
+
* the code. This is the honest default and what the scanner's pull requests propose.
|
|
35
|
+
* - `verified` a human read the source at that sha. Sorted first, badged in both surfaces.
|
|
36
|
+
* - `blocked` known-malicious or known-broken. It STAYS in the file with a reason rather than being deleted:
|
|
37
|
+
* removing the row hides it from people browsing and tells the people who already installed it
|
|
38
|
+
* nothing, which is backwards — they are the ones at risk.
|
|
39
|
+
*
|
|
40
|
+
* Absent on a third-party registry ⇒ `listed`, because a registry that doesn't use the field hasn't asserted
|
|
41
|
+
* anything and shouldn't be read as if it had. */
|
|
42
|
+
export const RegistryTrustSchema = z.enum(["verified", "listed", "blocked"]);
|
|
43
|
+
export type RegistryTrust = z.infer<typeof RegistryTrustSchema>;
|
|
44
|
+
|
|
45
|
+
const RegistryFileEntrySchema = z.object({
|
|
46
|
+
name: z.string(),
|
|
47
|
+
description: z.string().optional(),
|
|
48
|
+
version: z.string().optional(),
|
|
49
|
+
// "extension" installs as the sha-pinned `extension` capability; absent/"plugin" is a Claude Code plugin.
|
|
50
|
+
kind: z.enum(["plugin", "extension"]).optional(),
|
|
51
|
+
trust: RegistryTrustSchema.optional(),
|
|
52
|
+
// Why it is blocked, or what was checked to verify it — shown verbatim wherever the badge is, because a
|
|
53
|
+
// trust state with no stated reason is an opinion the reader can't weigh.
|
|
54
|
+
trustReason: z.string().optional(),
|
|
55
|
+
category: z.string().optional(),
|
|
56
|
+
/* The mark the row is drawn with, copied off the extension's manifest by the scanner exactly like the
|
|
57
|
+
* description and the version — the same two tiers the manifest declares (a simple-icons slug, then a name
|
|
58
|
+
* from the app's icon set), and neither is required.
|
|
59
|
+
*
|
|
60
|
+
* It rides the CURATED file rather than the generated one, which looks wrong for a derived value until you
|
|
61
|
+
* ask what a registry is for: a listing is what a human decided to publish, and the mark is part of how it
|
|
62
|
+
* presents itself, so it belongs in the row a reviewer reads and can strike out. The generated file holds
|
|
63
|
+
* only what a bot re-reads nightly and nobody reviews. It also has to be here to be of any use at all —
|
|
64
|
+
* this is what the gallery and the in-app browse list render, and neither of them has the manifest: the
|
|
65
|
+
* whole point of the row is that the code has NOT been cloned yet. */
|
|
66
|
+
logo: z.string().optional(),
|
|
67
|
+
icon: z.string().optional(),
|
|
68
|
+
homepage: z.url().optional(),
|
|
69
|
+
source: z.unknown(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export const RegistryFileSchema = z.object({
|
|
73
|
+
name: z.string(),
|
|
74
|
+
metadata: z.object({ pluginRoot: z.string().optional() }).optional(),
|
|
75
|
+
plugins: z.array(RegistryFileEntrySchema),
|
|
76
|
+
});
|
|
77
|
+
export type RegistryFile = z.infer<typeof RegistryFileSchema>;
|
|
78
|
+
|
|
79
|
+
/* Facts read off the source host, keyed by the curated entry's name. Deliberately NOT the upstream head sha:
|
|
80
|
+
* the approved sha is the one that runs, updating is a pull request, and a file that advertised "there's a
|
|
81
|
+
* newer commit over there" would be inviting a click that skips the review the whole model rests on. */
|
|
82
|
+
const RegistryFactsEntrySchema = z.object({
|
|
83
|
+
name: z.string(),
|
|
84
|
+
stars: z.number().int().nonnegative().optional(),
|
|
85
|
+
// ISO-8601, last push to the source repo's default branch — the tiebreaker that does the real work while
|
|
86
|
+
// every listing still has single-digit stars.
|
|
87
|
+
pushedAt: z.string().optional(),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export const RegistryFactsSchema = z.object({
|
|
91
|
+
scannedAt: z.string(),
|
|
92
|
+
entries: z.array(RegistryFactsEntrySchema),
|
|
93
|
+
});
|
|
94
|
+
export type RegistryFacts = z.infer<typeof RegistryFactsSchema>;
|
|
95
|
+
|
|
96
|
+
// One row as every surface consumes it: the curated decision, the resolved pointer, and the upstream facts,
|
|
97
|
+
// already joined. This is also the daemon's browse wire shape — the app renders what the site renders.
|
|
98
|
+
export const RegistryEntrySchema = z.object({
|
|
99
|
+
name: z.string(),
|
|
100
|
+
description: z.string().optional(),
|
|
101
|
+
version: z.string().optional(),
|
|
102
|
+
kind: z.enum(["plugin", "extension"]),
|
|
103
|
+
trust: RegistryTrustSchema,
|
|
104
|
+
trustReason: z.string().optional(),
|
|
105
|
+
category: z.string().optional(),
|
|
106
|
+
// The mark, as the gallery and the app's browse list draw it — see the curated file's fields above.
|
|
107
|
+
logo: z.string().optional(),
|
|
108
|
+
icon: z.string().optional(),
|
|
109
|
+
homepage: z.string().optional(),
|
|
110
|
+
// Absent = a source the registry format can express but this daemon can't clone (npm, say). The row still
|
|
111
|
+
// renders; the install button doesn't.
|
|
112
|
+
install: RegistryInstallSchema.optional(),
|
|
113
|
+
stars: z.number().int().nonnegative().optional(),
|
|
114
|
+
pushedAt: z.string().optional(),
|
|
115
|
+
});
|
|
116
|
+
export type RegistryEntry = z.infer<typeof RegistryEntrySchema>;
|
|
117
|
+
|
|
118
|
+
/* Join the curated file to the generated facts. `facts` is undefined for any registry that runs no scanner,
|
|
119
|
+
* which is most of them — a private registry of six internal extensions wants the pointers and nothing else. */
|
|
120
|
+
export const resolveRegistry = (file: RegistryFile, facts: RegistryFacts | undefined, registryUrl: string): RegistryEntry[] => {
|
|
121
|
+
const byName = new Map(facts?.entries.map((entry) => [entry.name, entry]) ?? []);
|
|
122
|
+
return file.plugins.map((plugin) => {
|
|
123
|
+
const upstream = byName.get(plugin.name);
|
|
124
|
+
const install: RegistryInstall | undefined = resolveSource(plugin.source, registryUrl, file.metadata?.pluginRoot);
|
|
125
|
+
return {
|
|
126
|
+
name: plugin.name,
|
|
127
|
+
kind: plugin.kind ?? "plugin",
|
|
128
|
+
trust: plugin.trust ?? "listed",
|
|
129
|
+
...(plugin.description !== undefined ? { description: plugin.description } : {}),
|
|
130
|
+
...(plugin.version !== undefined ? { version: plugin.version } : {}),
|
|
131
|
+
...(plugin.trustReason !== undefined ? { trustReason: plugin.trustReason } : {}),
|
|
132
|
+
...(plugin.category !== undefined ? { category: plugin.category } : {}),
|
|
133
|
+
...(plugin.logo !== undefined ? { logo: plugin.logo } : {}),
|
|
134
|
+
...(plugin.icon !== undefined ? { icon: plugin.icon } : {}),
|
|
135
|
+
...(plugin.homepage !== undefined ? { homepage: plugin.homepage } : {}),
|
|
136
|
+
...(install !== undefined ? { install } : {}),
|
|
137
|
+
...(upstream?.stars !== undefined ? { stars: upstream.stars } : {}),
|
|
138
|
+
...(upstream?.pushedAt !== undefined ? { pushedAt: upstream.pushedAt } : {}),
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/* THE ORDER, and why it isn't just stars.
|
|
144
|
+
*
|
|
145
|
+
* Stars are the obvious sort and the wrong one on day one: every listing will sit at nought to three of them
|
|
146
|
+
* for months, so a pure star sort is a random order wearing a merit badge — and it is the single most
|
|
147
|
+
* purchasable number on GitHub. So: the one field a human actually asserted leads, stars rank within that,
|
|
148
|
+
* and recency breaks the ties that will be the overwhelmingly common case early on. Stars stay VISIBLE
|
|
149
|
+
* either way; a reader can weigh them, they just don't get to be the whole ranking. */
|
|
150
|
+
export const compareEntries = (a: RegistryEntry, b: RegistryEntry): number => {
|
|
151
|
+
if ((a.trust === "verified") !== (b.trust === "verified")) {
|
|
152
|
+
return a.trust === "verified" ? -1 : 1;
|
|
153
|
+
}
|
|
154
|
+
if ((a.stars ?? 0) !== (b.stars ?? 0)) {
|
|
155
|
+
return (b.stars ?? 0) - (a.stars ?? 0);
|
|
156
|
+
}
|
|
157
|
+
if ((a.pushedAt ?? "") !== (b.pushedAt ?? "")) {
|
|
158
|
+
return (b.pushedAt ?? "").localeCompare(a.pushedAt ?? "");
|
|
159
|
+
}
|
|
160
|
+
return a.name.localeCompare(b.name);
|
|
161
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { githubRepoOf, isShaPinned, resolveSource } from "./source.js";
|
|
3
|
+
|
|
4
|
+
const SHA = "9f2c1ab0d4e5f60718293a4b5c6d7e8f90a1b2c3";
|
|
5
|
+
const REGISTRY = "https://github.com/intentic/registry";
|
|
6
|
+
|
|
7
|
+
describe(`resolveSource`, () => {
|
|
8
|
+
it(`reads a bare string as a path inside the registry repo, with pluginRoot prepended`, () => {
|
|
9
|
+
expect(resolveSource(`./incidents`, REGISTRY, undefined)).toEqual({ url: REGISTRY, path: `incidents` });
|
|
10
|
+
expect(resolveSource(`incidents`, REGISTRY, `./plugins/`)).toEqual({ url: REGISTRY, path: `plugins/incidents` });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it(`points a github source at the repo, preferring sha over ref`, () => {
|
|
14
|
+
expect(resolveSource({ source: `github`, repo: `acme/incidents`, sha: SHA, ref: `main` }, REGISTRY, undefined)).toEqual({
|
|
15
|
+
url: `https://github.com/acme/incidents.git`,
|
|
16
|
+
ref: SHA,
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it(`carries url and git-subdir sources through`, () => {
|
|
21
|
+
expect(resolveSource({ source: `url`, url: `https://gitlab.com/acme/x.git`, ref: SHA }, REGISTRY, undefined)).toEqual({
|
|
22
|
+
url: `https://gitlab.com/acme/x.git`,
|
|
23
|
+
ref: SHA,
|
|
24
|
+
});
|
|
25
|
+
expect(resolveSource({ source: `git-subdir`, url: `https://github.com/acme/tools.git`, path: `ext/a`, sha: SHA }, REGISTRY, undefined)).toEqual({
|
|
26
|
+
url: `https://github.com/acme/tools.git`,
|
|
27
|
+
path: `ext/a`,
|
|
28
|
+
ref: SHA,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// An unclonable source is a row we still show — undefined here becomes "not installable", not a dropped entry.
|
|
33
|
+
it(`gives up on shapes it cannot clone rather than throwing`, () => {
|
|
34
|
+
expect(resolveSource({ source: `npm`, package: `@acme/incidents` }, REGISTRY, undefined)).toBeUndefined();
|
|
35
|
+
expect(resolveSource({ source: `github` }, REGISTRY, undefined)).toBeUndefined();
|
|
36
|
+
expect(resolveSource(undefined, REGISTRY, undefined)).toBeUndefined();
|
|
37
|
+
expect(resolveSource(null, REGISTRY, undefined)).toBeUndefined();
|
|
38
|
+
expect(resolveSource(42, REGISTRY, undefined)).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe(`isShaPinned`, () => {
|
|
43
|
+
it(`accepts only a full 40-character sha`, () => {
|
|
44
|
+
expect(isShaPinned({ url: `x`, ref: SHA })).toBe(true);
|
|
45
|
+
expect(isShaPinned({ url: `x`, ref: SHA.slice(0, 7) })).toBe(false);
|
|
46
|
+
expect(isShaPinned({ url: `x`, ref: `main` })).toBe(false);
|
|
47
|
+
expect(isShaPinned({ url: `x`, ref: SHA.toUpperCase() })).toBe(false);
|
|
48
|
+
expect(isShaPinned({ url: `x` })).toBe(false);
|
|
49
|
+
expect(isShaPinned(undefined)).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe(`githubRepoOf`, () => {
|
|
54
|
+
it(`extracts owner/repo with or without the .git suffix, and only for github`, () => {
|
|
55
|
+
expect(githubRepoOf({ url: `https://github.com/acme/incidents.git` })).toBe(`acme/incidents`);
|
|
56
|
+
expect(githubRepoOf({ url: `https://github.com/acme/incidents` })).toBe(`acme/incidents`);
|
|
57
|
+
expect(githubRepoOf({ url: `https://gitlab.com/acme/incidents.git` })).toBeUndefined();
|
|
58
|
+
expect(githubRepoOf(undefined)).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
});
|
package/src/source.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/* WHERE A REGISTRY ENTRY'S CODE LIVES, and how that resolves to something cloneable.
|
|
4
|
+
*
|
|
5
|
+
* A registry never hosts code — an entry is a pointer to somebody else's repository at a commit. `source` is
|
|
6
|
+
* that pointer in the shapes Claude Code's plugin-marketplace format already defines, so one registry repo
|
|
7
|
+
* serves both consumers; `resolveSource` maps the shapes we can clone onto the url/ref/path a capability
|
|
8
|
+
* install takes, and returns undefined for the ones we can't (npm, say) rather than dropping the entry —
|
|
9
|
+
* an entry that exists but can't be installed in a click is information, a missing row is a bug report. */
|
|
10
|
+
|
|
11
|
+
// The resolved pointer: exactly the fields a plugin- or extension-capability install needs.
|
|
12
|
+
export const RegistryInstallSchema = z.object({
|
|
13
|
+
url: z.string(),
|
|
14
|
+
ref: z.string().optional(),
|
|
15
|
+
path: z.string().optional(),
|
|
16
|
+
});
|
|
17
|
+
export type RegistryInstall = z.infer<typeof RegistryInstallSchema>;
|
|
18
|
+
|
|
19
|
+
/* A relative path means the code lives inside the registry repo itself, and metadata.pluginRoot prepends (the
|
|
20
|
+
* Claude Code spec). Every other shape points outward at a repo of its own. Kept as `unknown` on the way in:
|
|
21
|
+
* the format is somebody else's and gains shapes we don't know, and a source we can't read must degrade to
|
|
22
|
+
* "not installable from here" rather than failing the whole file to parse. */
|
|
23
|
+
export const resolveSource = (source: unknown, registryUrl: string, pluginRoot: string | undefined): RegistryInstall | undefined => {
|
|
24
|
+
if (typeof source === "string") {
|
|
25
|
+
const relative = source.replace(/^\.\//, "");
|
|
26
|
+
const root = pluginRoot?.replace(/^\.\//, "").replace(/\/$/, "");
|
|
27
|
+
return { url: registryUrl, path: root !== undefined && root !== "" ? `${root}/${relative}` : relative };
|
|
28
|
+
}
|
|
29
|
+
if (typeof source !== "object" || source === null) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const s = source as { source?: string; repo?: string; url?: string; path?: string; ref?: string; sha?: string };
|
|
33
|
+
// An exact sha pins harder than a ref when both are present.
|
|
34
|
+
const ref = s.sha ?? s.ref;
|
|
35
|
+
if (s.source === "github" && typeof s.repo === "string") {
|
|
36
|
+
return { url: `https://github.com/${s.repo}.git`, ...(ref !== undefined ? { ref } : {}) };
|
|
37
|
+
}
|
|
38
|
+
if (s.source === "url" && typeof s.url === "string") {
|
|
39
|
+
return { url: s.url, ...(ref !== undefined ? { ref } : {}) };
|
|
40
|
+
}
|
|
41
|
+
if (s.source === "git-subdir" && typeof s.url === "string" && typeof s.path === "string") {
|
|
42
|
+
return { url: s.url, path: s.path, ...(ref !== undefined ? { ref } : {}) };
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const FULL_SHA = /^[0-9a-f]{40}$/;
|
|
48
|
+
|
|
49
|
+
/* Whether this pointer names one immutable commit. An EXTENSION install requires it — extension code runs
|
|
50
|
+
* trusted in the owner's browser, so the approved code and the running code have to be the same object, and a
|
|
51
|
+
* branch name is a promise the upstream can break with a force-push. A registry entry that gives only a branch
|
|
52
|
+
* is still listed and still readable; it just can't be a one-click install, which is the pressure that makes
|
|
53
|
+
* authors pin. Plugins are laxer by design: they load into the agent, not the browser. */
|
|
54
|
+
export const isShaPinned = (install: RegistryInstall | undefined): boolean => install?.ref !== undefined && FULL_SHA.test(install.ref);
|
|
55
|
+
|
|
56
|
+
// `owner/repo` for a GitHub pointer — what the scanner keys upstream facts by, and what the gallery links to.
|
|
57
|
+
// Undefined for any host that isn't GitHub, which is a listing we simply carry no stars for.
|
|
58
|
+
export const githubRepoOf = (install: RegistryInstall | undefined): string | undefined => {
|
|
59
|
+
if (install === undefined) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const match = /^https:\/\/github\.com\/([^/]+)\/(.+?)(?:\.git)?$/.exec(install.url);
|
|
63
|
+
return match === null ? undefined : `${match[1]}/${match[2]}`;
|
|
64
|
+
};
|