@camunda8/cli 2.8.0-alpha.1 → 2.8.0-alpha.11
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 +170 -8
- package/README.md +1186 -73
- package/dist/command-dispatch.d.ts.map +1 -1
- package/dist/command-dispatch.js +5 -3
- package/dist/command-dispatch.js.map +1 -1
- package/dist/command-framework.d.ts.map +1 -1
- package/dist/command-framework.js +22 -1
- package/dist/command-framework.js.map +1 -1
- package/dist/command-registry.d.ts +61 -311
- package/dist/command-registry.d.ts.map +1 -1
- package/dist/command-registry.js +64 -33
- package/dist/command-registry.js.map +1 -1
- package/dist/commands/completion.d.ts +16 -33
- package/dist/commands/completion.d.ts.map +1 -1
- package/dist/commands/completion.js +31 -689
- package/dist/commands/completion.js.map +1 -1
- package/dist/commands/mcp-proxy.d.ts +0 -17
- package/dist/commands/mcp-proxy.d.ts.map +1 -1
- package/dist/commands/mcp-proxy.js +3 -104
- package/dist/commands/mcp-proxy.js.map +1 -1
- package/dist/commands/open.d.ts +3 -44
- package/dist/commands/open.d.ts.map +1 -1
- package/dist/commands/open.js +5 -81
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/plugins.d.ts +23 -8
- package/dist/commands/plugins.d.ts.map +1 -1
- package/dist/commands/plugins.js +58 -29
- package/dist/commands/plugins.js.map +1 -1
- package/dist/commands/run.d.ts +8 -8
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +60 -60
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/search.d.ts +2 -39
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +2 -83
- package/dist/commands/search.js.map +1 -1
- package/dist/commands/session.d.ts +1 -0
- package/dist/commands/session.d.ts.map +1 -1
- package/dist/commands/session.js +5 -0
- package/dist/commands/session.js.map +1 -1
- package/dist/commands/watch.d.ts +2 -1
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +17 -8
- package/dist/commands/watch.js.map +1 -1
- package/dist/completion.d.ts +36 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +816 -0
- package/dist/completion.js.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +19 -1
- package/dist/config.js.map +1 -1
- package/dist/{commands/deployments.d.ts → deployments.d.ts} +1 -1
- package/dist/deployments.d.ts.map +1 -0
- package/dist/{commands/deployments.js → deployments.js} +8 -8
- package/dist/deployments.js.map +1 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/{commands/help.js → help.js} +68 -4
- package/dist/help.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +285 -9
- package/dist/index.js.map +1 -1
- package/dist/mcp-proxy-helpers.d.ts +23 -0
- package/dist/mcp-proxy-helpers.d.ts.map +1 -0
- package/dist/mcp-proxy-helpers.js +109 -0
- package/dist/mcp-proxy-helpers.js.map +1 -0
- package/dist/open-helpers.d.ts +52 -0
- package/dist/open-helpers.d.ts.map +1 -0
- package/dist/open-helpers.js +88 -0
- package/dist/open-helpers.js.map +1 -0
- package/dist/plugin-loader.d.ts +102 -2
- package/dist/plugin-loader.d.ts.map +1 -1
- package/dist/plugin-loader.js +262 -22
- package/dist/plugin-loader.js.map +1 -1
- package/dist/plugin-version.d.ts +15 -0
- package/dist/plugin-version.d.ts.map +1 -0
- package/dist/plugin-version.js +37 -0
- package/dist/plugin-version.js.map +1 -0
- package/dist/search-helpers.d.ts +46 -0
- package/dist/search-helpers.d.ts.map +1 -0
- package/dist/search-helpers.js +90 -0
- package/dist/search-helpers.js.map +1 -0
- package/dist/watch-constants.d.ts +7 -0
- package/dist/watch-constants.d.ts.map +1 -0
- package/dist/watch-constants.js +7 -0
- package/dist/watch-constants.js.map +1 -0
- package/package.json +11 -7
- package/dist/commands/deployments.d.ts.map +0 -1
- package/dist/commands/deployments.js.map +0 -1
- package/dist/commands/help.d.ts.map +0 -1
- package/dist/commands/help.js.map +0 -1
- /package/dist/{commands/help.d.ts → help.d.ts} +0 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers extracted from `src/commands/open.ts` so tests can import
|
|
3
|
+
* them without violating the test→commands import boundary (#291). The
|
|
4
|
+
* `openAppCommand` / `feedbackCommand` handlers in `src/commands/open.ts`
|
|
5
|
+
* re-import these.
|
|
6
|
+
*/
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { platform } from "node:os";
|
|
9
|
+
import { requireOneOf, requirePositional } from "./command-validation.js";
|
|
10
|
+
import { getLogger } from "./logger.js";
|
|
11
|
+
export const OPEN_APPS = [
|
|
12
|
+
"operate",
|
|
13
|
+
"tasklist",
|
|
14
|
+
"modeler",
|
|
15
|
+
"optimize",
|
|
16
|
+
];
|
|
17
|
+
export function isAppName(value) {
|
|
18
|
+
// biome-ignore lint/plugin: safe widening — readonly tuple to readonly string[] for .includes() compatibility
|
|
19
|
+
return OPEN_APPS.includes(value);
|
|
20
|
+
}
|
|
21
|
+
/** Pattern that matches a self-managed REST API version suffix, e.g. `/v2` */
|
|
22
|
+
const VERSION_SUFFIX_RE = /\/v\d+\/?$/;
|
|
23
|
+
/**
|
|
24
|
+
* Derive the URL of a Camunda web application from the cluster base URL.
|
|
25
|
+
*
|
|
26
|
+
* Only supported for self-managed clusters where the base URL is the REST API
|
|
27
|
+
* endpoint (e.g. `http://localhost:8080/v2`). Returns `null` when the URL
|
|
28
|
+
* does not look like a self-managed gateway (no `/v<n>` suffix).
|
|
29
|
+
*/
|
|
30
|
+
export function deriveAppUrl(baseUrl, app) {
|
|
31
|
+
if (!VERSION_SUFFIX_RE.test(baseUrl)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const base = baseUrl.replace(VERSION_SUFFIX_RE, "").replace(/\/$/, "");
|
|
35
|
+
return `${base}/${app}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Determine the platform-appropriate command and arguments to open a URL.
|
|
39
|
+
*
|
|
40
|
+
* WSL is detected explicitly because it reports `linux`, but should use the
|
|
41
|
+
* Windows opener via interop rather than relying on `xdg-open`.
|
|
42
|
+
*
|
|
43
|
+
* Accepts optional overrides for testing.
|
|
44
|
+
*/
|
|
45
|
+
export function getBrowserCommand(url, plat = platform(), env = process.env) {
|
|
46
|
+
const isWsl = plat === "linux" && Boolean(env.WSL_DISTRO_NAME || env.WSL_INTEROP);
|
|
47
|
+
if (plat === "darwin") {
|
|
48
|
+
return { command: "open", args: [url] };
|
|
49
|
+
}
|
|
50
|
+
if (plat === "win32" || isWsl) {
|
|
51
|
+
return { command: "cmd.exe", args: ["/c", "start", "", url] };
|
|
52
|
+
}
|
|
53
|
+
// Linux
|
|
54
|
+
return { command: "xdg-open", args: [url] };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Open a URL in the default system browser.
|
|
58
|
+
* Works on macOS, Linux, native Windows, and WSL.
|
|
59
|
+
*/
|
|
60
|
+
export function openUrl(url) {
|
|
61
|
+
const logger = getLogger();
|
|
62
|
+
const { command, args } = getBrowserCommand(url);
|
|
63
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
64
|
+
child.on("error", (error) => {
|
|
65
|
+
if (error.code === "ENOENT") {
|
|
66
|
+
logger.error(`Could not open the browser automatically because '${command}' is not available on PATH.`);
|
|
67
|
+
logger.info(`Open this URL manually: ${url}`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
logger.error(`Could not open the browser automatically: ${error.message}`);
|
|
71
|
+
logger.info(`Open this URL manually: ${url}`);
|
|
72
|
+
});
|
|
73
|
+
child.unref();
|
|
74
|
+
}
|
|
75
|
+
const OPEN_USAGE = "Usage: c8 open <app> [--profile <name>]";
|
|
76
|
+
/**
|
|
77
|
+
* Validate raw CLI positional + options for the open command.
|
|
78
|
+
* Returns a fully validated and type-narrowed input object.
|
|
79
|
+
* Exits with code 1 on invalid input.
|
|
80
|
+
*/
|
|
81
|
+
export function validateOpenAppOptions(app, options) {
|
|
82
|
+
return {
|
|
83
|
+
app: requireOneOf(requirePositional(app, "Application", OPEN_USAGE), OPEN_APPS, "application", OPEN_USAGE),
|
|
84
|
+
profile: options.profile,
|
|
85
|
+
dryRun: options.dryRun,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=open-helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"open-helpers.js","sourceRoot":"","sources":["../src/open-helpers.ts"],"names":[],"mappings":"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"}
|
package/dist/plugin-loader.d.ts
CHANGED
|
@@ -1,8 +1,76 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Plugin loader for dynamic command loading
|
|
3
3
|
*/
|
|
4
|
+
import type { FlagDef } from "./command-registry.ts";
|
|
5
|
+
type CommandHandler = (args: string[], flags?: Record<string, unknown>) => Promise<void>;
|
|
6
|
+
interface CommandWithFlags {
|
|
7
|
+
flags: Record<string, FlagDef>;
|
|
8
|
+
handler: CommandHandler;
|
|
9
|
+
}
|
|
10
|
+
type PluginCommand = CommandHandler | CommandWithFlags;
|
|
4
11
|
interface PluginCommands {
|
|
5
|
-
[commandName: string]:
|
|
12
|
+
[commandName: string]: PluginCommand;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Per-command plugin metadata.
|
|
16
|
+
*
|
|
17
|
+
* A plugin command is **either** metadata-driven (declares typed flags via
|
|
18
|
+
* the `{ flags, handler }` command form) **or** a passthrough command
|
|
19
|
+
* (`passthrough: true` + `passthroughHint`). Mutually exclusive — see
|
|
20
|
+
* #251 / #366. Declaring both is rejected at load time.
|
|
21
|
+
*/
|
|
22
|
+
export interface PluginCommandMeta {
|
|
23
|
+
description?: string;
|
|
24
|
+
helpDescription?: string;
|
|
25
|
+
examples?: {
|
|
26
|
+
command: string;
|
|
27
|
+
description: string;
|
|
28
|
+
}[];
|
|
29
|
+
/** Subcommands for shell completion (e.g. cluster → start, stop, status). */
|
|
30
|
+
subcommands?: {
|
|
31
|
+
name: string;
|
|
32
|
+
description: string;
|
|
33
|
+
}[];
|
|
34
|
+
/**
|
|
35
|
+
* If true, c8ctl strips GLOBAL_FLAGS from argv and forwards everything
|
|
36
|
+
* else verbatim to the bare-function handler. Help and JSON help
|
|
37
|
+
* advertise the boundary explicitly.
|
|
38
|
+
*
|
|
39
|
+
* MUST NOT appear together with the `{ flags, handler }` command form;
|
|
40
|
+
* the load-time validator drops any command that declares both.
|
|
41
|
+
*/
|
|
42
|
+
passthrough?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Required when `passthrough` is true. Short text rendered in help that
|
|
45
|
+
* advertises the boundary, e.g. "Forwards args to `kubectl`".
|
|
46
|
+
*/
|
|
47
|
+
passthroughHint?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Optional documentation-only flag list rendered in help under
|
|
50
|
+
* passthrough commands. NOT parsed by c8ctl.
|
|
51
|
+
*/
|
|
52
|
+
flagsHint?: string[];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Structured record of a load-time collision between two plugins,
|
|
56
|
+
* surfaced by `c8ctl doctor plugin` (#363). Two flavours:
|
|
57
|
+
*
|
|
58
|
+
* - `command-name`: two plugins exported a command under the same name.
|
|
59
|
+
* The earlier-loaded plugin's command stays in dispatch; the later
|
|
60
|
+
* plugin's was dropped. `winner`/`loser` reflect that ordering.
|
|
61
|
+
* - `plugin-name`: two plugins shared the same `package.json#name`.
|
|
62
|
+
* The entire later plugin was rejected (its module body was never
|
|
63
|
+
* imported); `command` is undefined for this kind.
|
|
64
|
+
*
|
|
65
|
+
* The doctor command is the only consumer; the loader appends to this
|
|
66
|
+
* list as it discovers collisions and never reads from it. Cleared by
|
|
67
|
+
* `clearLoadedPlugins()` so test fixtures stay isolated.
|
|
68
|
+
*/
|
|
69
|
+
export interface PluginCollision {
|
|
70
|
+
kind: "command-name" | "plugin-name";
|
|
71
|
+
winner: string;
|
|
72
|
+
loser: string;
|
|
73
|
+
command?: string;
|
|
6
74
|
}
|
|
7
75
|
/**
|
|
8
76
|
* Load all installed plugins from global plugins directory
|
|
@@ -15,7 +83,7 @@ export declare function getPluginCommands(): PluginCommands;
|
|
|
15
83
|
/**
|
|
16
84
|
* Execute a plugin command if it exists
|
|
17
85
|
*/
|
|
18
|
-
export declare function executePluginCommand(commandName: string, args: string[]): Promise<boolean>;
|
|
86
|
+
export declare function executePluginCommand(commandName: string, args: string[], flags?: Record<string, unknown>): Promise<boolean>;
|
|
19
87
|
/**
|
|
20
88
|
* Check if a command is provided by a plugin
|
|
21
89
|
*/
|
|
@@ -31,6 +99,7 @@ export interface PluginCommandInfo {
|
|
|
31
99
|
commandName: string;
|
|
32
100
|
pluginName: string;
|
|
33
101
|
description?: string;
|
|
102
|
+
helpDescription?: string;
|
|
34
103
|
examples?: {
|
|
35
104
|
command: string;
|
|
36
105
|
description: string;
|
|
@@ -40,11 +109,42 @@ export interface PluginCommandInfo {
|
|
|
40
109
|
name: string;
|
|
41
110
|
description: string;
|
|
42
111
|
}[];
|
|
112
|
+
/** True when the command opted into the #366 passthrough contract. */
|
|
113
|
+
passthrough?: boolean;
|
|
114
|
+
/** Required when passthrough is true — short text that names the boundary. */
|
|
115
|
+
passthroughHint?: string;
|
|
116
|
+
/** Optional documentation-only flag list rendered under passthrough help. */
|
|
117
|
+
flagsHint?: string[];
|
|
43
118
|
}
|
|
44
119
|
export declare function getPluginCommandsInfo(): PluginCommandInfo[];
|
|
120
|
+
/**
|
|
121
|
+
* True if the named command is a registered passthrough plugin command
|
|
122
|
+
* (#366). Used by the dispatcher to gate the strip-and-forward path.
|
|
123
|
+
*/
|
|
124
|
+
export declare function isPassthroughPluginCommand(commandName: string): boolean;
|
|
45
125
|
/**
|
|
46
126
|
* Clear all loaded plugins (useful for testing and after uninstall)
|
|
47
127
|
*/
|
|
48
128
|
export declare function clearLoadedPlugins(): void;
|
|
129
|
+
/**
|
|
130
|
+
* Snapshot of plugin collisions detected at load time (#363). Returns
|
|
131
|
+
* a deep defensive copy of frozen records so callers cannot mutate
|
|
132
|
+
* the loader's bookkeeping (neither the array nor the entries).
|
|
133
|
+
* Order reflects the order in which the loader observed the
|
|
134
|
+
* collisions.
|
|
135
|
+
*/
|
|
136
|
+
export declare function getPluginCollisions(): readonly Readonly<PluginCollision>[];
|
|
137
|
+
/**
|
|
138
|
+
* Snapshot of currently loaded plugins (#363). Returns the canonical
|
|
139
|
+
* `package.json#name` of each plugin together with the command names
|
|
140
|
+
* it actually registered (after duplicate-name rejection). Used by
|
|
141
|
+
* `c8ctl doctor plugin` to render an authoritative view of what was
|
|
142
|
+
* loaded vs. what was dropped.
|
|
143
|
+
*/
|
|
144
|
+
export interface LoadedPluginSummary {
|
|
145
|
+
name: string;
|
|
146
|
+
commands: string[];
|
|
147
|
+
}
|
|
148
|
+
export declare function getLoadedPluginSummaries(): LoadedPluginSummary[];
|
|
49
149
|
export {};
|
|
50
150
|
//# 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;
|
|
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,13 @@ 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
|
+
// Check for duplicate plugin name BEFORE the dynamic import
|
|
214
|
+
// so a duplicate-name plugin's module code never runs (the
|
|
215
|
+
// import has top-level side effects we don't want to execute
|
|
216
|
+
// only to throw the result away).
|
|
217
|
+
if (isDuplicatePluginName(pluginName)) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
67
220
|
const pluginFile = existsSync(pluginFileJs)
|
|
68
221
|
? pluginFileJs
|
|
69
222
|
: pluginFileTs;
|
|
@@ -72,12 +225,15 @@ async function loadDefaultPlugins() {
|
|
|
72
225
|
logger.debug(`Loading default plugin from: ${pluginUrl}`);
|
|
73
226
|
const plugin = await import(__rewriteRelativeImportExtension(pluginUrl));
|
|
74
227
|
if (plugin.commands && typeof plugin.commands === "object") {
|
|
75
|
-
|
|
228
|
+
const loaded = {
|
|
76
229
|
name: pluginName,
|
|
77
|
-
commands: plugin.commands,
|
|
230
|
+
commands: { ...plugin.commands },
|
|
78
231
|
metadata: plugin.metadata || {},
|
|
79
|
-
}
|
|
80
|
-
|
|
232
|
+
};
|
|
233
|
+
validatePassthroughCommands(loaded);
|
|
234
|
+
rejectDuplicateCommandNames(loaded);
|
|
235
|
+
loadedPlugins.set(pluginName, loaded);
|
|
236
|
+
const commandNames = Object.keys(loaded.commands);
|
|
81
237
|
logger.debug(`Successfully loaded default plugin: ${pluginName} with ${commandNames.length} commands:`, commandNames);
|
|
82
238
|
}
|
|
83
239
|
}
|
|
@@ -108,7 +264,12 @@ export async function loadInstalledPlugins() {
|
|
|
108
264
|
return;
|
|
109
265
|
}
|
|
110
266
|
try {
|
|
111
|
-
|
|
267
|
+
// Sort to make load order deterministic across filesystems/OSes.
|
|
268
|
+
// The first-registration-wins duplicate-name policy in
|
|
269
|
+
// `rejectDuplicateCommandNames` relies on this — without a stable
|
|
270
|
+
// sort, "who wins" would depend on `readdirSync()` order, which
|
|
271
|
+
// varies across platforms and filesystems.
|
|
272
|
+
const entries = readdirSync(nodeModulesPath).sort();
|
|
112
273
|
logger.debug(`Scanning ${entries.length} entries in node_modules`);
|
|
113
274
|
const packagesToScan = [];
|
|
114
275
|
// Collect regular packages and scoped packages
|
|
@@ -117,10 +278,10 @@ export async function loadInstalledPlugins() {
|
|
|
117
278
|
continue;
|
|
118
279
|
}
|
|
119
280
|
if (entry.startsWith("@")) {
|
|
120
|
-
// Scoped package - scan subdirectories
|
|
281
|
+
// Scoped package - scan subdirectories (sorted for determinism).
|
|
121
282
|
const scopePath = join(nodeModulesPath, entry);
|
|
122
283
|
try {
|
|
123
|
-
const scopedPackages = readdirSync(scopePath);
|
|
284
|
+
const scopedPackages = readdirSync(scopePath).sort();
|
|
124
285
|
for (const scopedPkg of scopedPackages) {
|
|
125
286
|
if (!scopedPkg.startsWith(".")) {
|
|
126
287
|
packagesToScan.push(join(entry, scopedPkg));
|
|
@@ -136,6 +297,11 @@ export async function loadInstalledPlugins() {
|
|
|
136
297
|
packagesToScan.push(entry);
|
|
137
298
|
}
|
|
138
299
|
}
|
|
300
|
+
// Final defensive sort: `@scope/pkg` paths interleave with bare
|
|
301
|
+
// `pkg` paths in the order we appended them, but for
|
|
302
|
+
// duplicate-name resolution we want a single, stable lexicographic
|
|
303
|
+
// order over the full set.
|
|
304
|
+
packagesToScan.sort();
|
|
139
305
|
logger.debug(`Found ${packagesToScan.length} packages to scan:`, packagesToScan);
|
|
140
306
|
for (const packageName of packagesToScan) {
|
|
141
307
|
const packagePath = join(nodeModulesPath, packageName);
|
|
@@ -165,18 +331,39 @@ export async function loadInstalledPlugins() {
|
|
|
165
331
|
const pluginFile = existsSync(pluginFileJs)
|
|
166
332
|
? pluginFileJs
|
|
167
333
|
: pluginFileTs;
|
|
334
|
+
// Use the package.json#name (not the filesystem directory
|
|
335
|
+
// entry) as the canonical plugin name / loadedPlugins key.
|
|
336
|
+
// Under npm aliases (e.g. `npm i my-alias@npm:real-plugin`),
|
|
337
|
+
// the install directory is `my-alias` but the package name is
|
|
338
|
+
// `real-plugin`. Keying by directory would miss real
|
|
339
|
+
// duplicate-name collisions and surface the wrong name in
|
|
340
|
+
// duplicate warnings. `packageName` is kept for filesystem /
|
|
341
|
+
// logging purposes only.
|
|
342
|
+
const pluginName = typeof packageJson.name === "string" && packageJson.name.length > 0
|
|
343
|
+
? packageJson.name
|
|
344
|
+
: packageName;
|
|
345
|
+
// Check for duplicate plugin name BEFORE the dynamic import
|
|
346
|
+
// so a duplicate-name plugin's module code never runs (the
|
|
347
|
+
// import has top-level side effects we don't want to execute
|
|
348
|
+
// only to throw the result away).
|
|
349
|
+
if (isDuplicatePluginName(pluginName)) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
168
352
|
// Use file:// protocol and add timestamp to bust cache
|
|
169
353
|
const pluginUrl = `file://${pluginFile}?t=${Date.now()}`;
|
|
170
354
|
logger.debug(`Loading plugin from: ${pluginUrl}`);
|
|
171
355
|
const plugin = await import(__rewriteRelativeImportExtension(pluginUrl));
|
|
172
356
|
if (plugin.commands && typeof plugin.commands === "object") {
|
|
173
|
-
|
|
174
|
-
name:
|
|
175
|
-
commands: plugin.commands,
|
|
357
|
+
const loaded = {
|
|
358
|
+
name: pluginName,
|
|
359
|
+
commands: { ...plugin.commands },
|
|
176
360
|
metadata: plugin.metadata || {},
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
361
|
+
};
|
|
362
|
+
validatePassthroughCommands(loaded);
|
|
363
|
+
rejectDuplicateCommandNames(loaded);
|
|
364
|
+
loadedPlugins.set(pluginName, loaded);
|
|
365
|
+
const commandNames = Object.keys(loaded.commands);
|
|
366
|
+
logger.debug(`Successfully loaded plugin: ${pluginName} (dir: ${packageName}) with ${commandNames.length} commands:`, commandNames);
|
|
180
367
|
}
|
|
181
368
|
}
|
|
182
369
|
catch (error) {
|
|
@@ -193,7 +380,7 @@ export async function loadInstalledPlugins() {
|
|
|
193
380
|
* Get all loaded plugin commands
|
|
194
381
|
*/
|
|
195
382
|
export function getPluginCommands() {
|
|
196
|
-
const allCommands =
|
|
383
|
+
const allCommands = Object.create(null);
|
|
197
384
|
for (const plugin of loadedPlugins.values()) {
|
|
198
385
|
Object.assign(allCommands, plugin.commands);
|
|
199
386
|
}
|
|
@@ -202,10 +389,23 @@ export function getPluginCommands() {
|
|
|
202
389
|
/**
|
|
203
390
|
* Execute a plugin command if it exists
|
|
204
391
|
*/
|
|
205
|
-
export async function executePluginCommand(commandName, args) {
|
|
392
|
+
export async function executePluginCommand(commandName, args, flags) {
|
|
206
393
|
const commands = getPluginCommands();
|
|
207
|
-
|
|
208
|
-
|
|
394
|
+
const cmd = Object.hasOwn(commands, commandName)
|
|
395
|
+
? commands[commandName]
|
|
396
|
+
: undefined;
|
|
397
|
+
if (cmd) {
|
|
398
|
+
if (typeof cmd === "function") {
|
|
399
|
+
if (flags !== undefined) {
|
|
400
|
+
await cmd(args, flags);
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
await cmd(args);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
await cmd.handler(args, flags);
|
|
408
|
+
}
|
|
209
409
|
return true;
|
|
210
410
|
}
|
|
211
411
|
return false;
|
|
@@ -215,7 +415,7 @@ export async function executePluginCommand(commandName, args) {
|
|
|
215
415
|
*/
|
|
216
416
|
export function isPluginCommand(commandName) {
|
|
217
417
|
const commands = getPluginCommands();
|
|
218
|
-
return commandName
|
|
418
|
+
return Object.hasOwn(commands, commandName);
|
|
219
419
|
}
|
|
220
420
|
/**
|
|
221
421
|
* Get list of all plugin command names
|
|
@@ -227,21 +427,61 @@ export function getPluginCommandsInfo() {
|
|
|
227
427
|
const infos = [];
|
|
228
428
|
for (const plugin of loadedPlugins.values()) {
|
|
229
429
|
for (const commandName of Object.keys(plugin.commands)) {
|
|
430
|
+
const meta = plugin.metadata?.commands?.[commandName];
|
|
230
431
|
infos.push({
|
|
231
432
|
commandName,
|
|
232
433
|
pluginName: plugin.name,
|
|
233
|
-
description:
|
|
234
|
-
|
|
235
|
-
|
|
434
|
+
description: meta?.description,
|
|
435
|
+
helpDescription: meta?.helpDescription,
|
|
436
|
+
examples: meta?.examples,
|
|
437
|
+
subcommands: meta?.subcommands,
|
|
438
|
+
passthrough: meta?.passthrough === true ? true : undefined,
|
|
439
|
+
passthroughHint: meta?.passthroughHint,
|
|
440
|
+
flagsHint: meta?.flagsHint,
|
|
236
441
|
});
|
|
237
442
|
}
|
|
238
443
|
}
|
|
239
444
|
return infos;
|
|
240
445
|
}
|
|
446
|
+
/**
|
|
447
|
+
* True if the named command is a registered passthrough plugin command
|
|
448
|
+
* (#366). Used by the dispatcher to gate the strip-and-forward path.
|
|
449
|
+
*/
|
|
450
|
+
export function isPassthroughPluginCommand(commandName) {
|
|
451
|
+
for (const plugin of loadedPlugins.values()) {
|
|
452
|
+
if (!Object.hasOwn(plugin.commands, commandName))
|
|
453
|
+
continue;
|
|
454
|
+
const meta = plugin.metadata?.commands?.[commandName];
|
|
455
|
+
if (meta?.passthrough === true)
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
241
460
|
/**
|
|
242
461
|
* Clear all loaded plugins (useful for testing and after uninstall)
|
|
243
462
|
*/
|
|
244
463
|
export function clearLoadedPlugins() {
|
|
245
464
|
loadedPlugins.clear();
|
|
465
|
+
pluginCollisions.length = 0;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Snapshot of plugin collisions detected at load time (#363). Returns
|
|
469
|
+
* a deep defensive copy of frozen records so callers cannot mutate
|
|
470
|
+
* the loader's bookkeeping (neither the array nor the entries).
|
|
471
|
+
* Order reflects the order in which the loader observed the
|
|
472
|
+
* collisions.
|
|
473
|
+
*/
|
|
474
|
+
export function getPluginCollisions() {
|
|
475
|
+
return Object.freeze(pluginCollisions.map((c) => Object.freeze({ ...c })));
|
|
476
|
+
}
|
|
477
|
+
export function getLoadedPluginSummaries() {
|
|
478
|
+
const summaries = [];
|
|
479
|
+
for (const plugin of loadedPlugins.values()) {
|
|
480
|
+
summaries.push({
|
|
481
|
+
name: plugin.name,
|
|
482
|
+
commands: Object.keys(plugin.commands),
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
return summaries;
|
|
246
486
|
}
|
|
247
487
|
//# sourceMappingURL=plugin-loader.js.map
|