@stigmer/plugin-package 3.18.0-dev.20260918103812 → 3.18.1-dev.20260919070736
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/client/builtin.d.ts +40 -0
- package/client/builtin.d.ts.map +1 -0
- package/client/builtin.js +66 -0
- package/client/builtin.js.map +1 -0
- package/client/prepare.d.ts +99 -0
- package/client/prepare.d.ts.map +1 -0
- package/client/prepare.js +89 -0
- package/client/prepare.js.map +1 -0
- package/client/presentation.d.ts +19 -0
- package/client/presentation.d.ts.map +1 -0
- package/client/presentation.js +44 -0
- package/client/presentation.js.map +1 -0
- package/client/release.d.ts +16 -0
- package/client/release.d.ts.map +1 -0
- package/client/release.js +24 -0
- package/client/release.js.map +1 -0
- package/client/reroot.d.ts +21 -0
- package/client/reroot.d.ts.map +1 -0
- package/client/reroot.js +37 -0
- package/client/reroot.js.map +1 -0
- package/client.d.ts +16 -2
- package/client.d.ts.map +1 -1
- package/client.js +16 -2
- package/client.js.map +1 -1
- package/detect.d.ts +7 -0
- package/detect.d.ts.map +1 -1
- package/detect.js +7 -0
- package/detect.js.map +1 -1
- package/files.d.ts +7 -0
- package/files.d.ts.map +1 -1
- package/files.js +7 -0
- package/files.js.map +1 -1
- package/index.d.ts +4 -2
- package/index.d.ts.map +1 -1
- package/index.js +4 -2
- package/index.js.map +1 -1
- package/marketplace/read-marketplace.d.ts +13 -0
- package/marketplace/read-marketplace.d.ts.map +1 -1
- package/marketplace/read-marketplace.js +38 -1
- package/marketplace/read-marketplace.js.map +1 -1
- package/package.json +1 -1
- package/presentation.d.ts +52 -0
- package/presentation.d.ts.map +1 -0
- package/presentation.js +151 -0
- package/presentation.js.map +1 -0
- package/src/__tests__/client-prepare.test.ts +167 -0
- package/src/__tests__/marketplace.test.ts +13 -1
- package/src/__tests__/presentation.test.ts +161 -0
- package/src/client/builtin.ts +79 -0
- package/src/client/prepare.ts +160 -0
- package/src/client/presentation.ts +47 -0
- package/src/client/release.ts +25 -0
- package/src/client/reroot.ts +35 -0
- package/src/client.ts +29 -2
- package/src/detect.ts +8 -0
- package/src/files.ts +7 -0
- package/src/index.ts +4 -2
- package/src/marketplace/read-marketplace.ts +40 -1
- package/src/presentation.ts +165 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one directory a zipped folder wraps itself in, so the archive reads
|
|
3
|
+
* as the folder.
|
|
4
|
+
*
|
|
5
|
+
* `zip -r plugin.zip my-plugin/`, a Finder "Compress", and a browser's
|
|
6
|
+
* download of a repository all produce an archive whose every path begins
|
|
7
|
+
* with the folder's own name; the manifest a reader looks for at the root
|
|
8
|
+
* is then one level down. This decides, from the paths alone, whether that
|
|
9
|
+
* is the case: it is when every path shares exactly one leading directory
|
|
10
|
+
* and nothing sits beside it. A tree with a root file, or two top-level
|
|
11
|
+
* directories, is taken as it is; a reader that then finds no manifest says
|
|
12
|
+
* so in its own words.
|
|
13
|
+
*
|
|
14
|
+
* Pure over paths; the caller strips the prefix. Kept out of `select.ts`,
|
|
15
|
+
* which reasons about a tree that already has its root.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** The shared leading directory of `paths`, or `null` when the tree is already rooted. */
|
|
19
|
+
export function rerootSingleDirectory(paths: readonly string[]): string | null {
|
|
20
|
+
let prefix: string | null = null;
|
|
21
|
+
for (const path of paths) {
|
|
22
|
+
const slash = path.indexOf("/");
|
|
23
|
+
if (slash <= 0) return null;
|
|
24
|
+
const head = path.slice(0, slash);
|
|
25
|
+
if (prefix === null) prefix = head;
|
|
26
|
+
else if (prefix !== head) return null;
|
|
27
|
+
}
|
|
28
|
+
return prefix;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** `paths` with `prefix/` removed from each; the caller has established every path carries it. */
|
|
32
|
+
export function stripDirectoryPrefix(paths: readonly string[], prefix: string): string[] {
|
|
33
|
+
const cut = prefix.length + 1;
|
|
34
|
+
return paths.map((path) => path.slice(cut));
|
|
35
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -15,13 +15,22 @@
|
|
|
15
15
|
* order the CLI's original walk produced them;
|
|
16
16
|
* - the archive and its digest (`archive.ts`): the deterministic zip and
|
|
17
17
|
* the SHA-256 the server records;
|
|
18
|
+
* - the preparation (`prepare.ts`): select, read, refuse, archive, digest,
|
|
19
|
+
* once, for a tree on disk, on a host, or in a browser's memory;
|
|
20
|
+
* - the presentation (`presentation.ts`): a card's face from a listed
|
|
21
|
+
* tree, reading the entry's manifest and nothing else;
|
|
18
22
|
* - the grammars (`refs.ts`): an install ref and a GitHub marketplace
|
|
19
23
|
* source, with their refusal sentences;
|
|
24
|
+
* - the built-in sources (`builtin.ts`): the official catalogue and the
|
|
25
|
+
* three vendors', listed by every client without configuration;
|
|
26
|
+
* - the release predicate (`release.ts`): which server versions the
|
|
27
|
+
* catalogue is published for;
|
|
28
|
+
* - the re-rooting rule (`reroot.ts`): a zipped folder reads as the folder;
|
|
20
29
|
* - the vocabulary (`vocabulary.ts`): the labels both surfaces print.
|
|
21
30
|
*
|
|
22
31
|
* Nothing here touches the filesystem, the network or any `node:*` module.
|
|
23
|
-
* A client's edge (a directory walk, a fetch) produces the
|
|
24
|
-
* the bytes; this entry decides what becomes of them.
|
|
32
|
+
* A client's edge (a directory walk, a fetch, a file pick) produces the
|
|
33
|
+
* candidates and the bytes; this entry decides what becomes of them.
|
|
25
34
|
*/
|
|
26
35
|
|
|
27
36
|
export { DEFAULT_PATTERNS } from "./client/ignore/defaults.js";
|
|
@@ -49,6 +58,24 @@ export {
|
|
|
49
58
|
selectPluginFiles,
|
|
50
59
|
} from "./client/select.js";
|
|
51
60
|
export { DETERMINISTIC_ZIP_MTIME, archivePlugin, digestArchive } from "./client/archive.js";
|
|
61
|
+
export {
|
|
62
|
+
type LazyCandidate,
|
|
63
|
+
type PrepareArchiveOutcome,
|
|
64
|
+
type PrepareFromTreeOptions,
|
|
65
|
+
type PreparePluginOutcome,
|
|
66
|
+
type PreparedPlugin,
|
|
67
|
+
preparePluginArchive,
|
|
68
|
+
preparePluginFromTree,
|
|
69
|
+
} from "./client/prepare.js";
|
|
70
|
+
export { readPluginPresentationFromTree } from "./client/presentation.js";
|
|
71
|
+
export {
|
|
72
|
+
type BuiltInMarketplace,
|
|
73
|
+
BUILT_IN_MARKETPLACES,
|
|
74
|
+
builtInSourceRefusal,
|
|
75
|
+
isBuiltInMarketplaceName,
|
|
76
|
+
} from "./client/builtin.js";
|
|
77
|
+
export { isReleaseVersion } from "./client/release.js";
|
|
78
|
+
export { rerootSingleDirectory, stripDirectoryPrefix } from "./client/reroot.js";
|
|
52
79
|
export {
|
|
53
80
|
type GitHubMarketplaceSource,
|
|
54
81
|
type GitHubSourceOutcome,
|
package/src/detect.ts
CHANGED
|
@@ -41,6 +41,14 @@ export const OPEN_MCP_CONFIG = "mcp.json";
|
|
|
41
41
|
/** The vendor precedence when no root manifest exists. */
|
|
42
42
|
const VENDOR_PRECEDENCE: readonly Exclude<PluginDialect, "agent-plugins">[] = ["claude", "cursor", "codex"];
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Every manifest location in the order that names a plugin: the root
|
|
46
|
+
* manifest, then the vendors. The one precedence the reader applies,
|
|
47
|
+
* exported so a partial read (`presentation.ts`) cannot rank the manifests
|
|
48
|
+
* differently from the full one.
|
|
49
|
+
*/
|
|
50
|
+
export const MANIFEST_PRECEDENCE: readonly PluginDialect[] = ["agent-plugins", ...VENDOR_PRECEDENCE];
|
|
51
|
+
|
|
44
52
|
const NAME_MAX_LENGTH = 64;
|
|
45
53
|
const NAME_PATTERN = /^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/;
|
|
46
54
|
|
package/src/files.ts
CHANGED
|
@@ -58,6 +58,13 @@ export const PLUGIN_DOCUMENT_LIMITS = {
|
|
|
58
58
|
subAgent: 1024 * 1024,
|
|
59
59
|
/** A document under `ai.stigmer/`: a resource YAML. */
|
|
60
60
|
overlay: 1024 * 1024,
|
|
61
|
+
/**
|
|
62
|
+
* A plugin's logo, the image a storefront card shows. Checked from the
|
|
63
|
+
* listing's declared size before a client hands the file's URL to an
|
|
64
|
+
* `<img>`; measured across the three vendors' catalogues on 2026-09-18,
|
|
65
|
+
* the largest published logo is 361 KB and the median 7.5 KB.
|
|
66
|
+
*/
|
|
67
|
+
logo: 1024 * 1024,
|
|
61
68
|
/**
|
|
62
69
|
* A marketplace file in any dialect. The largest public catalogue
|
|
63
70
|
* (`cursor/plugins`, 79 entries with descriptions) is under 32 KB; the cap
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* `@stigmer/plugin-package`: the public surface.
|
|
3
3
|
*
|
|
4
4
|
* `readPluginPackage` reads one plugin; `readMarketplace` reads a catalogue
|
|
5
|
-
* of them
|
|
5
|
+
* of them; `readPluginPresentation` reads what a card shows for one entry
|
|
6
|
+
* without opening the rest of it. Everything else here is what a consumer needs to supply its
|
|
6
7
|
* input (`PluginFiles`, the caps), route a directory (`MANIFEST_LOCATIONS`,
|
|
7
8
|
* `hasPluginManifest`, `MARKETPLACE_LOCATIONS`, `hasMarketplaceFile`), or
|
|
8
9
|
* render an outcome (the finding kinds and `isErrorKind`). The dialect
|
|
@@ -54,8 +55,9 @@ export type {
|
|
|
54
55
|
MarketplaceReadOutcome,
|
|
55
56
|
MarketplaceWarningKind,
|
|
56
57
|
} from "./marketplace/outcome.js";
|
|
57
|
-
export { hasMarketplaceFile, readMarketplace } from "./marketplace/read-marketplace.js";
|
|
58
|
+
export { hasMarketplaceFile, readMarketplace, readMarketplaceFile } from "./marketplace/read-marketplace.js";
|
|
58
59
|
export { readPluginPackage } from "./read-plugin-package.js";
|
|
60
|
+
export { type PluginPresentation, readPluginPresentation } from "./presentation.js";
|
|
59
61
|
export type {
|
|
60
62
|
IgnoredComponent,
|
|
61
63
|
IgnoredComponentKind,
|
|
@@ -25,6 +25,13 @@
|
|
|
25
25
|
* `readPluginPackage`; a consumer that wants to install it hands the same
|
|
26
26
|
* directory to the walker it already uses. Keeping the two reads apart is
|
|
27
27
|
* what lets a catalogue of eighty entries be listed without eighty reads.
|
|
28
|
+
*
|
|
29
|
+
* `readMarketplaceFile` is the same read without the tree: what the file
|
|
30
|
+
* declares, for a client that holds the file alone and asks only "does this
|
|
31
|
+
* source offer a plugin called X" before it pays for the tree (the CLI's
|
|
32
|
+
* bare-name search across GitHub sources fetches one small file per source
|
|
33
|
+
* instead of a zipball each). Whether the tree holds an entry is still
|
|
34
|
+
* `readMarketplace`'s to say, on the tree, before anything is installed.
|
|
28
35
|
*/
|
|
29
36
|
|
|
30
37
|
import { hasPluginManifest, isValidPluginName } from "../detect.js";
|
|
@@ -47,6 +54,24 @@ export function hasMarketplaceFile(paths: Iterable<string>): boolean {
|
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
export function readMarketplace(files: PluginFiles): MarketplaceReadOutcome {
|
|
57
|
+
return read(files, "tree");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The marketplace file as declared, the tree unverified: every entry with a
|
|
62
|
+
* readable source is offered, whether or not `files` holds its directory.
|
|
63
|
+
* For a client that has fetched the file alone; never for an install.
|
|
64
|
+
*/
|
|
65
|
+
export function readMarketplaceFile(files: PluginFiles): MarketplaceReadOutcome {
|
|
66
|
+
return read(files, "file");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* `tree`: entries whose directory the tree lacks, or holds without a plugin
|
|
71
|
+
* manifest, are dropped with their warning. `file`: entries are taken as the
|
|
72
|
+
* file declares them, and the tree is not consulted.
|
|
73
|
+
*/
|
|
74
|
+
function read(files: PluginFiles, verify: "tree" | "file"): MarketplaceReadOutcome {
|
|
50
75
|
const findings = new MarketplaceFindings();
|
|
51
76
|
const index = new PluginFileIndex(files);
|
|
52
77
|
|
|
@@ -62,7 +87,7 @@ export function readMarketplace(files: PluginFiles): MarketplaceReadOutcome {
|
|
|
62
87
|
|
|
63
88
|
const name = readName(object, path, findings);
|
|
64
89
|
const raw = readEntries(object, dialect, path, findings);
|
|
65
|
-
const entries = offeredEntries(raw, index, path, findings);
|
|
90
|
+
const entries = verify === "tree" ? offeredEntries(raw, index, path, findings) : declaredEntries(raw);
|
|
66
91
|
const defaults = resolveDefaults(object, raw, entries, dialect, path, findings);
|
|
67
92
|
|
|
68
93
|
if (findings.errors.length > 0 || name === undefined) return refused(findings);
|
|
@@ -349,6 +374,20 @@ function offeredEntries(
|
|
|
349
374
|
return offered;
|
|
350
375
|
}
|
|
351
376
|
|
|
377
|
+
/** Every entry with a readable source, as the file declares it; the tree is not asked. */
|
|
378
|
+
function declaredEntries(raw: readonly RawEntry[]): readonly MarketplaceEntry[] {
|
|
379
|
+
const declared: MarketplaceEntry[] = [];
|
|
380
|
+
for (const entry of raw) {
|
|
381
|
+
if (entry.dir === undefined) continue;
|
|
382
|
+
declared.push({
|
|
383
|
+
name: entry.name,
|
|
384
|
+
dir: entry.dir,
|
|
385
|
+
...(entry.description !== undefined && { description: entry.description }),
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
return declared;
|
|
389
|
+
}
|
|
390
|
+
|
|
352
391
|
/** The files under `dir` as plugin-relative paths, so `hasPluginManifest` reads them as it reads a plugin's own listing. */
|
|
353
392
|
function childFilesOf(index: PluginFileIndex, dir: string): readonly string[] {
|
|
354
393
|
const prefix = dir === "" ? "" : `${dir}/`;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a storefront shows for a plugin before anyone installs it: a display
|
|
3
|
+
* name, a logo, an author, a version, a description, a category.
|
|
4
|
+
*
|
|
5
|
+
* This is a read apart from `readPluginPackage` on purpose. The full read
|
|
6
|
+
* decides what Stigmer INSTALLS and refuses what it cannot; a card must
|
|
7
|
+
* render for every entry a catalogue lists, refused or not, and must cost
|
|
8
|
+
* one manifest, not the plugin's whole tree. So this reader opens only the
|
|
9
|
+
* manifests present, takes what they say about appearance, and never
|
|
10
|
+
* reports a finding: an unreadable manifest contributes nothing, a wrong
|
|
11
|
+
* type is treated as absent, and the caller draws a fallback face.
|
|
12
|
+
*
|
|
13
|
+
* Which manifest speaks is the full reader's rule (`MANIFEST_PRECEDENCE`:
|
|
14
|
+
* the root manifest, then Claude, Cursor, Codex), so a plugin shipping two
|
|
15
|
+
* dialects is named the same way on a card and in an install. Identity
|
|
16
|
+
* fields (`name`, `version`, `description`, `author`) come from the first
|
|
17
|
+
* manifest that carries each; the presentation fields come from the
|
|
18
|
+
* dialects that define them: Cursor's top-level `displayName`, `logo` and
|
|
19
|
+
* `category`; the legacy Codex manifest's `interface.displayName`,
|
|
20
|
+
* `interface.logo` and `interface.category`. Claude Code's manifest and the
|
|
21
|
+
* open format define none, and a logo rule for the `ai.stigmer` extension
|
|
22
|
+
* namespace waits for the first official plugin that needs one.
|
|
23
|
+
*
|
|
24
|
+
* A logo is a path inside the plugin, and the card that shows it fetches it
|
|
25
|
+
* by URL from wherever the tree lives. So the path is kept only when it is
|
|
26
|
+
* contained, names an image by extension, is LISTED in the tree, and its
|
|
27
|
+
* declared size is under `PLUGIN_DOCUMENT_LIMITS.logo`; the library's rule
|
|
28
|
+
* that a cap is checked from declared sizes before a byte moves holds here
|
|
29
|
+
* too. Both spellings the vendors use (`assets/logo.png`, `./assets/logo.png`)
|
|
30
|
+
* are accepted, as the marketplace reader accepts both for a source.
|
|
31
|
+
*
|
|
32
|
+
* `logo` stays an ignored component in the full read: nothing in an
|
|
33
|
+
* Organization carries the image after an install, and the preview's "Not
|
|
34
|
+
* installed" line is true until the stored plugin does.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { MANIFEST_PRECEDENCE } from "./detect.js";
|
|
38
|
+
import { decodeUtf8, isContainedPath, PLUGIN_DOCUMENT_LIMITS, PluginFileIndex, type PluginFiles } from "./files.js";
|
|
39
|
+
import { isJsonObject, type JsonObject } from "./documents.js";
|
|
40
|
+
import { MANIFEST_LOCATIONS } from "./messages.js";
|
|
41
|
+
import type { PluginAuthor, PluginDialect } from "./types.js";
|
|
42
|
+
|
|
43
|
+
/** What a card shows for a plugin; every field optional, because every dialect may omit it. */
|
|
44
|
+
export interface PluginPresentation {
|
|
45
|
+
/** The name for people (`Thermos`); the install name stays `name`. */
|
|
46
|
+
readonly displayName?: string;
|
|
47
|
+
/** A plugin-relative path to an image the tree lists, verified as described in the module header. */
|
|
48
|
+
readonly logo?: string;
|
|
49
|
+
readonly author?: PluginAuthor;
|
|
50
|
+
readonly version?: string;
|
|
51
|
+
readonly description?: string;
|
|
52
|
+
/** The catalogue's own grouping word (`developer-tools`, `Productivity`), as written. */
|
|
53
|
+
readonly category?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The image extensions a browser renders in an `<img>`; a logo named otherwise is not shown. */
|
|
57
|
+
const LOGO_EXTENSIONS: ReadonlySet<string> = new Set(["png", "svg", "jpg", "jpeg", "webp", "gif"]);
|
|
58
|
+
|
|
59
|
+
/** The appearance fields of every manifest present, merged in precedence order. */
|
|
60
|
+
export function readPluginPresentation(files: PluginFiles): PluginPresentation {
|
|
61
|
+
const index = new PluginFileIndex(files);
|
|
62
|
+
const presentation: { -readonly [K in keyof PluginPresentation]: PluginPresentation[K] } = {};
|
|
63
|
+
|
|
64
|
+
for (const dialect of MANIFEST_PRECEDENCE) {
|
|
65
|
+
const object = readManifest(index, MANIFEST_LOCATIONS[dialect]);
|
|
66
|
+
if (object === undefined) continue;
|
|
67
|
+
const contribution = contributionOf(dialect, object, index);
|
|
68
|
+
for (const key of Object.keys(contribution) as (keyof PluginPresentation)[]) {
|
|
69
|
+
if (presentation[key] === undefined) Object.assign(presentation, { [key]: contribution[key] });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return presentation;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The manifest at `path` as an object, or `undefined` for anything a card should not stumble on. */
|
|
77
|
+
function readManifest(index: PluginFileIndex, path: string): JsonObject | undefined {
|
|
78
|
+
const entry = index.entry(path);
|
|
79
|
+
if (entry === undefined || entry.size > PLUGIN_DOCUMENT_LIMITS.manifest) return undefined;
|
|
80
|
+
let bytes: Uint8Array;
|
|
81
|
+
try {
|
|
82
|
+
bytes = index.files.read(path);
|
|
83
|
+
} catch {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
if (bytes.length > PLUGIN_DOCUMENT_LIMITS.manifest) return undefined;
|
|
87
|
+
let value: unknown;
|
|
88
|
+
try {
|
|
89
|
+
value = JSON.parse(decodeUtf8(bytes));
|
|
90
|
+
} catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
return isJsonObject(value) ? value : undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function contributionOf(dialect: PluginDialect, object: JsonObject, index: PluginFileIndex): PluginPresentation {
|
|
97
|
+
const identity = identityOf(object);
|
|
98
|
+
switch (dialect) {
|
|
99
|
+
case "cursor":
|
|
100
|
+
return { ...identity, ...appearanceOf(object, index) };
|
|
101
|
+
case "codex": {
|
|
102
|
+
const surface = object["interface"];
|
|
103
|
+
return isJsonObject(surface) ? { ...identity, ...appearanceOf(surface, index) } : identity;
|
|
104
|
+
}
|
|
105
|
+
case "agent-plugins":
|
|
106
|
+
case "claude":
|
|
107
|
+
return identity;
|
|
108
|
+
default: {
|
|
109
|
+
const exhaustive: never = dialect;
|
|
110
|
+
return exhaustive;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The identity fields every dialect shares, typed loosely: a wrong type is absent, never a finding. */
|
|
116
|
+
function identityOf(object: JsonObject): PluginPresentation {
|
|
117
|
+
const author = authorOf(object["author"]);
|
|
118
|
+
return {
|
|
119
|
+
...optional("version", object["version"]),
|
|
120
|
+
...optional("description", object["description"]),
|
|
121
|
+
...(author !== undefined && { author }),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** `displayName`, `logo` and `category` from an object that carries them at its top level (a Cursor manifest, a Codex `interface`). */
|
|
126
|
+
function appearanceOf(object: JsonObject, index: PluginFileIndex): PluginPresentation {
|
|
127
|
+
const logo = logoPath(object["logo"], index);
|
|
128
|
+
return {
|
|
129
|
+
...optional("displayName", object["displayName"]),
|
|
130
|
+
...optional("category", object["category"]),
|
|
131
|
+
...(logo !== undefined && { logo }),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function authorOf(value: unknown): PluginAuthor | undefined {
|
|
136
|
+
if (!isJsonObject(value)) return undefined;
|
|
137
|
+
const author: { -readonly [K in keyof PluginAuthor]: PluginAuthor[K] } = {
|
|
138
|
+
...optional("name", value["name"]),
|
|
139
|
+
...optional("email", value["email"]),
|
|
140
|
+
...optional("url", value["url"]),
|
|
141
|
+
};
|
|
142
|
+
return Object.keys(author).length === 0 ? undefined : author;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* A declared logo as a plugin-relative path the tree lists, or nothing.
|
|
147
|
+
* `./` is stripped (Codex's spelling); an absolute path, a `..` segment, a
|
|
148
|
+
* URL, a non-image extension, an unlisted file or an over-cap file all
|
|
149
|
+
* mean "no logo", never a refusal.
|
|
150
|
+
*/
|
|
151
|
+
function logoPath(value: unknown, index: PluginFileIndex): string | undefined {
|
|
152
|
+
if (typeof value !== "string") return undefined;
|
|
153
|
+
const path = value.startsWith("./") ? value.slice(2) : value;
|
|
154
|
+
if (!isContainedPath(path)) return undefined;
|
|
155
|
+
const dot = path.lastIndexOf(".");
|
|
156
|
+
if (dot === -1 || !LOGO_EXTENSIONS.has(path.slice(dot + 1).toLowerCase())) return undefined;
|
|
157
|
+
const entry = index.entry(path);
|
|
158
|
+
if (entry === undefined || entry.size > PLUGIN_DOCUMENT_LIMITS.logo) return undefined;
|
|
159
|
+
return path;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** `{ [key]: value }` when `value` is a non-empty string; `{}` otherwise. */
|
|
163
|
+
function optional<K extends string>(key: K, value: unknown): Partial<Record<K, string>> {
|
|
164
|
+
return typeof value === "string" && value !== "" ? ({ [key]: value } as Record<K, string>) : {};
|
|
165
|
+
}
|