@camunda8/cli 2.8.0-alpha.9 → 3.0.0
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/EXAMPLES.md +22 -5
- package/PLUGIN-HELP.md +26 -0
- package/README.md +37 -4
- package/dist/command-dispatch.d.ts.map +1 -1
- package/dist/command-dispatch.js +2 -1
- package/dist/command-dispatch.js.map +1 -1
- package/dist/command-registry.d.ts +30 -0
- package/dist/command-registry.d.ts.map +1 -1
- package/dist/command-registry.js +34 -1
- package/dist/command-registry.js.map +1 -1
- package/dist/commands/plugins.d.ts +21 -0
- package/dist/commands/plugins.d.ts.map +1 -1
- package/dist/commands/plugins.js +74 -1
- package/dist/commands/plugins.js.map +1 -1
- package/dist/commands/resource-extensions.d.ts +12 -0
- package/dist/commands/resource-extensions.d.ts.map +1 -1
- package/dist/commands/resource-extensions.js +13 -1
- package/dist/commands/resource-extensions.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +5 -3
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +21 -12
- package/dist/commands/watch.js.map +1 -1
- package/dist/completion.d.ts.map +1 -1
- package/dist/completion.js +140 -13
- package/dist/completion.js.map +1 -1
- package/dist/default-plugins/bpmn/README.md +69 -0
- package/dist/default-plugins/bpmn/c8ctl-plugin.js +125 -0
- package/dist/default-plugins/bpmn/c8ctl-plugin.js.map +1 -0
- package/dist/default-plugins/bpmn/lint.js +259 -0
- package/dist/default-plugins/bpmn/lint.js.map +1 -0
- package/dist/default-plugins/bpmn/package.json +8 -0
- package/dist/default-plugins/cluster/package.json +1 -1
- package/dist/default-plugins/element-template/AGENTS.md +65 -0
- package/dist/default-plugins/element-template/README.md +331 -0
- package/dist/default-plugins/element-template/c8ctl-plugin.js +247 -0
- package/dist/default-plugins/element-template/c8ctl-plugin.js.map +1 -0
- package/dist/default-plugins/element-template/commands/apply.js +298 -0
- package/dist/default-plugins/element-template/commands/apply.js.map +1 -0
- package/dist/default-plugins/element-template/commands/get-properties.js +344 -0
- package/dist/default-plugins/element-template/commands/get-properties.js.map +1 -0
- package/dist/default-plugins/element-template/commands/get.js +107 -0
- package/dist/default-plugins/element-template/commands/get.js.map +1 -0
- package/dist/default-plugins/element-template/commands/info.js +189 -0
- package/dist/default-plugins/element-template/commands/info.js.map +1 -0
- package/dist/default-plugins/element-template/commands/search.js +101 -0
- package/dist/default-plugins/element-template/commands/search.js.map +1 -0
- package/dist/default-plugins/element-template/commands/sync.js +36 -0
- package/dist/default-plugins/element-template/commands/sync.js.map +1 -0
- package/dist/default-plugins/element-template/docs/design.md +145 -0
- package/dist/default-plugins/element-template/helpers.js +404 -0
- package/dist/default-plugins/element-template/helpers.js.map +1 -0
- package/dist/default-plugins/element-template/marketplace.js +396 -0
- package/dist/default-plugins/element-template/marketplace.js.map +1 -0
- package/dist/default-plugins/element-template/package.json +8 -0
- package/dist/default-plugins/element-template/template-ref.js +232 -0
- package/dist/default-plugins/element-template/template-ref.js.map +1 -0
- package/dist/default-plugins/element-template/vendor-src/bundle-entry.js +44 -0
- package/dist/default-plugins/feel/README.md +163 -0
- package/dist/default-plugins/feel/c8ctl-plugin.js +616 -0
- package/dist/default-plugins/feel/c8ctl-plugin.js.map +1 -0
- package/dist/default-plugins/feel/package.json +8 -0
- package/dist/deployments.d.ts +1 -0
- package/dist/deployments.d.ts.map +1 -1
- package/dist/deployments.js +89 -20
- package/dist/deployments.js.map +1 -1
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +156 -5
- package/dist/help.js.map +1 -1
- package/dist/ignore.d.ts +16 -1
- package/dist/ignore.d.ts.map +1 -1
- package/dist/ignore.js +55 -3
- package/dist/ignore.js.map +1 -1
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +294 -25
- package/dist/index.js.map +1 -1
- package/dist/plugin-loader.d.ts +168 -7
- package/dist/plugin-loader.d.ts.map +1 -1
- package/dist/plugin-loader.js +284 -21
- package/dist/plugin-loader.js.map +1 -1
- package/dist/runtime.d.ts +10 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +6 -0
- package/dist/runtime.js.map +1 -1
- package/dist/vendor/bpmn-element-templates.cjs +59639 -0
- package/package.json +20 -7
package/dist/plugin-loader.d.ts
CHANGED
|
@@ -1,16 +1,127 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Plugin loader for dynamic command loading
|
|
3
3
|
*/
|
|
4
|
+
import type { CamundaClient } from "@camunda8/orchestration-cluster-api";
|
|
4
5
|
import type { FlagDef } from "./command-registry.ts";
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
import { type Logger, type OutputMode } from "./logger.ts";
|
|
7
|
+
/**
|
|
8
|
+
* Typed, documented host context passed to plugin command handlers as
|
|
9
|
+
* the third argument (#377).
|
|
10
|
+
*
|
|
11
|
+
* Reflects every member of `GLOBAL_FLAGS` whose value is meaningful to
|
|
12
|
+
* a plugin handler; `help` and `version` are intentionally absent
|
|
13
|
+
* because they are intercepted by the host before dispatch. The
|
|
14
|
+
* class-scoped contract test
|
|
15
|
+
* (`tests/unit/plugin-host-context-contract.test.ts`) pins this
|
|
16
|
+
* relationship.
|
|
17
|
+
*
|
|
18
|
+
* `client` is exposed as a lazy getter so plugins that never need a
|
|
19
|
+
* Camunda client (e.g. local-only utilities, session inspectors) do
|
|
20
|
+
* not trigger credential resolution simply by receiving the ctx.
|
|
21
|
+
*
|
|
22
|
+
* Plugins authored before #377 use the two-argument signature
|
|
23
|
+
* `(args, flags)` — the third argument is additive and JavaScript's
|
|
24
|
+
* variadic call semantics keep those handlers working unchanged.
|
|
25
|
+
*/
|
|
26
|
+
export interface PluginCtx {
|
|
27
|
+
/**
|
|
28
|
+
* Active profile name (from `--profile` or session), or `undefined` when
|
|
29
|
+
* neither was set. Plugins should pass this through to `createClient()` /
|
|
30
|
+
* `resolveTenantId()` verbatim so env-var-only configurations (no
|
|
31
|
+
* profile, just `CAMUNDA_*` env vars) resolve the same way they do for
|
|
32
|
+
* built-in commands.
|
|
33
|
+
*/
|
|
34
|
+
profile: string | undefined;
|
|
35
|
+
/** True when `--dry-run` is set. Plugins SHOULD honour this. */
|
|
36
|
+
dryRun: boolean;
|
|
37
|
+
/** True when `--verbose` is set. */
|
|
38
|
+
verbose: boolean;
|
|
39
|
+
/** Effective output mode (`--json` toggles to `json`). */
|
|
40
|
+
outputMode: OutputMode;
|
|
41
|
+
/** Parsed `--fields a,b,c` list, or undefined when not set. */
|
|
42
|
+
fields?: string[];
|
|
43
|
+
/** Host logger — use `logger.json(...)` for structured output. */
|
|
44
|
+
logger: Logger;
|
|
45
|
+
/** Lazily-resolved Camunda client. Reading triggers credential resolution. */
|
|
46
|
+
readonly client: CamundaClient;
|
|
47
|
+
}
|
|
48
|
+
export type CommandHandler = (args: string[], flags?: Record<string, unknown>, ctx?: PluginCtx) => Promise<void>;
|
|
49
|
+
export interface CommandWithFlags {
|
|
7
50
|
flags: Record<string, FlagDef>;
|
|
8
51
|
handler: CommandHandler;
|
|
9
52
|
}
|
|
10
|
-
type PluginCommand = CommandHandler | CommandWithFlags;
|
|
11
|
-
interface PluginCommands {
|
|
53
|
+
export type PluginCommand = CommandHandler | CommandWithFlags;
|
|
54
|
+
export interface PluginCommands {
|
|
12
55
|
[commandName: string]: PluginCommand;
|
|
13
56
|
}
|
|
57
|
+
export interface PluginMetadata {
|
|
58
|
+
name?: string;
|
|
59
|
+
description?: string;
|
|
60
|
+
commands?: {
|
|
61
|
+
[commandName: string]: PluginCommandMeta;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Per-command plugin metadata.
|
|
66
|
+
*
|
|
67
|
+
* A plugin command is **either** metadata-driven (declares typed flags via
|
|
68
|
+
* the `{ flags, handler }` command form) **or** a passthrough command
|
|
69
|
+
* (`passthrough: true` + `passthroughHint`). Mutually exclusive — see
|
|
70
|
+
* #251 / #366. Declaring both is rejected at load time.
|
|
71
|
+
*/
|
|
72
|
+
export interface PluginCommandMeta {
|
|
73
|
+
description?: string;
|
|
74
|
+
helpDescription?: string;
|
|
75
|
+
examples?: {
|
|
76
|
+
command: string;
|
|
77
|
+
description: string;
|
|
78
|
+
}[];
|
|
79
|
+
/** Subcommands for shell completion (e.g. cluster → start, stop, status). */
|
|
80
|
+
subcommands?: {
|
|
81
|
+
name: string;
|
|
82
|
+
description: string;
|
|
83
|
+
}[];
|
|
84
|
+
/**
|
|
85
|
+
* If true, c8ctl strips GLOBAL_FLAGS from argv and forwards everything
|
|
86
|
+
* else verbatim to the bare-function handler. Help and JSON help
|
|
87
|
+
* advertise the boundary explicitly.
|
|
88
|
+
*
|
|
89
|
+
* MUST NOT appear together with the `{ flags, handler }` command form;
|
|
90
|
+
* the load-time validator drops any command that declares both.
|
|
91
|
+
*/
|
|
92
|
+
passthrough?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Required when `passthrough` is true. Short text rendered in help that
|
|
95
|
+
* advertises the boundary, e.g. "Forwards args to `kubectl`".
|
|
96
|
+
*/
|
|
97
|
+
passthroughHint?: string;
|
|
98
|
+
/**
|
|
99
|
+
* Optional documentation-only flag list rendered in help under
|
|
100
|
+
* passthrough commands. NOT parsed by c8ctl.
|
|
101
|
+
*/
|
|
102
|
+
flagsHint?: string[];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Structured record of a load-time collision between two plugins,
|
|
106
|
+
* surfaced by `c8ctl doctor plugin` (#363). Two flavours:
|
|
107
|
+
*
|
|
108
|
+
* - `command-name`: two plugins exported a command under the same name.
|
|
109
|
+
* The earlier-loaded plugin's command stays in dispatch; the later
|
|
110
|
+
* plugin's was dropped. `winner`/`loser` reflect that ordering.
|
|
111
|
+
* - `plugin-name`: two plugins shared the same `package.json#name`.
|
|
112
|
+
* The entire later plugin was rejected (its module body was never
|
|
113
|
+
* imported); `command` is undefined for this kind.
|
|
114
|
+
*
|
|
115
|
+
* The doctor command is the only consumer; the loader appends to this
|
|
116
|
+
* list as it discovers collisions and never reads from it. Cleared by
|
|
117
|
+
* `clearLoadedPlugins()` so test fixtures stay isolated.
|
|
118
|
+
*/
|
|
119
|
+
export interface PluginCollision {
|
|
120
|
+
kind: "command-name" | "plugin-name";
|
|
121
|
+
winner: string;
|
|
122
|
+
loser: string;
|
|
123
|
+
command?: string;
|
|
124
|
+
}
|
|
14
125
|
/**
|
|
15
126
|
* Load all installed plugins from global plugins directory
|
|
16
127
|
*/
|
|
@@ -20,9 +131,24 @@ export declare function loadInstalledPlugins(): Promise<void>;
|
|
|
20
131
|
*/
|
|
21
132
|
export declare function getPluginCommands(): PluginCommands;
|
|
22
133
|
/**
|
|
23
|
-
* Execute a plugin command if it exists
|
|
134
|
+
* Execute a plugin command if it exists.
|
|
135
|
+
*
|
|
136
|
+
* `ctx` is the typed host context introduced in #377. When provided it
|
|
137
|
+
* is passed as the third handler argument; legacy two-argument
|
|
138
|
+
* handlers ignore it. When omitted the call site is treating the
|
|
139
|
+
* plugin as a fire-and-forget passthrough/help-render shim and
|
|
140
|
+
* intentionally does not construct a client.
|
|
141
|
+
*/
|
|
142
|
+
export declare function executePluginCommand(commandName: string, args: string[], flags?: Record<string, unknown>, ctx?: PluginCtx): Promise<boolean>;
|
|
143
|
+
/**
|
|
144
|
+
* Look up the loaded version of a plugin by command name. Returns
|
|
145
|
+
* `undefined` if no plugin owns the command. Used by `--version` on a
|
|
146
|
+
* plugin verb to print the plugin's package version (#377).
|
|
24
147
|
*/
|
|
25
|
-
export declare function
|
|
148
|
+
export declare function getPluginVersionForCommand(commandName: string): {
|
|
149
|
+
pluginName: string;
|
|
150
|
+
version: string;
|
|
151
|
+
} | undefined;
|
|
26
152
|
/**
|
|
27
153
|
* Check if a command is provided by a plugin
|
|
28
154
|
*/
|
|
@@ -37,7 +163,10 @@ export declare function getPluginCommandNames(): string[];
|
|
|
37
163
|
export interface PluginCommandInfo {
|
|
38
164
|
commandName: string;
|
|
39
165
|
pluginName: string;
|
|
166
|
+
/** Plugin package version (`package.json#version`). */
|
|
167
|
+
pluginVersion: string;
|
|
40
168
|
description?: string;
|
|
169
|
+
helpDescription?: string;
|
|
41
170
|
examples?: {
|
|
42
171
|
command: string;
|
|
43
172
|
description: string;
|
|
@@ -47,11 +176,43 @@ export interface PluginCommandInfo {
|
|
|
47
176
|
name: string;
|
|
48
177
|
description: string;
|
|
49
178
|
}[];
|
|
179
|
+
/** True when the command opted into the #366 passthrough contract. */
|
|
180
|
+
passthrough?: boolean;
|
|
181
|
+
/** Required when passthrough is true — short text that names the boundary. */
|
|
182
|
+
passthroughHint?: string;
|
|
183
|
+
/** Optional documentation-only flag list rendered under passthrough help. */
|
|
184
|
+
flagsHint?: string[];
|
|
185
|
+
/** Typed flag declarations for flag-aware (non-passthrough) commands. */
|
|
186
|
+
flags?: Record<string, FlagDef>;
|
|
50
187
|
}
|
|
51
188
|
export declare function getPluginCommandsInfo(): PluginCommandInfo[];
|
|
189
|
+
/**
|
|
190
|
+
* True if the named command is a registered passthrough plugin command
|
|
191
|
+
* (#366). Used by the dispatcher to gate the strip-and-forward path.
|
|
192
|
+
*/
|
|
193
|
+
export declare function isPassthroughPluginCommand(commandName: string): boolean;
|
|
52
194
|
/**
|
|
53
195
|
* Clear all loaded plugins (useful for testing and after uninstall)
|
|
54
196
|
*/
|
|
55
197
|
export declare function clearLoadedPlugins(): void;
|
|
56
|
-
|
|
198
|
+
/**
|
|
199
|
+
* Snapshot of plugin collisions detected at load time (#363). Returns
|
|
200
|
+
* a deep defensive copy of frozen records so callers cannot mutate
|
|
201
|
+
* the loader's bookkeeping (neither the array nor the entries).
|
|
202
|
+
* Order reflects the order in which the loader observed the
|
|
203
|
+
* collisions.
|
|
204
|
+
*/
|
|
205
|
+
export declare function getPluginCollisions(): readonly Readonly<PluginCollision>[];
|
|
206
|
+
/**
|
|
207
|
+
* Snapshot of currently loaded plugins (#363). Returns the canonical
|
|
208
|
+
* `package.json#name` of each plugin together with the command names
|
|
209
|
+
* it actually registered (after duplicate-name rejection). Used by
|
|
210
|
+
* `c8ctl doctor plugin` to render an authoritative view of what was
|
|
211
|
+
* loaded vs. what was dropped.
|
|
212
|
+
*/
|
|
213
|
+
export interface LoadedPluginSummary {
|
|
214
|
+
name: string;
|
|
215
|
+
commands: string[];
|
|
216
|
+
}
|
|
217
|
+
export declare function getLoadedPluginSummaries(): LoadedPluginSummary[];
|
|
57
218
|
//# sourceMappingURL=plugin-loader.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-loader.d.ts","sourceRoot":"","sources":["../src/plugin-loader.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"plugin-loader.d.ts","sourceRoot":"","sources":["../src/plugin-loader.ts"],"names":[],"mappings":"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"}
|
package/dist/plugin-loader.js
CHANGED
|
@@ -15,6 +15,147 @@ import { ensurePluginsDir } from "./config.js";
|
|
|
15
15
|
import { getLogger } from "./logger.js";
|
|
16
16
|
import { c8ctl } from "./runtime.js";
|
|
17
17
|
const loadedPlugins = new Map();
|
|
18
|
+
const pluginCollisions = [];
|
|
19
|
+
/**
|
|
20
|
+
* Validate the passthrough/flags mutual-exclusion rule (#366). Removes
|
|
21
|
+
* offending commands from the registered set so they cannot be invoked,
|
|
22
|
+
* and emits a `logger.warn` naming the plugin and command so the
|
|
23
|
+
* misconfiguration is visible at startup.
|
|
24
|
+
*
|
|
25
|
+
* The contract: a command MUST NOT declare `passthrough: true` AND use the
|
|
26
|
+
* `{ flags, handler }` form. Pick one. A passthrough command without a
|
|
27
|
+
* `passthroughHint` is also rejected (the hint is what makes the boundary
|
|
28
|
+
* legible to users and agents).
|
|
29
|
+
*
|
|
30
|
+
* Mutates `plugin.commands` in place. Safe to call after each plugin is
|
|
31
|
+
* loaded.
|
|
32
|
+
*/
|
|
33
|
+
function validatePassthroughCommands(plugin) {
|
|
34
|
+
const logger = getLogger();
|
|
35
|
+
const meta = plugin.metadata?.commands ?? {};
|
|
36
|
+
for (const commandName of Object.keys(plugin.commands)) {
|
|
37
|
+
const commandMeta = meta[commandName];
|
|
38
|
+
if (commandMeta?.passthrough === undefined)
|
|
39
|
+
continue;
|
|
40
|
+
// `passthrough: false` is equivalent to "not opted in" — silently
|
|
41
|
+
// skip. Any other non-`true` value (e.g. the string "true", a
|
|
42
|
+
// number) is a contract violation: dispatch and help both gate on
|
|
43
|
+
// `=== true`, so a truthy non-true value would silently disagree
|
|
44
|
+
// with them. Reject loudly at load time.
|
|
45
|
+
if (commandMeta.passthrough === false)
|
|
46
|
+
continue;
|
|
47
|
+
if (commandMeta.passthrough !== true) {
|
|
48
|
+
logger.warn(`Plugin '${plugin.name}' command '${commandName}' has metadata.passthrough set to ` +
|
|
49
|
+
`${JSON.stringify(commandMeta.passthrough)} but the contract requires the boolean ` +
|
|
50
|
+
"literal `true` (or `false` / omitted to opt out). Dropping this command (#366).");
|
|
51
|
+
delete plugin.commands[commandName];
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const cmd = plugin.commands[commandName];
|
|
55
|
+
const hasFlagsForm = typeof cmd !== "function";
|
|
56
|
+
if (hasFlagsForm) {
|
|
57
|
+
logger.warn(`Plugin '${plugin.name}' command '${commandName}' declares both passthrough:true AND flags. ` +
|
|
58
|
+
"Pick one \u2014 a passthrough command must use the bare-function handler form. " +
|
|
59
|
+
"Dropping this command (#366).");
|
|
60
|
+
delete plugin.commands[commandName];
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (typeof commandMeta.passthroughHint !== "string" ||
|
|
64
|
+
commandMeta.passthroughHint.trim() === "") {
|
|
65
|
+
logger.warn(`Plugin '${plugin.name}' command '${commandName}' declares passthrough:true ` +
|
|
66
|
+
"but is missing a non-empty passthroughHint. The hint is required so help and " +
|
|
67
|
+
"agents can advertise the boundary. Dropping this command (#366).");
|
|
68
|
+
delete plugin.commands[commandName];
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
// `flagsHint` is documentation-only and consumed by the help
|
|
72
|
+
// renderer, which assumes `string[]`. Validate the shape here so a
|
|
73
|
+
// mis-typed value can't reach the renderer. The field is optional;
|
|
74
|
+
// invalid shapes are stripped (not fatal) so the command itself
|
|
75
|
+
// continues to work — only the doc affordance is lost.
|
|
76
|
+
const flagsHint = commandMeta.flagsHint;
|
|
77
|
+
if (flagsHint !== undefined) {
|
|
78
|
+
const valid = Array.isArray(flagsHint) &&
|
|
79
|
+
flagsHint.every((entry) => typeof entry === "string");
|
|
80
|
+
if (!valid) {
|
|
81
|
+
logger.warn(`Plugin '${plugin.name}' command '${commandName}' declares metadata.flagsHint ` +
|
|
82
|
+
"but it is not a string[]. Ignoring flagsHint (#366).");
|
|
83
|
+
delete commandMeta.flagsHint;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Reject duplicate plugin command names at load time. If `plugin` declares
|
|
90
|
+
* a command name that is already registered by an earlier-loaded plugin,
|
|
91
|
+
* drop it from `plugin.commands` and emit `logger.warn` naming both
|
|
92
|
+
* plugins so the conflict is visible at startup.
|
|
93
|
+
*
|
|
94
|
+
* **Conflict policy: first registration wins.** This is an explicit
|
|
95
|
+
* choice (#366) and replaces the previous implicit "last-loaded wins"
|
|
96
|
+
* behaviour produced by `Object.assign` over `loadedPlugins` in
|
|
97
|
+
* insertion order. Plugins cannot override one another by registering
|
|
98
|
+
* the same command name; if you want a different command, give it a
|
|
99
|
+
* different name. Default plugins always load first, so user-installed
|
|
100
|
+
* plugins cannot override default commands by name.
|
|
101
|
+
*
|
|
102
|
+
* This guarantees that the merged map returned by `getPluginCommands()`
|
|
103
|
+
* has a single owning plugin per command name, which keeps dispatch and
|
|
104
|
+
* `isPassthroughPluginCommand()` consistent: the help renderer and the
|
|
105
|
+
* runtime always agree on which plugin handles a given verb.
|
|
106
|
+
*
|
|
107
|
+
* Mutates `plugin.commands` in place. Safe to call after each plugin is
|
|
108
|
+
* loaded; relies on `loadedPlugins` already containing previously-loaded
|
|
109
|
+
* plugins.
|
|
110
|
+
*/
|
|
111
|
+
function rejectDuplicateCommandNames(plugin) {
|
|
112
|
+
const logger = getLogger();
|
|
113
|
+
for (const commandName of Object.keys(plugin.commands)) {
|
|
114
|
+
for (const existing of loadedPlugins.values()) {
|
|
115
|
+
if (Object.hasOwn(existing.commands, commandName)) {
|
|
116
|
+
logger.warn(`Plugin '${plugin.name}' tried to register command '${commandName}' but it is ` +
|
|
117
|
+
`already provided by plugin '${existing.name}'. The first registration wins; ` +
|
|
118
|
+
`dropping the duplicate from '${plugin.name}'.`);
|
|
119
|
+
pluginCollisions.push({
|
|
120
|
+
kind: "command-name",
|
|
121
|
+
winner: existing.name,
|
|
122
|
+
loser: plugin.name,
|
|
123
|
+
command: commandName,
|
|
124
|
+
});
|
|
125
|
+
delete plugin.commands[commandName];
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Reject a plugin whose name collides with an already-loaded plugin.
|
|
133
|
+
* This is a separate concern from the command-name collision policy
|
|
134
|
+
* tracked under #363: that policy rejects two plugins exporting the
|
|
135
|
+
* same command name, while this one rejects two plugins sharing the
|
|
136
|
+
* same `package.json#name`. Both follow first-registration-wins.
|
|
137
|
+
* Without this guard a user-installed package sharing a name with a
|
|
138
|
+
* default plugin (or with another already-loaded plugin) would
|
|
139
|
+
* silently overwrite the prior `loadedPlugins.set()` entry, bypassing
|
|
140
|
+
* the command-name policy entirely.
|
|
141
|
+
*
|
|
142
|
+
* Returns `true` when the caller should skip the load; `false` when
|
|
143
|
+
* the name is free.
|
|
144
|
+
*/
|
|
145
|
+
function isDuplicatePluginName(pluginName) {
|
|
146
|
+
const logger = getLogger();
|
|
147
|
+
if (loadedPlugins.has(pluginName)) {
|
|
148
|
+
logger.warn(`Plugin name '${pluginName}' is already loaded; refusing to load a second plugin ` +
|
|
149
|
+
`with the same name. The first registration wins.`);
|
|
150
|
+
pluginCollisions.push({
|
|
151
|
+
kind: "plugin-name",
|
|
152
|
+
winner: pluginName,
|
|
153
|
+
loser: pluginName,
|
|
154
|
+
});
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
18
159
|
/**
|
|
19
160
|
* Load default plugins bundled with c8ctl
|
|
20
161
|
*/
|
|
@@ -44,7 +185,12 @@ async function loadDefaultPlugins() {
|
|
|
44
185
|
logger.debug("No default-plugins directory found");
|
|
45
186
|
return;
|
|
46
187
|
}
|
|
47
|
-
|
|
188
|
+
// Sort to make load order deterministic across filesystems/OSes.
|
|
189
|
+
// The first-registration-wins duplicate-name policy in
|
|
190
|
+
// `rejectDuplicateCommandNames` relies on this — without a stable
|
|
191
|
+
// sort, "who wins" would depend on `readdirSync()` order, which
|
|
192
|
+
// varies across platforms and filesystems.
|
|
193
|
+
const pluginDirs = readdirSync(defaultPluginsDir).sort();
|
|
48
194
|
logger.debug(`Found ${pluginDirs.length} default plugin(s)`);
|
|
49
195
|
for (const pluginDir of pluginDirs) {
|
|
50
196
|
const pluginPath = join(defaultPluginsDir, pluginDir);
|
|
@@ -64,6 +210,17 @@ async function loadDefaultPlugins() {
|
|
|
64
210
|
// Read package.json
|
|
65
211
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
66
212
|
const pluginName = packageJson.name || `default-${pluginDir}`;
|
|
213
|
+
const pluginVersion = typeof packageJson.version === "string" &&
|
|
214
|
+
packageJson.version.length > 0
|
|
215
|
+
? packageJson.version
|
|
216
|
+
: "0.0.0";
|
|
217
|
+
// Check for duplicate plugin name BEFORE the dynamic import
|
|
218
|
+
// so a duplicate-name plugin's module code never runs (the
|
|
219
|
+
// import has top-level side effects we don't want to execute
|
|
220
|
+
// only to throw the result away).
|
|
221
|
+
if (isDuplicatePluginName(pluginName)) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
67
224
|
const pluginFile = existsSync(pluginFileJs)
|
|
68
225
|
? pluginFileJs
|
|
69
226
|
: pluginFileTs;
|
|
@@ -72,12 +229,16 @@ async function loadDefaultPlugins() {
|
|
|
72
229
|
logger.debug(`Loading default plugin from: ${pluginUrl}`);
|
|
73
230
|
const plugin = await import(__rewriteRelativeImportExtension(pluginUrl));
|
|
74
231
|
if (plugin.commands && typeof plugin.commands === "object") {
|
|
75
|
-
|
|
232
|
+
const loaded = {
|
|
76
233
|
name: pluginName,
|
|
77
|
-
|
|
234
|
+
version: pluginVersion,
|
|
235
|
+
commands: { ...plugin.commands },
|
|
78
236
|
metadata: plugin.metadata || {},
|
|
79
|
-
}
|
|
80
|
-
|
|
237
|
+
};
|
|
238
|
+
validatePassthroughCommands(loaded);
|
|
239
|
+
rejectDuplicateCommandNames(loaded);
|
|
240
|
+
loadedPlugins.set(pluginName, loaded);
|
|
241
|
+
const commandNames = Object.keys(loaded.commands);
|
|
81
242
|
logger.debug(`Successfully loaded default plugin: ${pluginName} with ${commandNames.length} commands:`, commandNames);
|
|
82
243
|
}
|
|
83
244
|
}
|
|
@@ -108,7 +269,12 @@ export async function loadInstalledPlugins() {
|
|
|
108
269
|
return;
|
|
109
270
|
}
|
|
110
271
|
try {
|
|
111
|
-
|
|
272
|
+
// Sort to make load order deterministic across filesystems/OSes.
|
|
273
|
+
// The first-registration-wins duplicate-name policy in
|
|
274
|
+
// `rejectDuplicateCommandNames` relies on this — without a stable
|
|
275
|
+
// sort, "who wins" would depend on `readdirSync()` order, which
|
|
276
|
+
// varies across platforms and filesystems.
|
|
277
|
+
const entries = readdirSync(nodeModulesPath).sort();
|
|
112
278
|
logger.debug(`Scanning ${entries.length} entries in node_modules`);
|
|
113
279
|
const packagesToScan = [];
|
|
114
280
|
// Collect regular packages and scoped packages
|
|
@@ -117,10 +283,10 @@ export async function loadInstalledPlugins() {
|
|
|
117
283
|
continue;
|
|
118
284
|
}
|
|
119
285
|
if (entry.startsWith("@")) {
|
|
120
|
-
// Scoped package - scan subdirectories
|
|
286
|
+
// Scoped package - scan subdirectories (sorted for determinism).
|
|
121
287
|
const scopePath = join(nodeModulesPath, entry);
|
|
122
288
|
try {
|
|
123
|
-
const scopedPackages = readdirSync(scopePath);
|
|
289
|
+
const scopedPackages = readdirSync(scopePath).sort();
|
|
124
290
|
for (const scopedPkg of scopedPackages) {
|
|
125
291
|
if (!scopedPkg.startsWith(".")) {
|
|
126
292
|
packagesToScan.push(join(entry, scopedPkg));
|
|
@@ -136,6 +302,11 @@ export async function loadInstalledPlugins() {
|
|
|
136
302
|
packagesToScan.push(entry);
|
|
137
303
|
}
|
|
138
304
|
}
|
|
305
|
+
// Final defensive sort: `@scope/pkg` paths interleave with bare
|
|
306
|
+
// `pkg` paths in the order we appended them, but for
|
|
307
|
+
// duplicate-name resolution we want a single, stable lexicographic
|
|
308
|
+
// order over the full set.
|
|
309
|
+
packagesToScan.sort();
|
|
139
310
|
logger.debug(`Found ${packagesToScan.length} packages to scan:`, packagesToScan);
|
|
140
311
|
for (const packageName of packagesToScan) {
|
|
141
312
|
const packagePath = join(nodeModulesPath, packageName);
|
|
@@ -165,18 +336,44 @@ export async function loadInstalledPlugins() {
|
|
|
165
336
|
const pluginFile = existsSync(pluginFileJs)
|
|
166
337
|
? pluginFileJs
|
|
167
338
|
: pluginFileTs;
|
|
339
|
+
// Use the package.json#name (not the filesystem directory
|
|
340
|
+
// entry) as the canonical plugin name / loadedPlugins key.
|
|
341
|
+
// Under npm aliases (e.g. `npm i my-alias@npm:real-plugin`),
|
|
342
|
+
// the install directory is `my-alias` but the package name is
|
|
343
|
+
// `real-plugin`. Keying by directory would miss real
|
|
344
|
+
// duplicate-name collisions and surface the wrong name in
|
|
345
|
+
// duplicate warnings. `packageName` is kept for filesystem /
|
|
346
|
+
// logging purposes only.
|
|
347
|
+
const pluginName = typeof packageJson.name === "string" && packageJson.name.length > 0
|
|
348
|
+
? packageJson.name
|
|
349
|
+
: packageName;
|
|
350
|
+
const pluginVersion = typeof packageJson.version === "string" &&
|
|
351
|
+
packageJson.version.length > 0
|
|
352
|
+
? packageJson.version
|
|
353
|
+
: "0.0.0";
|
|
354
|
+
// Check for duplicate plugin name BEFORE the dynamic import
|
|
355
|
+
// so a duplicate-name plugin's module code never runs (the
|
|
356
|
+
// import has top-level side effects we don't want to execute
|
|
357
|
+
// only to throw the result away).
|
|
358
|
+
if (isDuplicatePluginName(pluginName)) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
168
361
|
// Use file:// protocol and add timestamp to bust cache
|
|
169
362
|
const pluginUrl = `file://${pluginFile}?t=${Date.now()}`;
|
|
170
363
|
logger.debug(`Loading plugin from: ${pluginUrl}`);
|
|
171
364
|
const plugin = await import(__rewriteRelativeImportExtension(pluginUrl));
|
|
172
365
|
if (plugin.commands && typeof plugin.commands === "object") {
|
|
173
|
-
|
|
174
|
-
name:
|
|
175
|
-
|
|
366
|
+
const loaded = {
|
|
367
|
+
name: pluginName,
|
|
368
|
+
version: pluginVersion,
|
|
369
|
+
commands: { ...plugin.commands },
|
|
176
370
|
metadata: plugin.metadata || {},
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
371
|
+
};
|
|
372
|
+
validatePassthroughCommands(loaded);
|
|
373
|
+
rejectDuplicateCommandNames(loaded);
|
|
374
|
+
loadedPlugins.set(pluginName, loaded);
|
|
375
|
+
const commandNames = Object.keys(loaded.commands);
|
|
376
|
+
logger.debug(`Successfully loaded plugin: ${pluginName} (dir: ${packageName}) with ${commandNames.length} commands:`, commandNames);
|
|
180
377
|
}
|
|
181
378
|
}
|
|
182
379
|
catch (error) {
|
|
@@ -200,16 +397,25 @@ export function getPluginCommands() {
|
|
|
200
397
|
return allCommands;
|
|
201
398
|
}
|
|
202
399
|
/**
|
|
203
|
-
* Execute a plugin command if it exists
|
|
400
|
+
* Execute a plugin command if it exists.
|
|
401
|
+
*
|
|
402
|
+
* `ctx` is the typed host context introduced in #377. When provided it
|
|
403
|
+
* is passed as the third handler argument; legacy two-argument
|
|
404
|
+
* handlers ignore it. When omitted the call site is treating the
|
|
405
|
+
* plugin as a fire-and-forget passthrough/help-render shim and
|
|
406
|
+
* intentionally does not construct a client.
|
|
204
407
|
*/
|
|
205
|
-
export async function executePluginCommand(commandName, args, flags) {
|
|
408
|
+
export async function executePluginCommand(commandName, args, flags, ctx) {
|
|
206
409
|
const commands = getPluginCommands();
|
|
207
410
|
const cmd = Object.hasOwn(commands, commandName)
|
|
208
411
|
? commands[commandName]
|
|
209
412
|
: undefined;
|
|
210
413
|
if (cmd) {
|
|
211
414
|
if (typeof cmd === "function") {
|
|
212
|
-
if (
|
|
415
|
+
if (ctx !== undefined) {
|
|
416
|
+
await cmd(args, flags, ctx);
|
|
417
|
+
}
|
|
418
|
+
else if (flags !== undefined) {
|
|
213
419
|
await cmd(args, flags);
|
|
214
420
|
}
|
|
215
421
|
else {
|
|
@@ -217,12 +423,25 @@ export async function executePluginCommand(commandName, args, flags) {
|
|
|
217
423
|
}
|
|
218
424
|
}
|
|
219
425
|
else {
|
|
220
|
-
await cmd.handler(args, flags);
|
|
426
|
+
await cmd.handler(args, flags, ctx);
|
|
221
427
|
}
|
|
222
428
|
return true;
|
|
223
429
|
}
|
|
224
430
|
return false;
|
|
225
431
|
}
|
|
432
|
+
/**
|
|
433
|
+
* Look up the loaded version of a plugin by command name. Returns
|
|
434
|
+
* `undefined` if no plugin owns the command. Used by `--version` on a
|
|
435
|
+
* plugin verb to print the plugin's package version (#377).
|
|
436
|
+
*/
|
|
437
|
+
export function getPluginVersionForCommand(commandName) {
|
|
438
|
+
for (const plugin of loadedPlugins.values()) {
|
|
439
|
+
if (Object.hasOwn(plugin.commands, commandName)) {
|
|
440
|
+
return { pluginName: plugin.name, version: plugin.version };
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return undefined;
|
|
444
|
+
}
|
|
226
445
|
/**
|
|
227
446
|
* Check if a command is provided by a plugin
|
|
228
447
|
*/
|
|
@@ -240,21 +459,65 @@ export function getPluginCommandsInfo() {
|
|
|
240
459
|
const infos = [];
|
|
241
460
|
for (const plugin of loadedPlugins.values()) {
|
|
242
461
|
for (const commandName of Object.keys(plugin.commands)) {
|
|
462
|
+
const meta = plugin.metadata?.commands?.[commandName];
|
|
463
|
+
const cmd = plugin.commands[commandName];
|
|
464
|
+
const flags = typeof cmd !== "function" ? cmd.flags : undefined;
|
|
243
465
|
infos.push({
|
|
244
466
|
commandName,
|
|
245
467
|
pluginName: plugin.name,
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
468
|
+
pluginVersion: plugin.version,
|
|
469
|
+
description: meta?.description,
|
|
470
|
+
helpDescription: meta?.helpDescription,
|
|
471
|
+
examples: meta?.examples,
|
|
472
|
+
subcommands: meta?.subcommands,
|
|
473
|
+
passthrough: meta?.passthrough === true ? true : undefined,
|
|
474
|
+
passthroughHint: meta?.passthroughHint,
|
|
475
|
+
flagsHint: meta?.flagsHint,
|
|
476
|
+
flags,
|
|
249
477
|
});
|
|
250
478
|
}
|
|
251
479
|
}
|
|
252
480
|
return infos;
|
|
253
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* True if the named command is a registered passthrough plugin command
|
|
484
|
+
* (#366). Used by the dispatcher to gate the strip-and-forward path.
|
|
485
|
+
*/
|
|
486
|
+
export function isPassthroughPluginCommand(commandName) {
|
|
487
|
+
for (const plugin of loadedPlugins.values()) {
|
|
488
|
+
if (!Object.hasOwn(plugin.commands, commandName))
|
|
489
|
+
continue;
|
|
490
|
+
const meta = plugin.metadata?.commands?.[commandName];
|
|
491
|
+
if (meta?.passthrough === true)
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
254
496
|
/**
|
|
255
497
|
* Clear all loaded plugins (useful for testing and after uninstall)
|
|
256
498
|
*/
|
|
257
499
|
export function clearLoadedPlugins() {
|
|
258
500
|
loadedPlugins.clear();
|
|
501
|
+
pluginCollisions.length = 0;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Snapshot of plugin collisions detected at load time (#363). Returns
|
|
505
|
+
* a deep defensive copy of frozen records so callers cannot mutate
|
|
506
|
+
* the loader's bookkeeping (neither the array nor the entries).
|
|
507
|
+
* Order reflects the order in which the loader observed the
|
|
508
|
+
* collisions.
|
|
509
|
+
*/
|
|
510
|
+
export function getPluginCollisions() {
|
|
511
|
+
return Object.freeze(pluginCollisions.map((c) => Object.freeze({ ...c })));
|
|
512
|
+
}
|
|
513
|
+
export function getLoadedPluginSummaries() {
|
|
514
|
+
const summaries = [];
|
|
515
|
+
for (const plugin of loadedPlugins.values()) {
|
|
516
|
+
summaries.push({
|
|
517
|
+
name: plugin.name,
|
|
518
|
+
commands: Object.keys(plugin.commands),
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
return summaries;
|
|
259
522
|
}
|
|
260
523
|
//# sourceMappingURL=plugin-loader.js.map
|