@capybara-hq/plugin-tools 0.1.0-rc.0 → 0.1.0-rc.10
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 +26 -19
- package/bin/capybara-plugin.mjs +1 -1
- package/dist/check/applied-migrations.d.ts +23 -0
- package/dist/check/applied-migrations.js +86 -0
- package/dist/check/cli.d.ts +2 -1
- package/dist/check/cli.js +7 -3
- package/dist/check/config-forms.d.ts +13 -0
- package/dist/check/config-forms.js +71 -0
- package/dist/check/deps.d.ts +4 -2
- package/dist/check/deps.js +14 -2
- package/dist/check/index.d.ts +4 -1
- package/dist/check/index.js +4 -1
- package/dist/check/migrations.d.ts +1 -1
- package/dist/check/migrations.js +30 -1
- package/dist/check/org-override.d.ts +10 -0
- package/dist/check/org-override.js +62 -0
- package/dist/check/plugin-pack.js +2 -0
- package/dist/check/plugin-permissions.d.ts +34 -1
- package/dist/check/plugin-permissions.js +66 -6
- package/dist/check/plugin-tool-keys.d.ts +13 -0
- package/dist/check/plugin-tool-keys.js +38 -0
- package/dist/check/release-metadata.d.ts +5 -0
- package/dist/check/release-metadata.js +47 -0
- package/dist/check/report.d.ts +4 -3
- package/dist/check/report.js +13 -3
- package/dist/check/run.d.ts +3 -1
- package/dist/check/run.js +12 -1
- package/dist/check/source.d.ts +7 -0
- package/dist/check/source.js +2 -0
- package/dist/check/themes.d.ts +19 -0
- package/dist/check/themes.js +112 -0
- package/dist/check/tool-design.d.ts +1 -1
- package/dist/check/tool-design.js +3 -3
- package/dist/check/ui-slots.d.ts +7 -0
- package/dist/check/ui-slots.js +30 -0
- package/dist/cli.js +0 -2
- package/dist/marketplace/cli.d.ts +10 -7
- package/dist/marketplace/cli.js +20 -10
- package/dist/marketplace/marketplace.d.ts +46 -24
- package/dist/marketplace/marketplace.js +232 -60
- package/dist/pack/cli.d.ts +3 -10
- package/dist/pack/cli.js +3 -40
- package/dist/pack/config-schemas.d.ts +34 -0
- package/dist/pack/config-schemas.js +151 -0
- package/dist/pack/index.d.ts +2 -2
- package/dist/pack/index.js +2 -2
- package/dist/pack/pack.d.ts +8 -2
- package/dist/pack/pack.js +154 -38
- package/package.json +4 -4
- package/dist/pack/bootstrap.d.ts +0 -25
- package/dist/pack/bootstrap.js +0 -70
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
export declare const MARKETPLACE_USAGE: string;
|
|
2
2
|
/**
|
|
3
3
|
* `capybara-plugin marketplace`: writes (`--write`) or checks (`--check`) the repository's
|
|
4
|
-
* marketplace manifest
|
|
4
|
+
* marketplace manifest, one entry per public plugin at its current version, against reproducible
|
|
5
|
+
* packs of its public plugins ({@link syncMarketplace}).
|
|
5
6
|
*
|
|
6
7
|
* - `--root <dir>`: the repository (default: the current directory);
|
|
7
8
|
* - `--file <path>`: the manifest, relative to the root (default `.capybara-plugin/marketplace.json`);
|
|
8
9
|
* - `--plugins <glob>`: the plugin directories, relative to the root (default `plugins/*`, or the
|
|
9
10
|
* root itself when it is a plugin).
|
|
10
11
|
*
|
|
11
|
-
* - `--base <rev>` (with `--check`, on pull requests):
|
|
12
|
-
*
|
|
13
|
-
* match the tree's packs (`marketplace-integrity`).
|
|
12
|
+
* - `--base <rev>` (with `--check`, on pull requests): an entry's version may only increase over the
|
|
13
|
+
* versions `<rev>` lists (`marketplace-version-decrease`), a version `<rev>` lists keeps its
|
|
14
|
+
* integrity, and the entries added since must match the tree's packs (`marketplace-integrity`).
|
|
14
15
|
*
|
|
15
|
-
* `--check` exits 1 on any finding (`marketplace-missing`,
|
|
16
|
-
* `marketplace-
|
|
17
|
-
*
|
|
16
|
+
* `--check` exits 1 on any finding that is not a warning (`marketplace-missing`,
|
|
17
|
+
* `marketplace-display-stale`, `marketplace-integrity`, `marketplace-version-decrease`,
|
|
18
|
+
* `marketplace-invalid`); `--write` writes each plugin's entry (replacing its other versions,
|
|
19
|
+
* refreshing a listed entry's summary, displayName and icon) and exits 1 only on
|
|
20
|
+
* `marketplace-invalid`.
|
|
18
21
|
* Relative paths resolve against `cwd`. Returns the exit code.
|
|
19
22
|
*/
|
|
20
23
|
export declare function marketplaceCommand(args: string[], cwd: string): Promise<number>;
|
package/dist/marketplace/cli.js
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
3
|
import { formatFinding } from "../check/report.js";
|
|
4
|
+
import { errorsOf } from "../check/source.js";
|
|
4
5
|
import { MARKETPLACE_FILE, syncMarketplace } from "./marketplace.js";
|
|
5
6
|
export const MARKETPLACE_USAGE = "capybara-plugin marketplace --write|--check [--base <rev>] [--root <dir>] [--file <path>]\n" +
|
|
6
7
|
" [--plugins <glob>]";
|
|
7
8
|
/**
|
|
8
9
|
* `capybara-plugin marketplace`: writes (`--write`) or checks (`--check`) the repository's
|
|
9
|
-
* marketplace manifest
|
|
10
|
+
* marketplace manifest, one entry per public plugin at its current version, against reproducible
|
|
11
|
+
* packs of its public plugins ({@link syncMarketplace}).
|
|
10
12
|
*
|
|
11
13
|
* - `--root <dir>`: the repository (default: the current directory);
|
|
12
14
|
* - `--file <path>`: the manifest, relative to the root (default `.capybara-plugin/marketplace.json`);
|
|
13
15
|
* - `--plugins <glob>`: the plugin directories, relative to the root (default `plugins/*`, or the
|
|
14
16
|
* root itself when it is a plugin).
|
|
15
17
|
*
|
|
16
|
-
* - `--base <rev>` (with `--check`, on pull requests):
|
|
17
|
-
*
|
|
18
|
-
* match the tree's packs (`marketplace-integrity`).
|
|
18
|
+
* - `--base <rev>` (with `--check`, on pull requests): an entry's version may only increase over the
|
|
19
|
+
* versions `<rev>` lists (`marketplace-version-decrease`), a version `<rev>` lists keeps its
|
|
20
|
+
* integrity, and the entries added since must match the tree's packs (`marketplace-integrity`).
|
|
19
21
|
*
|
|
20
|
-
* `--check` exits 1 on any finding (`marketplace-missing`,
|
|
21
|
-
* `marketplace-
|
|
22
|
-
*
|
|
22
|
+
* `--check` exits 1 on any finding that is not a warning (`marketplace-missing`,
|
|
23
|
+
* `marketplace-display-stale`, `marketplace-integrity`, `marketplace-version-decrease`,
|
|
24
|
+
* `marketplace-invalid`); `--write` writes each plugin's entry (replacing its other versions,
|
|
25
|
+
* refreshing a listed entry's summary, displayName and icon) and exits 1 only on
|
|
26
|
+
* `marketplace-invalid`.
|
|
23
27
|
* Relative paths resolve against `cwd`. Returns the exit code.
|
|
24
28
|
*/
|
|
25
29
|
export async function marketplaceCommand(args, cwd) {
|
|
@@ -59,14 +63,20 @@ export async function marketplaceCommand(args, cwd) {
|
|
|
59
63
|
});
|
|
60
64
|
for (const f of r.findings)
|
|
61
65
|
console.error(formatFinding(f, root));
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
const errors = errorsOf(r.findings);
|
|
67
|
+
if (errors.length > 0) {
|
|
68
|
+
console.error(`\n${errors.length} marketplace finding(s)`);
|
|
64
69
|
return 1;
|
|
65
70
|
}
|
|
66
71
|
if (values.write) {
|
|
67
72
|
for (const e of r.added)
|
|
68
73
|
console.log(`added ${e.name}@${e.version}`);
|
|
69
|
-
|
|
74
|
+
for (const k of r.removed)
|
|
75
|
+
console.log(`removed ${k}`);
|
|
76
|
+
for (const k of r.refreshed)
|
|
77
|
+
console.log(`refreshed ${k}`);
|
|
78
|
+
const changed = r.added.length + r.removed.length + r.refreshed.length > 0;
|
|
79
|
+
console.log(changed ? `wrote ${file}` : "marketplace is up to date");
|
|
70
80
|
}
|
|
71
81
|
else {
|
|
72
82
|
console.log("marketplace is up to date");
|
|
@@ -9,44 +9,66 @@ export interface SyncMarketplaceOptions {
|
|
|
9
9
|
file: string;
|
|
10
10
|
/** Plugin directories relative to `root` (`*` within one segment); default as `discoverPlugins`. */
|
|
11
11
|
plugins?: string;
|
|
12
|
-
/**
|
|
12
|
+
/** Write the plugins' entries into `file` (`--write`); otherwise only report them (`--check`). */
|
|
13
13
|
write: boolean;
|
|
14
14
|
/**
|
|
15
15
|
* A git revision (`--check --base <rev>`, pull requests): the entries the file had there are
|
|
16
|
-
* published
|
|
17
|
-
*
|
|
18
|
-
* (`marketplace-
|
|
16
|
+
* published. An entry of a version the base lists must keep its source and integrity
|
|
17
|
+
* (`marketplace-integrity`); an entry of a version the base does not list must be newer than
|
|
18
|
+
* every version the base lists for that plugin (`marketplace-version-decrease`) and, when it is
|
|
19
|
+
* the tree's version, match the tree's pack (`marketplace-integrity`). A file the base does not
|
|
20
|
+
* have constrains nothing.
|
|
19
21
|
*/
|
|
20
22
|
base?: string;
|
|
21
23
|
}
|
|
22
24
|
/** The result of {@link syncMarketplace}. */
|
|
23
25
|
export interface SyncMarketplaceResult {
|
|
24
|
-
/**
|
|
26
|
+
/** New entries: plugins not listed at their current version (written with `write`). */
|
|
25
27
|
added: MarketplaceEntry[];
|
|
28
|
+
/** `name@version` of the other entries of the tree's plugins (dropped with `write`). */
|
|
29
|
+
removed: string[];
|
|
26
30
|
/**
|
|
27
|
-
* `
|
|
28
|
-
*
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
* `name@version` of listed entries whose `summary` / `displayName` / `icon` differ from plugin.json
|
|
32
|
+
* (rewritten with `write`; source and integrity never change).
|
|
33
|
+
*/
|
|
34
|
+
refreshed: string[];
|
|
35
|
+
/**
|
|
36
|
+
* `marketplace-invalid` (the file does not parse, or would not after writing),
|
|
37
|
+
* `marketplace-integrity` and `marketplace-version-decrease` (with a base, see
|
|
38
|
+
* {@link SyncMarketplaceOptions.base}) and, without `write`, `marketplace-missing` (one per
|
|
39
|
+
* plugin whose current version is not listed, or not as its only entry) and
|
|
40
|
+
* `marketplace-display-stale` (a listed entry's summary / displayName / icon differ from plugin.json).
|
|
31
41
|
*/
|
|
32
42
|
findings: Finding[];
|
|
33
43
|
}
|
|
34
44
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
45
|
+
* The repository path of a plugin's icon: the plugin.json `icon` (relative to the plugin's
|
|
46
|
+
* directory `dir`) as `./<path from the root>` (an entry's `icon`, read at the marketplace's
|
|
47
|
+
* commit, so it follows every release without naming a tag).
|
|
48
|
+
*/
|
|
49
|
+
export declare function repoIconPath(root: string, dir: string, icon: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* `capybara-plugin marketplace`: keeps a plugin repository's `marketplace.json` listing one entry
|
|
52
|
+
* per public plugin, its current version (plugin component management spec §13.2, replacing the
|
|
53
|
+
* one-entry-per-version rule of the package ecosystem spec §5.2). Each public plugin (not
|
|
54
|
+
* `"private": true`) not listed at its version is packed reproducibly with {@link packPlugin} and
|
|
55
|
+
* gets an entry `{ name, version, summary, displayName?, icon?, homepage?, permissions, source: {
|
|
56
|
+
* npm: { package, version, integrity } } }` (`summary` from the plugin.json `description`, a string
|
|
57
|
+
* being `en`; `displayName` copied from plugin.json; `icon` the plugin.json `icon` as a path in the
|
|
58
|
+
* repository, `./<plugin dir>/<icon>`, which a git marketplace reads at its refreshed commit;
|
|
59
|
+
* `integrity` the pack's sha512, which the release
|
|
60
|
+
* then publishes byte for byte; `permissions` display only). The new entry takes the place of the
|
|
61
|
+
* plugin's first entry (a version that moved), the plugin's other entries are dropped (also those
|
|
62
|
+
* of a file written in the older one-entry-per-version form), and a plugin not listed yet is
|
|
63
|
+
* appended. Entries of plugins not in the tree are kept. A version already listed is not packed or
|
|
64
|
+
* compared: once published, its bytes are the release gate's business (`release --marketplace`),
|
|
65
|
+
* so a later source or toolchain change at the same version is not a finding; only its display
|
|
66
|
+
* fields (`summary`, `displayName`, `icon`) are refreshed from the source plugin.json. Every other field
|
|
67
|
+
* of a kept entry stays as written, among them `deprecated`, which the maintainer sets by hand on
|
|
68
|
+
* a published version's entry (not in plugin.json: that would change the packed bytes); a new
|
|
69
|
+
* version's entry has none, and no rule compares it with the base. With `base`, see
|
|
70
|
+
* {@link SyncMarketplaceOptions.base}. The file is written formatted as Biome formats it
|
|
71
|
+
* ({@link formatMarketplaceJson}); nothing is written when there is any other finding.
|
|
50
72
|
*/
|
|
51
73
|
export declare function syncMarketplace(opts: SyncMarketplaceOptions): Promise<SyncMarketplaceResult>;
|
|
52
74
|
/**
|
|
@@ -4,16 +4,22 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join, relative } from "node:path";
|
|
6
6
|
import { isSensitivePermissionKey, PACKAGES_MAX_BYTES_DEFAULT, permissionKeys, permissionSetOfPackage, readExtensionPackage, WARNINGS_KEY_PREFIX, } from "@capybara-hq/kernel";
|
|
7
|
-
import { MARKETPLACE_LIMITS, PLUGIN_MANIFEST_FILE, parseMarketplaceManifest, SDK_VERSION, } from "@capybara-hq/plugin-sdk";
|
|
7
|
+
import { MARKETPLACE_LIMITS, PLUGIN_MANIFEST_FILE, parseMarketplaceManifest, parsePluginManifest, pluginDescription, SDK_VERSION, } from "@capybara-hq/plugin-sdk";
|
|
8
8
|
import { discoverPlugins } from "../discover.js";
|
|
9
9
|
import { integrityOf } from "../integrity.js";
|
|
10
10
|
import { packPlugin } from "../pack/pack.js";
|
|
11
|
+
import { compareSemver, parseSemver } from "../release/semver.js";
|
|
11
12
|
export { MARKETPLACE_FILE } from "./file.js";
|
|
12
13
|
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
13
|
-
/** The marketplace summary
|
|
14
|
-
function
|
|
14
|
+
/** The marketplace summary text: a description cut to the summary limit. */
|
|
15
|
+
function cut(text) {
|
|
15
16
|
const max = MARKETPLACE_LIMITS.summaryChars;
|
|
16
|
-
return
|
|
17
|
+
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
18
|
+
}
|
|
19
|
+
/** The marketplace summary of a plugin: its plugin.json description (`{ en, zh? }`), each cut. */
|
|
20
|
+
function summaryOf(description) {
|
|
21
|
+
const d = pluginDescription(description);
|
|
22
|
+
return { en: cut(d.en), ...(d.zh !== undefined && { zh: cut(d.zh) }) };
|
|
17
23
|
}
|
|
18
24
|
/** The permission summary of a packed plugin, as the kernel computes it at import (display only). */
|
|
19
25
|
async function permissionsOf(bytes, hasExtension) {
|
|
@@ -31,28 +37,92 @@ async function permissionsOf(bytes, hasExtension) {
|
|
|
31
37
|
/** An entry's version: `version`, else its npm source's. */
|
|
32
38
|
const versionOf = (e) => e.version ?? ("npm" in e.source ? e.source.npm.version : undefined);
|
|
33
39
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
40
|
+
* The repository path of a plugin's icon: the plugin.json `icon` (relative to the plugin's
|
|
41
|
+
* directory `dir`) as `./<path from the root>` (an entry's `icon`, read at the marketplace's
|
|
42
|
+
* commit, so it follows every release without naming a tag).
|
|
43
|
+
*/
|
|
44
|
+
export function repoIconPath(root, dir, icon) {
|
|
45
|
+
const rel = relative(root, dir).split("\\").join("/");
|
|
46
|
+
const file = icon.replace(/^\.\//, "");
|
|
47
|
+
return `./${rel === "" ? file : `${rel}/${file}`}`;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The display fields of a plugin's source plugin.json (`dir` its directory under `root`); undefined
|
|
51
|
+
* when it does not parse.
|
|
52
|
+
*/
|
|
53
|
+
function displayOf(json, root, dir) {
|
|
54
|
+
const m = parsePluginManifest(json);
|
|
55
|
+
if ("errors" in m)
|
|
56
|
+
return undefined;
|
|
57
|
+
return {
|
|
58
|
+
summary: summaryOf(m.description),
|
|
59
|
+
...(m.displayName !== undefined && { displayName: m.displayName }),
|
|
60
|
+
...(m.icon !== undefined && { icon: repoIconPath(root, dir, m.icon) }),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** The display fields after `summary`, in this order. */
|
|
64
|
+
const AFTER_SUMMARY = ["displayName", "icon"];
|
|
65
|
+
/**
|
|
66
|
+
* `raw` with its `summary`, `displayName` and `icon` set to `d` (in place; a new `displayName` /
|
|
67
|
+
* `icon` right after `summary`, removed when plugin.json has none); undefined when they already
|
|
68
|
+
* equal.
|
|
69
|
+
*/
|
|
70
|
+
function refreshDisplay(raw, d) {
|
|
71
|
+
if (!isObj(raw))
|
|
72
|
+
return undefined;
|
|
73
|
+
if (canonical(raw.summary) === canonical(d.summary) &&
|
|
74
|
+
AFTER_SUMMARY.every((k) => canonical(raw[k]) === canonical(d[k]))) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const after = (out) => {
|
|
78
|
+
for (const k of AFTER_SUMMARY)
|
|
79
|
+
if (d[k] !== undefined)
|
|
80
|
+
out[k] = d[k];
|
|
81
|
+
};
|
|
82
|
+
const out = {};
|
|
83
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
84
|
+
if (AFTER_SUMMARY.includes(k))
|
|
85
|
+
continue;
|
|
86
|
+
out[k] = k === "summary" ? d.summary : v;
|
|
87
|
+
if (k === "summary")
|
|
88
|
+
after(out);
|
|
89
|
+
}
|
|
90
|
+
if (!("summary" in raw)) {
|
|
91
|
+
out.summary = d.summary;
|
|
92
|
+
after(out);
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* `capybara-plugin marketplace`: keeps a plugin repository's `marketplace.json` listing one entry
|
|
98
|
+
* per public plugin, its current version (plugin component management spec §13.2, replacing the
|
|
99
|
+
* one-entry-per-version rule of the package ecosystem spec §5.2). Each public plugin (not
|
|
100
|
+
* `"private": true`) not listed at its version is packed reproducibly with {@link packPlugin} and
|
|
101
|
+
* gets an entry `{ name, version, summary, displayName?, icon?, homepage?, permissions, source: {
|
|
102
|
+
* npm: { package, version, integrity } } }` (`summary` from the plugin.json `description`, a string
|
|
103
|
+
* being `en`; `displayName` copied from plugin.json; `icon` the plugin.json `icon` as a path in the
|
|
104
|
+
* repository, `./<plugin dir>/<icon>`, which a git marketplace reads at its refreshed commit;
|
|
105
|
+
* `integrity` the pack's sha512, which the release
|
|
106
|
+
* then publishes byte for byte; `permissions` display only). The new entry takes the place of the
|
|
107
|
+
* plugin's first entry (a version that moved), the plugin's other entries are dropped (also those
|
|
108
|
+
* of a file written in the older one-entry-per-version form), and a plugin not listed yet is
|
|
109
|
+
* appended. Entries of plugins not in the tree are kept. A version already listed is not packed or
|
|
110
|
+
* compared: once published, its bytes are the release gate's business (`release --marketplace`),
|
|
111
|
+
* so a later source or toolchain change at the same version is not a finding; only its display
|
|
112
|
+
* fields (`summary`, `displayName`, `icon`) are refreshed from the source plugin.json. Every other field
|
|
113
|
+
* of a kept entry stays as written, among them `deprecated`, which the maintainer sets by hand on
|
|
114
|
+
* a published version's entry (not in plugin.json: that would change the packed bytes); a new
|
|
115
|
+
* version's entry has none, and no rule compares it with the base. With `base`, see
|
|
116
|
+
* {@link SyncMarketplaceOptions.base}. The file is written formatted as Biome formats it
|
|
117
|
+
* ({@link formatMarketplaceJson}); nothing is written when there is any other finding.
|
|
49
118
|
*/
|
|
50
119
|
export async function syncMarketplace(opts) {
|
|
51
120
|
const findings = [];
|
|
52
121
|
const at = (rule, message) => findings.push({ file: opts.file, line: 1, rule, message });
|
|
122
|
+
const none = () => ({ added: [], removed: [], refreshed: [], findings });
|
|
53
123
|
if (!existsSync(opts.file)) {
|
|
54
124
|
at("marketplace-invalid", `${opts.file} does not exist: create it with schemaVersion, name, owner and "plugins": []`);
|
|
55
|
-
return
|
|
125
|
+
return none();
|
|
56
126
|
}
|
|
57
127
|
let raw;
|
|
58
128
|
try {
|
|
@@ -60,22 +130,23 @@ export async function syncMarketplace(opts) {
|
|
|
60
130
|
}
|
|
61
131
|
catch (e) {
|
|
62
132
|
at("marketplace-invalid", `not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
63
|
-
return
|
|
133
|
+
return none();
|
|
64
134
|
}
|
|
65
135
|
const parsed = parseMarketplaceManifest(raw);
|
|
66
136
|
if ("errors" in parsed) {
|
|
67
137
|
at("marketplace-invalid", parsed.errors.join("; "));
|
|
68
|
-
return
|
|
138
|
+
return none();
|
|
69
139
|
}
|
|
140
|
+
// The parser keeps the order, so parsed.plugins[i] is rawEntries[i].
|
|
70
141
|
const rawEntries = isObj(raw) && Array.isArray(raw.plugins) ? raw.plugins : [];
|
|
71
|
-
/** `name@version` of the entries
|
|
72
|
-
let
|
|
142
|
+
/** `name@version` of the entries the base lists (undefined without a base). */
|
|
143
|
+
let baseKeys;
|
|
73
144
|
if (opts.base !== undefined) {
|
|
74
145
|
const baseEntries = entriesAt(opts.root, opts.file, opts.base);
|
|
75
|
-
findings.push(...
|
|
76
|
-
|
|
146
|
+
findings.push(...publishedRules(baseEntries, rawEntries, opts.file));
|
|
147
|
+
baseKeys = new Set(baseEntries.map(keyOfRaw));
|
|
77
148
|
}
|
|
78
|
-
const
|
|
149
|
+
const updates = [];
|
|
79
150
|
const out = await mkdtemp(join(tmpdir(), "capybara-marketplace-"));
|
|
80
151
|
try {
|
|
81
152
|
for (const dir of discoverPlugins(opts.root, opts.plugins)) {
|
|
@@ -87,15 +158,33 @@ export async function syncMarketplace(opts) {
|
|
|
87
158
|
if (pkg.private === true)
|
|
88
159
|
continue;
|
|
89
160
|
const source = JSON.parse(await readFile(manifestFile, "utf8"));
|
|
161
|
+
// (the display fields of a listed entry are refreshed from this source plugin.json)
|
|
90
162
|
const name = String(source.name);
|
|
91
163
|
const version = String(pkg.version);
|
|
92
|
-
const
|
|
164
|
+
const mine = parsed.plugins.flatMap((e, i) => (e.name === name ? [{ e, i }] : []));
|
|
165
|
+
const listed = mine.filter(({ e }) => versionOf(e) === version);
|
|
166
|
+
const dropped = mine
|
|
167
|
+
.filter(({ e }) => versionOf(e) !== version)
|
|
168
|
+
.map(({ e }) => `${name}@${versionOf(e) ?? "(no version)"}`);
|
|
93
169
|
// A version already listed is published (or about to be): its bytes are the release gate's
|
|
94
170
|
// business, so a later source or toolchain change at the same version is not a finding here.
|
|
95
171
|
// Only an entry added since the base is compared with the tree's pack.
|
|
96
|
-
const toCompare = listed.filter((e) =>
|
|
97
|
-
|
|
172
|
+
const toCompare = listed.filter(({ e }) => baseKeys !== undefined && !baseKeys.has(keyOf(e)));
|
|
173
|
+
const kept = listed[0];
|
|
174
|
+
/** Keeps the listed entry: drops the other versions, refreshes its display fields. */
|
|
175
|
+
const keep = (raw) => {
|
|
176
|
+
const display = displayOf(source, opts.root, dir);
|
|
177
|
+
const fresh = display === undefined ? undefined : refreshDisplay(raw, display);
|
|
178
|
+
if (dropped.length > 0 || fresh !== undefined) {
|
|
179
|
+
const entry = fresh ?? raw;
|
|
180
|
+
updates.push({ name, version, entry, isNew: false, dropped, refreshed: !!fresh });
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
if (kept !== undefined && toCompare.length === 0) {
|
|
184
|
+
keep(rawEntries[kept.i]);
|
|
98
185
|
continue;
|
|
186
|
+
}
|
|
187
|
+
// Packed: the plugin is not listed at its version, or its entry is new since the base.
|
|
99
188
|
const r = await packPlugin(dir, out);
|
|
100
189
|
const bytes = new Uint8Array(await readFile(r.file));
|
|
101
190
|
const npm = {
|
|
@@ -103,7 +192,7 @@ export async function syncMarketplace(opts) {
|
|
|
103
192
|
version: r.manifest.version,
|
|
104
193
|
integrity: integrityOf(bytes),
|
|
105
194
|
};
|
|
106
|
-
for (const e of toCompare) {
|
|
195
|
+
for (const { e } of toCompare) {
|
|
107
196
|
const src = "npm" in e.source ? e.source.npm : undefined;
|
|
108
197
|
if (src?.package !== npm.package || src.integrity !== npm.integrity) {
|
|
109
198
|
const was = src === undefined
|
|
@@ -113,39 +202,79 @@ export async function syncMarketplace(opts) {
|
|
|
113
202
|
`with integrity ${npm.integrity}: run capybara-plugin marketplace --write`);
|
|
114
203
|
}
|
|
115
204
|
}
|
|
116
|
-
if (
|
|
205
|
+
if (kept !== undefined) {
|
|
206
|
+
keep(rawEntries[kept.i]);
|
|
117
207
|
continue;
|
|
118
|
-
|
|
208
|
+
}
|
|
209
|
+
const entry = {
|
|
119
210
|
name: r.manifest.name,
|
|
120
211
|
version: npm.version,
|
|
121
|
-
summary:
|
|
212
|
+
summary: summaryOf(r.manifest.description),
|
|
213
|
+
...(r.manifest.displayName !== undefined && { displayName: r.manifest.displayName }),
|
|
214
|
+
...(r.manifest.icon !== undefined && {
|
|
215
|
+
icon: repoIconPath(opts.root, dir, r.manifest.icon),
|
|
216
|
+
}),
|
|
122
217
|
...(r.manifest.homepage !== undefined && { homepage: r.manifest.homepage }),
|
|
123
218
|
permissions: await permissionsOf(bytes, r.manifest.extension !== undefined),
|
|
124
219
|
source: { npm },
|
|
125
|
-
}
|
|
220
|
+
};
|
|
221
|
+
updates.push({ name, version, entry, isNew: true, dropped, refreshed: false });
|
|
126
222
|
}
|
|
127
223
|
}
|
|
128
224
|
finally {
|
|
129
225
|
await rm(out, { recursive: true, force: true });
|
|
130
226
|
}
|
|
227
|
+
const added = updates.filter((u) => u.isNew).map((u) => u.entry);
|
|
228
|
+
const removed = updates.flatMap((u) => u.dropped);
|
|
229
|
+
const refreshed = updates.filter((u) => u.refreshed).map((u) => `${u.name}@${u.version}`);
|
|
131
230
|
if (!opts.write) {
|
|
132
|
-
for (const
|
|
133
|
-
|
|
231
|
+
for (const u of updates) {
|
|
232
|
+
if (u.refreshed) {
|
|
233
|
+
at("marketplace-display-stale", `${u.name}@${u.version} has a summary or displayName other than its plugin.json's: ` +
|
|
234
|
+
"run capybara-plugin marketplace --write");
|
|
235
|
+
}
|
|
236
|
+
if (!u.isNew && u.dropped.length === 0)
|
|
237
|
+
continue;
|
|
238
|
+
const also = u.dropped.map((d) => d.slice(u.name.length + 1)).join(", ");
|
|
239
|
+
at("marketplace-missing", u.isNew
|
|
240
|
+
? `${u.name}@${u.version} is not listed${also === "" ? "" : ` (the marketplace lists ${also})`}: ` +
|
|
241
|
+
"one entry per plugin, its current version; run capybara-plugin marketplace --write"
|
|
242
|
+
: `${u.name}@${u.version} is not the plugin's only entry (also ${also}): ` +
|
|
243
|
+
"one entry per plugin, its current version; run capybara-plugin marketplace --write");
|
|
134
244
|
}
|
|
135
|
-
return { added, findings };
|
|
245
|
+
return { added, removed, refreshed, findings };
|
|
136
246
|
}
|
|
137
|
-
if (findings.length > 0 ||
|
|
138
|
-
return { added, findings };
|
|
247
|
+
if (findings.length > 0 || updates.length === 0)
|
|
248
|
+
return { added, removed, refreshed, findings };
|
|
249
|
+
const byName = new Map(updates.map((u) => [u.name, u]));
|
|
250
|
+
const placed = new Set();
|
|
251
|
+
const plugins = [];
|
|
252
|
+
for (const e of rawEntries) {
|
|
253
|
+
const name = isObj(e) ? String(e.name) : "";
|
|
254
|
+
const u = byName.get(name);
|
|
255
|
+
if (u === undefined) {
|
|
256
|
+
plugins.push(e);
|
|
257
|
+
}
|
|
258
|
+
else if (!placed.has(name)) {
|
|
259
|
+
placed.add(name);
|
|
260
|
+
plugins.push(u.entry);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
for (const u of updates)
|
|
264
|
+
if (!placed.has(u.name))
|
|
265
|
+
plugins.push(u.entry);
|
|
139
266
|
const next = isObj(raw) ? { ...raw } : {};
|
|
140
|
-
next.plugins =
|
|
267
|
+
next.plugins = plugins;
|
|
141
268
|
const check = parseMarketplaceManifest(next);
|
|
142
269
|
if ("errors" in check) {
|
|
143
270
|
at("marketplace-invalid", `the new entries do not parse: ${check.errors.join("; ")}`);
|
|
144
|
-
return { added, findings };
|
|
271
|
+
return { added, removed, refreshed, findings };
|
|
145
272
|
}
|
|
146
273
|
await writeFile(opts.file, formatMarketplaceJson(next));
|
|
147
|
-
return { added, findings };
|
|
274
|
+
return { added, removed, refreshed, findings };
|
|
148
275
|
}
|
|
276
|
+
/** `name@version` of a parsed entry (`?` without a version). */
|
|
277
|
+
const keyOf = (e) => `${e.name}@${versionOf(e) ?? "?"}`;
|
|
149
278
|
/** `name@version` of a raw entry (`?` for what is missing). */
|
|
150
279
|
function keyOfRaw(e) {
|
|
151
280
|
if (!isObj(e))
|
|
@@ -153,7 +282,7 @@ function keyOfRaw(e) {
|
|
|
153
282
|
const npm = isObj(e.source) && isObj(e.source.npm) ? e.source.npm : {};
|
|
154
283
|
return `${String(e.name ?? "?")}@${String(e.version ?? npm.version ?? "?")}`;
|
|
155
284
|
}
|
|
156
|
-
/** JSON with object keys sorted (deep equality
|
|
285
|
+
/** JSON with object keys sorted (deep equality regardless of key order). */
|
|
157
286
|
function canonical(v) {
|
|
158
287
|
if (Array.isArray(v))
|
|
159
288
|
return `[${v.map(canonical).join(",")}]`;
|
|
@@ -184,25 +313,68 @@ function entriesAt(root, file, base) {
|
|
|
184
313
|
const raw = JSON.parse(text);
|
|
185
314
|
return isObj(raw) && Array.isArray(raw.plugins) ? raw.plugins : [];
|
|
186
315
|
}
|
|
316
|
+
/** A raw entry's version when it is a plain semver (else undefined: not ordered). */
|
|
317
|
+
function semverOfRaw(e) {
|
|
318
|
+
const v = keyOfRaw(e).split("@").pop();
|
|
319
|
+
return v !== undefined && parseSemver(v) !== undefined ? v : undefined;
|
|
320
|
+
}
|
|
187
321
|
/**
|
|
188
|
-
*
|
|
189
|
-
*
|
|
322
|
+
* The rules on the entries published at the base: `marketplace-integrity`, a version the base
|
|
323
|
+
* lists keeps its source (package and integrity); `marketplace-version-decrease`, a version the
|
|
324
|
+
* base does not list must be newer than every version the base lists for that plugin, and a
|
|
325
|
+
* plugin's newest listed version may not be older than the newest the base lists. Display
|
|
326
|
+
* fields (summary, displayName, permissions, homepage) may change, entries may move, and a
|
|
327
|
+
* plugin's older versions may go (one entry per plugin).
|
|
190
328
|
*/
|
|
191
|
-
function
|
|
329
|
+
function publishedRules(base, current, file) {
|
|
192
330
|
const out = [];
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
331
|
+
const finding = (rule, message) => out.push({ file, line: 1, rule, message });
|
|
332
|
+
const baseByKey = new Map(base.map((b) => [keyOfRaw(b), b]));
|
|
333
|
+
const newest = new Map();
|
|
334
|
+
for (const b of base) {
|
|
335
|
+
const v = semverOfRaw(b);
|
|
336
|
+
const name = isObj(b) ? String(b.name) : "?";
|
|
337
|
+
const top = newest.get(name);
|
|
338
|
+
if (v !== undefined && (top === undefined || compareSemver(v, top) > 0))
|
|
339
|
+
newest.set(name, v);
|
|
340
|
+
}
|
|
341
|
+
/** `name@version` of the entries reported as `marketplace-version-decrease`. */
|
|
342
|
+
const decreased = new Set();
|
|
343
|
+
for (const c of current) {
|
|
344
|
+
const key = keyOfRaw(c);
|
|
345
|
+
const b = baseByKey.get(key);
|
|
346
|
+
const src = (e) => (isObj(e) ? e.source : undefined);
|
|
347
|
+
if (b !== undefined) {
|
|
348
|
+
if (canonical(src(c)) !== canonical(src(b))) {
|
|
349
|
+
finding("marketplace-integrity", `${key} is published at the base with the source ${canonical(src(b))}, now ` +
|
|
350
|
+
`${canonical(src(c))}: a published version keeps its integrity (bump the version instead)`);
|
|
351
|
+
}
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
const v = semverOfRaw(c);
|
|
355
|
+
const top = newest.get(isObj(c) ? String(c.name) : "?");
|
|
356
|
+
if (v !== undefined && top !== undefined && compareSemver(v, top) < 0)
|
|
357
|
+
decreased.add(key);
|
|
358
|
+
}
|
|
359
|
+
// A plugin's newest listed version may not fall below the newest the base lists, also when it
|
|
360
|
+
// moves back to an older version the base lists too (a base in the older form).
|
|
361
|
+
const newestNow = new Map();
|
|
362
|
+
for (const c of current) {
|
|
363
|
+
const v = semverOfRaw(c);
|
|
364
|
+
const name = isObj(c) ? String(c.name) : "?";
|
|
365
|
+
const top = newestNow.get(name);
|
|
366
|
+
if (v !== undefined && (top === undefined || compareSemver(v, top) > 0))
|
|
367
|
+
newestNow.set(name, v);
|
|
368
|
+
}
|
|
369
|
+
for (const [name, v] of newestNow) {
|
|
370
|
+
const top = newest.get(name);
|
|
371
|
+
if (top !== undefined && compareSemver(v, top) < 0)
|
|
372
|
+
decreased.add(`${name}@${v}`);
|
|
373
|
+
}
|
|
374
|
+
for (const key of decreased) {
|
|
375
|
+
const name = key.slice(0, key.lastIndexOf("@"));
|
|
376
|
+
finding("marketplace-version-decrease", `${key} is older than ${newest.get(name)}, which the base lists: an entry's version may only increase`);
|
|
377
|
+
}
|
|
206
378
|
return out;
|
|
207
379
|
}
|
|
208
380
|
const INDENT = " ";
|
package/dist/pack/cli.d.ts
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
|
-
export declare const PACK_USAGE
|
|
1
|
+
export declare const PACK_USAGE = "capybara-plugin pack [<dir>...] [--all <root>] [--out <dir>]";
|
|
2
2
|
/**
|
|
3
3
|
* `capybara-plugin pack`: packs plugin directories into plugin tgz files.
|
|
4
4
|
*
|
|
5
5
|
* - `<dir>...`: the plugin directories to pack;
|
|
6
6
|
* - `--all <root>`: also every plugin of the repository at `<root>` (`<root>/plugins/*`, or `<root>`
|
|
7
7
|
* itself when it is a plugin);
|
|
8
|
-
* - `--
|
|
9
|
-
*
|
|
10
|
-
* relative to `--root`, default the current directory; npm entries are downloaded from
|
|
11
|
-
* `--registry`, default `CAPYBARA_PACKAGES__NPM_REGISTRY` (the kernel's `packages.npmRegistry`)
|
|
12
|
-
* or else the public npm registry, and must match their pinned sha512; `--npm-token`, default
|
|
13
|
-
* `CAPYBARA_PACKAGES__NPM_TOKEN` (the kernel's `packages.npmToken`), is sent as a Bearer token to
|
|
14
|
-
* the registry origin only);
|
|
15
|
-
* - `--out <dir>`: output directory, default `artifacts/plugins` (`artifacts/bootstrap` with
|
|
16
|
-
* `--bootstrap`) under the current directory. The last `--out` wins.
|
|
8
|
+
* - `--out <dir>`: output directory, default `artifacts/plugins` under the current directory. The
|
|
9
|
+
* last `--out` wins.
|
|
17
10
|
*
|
|
18
11
|
* Relative paths resolve against `cwd` (the current directory). Returns the exit code.
|
|
19
12
|
*/
|