@camunda8/cli 4.0.0-alpha.2 → 4.0.0-alpha.4
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/PLUGIN-HELP.md +18 -0
- package/dist/commands/jobs.d.ts.map +1 -1
- package/dist/commands/jobs.js +2 -2
- package/dist/commands/jobs.js.map +1 -1
- package/dist/commands/messages.d.ts.map +1 -1
- package/dist/commands/messages.js +3 -2
- package/dist/commands/messages.js.map +1 -1
- package/dist/commands/plugins.d.ts.map +1 -1
- package/dist/commands/plugins.js +111 -5
- package/dist/commands/plugins.js.map +1 -1
- package/dist/commands/process-instances.d.ts.map +1 -1
- package/dist/commands/process-instances.js +4 -14
- package/dist/commands/process-instances.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +2 -6
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/user-tasks.d.ts.map +1 -1
- package/dist/commands/user-tasks.js +2 -2
- package/dist/commands/user-tasks.js.map +1 -1
- package/dist/commands/variables.d.ts.map +1 -1
- package/dist/commands/variables.js +5 -15
- package/dist/commands/variables.js.map +1 -1
- package/dist/core/runtime.d.ts +15 -0
- package/dist/core/runtime.d.ts.map +1 -1
- package/dist/core/runtime.js +27 -2
- package/dist/core/runtime.js.map +1 -1
- package/dist/core/update-check.js +2 -2
- package/dist/core/update-check.js.map +1 -1
- package/dist/default-plugins/element-template/package.json +1 -3
- package/dist/framework/command-registry.d.ts +1 -1
- package/dist/framework/command-registry.d.ts.map +1 -1
- package/dist/framework/command-registry.js +25 -7
- package/dist/framework/command-registry.js.map +1 -1
- package/dist/framework/index.d.ts +1 -0
- package/dist/framework/index.d.ts.map +1 -1
- package/dist/framework/index.js +1 -0
- package/dist/framework/index.js.map +1 -1
- package/dist/framework/plugins/plugin-compat.d.ts +104 -0
- package/dist/framework/plugins/plugin-compat.d.ts.map +1 -0
- package/dist/framework/plugins/plugin-compat.js +141 -0
- package/dist/framework/plugins/plugin-compat.js.map +1 -0
- package/dist/framework/plugins/plugin-loader.d.ts +59 -5
- package/dist/framework/plugins/plugin-loader.d.ts.map +1 -1
- package/dist/framework/plugins/plugin-loader.js +161 -10
- package/dist/framework/plugins/plugin-loader.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -3
- package/dist/index.js.map +1 -1
- package/dist/templates/AGENTS.md +5 -0
- package/dist/templates/package.json +3 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/shared/variables-input.d.ts +31 -0
- package/dist/utils/shared/variables-input.d.ts.map +1 -0
- package/dist/utils/shared/variables-input.js +177 -0
- package/dist/utils/shared/variables-input.js.map +1 -0
- package/package.json +3 -2
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin → host version contract (#523).
|
|
3
|
+
*
|
|
4
|
+
* A plugin declares the c8ctl it needs in its own `package.json`:
|
|
5
|
+
*
|
|
6
|
+
* ```json
|
|
7
|
+
* { "engines": { "c8ctl": ">=4.0.0-alpha.1" } }
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* `engines` is the conventional home for a host requirement and npm ignores
|
|
11
|
+
* engine keys it does not know, so declaring one changes nothing about how the
|
|
12
|
+
* plugin installs. The field is optional — a plugin that declares nothing is
|
|
13
|
+
* `undeclared` and behaves exactly as it did before this module existed.
|
|
14
|
+
*
|
|
15
|
+
* Why this exists: the plugin runtime grows over time (`c8ctl.npm()` is the
|
|
16
|
+
* case that prompted #523). Without a declared requirement, a plugin built
|
|
17
|
+
* against a newer runtime fails inside the plugin, at command time, with
|
|
18
|
+
* whatever error the missing API happens to raise — usually a bare
|
|
19
|
+
* `TypeError`. With one, c8ctl can say which version is needed, at install
|
|
20
|
+
* time, before anything is run.
|
|
21
|
+
*
|
|
22
|
+
* Range evaluation is delegated to `semver` — the library npm itself uses for
|
|
23
|
+
* `engines` fields — rather than hand-parsed. That gets the full npm range
|
|
24
|
+
* grammar (set unions, hyphen ranges, wildcards) for free, and correctness for
|
|
25
|
+
* the parts that are easy to get wrong by hand (prerelease ordering, 0.x caret
|
|
26
|
+
* carve-outs, build-metadata stripping). It is a genuine root dependency of
|
|
27
|
+
* this module (see `CORE_DEPENDENCIES` in
|
|
28
|
+
* `tests/unit/root-dependencies-isolation.test.ts`), not something bundled
|
|
29
|
+
* per-plugin the way a default plugin's own dependencies are.
|
|
30
|
+
*
|
|
31
|
+
* **Failing open is a design rule, not an oversight.** Three situations mean
|
|
32
|
+
* "cannot evaluate": an unpublished development build of c8ctl, a host version
|
|
33
|
+
* `semver` cannot parse, and a range `semver` cannot parse. All three yield
|
|
34
|
+
* `unverifiable` and leave the plugin fully working. Disabling a plugin over a
|
|
35
|
+
* question we could not answer would turn a diagnostic into an outage.
|
|
36
|
+
*
|
|
37
|
+
* **Prereleases participate in ranges**, unlike `npm install`'s default. A host
|
|
38
|
+
* on `4.1.0-alpha.3` satisfies `>=4.0.0-alpha.1` here; npm would exclude it
|
|
39
|
+
* without `includePrerelease`. c8ctl publishes alphas, so the npm default would
|
|
40
|
+
* disable plugins on the exact channel their requirement was written for. `^`
|
|
41
|
+
* and `~` still desugar to npm's prerelease-excluding upper bound (`^4.1.0` is
|
|
42
|
+
* `>=4.1.0 <5.0.0-0`); an explicit `<5.0.0` does not, because it says what it
|
|
43
|
+
* says.
|
|
44
|
+
*/
|
|
45
|
+
/** The `engines` key a plugin declares its host requirement under. */
|
|
46
|
+
export declare const HOST_ENGINE_KEY = "c8ctl";
|
|
47
|
+
/**
|
|
48
|
+
* - `undeclared` — the plugin declared no requirement (the common case).
|
|
49
|
+
* - `satisfied` — the running c8ctl meets it.
|
|
50
|
+
* - `incompatible` — it does not. The plugin's commands are disabled and
|
|
51
|
+
* `message` explains why.
|
|
52
|
+
* - `unverifiable` — the question could not be answered; the plugin keeps
|
|
53
|
+
* working and `message` says what was skipped.
|
|
54
|
+
*/
|
|
55
|
+
export type HostCompatStatus = "undeclared" | "satisfied" | "incompatible" | "unverifiable";
|
|
56
|
+
export interface HostCompatVerdict {
|
|
57
|
+
status: HostCompatStatus;
|
|
58
|
+
/** The declared range, verbatim, when there was one. */
|
|
59
|
+
range?: string;
|
|
60
|
+
/** Ready-to-print explanation. Set for `incompatible` and `unverifiable`. */
|
|
61
|
+
message?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Why the question could not be answered, on `unverifiable` only.
|
|
64
|
+
*
|
|
65
|
+
* Callers log these differently on purpose. `unreadable-range` is a mistake
|
|
66
|
+
* in a published plugin and somebody should hear about it, so it warns.
|
|
67
|
+
* `dev-build` is the expected state of every source checkout, and
|
|
68
|
+
* `unreadable-host-version` is a property of the host rather than of the
|
|
69
|
+
* plugin it would name — warning about either would blame a plugin for
|
|
70
|
+
* something its author cannot fix, on every single invocation.
|
|
71
|
+
*/
|
|
72
|
+
reason?: "dev-build" | "unreadable-range" | "unreadable-host-version";
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Whether `version` satisfies `range`, evaluated by `semver` with
|
|
76
|
+
* `includePrerelease` on (see the module doc for why).
|
|
77
|
+
*
|
|
78
|
+
* Returns `null` when either `version` or `range` cannot be parsed — callers
|
|
79
|
+
* must treat that as "cannot evaluate", never as "not satisfied".
|
|
80
|
+
*/
|
|
81
|
+
export declare function satisfiesRange({ version, range, }: {
|
|
82
|
+
version: string;
|
|
83
|
+
range: string;
|
|
84
|
+
}): boolean | null;
|
|
85
|
+
/**
|
|
86
|
+
* Read a plugin's declared host requirement from its parsed `package.json`.
|
|
87
|
+
* Returns `null` for anything that is not a non-empty string, so a malformed
|
|
88
|
+
* declaration is indistinguishable from no declaration at all.
|
|
89
|
+
*/
|
|
90
|
+
export declare function readHostRequirement(packageJson: unknown): string | null;
|
|
91
|
+
/**
|
|
92
|
+
* Decide whether a plugin can run on this c8ctl.
|
|
93
|
+
*
|
|
94
|
+
* `hostVersion` is a parameter rather than a read of the runtime singleton so
|
|
95
|
+
* both this decision and the loader that consumes it can be tested against
|
|
96
|
+
* arbitrary versions — a source checkout always reports the unpublished
|
|
97
|
+
* development version, which by design answers `unverifiable`.
|
|
98
|
+
*/
|
|
99
|
+
export declare function checkHostCompat({ pluginName, declaredRange, hostVersion, }: {
|
|
100
|
+
pluginName: string;
|
|
101
|
+
declaredRange: string | null;
|
|
102
|
+
hostVersion: string;
|
|
103
|
+
}): HostCompatVerdict;
|
|
104
|
+
//# sourceMappingURL=plugin-compat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-compat.d.ts","sourceRoot":"","sources":["../../../src/framework/plugins/plugin-compat.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAKH,sEAAsE;AACtE,eAAO,MAAM,eAAe,UAAU,CAAC;AAEvC;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,GACzB,YAAY,GACZ,WAAW,GACX,cAAc,GACd,cAAc,CAAC;AAElB,MAAM,WAAW,iBAAiB;IACjC,MAAM,EAAE,gBAAgB,CAAC;IACzB,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,WAAW,GAAG,kBAAkB,GAAG,yBAAyB,CAAC;CACtE;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,EAC9B,OAAO,EACP,KAAK,GACL,EAAE;IACF,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,GAAG,IAAI,CAIjB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAQvE;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,EAC/B,UAAU,EACV,aAAa,EACb,WAAW,GACX,EAAE;IACF,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;CACpB,GAAG,iBAAiB,CA2DpB"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin → host version contract (#523).
|
|
3
|
+
*
|
|
4
|
+
* A plugin declares the c8ctl it needs in its own `package.json`:
|
|
5
|
+
*
|
|
6
|
+
* ```json
|
|
7
|
+
* { "engines": { "c8ctl": ">=4.0.0-alpha.1" } }
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* `engines` is the conventional home for a host requirement and npm ignores
|
|
11
|
+
* engine keys it does not know, so declaring one changes nothing about how the
|
|
12
|
+
* plugin installs. The field is optional — a plugin that declares nothing is
|
|
13
|
+
* `undeclared` and behaves exactly as it did before this module existed.
|
|
14
|
+
*
|
|
15
|
+
* Why this exists: the plugin runtime grows over time (`c8ctl.npm()` is the
|
|
16
|
+
* case that prompted #523). Without a declared requirement, a plugin built
|
|
17
|
+
* against a newer runtime fails inside the plugin, at command time, with
|
|
18
|
+
* whatever error the missing API happens to raise — usually a bare
|
|
19
|
+
* `TypeError`. With one, c8ctl can say which version is needed, at install
|
|
20
|
+
* time, before anything is run.
|
|
21
|
+
*
|
|
22
|
+
* Range evaluation is delegated to `semver` — the library npm itself uses for
|
|
23
|
+
* `engines` fields — rather than hand-parsed. That gets the full npm range
|
|
24
|
+
* grammar (set unions, hyphen ranges, wildcards) for free, and correctness for
|
|
25
|
+
* the parts that are easy to get wrong by hand (prerelease ordering, 0.x caret
|
|
26
|
+
* carve-outs, build-metadata stripping). It is a genuine root dependency of
|
|
27
|
+
* this module (see `CORE_DEPENDENCIES` in
|
|
28
|
+
* `tests/unit/root-dependencies-isolation.test.ts`), not something bundled
|
|
29
|
+
* per-plugin the way a default plugin's own dependencies are.
|
|
30
|
+
*
|
|
31
|
+
* **Failing open is a design rule, not an oversight.** Three situations mean
|
|
32
|
+
* "cannot evaluate": an unpublished development build of c8ctl, a host version
|
|
33
|
+
* `semver` cannot parse, and a range `semver` cannot parse. All three yield
|
|
34
|
+
* `unverifiable` and leave the plugin fully working. Disabling a plugin over a
|
|
35
|
+
* question we could not answer would turn a diagnostic into an outage.
|
|
36
|
+
*
|
|
37
|
+
* **Prereleases participate in ranges**, unlike `npm install`'s default. A host
|
|
38
|
+
* on `4.1.0-alpha.3` satisfies `>=4.0.0-alpha.1` here; npm would exclude it
|
|
39
|
+
* without `includePrerelease`. c8ctl publishes alphas, so the npm default would
|
|
40
|
+
* disable plugins on the exact channel their requirement was written for. `^`
|
|
41
|
+
* and `~` still desugar to npm's prerelease-excluding upper bound (`^4.1.0` is
|
|
42
|
+
* `>=4.1.0 <5.0.0-0`); an explicit `<5.0.0` does not, because it says what it
|
|
43
|
+
* says.
|
|
44
|
+
*/
|
|
45
|
+
import semver from "semver";
|
|
46
|
+
import { isRecord, isUnversionedDevBuild } from "../../core/index.js";
|
|
47
|
+
/** The `engines` key a plugin declares its host requirement under. */
|
|
48
|
+
export const HOST_ENGINE_KEY = "c8ctl";
|
|
49
|
+
/**
|
|
50
|
+
* Whether `version` satisfies `range`, evaluated by `semver` with
|
|
51
|
+
* `includePrerelease` on (see the module doc for why).
|
|
52
|
+
*
|
|
53
|
+
* Returns `null` when either `version` or `range` cannot be parsed — callers
|
|
54
|
+
* must treat that as "cannot evaluate", never as "not satisfied".
|
|
55
|
+
*/
|
|
56
|
+
export function satisfiesRange({ version, range, }) {
|
|
57
|
+
if (semver.valid(version) === null)
|
|
58
|
+
return null;
|
|
59
|
+
if (semver.validRange(range) === null)
|
|
60
|
+
return null;
|
|
61
|
+
return semver.satisfies(version, range, { includePrerelease: true });
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Read a plugin's declared host requirement from its parsed `package.json`.
|
|
65
|
+
* Returns `null` for anything that is not a non-empty string, so a malformed
|
|
66
|
+
* declaration is indistinguishable from no declaration at all.
|
|
67
|
+
*/
|
|
68
|
+
export function readHostRequirement(packageJson) {
|
|
69
|
+
if (!isRecord(packageJson))
|
|
70
|
+
return null;
|
|
71
|
+
const { engines } = packageJson;
|
|
72
|
+
if (!isRecord(engines))
|
|
73
|
+
return null;
|
|
74
|
+
const declared = engines[HOST_ENGINE_KEY];
|
|
75
|
+
if (typeof declared !== "string")
|
|
76
|
+
return null;
|
|
77
|
+
const trimmed = declared.trim();
|
|
78
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Decide whether a plugin can run on this c8ctl.
|
|
82
|
+
*
|
|
83
|
+
* `hostVersion` is a parameter rather than a read of the runtime singleton so
|
|
84
|
+
* both this decision and the loader that consumes it can be tested against
|
|
85
|
+
* arbitrary versions — a source checkout always reports the unpublished
|
|
86
|
+
* development version, which by design answers `unverifiable`.
|
|
87
|
+
*/
|
|
88
|
+
export function checkHostCompat({ pluginName, declaredRange, hostVersion, }) {
|
|
89
|
+
if (declaredRange === null)
|
|
90
|
+
return { status: "undeclared" };
|
|
91
|
+
if (isUnversionedDevBuild(hostVersion)) {
|
|
92
|
+
return {
|
|
93
|
+
status: "unverifiable",
|
|
94
|
+
range: declaredRange,
|
|
95
|
+
reason: "dev-build",
|
|
96
|
+
message: `Plugin '${pluginName}' requires c8ctl ${declaredRange}, but this is an unpublished ` +
|
|
97
|
+
`development build (${hostVersion}) whose version says nothing about its API surface. ` +
|
|
98
|
+
"Skipping the check — the plugin stays enabled.",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// Checked before the range, because `satisfiesRange` answers `null` for both
|
|
102
|
+
// "unreadable range" and "unreadable version" and only the plugin author can
|
|
103
|
+
// act on the first. Collapsing them blames a plugin for the host's version
|
|
104
|
+
// string — and since the scaffold ships `engines.c8ctl: "*"`, any host whose
|
|
105
|
+
// version this module cannot parse (a fork, a nightly tag, a hand-edited
|
|
106
|
+
// manifest) would make every scaffolded plugin warn about itself.
|
|
107
|
+
if (semver.valid(hostVersion) === null) {
|
|
108
|
+
return {
|
|
109
|
+
status: "unverifiable",
|
|
110
|
+
range: declaredRange,
|
|
111
|
+
reason: "unreadable-host-version",
|
|
112
|
+
message: `Plugin '${pluginName}' requires c8ctl ${declaredRange}, but this c8ctl reports its version ` +
|
|
113
|
+
`as '${hostVersion}', which is not a version this check can compare. Skipping the check — ` +
|
|
114
|
+
"the plugin stays enabled.",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const satisfied = satisfiesRange({
|
|
118
|
+
version: hostVersion,
|
|
119
|
+
range: declaredRange,
|
|
120
|
+
});
|
|
121
|
+
if (satisfied === null) {
|
|
122
|
+
return {
|
|
123
|
+
status: "unverifiable",
|
|
124
|
+
range: declaredRange,
|
|
125
|
+
reason: "unreadable-range",
|
|
126
|
+
message: `Plugin '${pluginName}' declares engines.${HOST_ENGINE_KEY} '${declaredRange}', which is not a ` +
|
|
127
|
+
"version range c8ctl understands (npm's semver range syntax — see " +
|
|
128
|
+
"https://github.com/npm/node-semver#ranges). Ignoring the requirement — the plugin stays enabled.",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (satisfied)
|
|
132
|
+
return { status: "satisfied", range: declaredRange };
|
|
133
|
+
return {
|
|
134
|
+
status: "incompatible",
|
|
135
|
+
range: declaredRange,
|
|
136
|
+
message: `Plugin '${pluginName}' requires c8ctl ${declaredRange}, but this is c8ctl ${hostVersion}. ` +
|
|
137
|
+
"Upgrade with 'npm install -g @camunda8/cli@latest', or install a plugin release that " +
|
|
138
|
+
`supports c8ctl ${hostVersion}.`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=plugin-compat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-compat.js","sourceRoot":"","sources":["../../../src/framework/plugins/plugin-compat.ts"],"names":[],"mappings":"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"}
|
|
@@ -114,14 +114,20 @@ export interface PluginCommandMeta {
|
|
|
114
114
|
* surfaced by `c8ctl doctor plugin` (#363). Two flavours:
|
|
115
115
|
*
|
|
116
116
|
* - `command-name`: two plugins exported a command under the same name.
|
|
117
|
-
*
|
|
118
|
-
* plugin's was dropped
|
|
117
|
+
* Normally the earlier-loaded plugin's command stays in dispatch and the
|
|
118
|
+
* later plugin's was dropped, so `winner`/`loser` reflect load order. The
|
|
119
|
+
* exception is a `hostIncompatible` incumbent (#523), which loses the name
|
|
120
|
+
* to a working newcomer regardless of order.
|
|
119
121
|
* - `plugin-name`: two plugins shared the same `package.json#name`.
|
|
120
122
|
* The entire later plugin was rejected (its module body was never
|
|
121
123
|
* imported); `command` is undefined for this kind.
|
|
122
124
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
+
* `winner` always names the plugin that ends up owning the command. When a
|
|
126
|
+
* takeover supersedes an earlier decision, `rejectDuplicateCommandNames`
|
|
127
|
+
* re-points the affected records rather than dropping them — the losers are
|
|
128
|
+
* still losers, and erasing their records would hide a real collision.
|
|
129
|
+
*
|
|
130
|
+
* The doctor command is the only external consumer. Cleared by
|
|
125
131
|
* `clearLoadedPlugins()` so test fixtures stay isolated.
|
|
126
132
|
*/
|
|
127
133
|
export interface PluginCollision {
|
|
@@ -130,6 +136,30 @@ export interface PluginCollision {
|
|
|
130
136
|
loser: string;
|
|
131
137
|
command?: string;
|
|
132
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Structured record of a plugin whose declared `engines.c8ctl` this c8ctl does
|
|
141
|
+
* not satisfy (#523), surfaced by `c8ctl doctor plugin`.
|
|
142
|
+
*
|
|
143
|
+
* The plugin stays loaded and keeps its place in help — what changed is that its
|
|
144
|
+
* commands now refuse to run and print `message` instead (bar any a working
|
|
145
|
+
* plugin has taken over). Removing it from help would trade one confusing
|
|
146
|
+
* failure ("unknown command os") for another; leaving it visible means the
|
|
147
|
+
* command that a user or an agent already knows about is the one that explains
|
|
148
|
+
* itself.
|
|
149
|
+
*
|
|
150
|
+
* The loader only appends and the doctor command only reads;
|
|
151
|
+
* `clearLoadedPlugins()` resets it so test fixtures stay isolated.
|
|
152
|
+
*/
|
|
153
|
+
export interface PluginIncompatibility {
|
|
154
|
+
plugin: string;
|
|
155
|
+
/** The plugin's own version, for "which release did this come from". */
|
|
156
|
+
pluginVersion: string;
|
|
157
|
+
/** The declared range, verbatim. */
|
|
158
|
+
required: string;
|
|
159
|
+
/** The c8ctl the requirement was evaluated against. */
|
|
160
|
+
running: string;
|
|
161
|
+
message: string;
|
|
162
|
+
}
|
|
133
163
|
/**
|
|
134
164
|
* Compute the candidate `default-plugins` directories, relative to the
|
|
135
165
|
* directory containing this loader module.
|
|
@@ -149,8 +179,14 @@ export interface PluginCollision {
|
|
|
149
179
|
export declare function defaultPluginsCandidateDirs(loaderDir: string): string[];
|
|
150
180
|
/**
|
|
151
181
|
* Load all installed plugins from global plugins directory
|
|
182
|
+
*
|
|
183
|
+
* `hostVersion` defaults to the running c8ctl and exists so the `engines.c8ctl`
|
|
184
|
+
* enforcement (#523) can be exercised against other versions: a source checkout
|
|
185
|
+
* reports the unpublished development version, which by design skips the check.
|
|
152
186
|
*/
|
|
153
|
-
export declare function loadInstalledPlugins(
|
|
187
|
+
export declare function loadInstalledPlugins({ hostVersion, }?: {
|
|
188
|
+
hostVersion?: string;
|
|
189
|
+
}): Promise<void>;
|
|
154
190
|
/**
|
|
155
191
|
* Get all loaded plugin commands
|
|
156
192
|
*/
|
|
@@ -216,6 +252,16 @@ export declare function getPluginCommandsInfo(): PluginCommandInfo[];
|
|
|
216
252
|
* (#366). Used by the dispatcher to gate the strip-and-forward path.
|
|
217
253
|
*/
|
|
218
254
|
export declare function isPassthroughPluginCommand(commandName: string): boolean;
|
|
255
|
+
/**
|
|
256
|
+
* True if the named command belongs to a plugin disabled by its declared
|
|
257
|
+
* `engines.c8ctl` (#523).
|
|
258
|
+
*
|
|
259
|
+
* The dispatcher uses this to skip flag validation for such a command: its
|
|
260
|
+
* handler exists only to report which c8ctl the plugin needs, and rejecting the
|
|
261
|
+
* invocation for a missing required flag first would replace that explanation
|
|
262
|
+
* with an instruction the user cannot usefully follow.
|
|
263
|
+
*/
|
|
264
|
+
export declare function isHostIncompatiblePluginCommand(commandName: string): boolean;
|
|
219
265
|
/**
|
|
220
266
|
* Clear all loaded plugins (useful for testing and after uninstall)
|
|
221
267
|
*/
|
|
@@ -228,6 +274,12 @@ export declare function clearLoadedPlugins(): void;
|
|
|
228
274
|
* collisions.
|
|
229
275
|
*/
|
|
230
276
|
export declare function getPluginCollisions(): readonly Readonly<PluginCollision>[];
|
|
277
|
+
/**
|
|
278
|
+
* Snapshot of plugins whose declared `engines.c8ctl` this c8ctl does not
|
|
279
|
+
* satisfy (#523). Same defensive-copy contract as
|
|
280
|
+
* {@link getPluginCollisions}; order reflects load order.
|
|
281
|
+
*/
|
|
282
|
+
export declare function getPluginIncompatibilities(): readonly Readonly<PluginIncompatibility>[];
|
|
231
283
|
/**
|
|
232
284
|
* Snapshot of currently loaded plugins (#363). Returns the canonical
|
|
233
285
|
* `package.json#name` of each plugin together with the command names
|
|
@@ -238,6 +290,8 @@ export declare function getPluginCollisions(): readonly Readonly<PluginCollision
|
|
|
238
290
|
export interface LoadedPluginSummary {
|
|
239
291
|
name: string;
|
|
240
292
|
commands: string[];
|
|
293
|
+
/** Declared `engines.c8ctl` range, when the plugin declared one (#523). */
|
|
294
|
+
requires?: string;
|
|
241
295
|
}
|
|
242
296
|
export declare function getLoadedPluginSummaries(): LoadedPluginSummary[];
|
|
243
297
|
//# sourceMappingURL=plugin-loader.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-loader.d.ts","sourceRoot":"","sources":["../../../src/framework/plugins/plugin-loader.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC;AACzE,OAAO,EAIN,KAAK,MAAM,EACX,KAAK,UAAU,
|
|
1
|
+
{"version":3,"file":"plugin-loader.d.ts","sourceRoot":"","sources":["../../../src/framework/plugins/plugin-loader.ts"],"names":[],"mappings":"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"}
|
|
@@ -11,9 +11,11 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
11
11
|
};
|
|
12
12
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
|
-
import { c8ctl, ensurePluginsDir, getLogger, } from "../../core/index.js";
|
|
14
|
+
import { c8ctl, ensurePluginsDir, getLogger, SilentError, } from "../../core/index.js";
|
|
15
|
+
import { checkHostCompat, readHostRequirement } from "./plugin-compat.js";
|
|
15
16
|
const loadedPlugins = new Map();
|
|
16
17
|
const pluginCollisions = [];
|
|
18
|
+
const pluginIncompatibilities = [];
|
|
17
19
|
/**
|
|
18
20
|
* Validate the passthrough/flags mutual-exclusion rule (#366). Removes
|
|
19
21
|
* offending commands from the registered set so they cannot be invoked,
|
|
@@ -97,6 +99,11 @@ function validatePassthroughCommands(plugin) {
|
|
|
97
99
|
* different name. Default plugins always load first, so user-installed
|
|
98
100
|
* plugins cannot override default commands by name.
|
|
99
101
|
*
|
|
102
|
+
* **One exception (#523):** an incumbent whose declared `engines.c8ctl` this
|
|
103
|
+
* c8ctl does not satisfy loses the name to a working newcomer — its command
|
|
104
|
+
* could only ever throw. A default plugin can never be on the losing side of
|
|
105
|
+
* this, since `enforceHostRequirement` runs for installed plugins only.
|
|
106
|
+
*
|
|
100
107
|
* This guarantees that the merged map returned by `getPluginCommands()`
|
|
101
108
|
* has a single owning plugin per command name, which keeps dispatch and
|
|
102
109
|
* `isPassthroughPluginCommand()` consistent: the help renderer and the
|
|
@@ -110,19 +117,54 @@ function rejectDuplicateCommandNames(plugin) {
|
|
|
110
117
|
const logger = getLogger();
|
|
111
118
|
for (const commandName of Object.keys(plugin.commands)) {
|
|
112
119
|
for (const existing of loadedPlugins.values()) {
|
|
113
|
-
if (Object.hasOwn(existing.commands, commandName))
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
120
|
+
if (!Object.hasOwn(existing.commands, commandName))
|
|
121
|
+
continue;
|
|
122
|
+
// One exception to first-registration-wins: an incumbent that cannot
|
|
123
|
+
// run on this c8ctl (#523) yields to a newcomer that can. Otherwise
|
|
124
|
+
// load order — lexicographic by package path — would decide that a
|
|
125
|
+
// disabled `aaa-plugin` keeps the command name and a working
|
|
126
|
+
// `zzz-plugin` loses it, leaving the user with a command that only
|
|
127
|
+
// ever throws while a functioning implementation sits unreachable.
|
|
128
|
+
// The incumbent keeps its plugin entry in help, but loses this command.
|
|
129
|
+
if (existing.hostIncompatible && !plugin.hostIncompatible) {
|
|
130
|
+
logger.debug(`Plugin '${plugin.name}' takes over command '${commandName}' from ` +
|
|
131
|
+
`'${existing.name}', which is disabled on this c8ctl.`);
|
|
132
|
+
// Re-point, don't delete. Earlier records for this name are still
|
|
133
|
+
// true about who *lost* it — a third plugin that lost to the
|
|
134
|
+
// incumbent has genuinely lost the command — but their `winner` is
|
|
135
|
+
// now stale. Dropping them would erase that plugin's collision from
|
|
136
|
+
// `doctor plugin` entirely; leaving them unedited would credit a win
|
|
137
|
+
// to a plugin that no longer owns the name.
|
|
138
|
+
for (const record of pluginCollisions) {
|
|
139
|
+
if (record.kind === "command-name" &&
|
|
140
|
+
record.command === commandName &&
|
|
141
|
+
record.winner === existing.name) {
|
|
142
|
+
record.winner = plugin.name;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
117
145
|
pluginCollisions.push({
|
|
118
146
|
kind: "command-name",
|
|
119
|
-
winner:
|
|
120
|
-
loser:
|
|
147
|
+
winner: plugin.name,
|
|
148
|
+
loser: existing.name,
|
|
121
149
|
command: commandName,
|
|
122
150
|
});
|
|
123
|
-
|
|
124
|
-
|
|
151
|
+
// `continue`, not `break`: this function is what maintains "at most
|
|
152
|
+
// one loaded plugin owns a given command name", so there is nothing
|
|
153
|
+
// further to find for this name.
|
|
154
|
+
delete existing.commands[commandName];
|
|
155
|
+
continue;
|
|
125
156
|
}
|
|
157
|
+
logger.warn(`Plugin '${plugin.name}' tried to register command '${commandName}' but it is ` +
|
|
158
|
+
`already provided by plugin '${existing.name}'. The first registration wins; ` +
|
|
159
|
+
`dropping the duplicate from '${plugin.name}'.`);
|
|
160
|
+
pluginCollisions.push({
|
|
161
|
+
kind: "command-name",
|
|
162
|
+
winner: existing.name,
|
|
163
|
+
loser: plugin.name,
|
|
164
|
+
command: commandName,
|
|
165
|
+
});
|
|
166
|
+
delete plugin.commands[commandName];
|
|
167
|
+
break;
|
|
126
168
|
}
|
|
127
169
|
}
|
|
128
170
|
}
|
|
@@ -154,6 +196,73 @@ function isDuplicatePluginName(pluginName) {
|
|
|
154
196
|
}
|
|
155
197
|
return false;
|
|
156
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Enforce a plugin's declared `engines.c8ctl` (#523).
|
|
201
|
+
*
|
|
202
|
+
* On an unmet requirement, every command the plugin registered is replaced with
|
|
203
|
+
* a handler that throws the explanation. The plugin stays in `loadedPlugins` and
|
|
204
|
+
* therefore in help — see {@link PluginIncompatibility} for why disabling is
|
|
205
|
+
* done this way round.
|
|
206
|
+
*
|
|
207
|
+
* Runs *before* the collision passes, which read `hostIncompatible` to let a
|
|
208
|
+
* disabled incumbent lose a command name to a working newcomer. Takes the host
|
|
209
|
+
* version as an argument so the behaviour is testable against versions other
|
|
210
|
+
* than whatever this build happens to report.
|
|
211
|
+
*
|
|
212
|
+
* Mutates `plugin.commands` in place.
|
|
213
|
+
*/
|
|
214
|
+
function enforceHostRequirement(plugin, hostVersion) {
|
|
215
|
+
const logger = getLogger();
|
|
216
|
+
const verdict = checkHostCompat({
|
|
217
|
+
pluginName: plugin.name,
|
|
218
|
+
declaredRange: plugin.hostRequirement ?? null,
|
|
219
|
+
hostVersion,
|
|
220
|
+
});
|
|
221
|
+
if (verdict.status === "unverifiable") {
|
|
222
|
+
// Only an unreadable *range* warns: that is an authoring mistake, and the
|
|
223
|
+
// plugin is silently not getting the guarantee it asked for. The other two
|
|
224
|
+
// reasons are properties of the host — a source checkout, or a version
|
|
225
|
+
// string this check cannot parse — and would name a blameless plugin on
|
|
226
|
+
// every single invocation.
|
|
227
|
+
const message = verdict.message ?? "";
|
|
228
|
+
if (verdict.reason === "unreadable-range")
|
|
229
|
+
logger.warn(message);
|
|
230
|
+
else
|
|
231
|
+
logger.debug(message);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (verdict.status !== "incompatible")
|
|
235
|
+
return;
|
|
236
|
+
const message = verdict.message ?? "";
|
|
237
|
+
plugin.hostIncompatible = true;
|
|
238
|
+
// Debug, not warn: unlike a dropped colliding command — which has no other
|
|
239
|
+
// channel to announce itself — a disabled plugin explains itself the moment
|
|
240
|
+
// one of its commands is used, and `doctor plugin` lists it on demand.
|
|
241
|
+
// Warning on every unrelated invocation would be noise with no new signal.
|
|
242
|
+
logger.debug(message);
|
|
243
|
+
pluginIncompatibilities.push({
|
|
244
|
+
plugin: plugin.name,
|
|
245
|
+
pluginVersion: plugin.version,
|
|
246
|
+
required: verdict.range ?? "",
|
|
247
|
+
running: hostVersion,
|
|
248
|
+
message,
|
|
249
|
+
});
|
|
250
|
+
// SilentError: the message is the whole diagnosis and is rendered as one
|
|
251
|
+
// error line by the top-level handler. A plain Error would come back out as
|
|
252
|
+
// "Unexpected error" plus a stack trace — the shape of failure this check
|
|
253
|
+
// exists to replace.
|
|
254
|
+
const refuse = async () => {
|
|
255
|
+
throw new SilentError(message);
|
|
256
|
+
};
|
|
257
|
+
for (const commandName of Object.keys(plugin.commands)) {
|
|
258
|
+
const command = plugin.commands[commandName];
|
|
259
|
+
// Keep the `{ flags, handler }` shape intact: help rendering and flag
|
|
260
|
+
// parsing both branch on it, and a command that loses its flags would
|
|
261
|
+
// fail with a parse error instead of the explanation.
|
|
262
|
+
plugin.commands[commandName] =
|
|
263
|
+
typeof command === "function" ? refuse : { ...command, handler: refuse };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
157
266
|
/**
|
|
158
267
|
* Compute the candidate `default-plugins` directories, relative to the
|
|
159
268
|
* directory containing this loader module.
|
|
@@ -269,8 +378,12 @@ async function loadDefaultPlugins() {
|
|
|
269
378
|
}
|
|
270
379
|
/**
|
|
271
380
|
* Load all installed plugins from global plugins directory
|
|
381
|
+
*
|
|
382
|
+
* `hostVersion` defaults to the running c8ctl and exists so the `engines.c8ctl`
|
|
383
|
+
* enforcement (#523) can be exercised against other versions: a source checkout
|
|
384
|
+
* reports the unpublished development version, which by design skips the check.
|
|
272
385
|
*/
|
|
273
|
-
export async function loadInstalledPlugins() {
|
|
386
|
+
export async function loadInstalledPlugins({ hostVersion = c8ctl.version, } = {}) {
|
|
274
387
|
const logger = getLogger();
|
|
275
388
|
// Expose the runtime to plugins via globalThis.
|
|
276
389
|
// C8ctl implements C8ctlPluginRuntime directly — no monkey-patching needed.
|
|
@@ -379,13 +492,21 @@ export async function loadInstalledPlugins() {
|
|
|
379
492
|
logger.debug(`Loading plugin from: ${pluginUrl}`);
|
|
380
493
|
const plugin = await import(__rewriteRelativeImportExtension(pluginUrl));
|
|
381
494
|
if (plugin.commands && typeof plugin.commands === "object") {
|
|
495
|
+
const hostRequirement = readHostRequirement(packageJson);
|
|
382
496
|
const loaded = {
|
|
383
497
|
name: pluginName,
|
|
384
498
|
version: pluginVersion,
|
|
385
499
|
commands: { ...plugin.commands },
|
|
386
500
|
metadata: plugin.metadata || {},
|
|
501
|
+
...(hostRequirement === null ? {} : { hostRequirement }),
|
|
387
502
|
};
|
|
388
503
|
validatePassthroughCommands(loaded);
|
|
504
|
+
// Before the collision pass, which needs to know whether either
|
|
505
|
+
// side of a command-name clash is disabled. Installed plugins
|
|
506
|
+
// only: a default plugin ships inside the c8ctl package, so its
|
|
507
|
+
// host is itself and there is no version skew for
|
|
508
|
+
// `engines.c8ctl` (#523) to catch.
|
|
509
|
+
enforceHostRequirement(loaded, hostVersion);
|
|
389
510
|
rejectDuplicateCommandNames(loaded);
|
|
390
511
|
loadedPlugins.set(pluginName, loaded);
|
|
391
512
|
const commandNames = Object.keys(loaded.commands);
|
|
@@ -509,12 +630,31 @@ export function isPassthroughPluginCommand(commandName) {
|
|
|
509
630
|
}
|
|
510
631
|
return false;
|
|
511
632
|
}
|
|
633
|
+
/**
|
|
634
|
+
* True if the named command belongs to a plugin disabled by its declared
|
|
635
|
+
* `engines.c8ctl` (#523).
|
|
636
|
+
*
|
|
637
|
+
* The dispatcher uses this to skip flag validation for such a command: its
|
|
638
|
+
* handler exists only to report which c8ctl the plugin needs, and rejecting the
|
|
639
|
+
* invocation for a missing required flag first would replace that explanation
|
|
640
|
+
* with an instruction the user cannot usefully follow.
|
|
641
|
+
*/
|
|
642
|
+
export function isHostIncompatiblePluginCommand(commandName) {
|
|
643
|
+
for (const plugin of loadedPlugins.values()) {
|
|
644
|
+
if (!Object.hasOwn(plugin.commands, commandName))
|
|
645
|
+
continue;
|
|
646
|
+
if (plugin.hostIncompatible === true)
|
|
647
|
+
return true;
|
|
648
|
+
}
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
512
651
|
/**
|
|
513
652
|
* Clear all loaded plugins (useful for testing and after uninstall)
|
|
514
653
|
*/
|
|
515
654
|
export function clearLoadedPlugins() {
|
|
516
655
|
loadedPlugins.clear();
|
|
517
656
|
pluginCollisions.length = 0;
|
|
657
|
+
pluginIncompatibilities.length = 0;
|
|
518
658
|
}
|
|
519
659
|
/**
|
|
520
660
|
* Snapshot of plugin collisions detected at load time (#363). Returns
|
|
@@ -526,12 +666,23 @@ export function clearLoadedPlugins() {
|
|
|
526
666
|
export function getPluginCollisions() {
|
|
527
667
|
return Object.freeze(pluginCollisions.map((c) => Object.freeze({ ...c })));
|
|
528
668
|
}
|
|
669
|
+
/**
|
|
670
|
+
* Snapshot of plugins whose declared `engines.c8ctl` this c8ctl does not
|
|
671
|
+
* satisfy (#523). Same defensive-copy contract as
|
|
672
|
+
* {@link getPluginCollisions}; order reflects load order.
|
|
673
|
+
*/
|
|
674
|
+
export function getPluginIncompatibilities() {
|
|
675
|
+
return Object.freeze(pluginIncompatibilities.map((i) => Object.freeze({ ...i })));
|
|
676
|
+
}
|
|
529
677
|
export function getLoadedPluginSummaries() {
|
|
530
678
|
const summaries = [];
|
|
531
679
|
for (const plugin of loadedPlugins.values()) {
|
|
532
680
|
summaries.push({
|
|
533
681
|
name: plugin.name,
|
|
534
682
|
commands: Object.keys(plugin.commands),
|
|
683
|
+
...(plugin.hostRequirement === undefined
|
|
684
|
+
? {}
|
|
685
|
+
: { requires: plugin.hostRequirement }),
|
|
535
686
|
});
|
|
536
687
|
}
|
|
537
688
|
return summaries;
|