@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.
Files changed (88) hide show
  1. package/EXAMPLES.md +22 -5
  2. package/PLUGIN-HELP.md +26 -0
  3. package/README.md +37 -4
  4. package/dist/command-dispatch.d.ts.map +1 -1
  5. package/dist/command-dispatch.js +2 -1
  6. package/dist/command-dispatch.js.map +1 -1
  7. package/dist/command-registry.d.ts +30 -0
  8. package/dist/command-registry.d.ts.map +1 -1
  9. package/dist/command-registry.js +34 -1
  10. package/dist/command-registry.js.map +1 -1
  11. package/dist/commands/plugins.d.ts +21 -0
  12. package/dist/commands/plugins.d.ts.map +1 -1
  13. package/dist/commands/plugins.js +74 -1
  14. package/dist/commands/plugins.js.map +1 -1
  15. package/dist/commands/resource-extensions.d.ts +12 -0
  16. package/dist/commands/resource-extensions.d.ts.map +1 -1
  17. package/dist/commands/resource-extensions.js +13 -1
  18. package/dist/commands/resource-extensions.js.map +1 -1
  19. package/dist/commands/run.d.ts.map +1 -1
  20. package/dist/commands/run.js +5 -3
  21. package/dist/commands/run.js.map +1 -1
  22. package/dist/commands/watch.d.ts.map +1 -1
  23. package/dist/commands/watch.js +21 -12
  24. package/dist/commands/watch.js.map +1 -1
  25. package/dist/completion.d.ts.map +1 -1
  26. package/dist/completion.js +140 -13
  27. package/dist/completion.js.map +1 -1
  28. package/dist/default-plugins/bpmn/README.md +69 -0
  29. package/dist/default-plugins/bpmn/c8ctl-plugin.js +125 -0
  30. package/dist/default-plugins/bpmn/c8ctl-plugin.js.map +1 -0
  31. package/dist/default-plugins/bpmn/lint.js +259 -0
  32. package/dist/default-plugins/bpmn/lint.js.map +1 -0
  33. package/dist/default-plugins/bpmn/package.json +8 -0
  34. package/dist/default-plugins/cluster/package.json +1 -1
  35. package/dist/default-plugins/element-template/AGENTS.md +65 -0
  36. package/dist/default-plugins/element-template/README.md +331 -0
  37. package/dist/default-plugins/element-template/c8ctl-plugin.js +247 -0
  38. package/dist/default-plugins/element-template/c8ctl-plugin.js.map +1 -0
  39. package/dist/default-plugins/element-template/commands/apply.js +298 -0
  40. package/dist/default-plugins/element-template/commands/apply.js.map +1 -0
  41. package/dist/default-plugins/element-template/commands/get-properties.js +344 -0
  42. package/dist/default-plugins/element-template/commands/get-properties.js.map +1 -0
  43. package/dist/default-plugins/element-template/commands/get.js +107 -0
  44. package/dist/default-plugins/element-template/commands/get.js.map +1 -0
  45. package/dist/default-plugins/element-template/commands/info.js +189 -0
  46. package/dist/default-plugins/element-template/commands/info.js.map +1 -0
  47. package/dist/default-plugins/element-template/commands/search.js +101 -0
  48. package/dist/default-plugins/element-template/commands/search.js.map +1 -0
  49. package/dist/default-plugins/element-template/commands/sync.js +36 -0
  50. package/dist/default-plugins/element-template/commands/sync.js.map +1 -0
  51. package/dist/default-plugins/element-template/docs/design.md +145 -0
  52. package/dist/default-plugins/element-template/helpers.js +404 -0
  53. package/dist/default-plugins/element-template/helpers.js.map +1 -0
  54. package/dist/default-plugins/element-template/marketplace.js +396 -0
  55. package/dist/default-plugins/element-template/marketplace.js.map +1 -0
  56. package/dist/default-plugins/element-template/package.json +8 -0
  57. package/dist/default-plugins/element-template/template-ref.js +232 -0
  58. package/dist/default-plugins/element-template/template-ref.js.map +1 -0
  59. package/dist/default-plugins/element-template/vendor-src/bundle-entry.js +44 -0
  60. package/dist/default-plugins/feel/README.md +163 -0
  61. package/dist/default-plugins/feel/c8ctl-plugin.js +616 -0
  62. package/dist/default-plugins/feel/c8ctl-plugin.js.map +1 -0
  63. package/dist/default-plugins/feel/package.json +8 -0
  64. package/dist/deployments.d.ts +1 -0
  65. package/dist/deployments.d.ts.map +1 -1
  66. package/dist/deployments.js +89 -20
  67. package/dist/deployments.js.map +1 -1
  68. package/dist/help.d.ts.map +1 -1
  69. package/dist/help.js +156 -5
  70. package/dist/help.js.map +1 -1
  71. package/dist/ignore.d.ts +16 -1
  72. package/dist/ignore.d.ts.map +1 -1
  73. package/dist/ignore.js +55 -3
  74. package/dist/ignore.js.map +1 -1
  75. package/dist/index.d.ts +23 -0
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +294 -25
  78. package/dist/index.js.map +1 -1
  79. package/dist/plugin-loader.d.ts +168 -7
  80. package/dist/plugin-loader.d.ts.map +1 -1
  81. package/dist/plugin-loader.js +284 -21
  82. package/dist/plugin-loader.js.map +1 -1
  83. package/dist/runtime.d.ts +10 -0
  84. package/dist/runtime.d.ts.map +1 -1
  85. package/dist/runtime.js +6 -0
  86. package/dist/runtime.js.map +1 -1
  87. package/dist/vendor/bpmn-element-templates.cjs +59639 -0
  88. package/package.json +20 -7
@@ -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
- type CommandHandler = (args: string[], flags?: Record<string, unknown>) => Promise<void>;
6
- interface CommandWithFlags {
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 executePluginCommand(commandName: string, args: string[], flags?: Record<string, unknown>): Promise<boolean>;
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
- export {};
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;AAKrD,KAAK,cAAc,GAAG,CACrB,IAAI,EAAE,MAAM,EAAE,EACd,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC3B,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnB,UAAU,gBAAgB;IACzB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,OAAO,EAAE,cAAc,CAAC;CACxB;AAED,KAAK,aAAa,GAAG,cAAc,GAAG,gBAAgB,CAAC;AAEvD,UAAU,cAAc;IACvB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,CAAC;CACrC;AAiHD;;GAEG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CA2H1D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,cAAc,CAQlD;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACzC,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,EAAE,EACd,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAAC,OAAO,CAAC,CAoBlB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAG5D;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,EAAE,CAEhD;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACtD,6EAA6E;IAC7E,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACtD;AAED,wBAAgB,qBAAqB,IAAI,iBAAiB,EAAE,CAgB3D;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC"}
1
+ {"version":3,"file":"plugin-loader.d.ts","sourceRoot":"","sources":["../src/plugin-loader.ts"],"names":[],"mappings":"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"}
@@ -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
- const pluginDirs = readdirSync(defaultPluginsDir);
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
- loadedPlugins.set(pluginName, {
232
+ const loaded = {
76
233
  name: pluginName,
77
- commands: plugin.commands,
234
+ version: pluginVersion,
235
+ commands: { ...plugin.commands },
78
236
  metadata: plugin.metadata || {},
79
- });
80
- const commandNames = Object.keys(plugin.commands);
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
- const entries = readdirSync(nodeModulesPath);
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
- loadedPlugins.set(packageName, {
174
- name: packageName,
175
- commands: plugin.commands,
366
+ const loaded = {
367
+ name: pluginName,
368
+ version: pluginVersion,
369
+ commands: { ...plugin.commands },
176
370
  metadata: plugin.metadata || {},
177
- });
178
- const commandNames = Object.keys(plugin.commands);
179
- logger.debug(`Successfully loaded plugin: ${packageName} with ${commandNames.length} commands:`, commandNames);
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 (flags !== undefined) {
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
- description: plugin.metadata?.commands?.[commandName]?.description,
247
- examples: plugin.metadata?.commands?.[commandName]?.examples,
248
- subcommands: plugin.metadata?.commands?.[commandName]?.subcommands,
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