@camunda8/cli 2.6.1 → 2.7.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +80 -1
  2. package/dist/command-dispatch.d.ts +16 -0
  3. package/dist/command-dispatch.d.ts.map +1 -0
  4. package/dist/command-dispatch.js +129 -0
  5. package/dist/command-dispatch.js.map +1 -0
  6. package/dist/command-framework.d.ts +248 -0
  7. package/dist/command-framework.d.ts.map +1 -0
  8. package/dist/command-framework.js +229 -0
  9. package/dist/command-framework.js.map +1 -0
  10. package/dist/command-registry.d.ts +2089 -0
  11. package/dist/command-registry.d.ts.map +1 -0
  12. package/dist/command-registry.js +1482 -0
  13. package/dist/command-registry.js.map +1 -0
  14. package/dist/command-validation.d.ts +30 -4
  15. package/dist/command-validation.d.ts.map +1 -1
  16. package/dist/command-validation.js +90 -4
  17. package/dist/command-validation.js.map +1 -1
  18. package/dist/commands/completion.d.ts +25 -1
  19. package/dist/commands/completion.d.ts.map +1 -1
  20. package/dist/commands/completion.js +590 -1223
  21. package/dist/commands/completion.js.map +1 -1
  22. package/dist/commands/deployments.d.ts +2 -0
  23. package/dist/commands/deployments.d.ts.map +1 -1
  24. package/dist/commands/deployments.js +13 -2
  25. package/dist/commands/deployments.js.map +1 -1
  26. package/dist/commands/forms.d.ts +2 -20
  27. package/dist/commands/forms.d.ts.map +1 -1
  28. package/dist/commands/forms.js +76 -79
  29. package/dist/commands/forms.js.map +1 -1
  30. package/dist/commands/help.d.ts +4 -88
  31. package/dist/commands/help.d.ts.map +1 -1
  32. package/dist/commands/help.js +574 -1941
  33. package/dist/commands/help.js.map +1 -1
  34. package/dist/commands/identity-authorizations.d.ts +5 -24
  35. package/dist/commands/identity-authorizations.d.ts.map +1 -1
  36. package/dist/commands/identity-authorizations.js +113 -137
  37. package/dist/commands/identity-authorizations.js.map +1 -1
  38. package/dist/commands/identity-groups.d.ts +5 -26
  39. package/dist/commands/identity-groups.d.ts.map +1 -1
  40. package/dist/commands/identity-groups.js +91 -124
  41. package/dist/commands/identity-groups.js.map +1 -1
  42. package/dist/commands/identity-mapping-rules.d.ts +5 -30
  43. package/dist/commands/identity-mapping-rules.d.ts.map +1 -1
  44. package/dist/commands/identity-mapping-rules.js +106 -136
  45. package/dist/commands/identity-mapping-rules.js.map +1 -1
  46. package/dist/commands/identity-roles.d.ts +5 -26
  47. package/dist/commands/identity-roles.d.ts.map +1 -1
  48. package/dist/commands/identity-roles.js +91 -124
  49. package/dist/commands/identity-roles.js.map +1 -1
  50. package/dist/commands/identity-tenants.d.ts +5 -26
  51. package/dist/commands/identity-tenants.d.ts.map +1 -1
  52. package/dist/commands/identity-tenants.js +92 -126
  53. package/dist/commands/identity-tenants.js.map +1 -1
  54. package/dist/commands/identity-users.d.ts +5 -29
  55. package/dist/commands/identity-users.d.ts.map +1 -1
  56. package/dist/commands/identity-users.js +95 -129
  57. package/dist/commands/identity-users.js.map +1 -1
  58. package/dist/commands/identity.d.ts +6 -6
  59. package/dist/commands/identity.d.ts.map +1 -1
  60. package/dist/commands/identity.js +6 -7
  61. package/dist/commands/identity.js.map +1 -1
  62. package/dist/commands/incidents.d.ts +3 -16
  63. package/dist/commands/incidents.d.ts.map +1 -1
  64. package/dist/commands/incidents.js +71 -98
  65. package/dist/commands/incidents.js.map +1 -1
  66. package/dist/commands/jobs.d.ts +4 -26
  67. package/dist/commands/jobs.d.ts.map +1 -1
  68. package/dist/commands/jobs.js +143 -159
  69. package/dist/commands/jobs.js.map +1 -1
  70. package/dist/commands/mcp-proxy.d.ts +1 -0
  71. package/dist/commands/mcp-proxy.d.ts.map +1 -1
  72. package/dist/commands/mcp-proxy.js +10 -2
  73. package/dist/commands/mcp-proxy.js.map +1 -1
  74. package/dist/commands/messages.d.ts +2 -12
  75. package/dist/commands/messages.d.ts.map +1 -1
  76. package/dist/commands/messages.js +87 -81
  77. package/dist/commands/messages.js.map +1 -1
  78. package/dist/commands/open.d.ts +4 -0
  79. package/dist/commands/open.d.ts.map +1 -1
  80. package/dist/commands/open.js +18 -0
  81. package/dist/commands/open.js.map +1 -1
  82. package/dist/commands/plugins.d.ts +7 -9
  83. package/dist/commands/plugins.d.ts.map +1 -1
  84. package/dist/commands/plugins.js +32 -25
  85. package/dist/commands/plugins.js.map +1 -1
  86. package/dist/commands/process-definitions.d.ts +2 -14
  87. package/dist/commands/process-definitions.d.ts.map +1 -1
  88. package/dist/commands/process-definitions.js +57 -80
  89. package/dist/commands/process-definitions.js.map +1 -1
  90. package/dist/commands/process-instances.d.ts +8 -37
  91. package/dist/commands/process-instances.d.ts.map +1 -1
  92. package/dist/commands/process-instances.js +242 -193
  93. package/dist/commands/process-instances.js.map +1 -1
  94. package/dist/commands/profiles.d.ts +4 -0
  95. package/dist/commands/profiles.d.ts.map +1 -1
  96. package/dist/commands/profiles.js +29 -0
  97. package/dist/commands/profiles.js.map +1 -1
  98. package/dist/commands/run.d.ts +2 -0
  99. package/dist/commands/run.d.ts.map +1 -1
  100. package/dist/commands/run.js +10 -0
  101. package/dist/commands/run.js.map +1 -1
  102. package/dist/commands/search.d.ts +7 -100
  103. package/dist/commands/search.d.ts.map +1 -1
  104. package/dist/commands/search.js +530 -694
  105. package/dist/commands/search.js.map +1 -1
  106. package/dist/commands/session.d.ts +3 -0
  107. package/dist/commands/session.d.ts.map +1 -1
  108. package/dist/commands/session.js +30 -0
  109. package/dist/commands/session.js.map +1 -1
  110. package/dist/commands/topology.d.ts +1 -3
  111. package/dist/commands/topology.d.ts.map +1 -1
  112. package/dist/commands/topology.js +11 -18
  113. package/dist/commands/topology.js.map +1 -1
  114. package/dist/commands/user-tasks.d.ts +2 -16
  115. package/dist/commands/user-tasks.d.ts.map +1 -1
  116. package/dist/commands/user-tasks.js +73 -101
  117. package/dist/commands/user-tasks.js.map +1 -1
  118. package/dist/commands/watch.d.ts +1 -0
  119. package/dist/commands/watch.d.ts.map +1 -1
  120. package/dist/commands/watch.js +15 -0
  121. package/dist/commands/watch.js.map +1 -1
  122. package/dist/config.d.ts.map +1 -1
  123. package/dist/config.js +5 -0
  124. package/dist/config.js.map +1 -1
  125. package/dist/default-plugins/cluster/README.md +1 -1
  126. package/dist/default-plugins/cluster/c8ctl-plugin.js +281 -59
  127. package/dist/index.d.ts +0 -4
  128. package/dist/index.d.ts.map +1 -1
  129. package/dist/index.js +111 -1023
  130. package/dist/index.js.map +1 -1
  131. package/dist/logger.d.ts +5 -0
  132. package/dist/logger.d.ts.map +1 -1
  133. package/dist/logger.js +7 -1
  134. package/dist/logger.js.map +1 -1
  135. package/dist/plugin-loader.d.ts +5 -0
  136. package/dist/plugin-loader.d.ts.map +1 -1
  137. package/dist/plugin-loader.js +1 -0
  138. package/dist/plugin-loader.js.map +1 -1
  139. package/dist/update-check.d.ts +67 -0
  140. package/dist/update-check.d.ts.map +1 -0
  141. package/dist/update-check.js +284 -0
  142. package/dist/update-check.js.map +1 -0
  143. package/package.json +3 -2
  144. /package/dist/templates/{tsconfig.json → tsconfig.json.template} +0 -0
package/dist/index.js CHANGED
@@ -7,56 +7,17 @@ import { realpathSync } from "node:fs";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { parseArgs } from "node:util";
9
9
  import { createClient } from "./client.js";
10
- import { showCompletion } from "./commands/completion.js";
11
- import { deploy } from "./commands/deployments.js";
12
- import { getForm, getStartForm, getUserTaskForm } from "./commands/forms.js";
10
+ import { COMMAND_DISPATCH } from "./command-dispatch.js";
11
+ import { COMMAND_REGISTRY, deriveParseArgsOptions, getCommandDef, resolveAlias, } from "./command-registry.js";
12
+ import { detectUnknownFlags, validateFlags } from "./command-validation.js";
13
+ import { installCompletion, refreshCompletionsIfStale, showCompletion, } from "./commands/completion.js";
13
14
  import { showCommandHelp, showHelp, showVerbResources, showVersion, } from "./commands/help.js";
14
- import { createIdentityAuthorization, createIdentityGroup, createIdentityMappingRule, createIdentityRole, createIdentityTenant, createIdentityUser, deleteIdentityAuthorization, deleteIdentityGroup, deleteIdentityMappingRule, deleteIdentityRole, deleteIdentityTenant, deleteIdentityUser, getIdentityAuthorization, getIdentityGroup, getIdentityMappingRule, getIdentityRole, getIdentityTenant, getIdentityUser, handleAssign, handleUnassign, listAuthorizations, listGroups, listMappingRules, listRoles, listTenants, listUsers, searchIdentityAuthorizations, searchIdentityGroups, searchIdentityMappingRules, searchIdentityRoles, searchIdentityTenants, searchIdentityUsers, validateCreateAuthorizationOptions, } from "./commands/identity.js";
15
- import { getIncident, listIncidents, resolveIncident, } from "./commands/incidents.js";
16
- import { activateJobs, completeJob, failJob, listJobs, } from "./commands/jobs.js";
17
- import { mcpProxy } from "./commands/mcp-proxy.js";
18
- import { correlateMessage, publishMessage } from "./commands/messages.js";
19
- import { openApp, openUrl, validateOpenAppOptions } from "./commands/open.js";
20
- import { downgradePlugin, initPlugin, listPlugins, loadPlugin, syncPlugins, unloadPlugin, upgradePlugin, } from "./commands/plugins.js";
21
- import { getProcessDefinition, listProcessDefinitions, } from "./commands/process-definitions.js";
22
- import { cancelProcessInstance, createProcessInstance, getProcessInstance, listProcessInstances, } from "./commands/process-instances.js";
23
- import { addProfile, listProfiles, removeProfile, whichProfile, } from "./commands/profiles.js";
24
- import { run } from "./commands/run.js";
25
- import { detectUnknownSearchFlags, searchIncidents, searchJobs, searchProcessDefinitions, searchProcessInstances, searchUserTasks, searchVariables, } from "./commands/search.js";
26
- import { setOutputFormat, useProfile, useTenant } from "./commands/session.js";
27
- import { getTopology } from "./commands/topology.js";
28
- import { completeUserTask, listUserTasks } from "./commands/user-tasks.js";
29
- import { watchFiles } from "./commands/watch.js";
15
+ import { handleAssign, handleUnassign } from "./commands/identity.js";
30
16
  import { loadSessionState, resolveTenantId } from "./config.js";
31
17
  import { getLogger } from "./logger.js";
32
18
  import { executePluginCommand, loadInstalledPlugins } from "./plugin-loader.js";
33
19
  import { c8ctl } from "./runtime.js";
34
- /**
35
- * Normalize resource aliases
36
- */
37
- function normalizeResource(resource) {
38
- const aliases = {
39
- pi: "process-instance",
40
- pd: "process-definition",
41
- ut: "user-task",
42
- inc: "incident",
43
- msg: "message",
44
- vars: "variable",
45
- profile: "profile",
46
- profiles: "profile",
47
- plugin: "plugin",
48
- plugins: "plugin",
49
- auth: "authorization",
50
- authorizations: "authorization",
51
- mr: "mapping-rule",
52
- "mapping-rules": "mapping-rule",
53
- users: "user",
54
- roles: "role",
55
- groups: "group",
56
- tenants: "tenant",
57
- };
58
- return aliases[resource] || resource;
59
- }
20
+ import { printUpdateNotification, startUpdateCheck } from "./update-check.js";
60
21
  /**
61
22
  * Type guard: extract a string value from parseArgs values, or undefined.
62
23
  * parseArgs with strict:false returns values typed as string | boolean | (string|boolean)[] | undefined.
@@ -71,12 +32,6 @@ function str(value) {
71
32
  function bool(value) {
72
33
  return typeof value === "boolean" ? value : undefined;
73
34
  }
74
- /**
75
- * Type guard: narrow unknown to Record<string, unknown>.
76
- */
77
- export function isRecord(value) {
78
- return value != null && typeof value === "object";
79
- }
80
35
  /**
81
36
  * Parse --version flag value into a number, or undefined if not set.
82
37
  */
@@ -86,96 +41,14 @@ function parseVersionFlag(values) {
86
41
  : undefined;
87
42
  }
88
43
  /**
89
- * Parse command line arguments
44
+ * Parse command line arguments.
45
+ * Options are derived from the command registry — no manual duplication.
90
46
  */
91
47
  function parseCliArgs() {
92
48
  try {
93
49
  const { values, positionals } = parseArgs({
94
50
  args: process.argv.slice(2),
95
- options: {
96
- help: { type: "boolean", short: "h" },
97
- version: { type: "string", short: "v" },
98
- all: { type: "boolean" },
99
- xml: { type: "boolean" },
100
- profile: { type: "string" },
101
- bpmnProcessId: { type: "string" },
102
- id: { type: "string" },
103
- processDefinitionId: { type: "string" },
104
- processInstanceKey: { type: "string" },
105
- processDefinitionKey: { type: "string" },
106
- parentProcessInstanceKey: { type: "string" },
107
- variables: { type: "string" },
108
- state: { type: "string" },
109
- assignee: { type: "string" },
110
- type: { type: "string" },
111
- correlationKey: { type: "string" },
112
- timeToLive: { type: "string" },
113
- maxJobsToActivate: { type: "string" },
114
- timeout: { type: "string" },
115
- worker: { type: "string" },
116
- retries: { type: "string" },
117
- errorMessage: { type: "string" },
118
- baseUrl: { type: "string" },
119
- clientId: { type: "string" },
120
- clientSecret: { type: "string" },
121
- audience: { type: "string" },
122
- oAuthUrl: { type: "string" },
123
- defaultTenantId: { type: "string" },
124
- from: { type: "string" },
125
- name: { type: "string" },
126
- key: { type: "string" },
127
- elementId: { type: "string" },
128
- errorType: { type: "string" },
129
- awaitCompletion: { type: "boolean" },
130
- fetchVariables: { type: "boolean" },
131
- requestTimeout: { type: "string" },
132
- value: { type: "string" },
133
- scopeKey: { type: "string" },
134
- fullValue: { type: "boolean" },
135
- userTask: { type: "boolean" },
136
- processDefinition: { type: "boolean" },
137
- iname: { type: "string" },
138
- iid: { type: "string" },
139
- iassignee: { type: "string" },
140
- ierrorMessage: { type: "string" },
141
- itype: { type: "string" },
142
- ivalue: { type: "string" },
143
- sortBy: { type: "string" },
144
- asc: { type: "boolean" },
145
- desc: { type: "boolean" },
146
- limit: { type: "string" },
147
- between: { type: "string" },
148
- dateField: { type: "string" },
149
- fields: { type: "string" },
150
- "dry-run": { type: "boolean" },
151
- verbose: { type: "boolean" },
152
- force: { type: "boolean" },
153
- none: { type: "boolean" },
154
- "from-file": { type: "string" },
155
- "from-env": { type: "boolean" },
156
- username: { type: "string" },
157
- email: { type: "string" },
158
- password: { type: "string" },
159
- ownerId: { type: "string" },
160
- ownerType: { type: "string" },
161
- resourceType: { type: "string" },
162
- resourceId: { type: "string" },
163
- permissions: { type: "string" },
164
- roleId: { type: "string" },
165
- groupId: { type: "string" },
166
- tenantId: { type: "string" },
167
- claimName: { type: "string" },
168
- claimValue: { type: "string" },
169
- mappingRuleId: { type: "string" },
170
- "to-user": { type: "string" },
171
- "to-group": { type: "string" },
172
- "to-tenant": { type: "string" },
173
- "to-mapping-rule": { type: "string" },
174
- "from-user": { type: "string" },
175
- "from-group": { type: "string" },
176
- "from-tenant": { type: "string" },
177
- "from-mapping-rule": { type: "string" },
178
- },
51
+ options: deriveParseArgsOptions(),
179
52
  allowPositionals: true,
180
53
  strict: false,
181
54
  });
@@ -196,20 +69,29 @@ export function resolveProcessDefinitionId(values) {
196
69
  str(values.bpmnProcessId));
197
70
  }
198
71
  /**
199
- * Warn about unrecognized flags for a search resource.
72
+ * Warn about unrecognized flags for a verb × resource combination.
200
73
  */
201
- function warnUnknownSearchFlags(logger, unknownFlags, resource) {
74
+ function warnUnknownFlags(logger, unknownFlags, verb, resource) {
202
75
  if (unknownFlags.length === 0)
203
76
  return;
204
77
  const flagList = unknownFlags.map((f) => `--${f}`).join(", ");
205
- logger.warn(`Flag(s) ${flagList} not recognized for 'search ${resource}'. They will be ignored. Run "c8ctl help search" for valid options.`);
78
+ const command = resource ? `${verb} ${resource}` : verb;
79
+ logger.warn(`Flag(s) ${flagList} not recognized for '${command}'. They will be ignored. Run "c8ctl help ${verb}" for valid options.`);
206
80
  }
81
+ /** Verbs that require a resource argument — derived from COMMAND_REGISTRY (includes aliases). */
82
+ const VERB_REQUIRES_RESOURCE = new Set(
83
+ // biome-ignore lint/plugin: widen to CommandDef to access optional aliases property
84
+ Object.entries(COMMAND_REGISTRY)
85
+ .filter(([, def]) => def.requiresResource)
86
+ .flatMap(([verb, def]) => [verb, ...(def.aliases ?? [])]));
207
87
  /**
208
88
  * Main CLI handler
209
89
  */
210
90
  async function main() {
211
91
  // Load session state from disk at startup
212
92
  loadSessionState();
93
+ // Fire-and-forget: check for CLI updates in the background
94
+ startUpdateCheck(c8ctl.version);
213
95
  const { values, positionals } = parseCliArgs();
214
96
  // Initialize logger with current output mode from c8ctl runtime
215
97
  const logger = getLogger(c8ctl.outputMode);
@@ -245,6 +127,8 @@ async function main() {
245
127
  c8ctl.init({ createClient, resolveTenantId, getLogger });
246
128
  // Load installed plugins
247
129
  await loadInstalledPlugins();
130
+ // Auto-refresh installed completions if CLI version changed
131
+ refreshCompletionsIfStale();
248
132
  // Extract command and resource
249
133
  const [verb, resource, ...args] = positionals;
250
134
  // Handle global --version flag (only when no verb/command is provided)
@@ -267,7 +151,7 @@ async function main() {
267
151
  verb === "-h") {
268
152
  // Check if user wants help for a specific command
269
153
  if (resource) {
270
- showCommandHelp(resource);
154
+ await showCommandHelp(resource);
271
155
  }
272
156
  else {
273
157
  showHelp();
@@ -276,886 +160,43 @@ async function main() {
276
160
  }
277
161
  // Handle completion command
278
162
  if (verb === "completion") {
279
- showCompletion(resource);
280
- return;
281
- }
282
- // Normalize resource
283
- const normalizedResource = resource ? normalizeResource(resource) : "";
284
- // Handle session commands
285
- if (verb === "use") {
286
- if (normalizedResource === "profile") {
287
- if (values.none) {
288
- useProfile("--none");
289
- return;
290
- }
291
- if (!args[0]) {
292
- logger.error("Profile name required. Usage: c8 use profile <name>");
293
- process.exit(1);
294
- }
295
- useProfile(args[0]);
163
+ // Run unknown-flag detection before early return (completion returns
164
+ // before the general detectUnknownFlags call below).
165
+ const completionUnknownFlags = detectUnknownFlags(verb, resource ?? "", values);
166
+ warnUnknownFlags(logger, completionUnknownFlags, verb, resource ?? "");
167
+ if (resource === "install") {
168
+ installCompletion(str(values.shell));
296
169
  return;
297
170
  }
298
- if (normalizedResource === "tenant") {
299
- if (!args[0]) {
300
- logger.error("Tenant ID required. Usage: c8 use tenant <id>");
301
- process.exit(1);
302
- }
303
- useTenant(args[0]);
304
- return;
305
- }
306
- showVerbResources("use");
307
- return;
308
- }
309
- if (verb === "output") {
310
- if (!resource) {
311
- logger.info(`Current output mode: ${c8ctl.outputMode}`);
312
- if (c8ctl.outputMode === "text") {
313
- logger.info("");
314
- }
315
- logger.info("Available modes: json|text");
316
- return;
317
- }
318
- setOutputFormat(resource);
319
- return;
320
- }
321
- // Handle profile commands
322
- if (verb === "list" && normalizedResource === "profile") {
323
- listProfiles();
324
- return;
325
- }
326
- if (verb === "add" && normalizedResource === "profile") {
327
- if (!args[0]) {
328
- logger.error("Profile name required. Usage: c8 add profile <name> --baseUrl=<url>");
329
- process.exit(1);
330
- }
331
- const envFile = typeof values["from-file"] === "string" ? values["from-file"] : undefined;
332
- const fromEnv = values["from-env"] === true;
333
- addProfile(args[0], {
334
- url: typeof values.baseUrl === "string" ? values.baseUrl : undefined,
335
- clientId: typeof values.clientId === "string" ? values.clientId : undefined,
336
- clientSecret: typeof values.clientSecret === "string"
337
- ? values.clientSecret
338
- : undefined,
339
- audience: typeof values.audience === "string" ? values.audience : undefined,
340
- oauthUrl: typeof values.oAuthUrl === "string" ? values.oAuthUrl : undefined,
341
- tenantId: typeof values.defaultTenantId === "string"
342
- ? values.defaultTenantId
343
- : undefined,
344
- envFile,
345
- fromEnv,
346
- });
347
- return;
348
- }
349
- if ((verb === "remove" || verb === "rm") &&
350
- normalizedResource === "profile") {
351
- if (!args[0]) {
352
- logger.error("Profile name required. Usage: c8 remove profile <name>");
353
- process.exit(1);
354
- }
355
- removeProfile(args[0]);
356
- return;
357
- }
358
- if (verb === "which" && normalizedResource === "profile") {
359
- whichProfile();
360
- return;
361
- }
362
- // Handle plugin commands
363
- if (verb === "list" && normalizedResource === "plugin") {
364
- listPlugins();
365
- return;
366
- }
367
- if (verb === "load" && normalizedResource === "plugin") {
368
- const fromUrl = str(values.from);
369
- const packageName = args[0];
370
- await loadPlugin(packageName, fromUrl);
371
- return;
372
- }
373
- if ((verb === "unload" || verb === "remove" || verb === "rm") &&
374
- normalizedResource === "plugin") {
375
- if (!args[0]) {
376
- logger.error("Package name required. Usage: c8 unload plugin <package-name>");
377
- process.exit(1);
378
- }
379
- await unloadPlugin(args[0], { force: bool(values.force) });
380
- return;
381
- }
382
- if (verb === "sync" && normalizedResource === "plugin") {
383
- await syncPlugins();
384
- return;
385
- }
386
- if (verb === "upgrade" && normalizedResource === "plugin") {
387
- if (!args[0]) {
388
- logger.error("Package name required. Usage: c8 upgrade plugin <package-name> [version]");
389
- process.exit(1);
390
- }
391
- await upgradePlugin(args[0], args[1]);
392
- return;
393
- }
394
- if (verb === "downgrade" && normalizedResource === "plugin") {
395
- if (!args[0] || !args[1]) {
396
- logger.error("Package name and version required. Usage: c8 downgrade plugin <package-name> <version>");
397
- process.exit(1);
398
- }
399
- await downgradePlugin(args[0], args[1]);
400
- return;
401
- }
402
- if (verb === "init" && normalizedResource === "plugin") {
403
- await initPlugin(args[0]);
404
- return;
405
- }
406
- // Handle process instance commands
407
- if (verb === "list" &&
408
- (normalizedResource === "process-instance" ||
409
- normalizedResource === "process-instances")) {
410
- await listProcessInstances({
411
- profile: str(values.profile),
412
- processDefinitionId: resolveProcessDefinitionId(values),
413
- version: parseVersionFlag(values),
414
- state: str(values.state),
415
- all: bool(values.all),
416
- sortBy: str(values.sortBy),
417
- sortOrder,
418
- limit,
419
- between: str(values.between),
420
- dateField: str(values.dateField),
421
- });
422
- return;
423
- }
424
- if (verb === "get" && normalizedResource === "process-instance") {
425
- if (!args[0]) {
426
- logger.error("Process instance key required. Usage: c8 get pi <key>");
427
- process.exit(1);
428
- }
429
- // Check if --variables flag is present (for get command, it's a boolean flag)
430
- const includeVariables = process.argv.includes("--variables");
431
- await getProcessInstance(args[0], {
432
- profile: str(values.profile),
433
- variables: includeVariables,
434
- });
435
- return;
436
- }
437
- if (verb === "create" && normalizedResource === "process-instance") {
438
- await createProcessInstance({
439
- profile: str(values.profile),
440
- processDefinitionId: resolveProcessDefinitionId(values),
441
- version: parseVersionFlag(values),
442
- variables: str(values.variables),
443
- awaitCompletion: bool(values.awaitCompletion),
444
- fetchVariables: bool(values.fetchVariables),
445
- requestTimeout: values.requestTimeout && typeof values.requestTimeout === "string"
446
- ? parseInt(values.requestTimeout, 10)
447
- : undefined,
448
- });
449
- return;
450
- }
451
- if (verb === "cancel" && normalizedResource === "process-instance") {
452
- if (!args[0]) {
453
- logger.error("Process instance key required. Usage: c8 cancel pi <key>");
454
- process.exit(1);
455
- }
456
- await cancelProcessInstance(args[0], {
457
- profile: str(values.profile),
458
- });
459
- return;
460
- }
461
- // Handle await command - alias for create with awaitCompletion
462
- if (verb === "await" && normalizedResource === "process-instance") {
463
- // await pi is an alias for create pi with --awaitCompletion
464
- // It supports the same flags as create (id, variables, version, etc.)
465
- await createProcessInstance({
466
- profile: str(values.profile),
467
- processDefinitionId: resolveProcessDefinitionId(values),
468
- version: parseVersionFlag(values),
469
- variables: str(values.variables),
470
- awaitCompletion: true, // Always true for await command
471
- fetchVariables: bool(values.fetchVariables),
472
- requestTimeout: values.requestTimeout && typeof values.requestTimeout === "string"
473
- ? parseInt(values.requestTimeout, 10)
474
- : undefined,
475
- });
476
- return;
477
- }
478
- // Handle process definition commands
479
- if (verb === "list" &&
480
- (normalizedResource === "process-definition" ||
481
- normalizedResource === "process-definitions")) {
482
- await listProcessDefinitions({
483
- profile: str(values.profile),
484
- sortBy: str(values.sortBy),
485
- sortOrder,
486
- limit,
487
- });
488
- return;
489
- }
490
- if (verb === "get" && normalizedResource === "process-definition") {
491
- if (!args[0]) {
492
- logger.error("Process definition key required. Usage: c8 get pd <key>");
493
- process.exit(1);
494
- }
495
- await getProcessDefinition(args[0], {
496
- profile: str(values.profile),
497
- xml: bool(values.xml),
498
- });
499
- return;
500
- }
501
- // Handle user task commands
502
- if (verb === "list" &&
503
- (normalizedResource === "user-task" || normalizedResource === "user-tasks")) {
504
- await listUserTasks({
505
- profile: str(values.profile),
506
- state: str(values.state),
507
- assignee: str(values.assignee),
508
- all: bool(values.all),
509
- sortBy: str(values.sortBy),
510
- sortOrder,
511
- limit,
512
- between: str(values.between),
513
- dateField: str(values.dateField),
514
- });
515
- return;
516
- }
517
- if (verb === "complete" && normalizedResource === "user-task") {
518
- if (!args[0]) {
519
- logger.error("User task key required. Usage: c8 complete ut <key>");
520
- process.exit(1);
521
- }
522
- await completeUserTask(args[0], {
523
- profile: str(values.profile),
524
- variables: str(values.variables),
525
- });
526
- return;
527
- }
528
- // Handle incident commands
529
- if (verb === "list" &&
530
- (normalizedResource === "incident" || normalizedResource === "incidents")) {
531
- await listIncidents({
532
- profile: str(values.profile),
533
- state: str(values.state),
534
- processInstanceKey: str(values.processInstanceKey),
535
- sortBy: str(values.sortBy),
536
- sortOrder,
537
- limit,
538
- between: str(values.between),
539
- });
540
- return;
541
- }
542
- if (verb === "get" && normalizedResource === "incident") {
543
- if (!args[0]) {
544
- logger.error("Incident key required. Usage: c8 get inc <key>");
545
- process.exit(1);
546
- }
547
- await getIncident(args[0], {
548
- profile: str(values.profile),
549
- });
550
- return;
551
- }
552
- if (verb === "resolve" && normalizedResource === "incident") {
553
- if (!args[0]) {
554
- logger.error("Incident key required. Usage: c8 resolve inc <key>");
555
- process.exit(1);
556
- }
557
- await resolveIncident(args[0], {
558
- profile: str(values.profile),
559
- });
560
- return;
561
- }
562
- // Handle job commands
563
- if (verb === "list" && normalizedResource === "jobs") {
564
- await listJobs({
565
- profile: str(values.profile),
566
- state: str(values.state),
567
- type: str(values.type),
568
- sortBy: str(values.sortBy),
569
- sortOrder,
570
- limit,
571
- between: str(values.between),
572
- dateField: str(values.dateField),
573
- });
574
- return;
575
- }
576
- if (verb === "activate" && normalizedResource === "jobs") {
577
- if (!args[0]) {
578
- logger.error("Job type required. Usage: c8 activate jobs <type>");
579
- process.exit(1);
580
- }
581
- await activateJobs(args[0], {
582
- profile: str(values.profile),
583
- maxJobsToActivate: values.maxJobsToActivate && typeof values.maxJobsToActivate === "string"
584
- ? parseInt(values.maxJobsToActivate, 10)
585
- : undefined,
586
- timeout: values.timeout && typeof values.timeout === "string"
587
- ? parseInt(values.timeout, 10)
588
- : undefined,
589
- worker: str(values.worker),
590
- });
591
- return;
592
- }
593
- if (verb === "complete" && normalizedResource === "job") {
594
- if (!args[0]) {
595
- logger.error("Job key required. Usage: c8 complete job <key>");
596
- process.exit(1);
597
- }
598
- await completeJob(args[0], {
599
- profile: str(values.profile),
600
- variables: str(values.variables),
601
- });
602
- return;
603
- }
604
- if (verb === "fail" && normalizedResource === "job") {
605
- if (!args[0]) {
606
- logger.error("Job key required. Usage: c8 fail job <key>");
607
- process.exit(1);
608
- }
609
- await failJob(args[0], {
610
- profile: str(values.profile),
611
- retries: values.retries && typeof values.retries === "string"
612
- ? parseInt(values.retries, 10)
613
- : undefined,
614
- errorMessage: str(values.errorMessage),
615
- });
616
- return;
617
- }
618
- // Handle message commands
619
- if (verb === "publish" && normalizedResource === "message") {
620
- if (!args[0]) {
621
- logger.error("Message name required. Usage: c8 publish msg <name>");
622
- process.exit(1);
623
- }
624
- await publishMessage(args[0], {
625
- profile: str(values.profile),
626
- correlationKey: str(values.correlationKey),
627
- variables: str(values.variables),
628
- timeToLive: values.timeToLive && typeof values.timeToLive === "string"
629
- ? parseInt(values.timeToLive, 10)
630
- : undefined,
631
- });
632
- return;
633
- }
634
- if (verb === "correlate" && normalizedResource === "message") {
635
- if (!args[0]) {
636
- logger.error("Message name required. Usage: c8 correlate msg <name>");
637
- process.exit(1);
638
- }
639
- await correlateMessage(args[0], {
640
- profile: str(values.profile),
641
- correlationKey: str(values.correlationKey),
642
- variables: str(values.variables),
643
- timeToLive: values.timeToLive && typeof values.timeToLive === "string"
644
- ? parseInt(values.timeToLive, 10)
645
- : undefined,
646
- });
647
- return;
648
- }
649
- // Handle topology command
650
- if (verb === "get" && normalizedResource === "topology") {
651
- await getTopology({
652
- profile: str(values.profile),
653
- });
654
- return;
655
- }
656
- // Handle form commands
657
- if (verb === "get" && normalizedResource === "form") {
658
- if (!args[0]) {
659
- logger.error("Key required. Usage: c8 get form <key> [--userTask|--ut] [--processDefinition|--pd]");
660
- process.exit(1);
661
- }
662
- // Check for flags and their aliases
663
- const isUserTask = process.argv.includes("--userTask") || process.argv.includes("--ut");
664
- const isProcessDefinition = process.argv.includes("--processDefinition") ||
665
- process.argv.includes("--pd");
666
- // If both flags specified, error
667
- if (isUserTask && isProcessDefinition) {
668
- logger.error("Cannot specify both --userTask|--ut and --processDefinition|--pd. Use one or the other, or omit both to search both types.");
669
- process.exit(1);
670
- }
671
- // If specific flag provided, use that API
672
- if (isUserTask) {
673
- await getUserTaskForm(args[0], {
674
- profile: str(values.profile),
675
- });
676
- }
677
- else if (isProcessDefinition) {
678
- await getStartForm(args[0], {
679
- profile: str(values.profile),
680
- });
681
- }
682
- else {
683
- // No flag provided - try both
684
- await getForm(args[0], {
685
- profile: str(values.profile),
686
- });
687
- }
688
- return;
689
- }
690
- // Handle deploy command
691
- if (verb === "deploy") {
692
- const paths = resource
693
- ? [resource, ...args]
694
- : args.length > 0
695
- ? args
696
- : ["."];
697
- await deploy(paths, {
698
- profile: str(values.profile),
699
- });
700
- return;
701
- }
702
- // Handle run command
703
- if (verb === "run") {
704
- if (!resource) {
705
- logger.error("BPMN file path required. Usage: c8 run <path>");
706
- process.exit(1);
707
- }
708
- await run(resource, {
709
- profile: str(values.profile),
710
- variables: str(values.variables),
711
- });
712
- return;
713
- }
714
- // Handle watch command
715
- if (verb === "watch" || verb === "w") {
716
- const paths = resource
717
- ? [resource, ...args]
718
- : args.length > 0
719
- ? args
720
- : ["."];
721
- await watchFiles(paths, {
722
- profile: str(values.profile),
723
- force: bool(values.force),
724
- });
725
- return;
726
- }
727
- // Handle open command
728
- if (verb === "open") {
729
- const validated = validateOpenAppOptions(resource, {
730
- profile: str(values.profile),
731
- dryRun: bool(values["dry-run"]),
732
- });
733
- await openApp(validated);
734
- return;
735
- }
736
- // Handle feedback command
737
- if (verb === "feedback") {
738
- const logger = getLogger();
739
- const url = "https://github.com/camunda/c8ctl/issues";
740
- logger.info(`Opening feedback page: ${url}`);
741
- openUrl(url);
742
- return;
743
- }
744
- // Handle mcp-proxy command
745
- if (verb === "mcp-proxy") {
746
- await mcpProxy(positionals.slice(1), {
747
- profile: str(values.profile),
748
- });
171
+ showCompletion(resource);
749
172
  return;
750
173
  }
751
- // Handle search commands
752
- if (verb === "search") {
753
- if (!resource) {
754
- showVerbResources("search");
755
- return;
756
- }
757
- const normalizedSearchResource = normalizeResource(resource);
758
- const unknownFlags = detectUnknownSearchFlags(values, normalizedSearchResource);
759
- warnUnknownSearchFlags(logger, unknownFlags, resource);
760
- if (normalizedSearchResource === "process-definition" ||
761
- normalizedSearchResource === "process-definitions") {
762
- await searchProcessDefinitions({
763
- profile: str(values.profile),
764
- processDefinitionId: resolveProcessDefinitionId(values),
765
- name: str(values.name),
766
- version: parseVersionFlag(values),
767
- key: str(values.key),
768
- iProcessDefinitionId: str(values.iid),
769
- iName: str(values.iname),
770
- sortBy: str(values.sortBy),
771
- sortOrder,
772
- _unknownFlags: unknownFlags,
773
- });
774
- return;
775
- }
776
- if (normalizedSearchResource === "process-instance" ||
777
- normalizedSearchResource === "process-instances") {
778
- await searchProcessInstances({
779
- profile: str(values.profile),
780
- processDefinitionId: resolveProcessDefinitionId(values),
781
- processDefinitionKey: str(values.processDefinitionKey),
782
- version: parseVersionFlag(values),
783
- state: str(values.state),
784
- key: str(values.key),
785
- parentProcessInstanceKey: str(values.parentProcessInstanceKey),
786
- iProcessDefinitionId: str(values.iid),
787
- sortBy: str(values.sortBy),
788
- sortOrder,
789
- _unknownFlags: unknownFlags,
790
- between: str(values.between),
791
- dateField: str(values.dateField),
792
- });
793
- return;
794
- }
795
- if (normalizedSearchResource === "user-task" ||
796
- normalizedSearchResource === "user-tasks") {
797
- await searchUserTasks({
798
- profile: str(values.profile),
799
- state: str(values.state),
800
- assignee: str(values.assignee),
801
- processInstanceKey: str(values.processInstanceKey),
802
- processDefinitionKey: str(values.processDefinitionKey),
803
- elementId: str(values.elementId),
804
- iAssignee: str(values.iassignee),
805
- sortBy: str(values.sortBy),
806
- sortOrder,
807
- _unknownFlags: unknownFlags,
808
- between: str(values.between),
809
- dateField: str(values.dateField),
810
- });
811
- return;
812
- }
813
- if (normalizedSearchResource === "incident" ||
814
- normalizedSearchResource === "incidents") {
815
- await searchIncidents({
816
- profile: str(values.profile),
817
- state: str(values.state),
818
- processInstanceKey: str(values.processInstanceKey),
819
- processDefinitionKey: str(values.processDefinitionKey),
820
- processDefinitionId: resolveProcessDefinitionId(values),
821
- errorType: str(values.errorType),
822
- errorMessage: str(values.errorMessage),
823
- iErrorMessage: str(values.ierrorMessage),
824
- iProcessDefinitionId: str(values.iid),
825
- sortBy: str(values.sortBy),
826
- sortOrder,
827
- _unknownFlags: unknownFlags,
828
- between: str(values.between),
829
- });
830
- return;
831
- }
832
- if (normalizedSearchResource === "jobs") {
833
- await searchJobs({
834
- profile: str(values.profile),
835
- state: str(values.state),
836
- type: str(values.type),
837
- processInstanceKey: str(values.processInstanceKey),
838
- processDefinitionKey: str(values.processDefinitionKey),
839
- iType: str(values.itype),
840
- sortBy: str(values.sortBy),
841
- sortOrder,
842
- _unknownFlags: unknownFlags,
843
- between: str(values.between),
844
- dateField: str(values.dateField),
845
- });
846
- return;
847
- }
848
- if (normalizedSearchResource === "variable" ||
849
- normalizedSearchResource === "variables") {
850
- await searchVariables({
851
- profile: str(values.profile),
852
- name: str(values.name),
853
- value: str(values.value),
854
- processInstanceKey: str(values.processInstanceKey),
855
- scopeKey: str(values.scopeKey),
856
- fullValue: bool(values.fullValue),
857
- iName: str(values.iname),
858
- iValue: str(values.ivalue),
859
- sortBy: str(values.sortBy),
860
- sortOrder,
861
- limit,
862
- _unknownFlags: unknownFlags,
863
- });
864
- return;
865
- }
866
- if (normalizedSearchResource === "user") {
867
- await searchIdentityUsers({
868
- profile: str(values.profile),
869
- username: str(values.username),
870
- name: str(values.name),
871
- email: str(values.email),
872
- sortBy: str(values.sortBy),
873
- sortOrder,
874
- limit,
875
- });
876
- return;
877
- }
878
- if (normalizedSearchResource === "role") {
879
- await searchIdentityRoles({
880
- profile: str(values.profile),
881
- roleId: str(values.roleId),
882
- name: str(values.name),
883
- sortBy: str(values.sortBy),
884
- sortOrder,
885
- limit,
886
- });
887
- return;
888
- }
889
- if (normalizedSearchResource === "group") {
890
- await searchIdentityGroups({
891
- profile: str(values.profile),
892
- groupId: str(values.groupId),
893
- name: str(values.name),
894
- sortBy: str(values.sortBy),
895
- sortOrder,
896
- limit,
897
- });
898
- return;
899
- }
900
- if (normalizedSearchResource === "tenant") {
901
- await searchIdentityTenants({
902
- profile: str(values.profile),
903
- name: str(values.name),
904
- tenantId: str(values.tenantId),
905
- sortBy: str(values.sortBy),
906
- sortOrder,
907
- limit,
908
- });
909
- return;
910
- }
911
- if (normalizedSearchResource === "authorization") {
912
- await searchIdentityAuthorizations({
913
- profile: str(values.profile),
914
- ownerId: str(values.ownerId),
915
- ownerType: str(values.ownerType),
916
- resourceType: str(values.resourceType),
917
- resourceId: str(values.resourceId),
918
- sortBy: str(values.sortBy),
919
- sortOrder,
920
- limit,
921
- });
922
- return;
923
- }
924
- if (normalizedSearchResource === "mapping-rule") {
925
- await searchIdentityMappingRules({
926
- profile: str(values.profile),
927
- mappingRuleId: str(values.mappingRuleId),
928
- name: str(values.name),
929
- claimName: str(values.claimName),
930
- claimValue: str(values.claimValue),
931
- sortBy: str(values.sortBy),
932
- sortOrder,
933
- limit,
934
- });
174
+ // Normalize resource
175
+ const normalizedResource = resource ? resolveAlias(resource) : "";
176
+ // Resource validation guard — single chokepoint for all verbs that require a resource.
177
+ // Derived from COMMAND_REGISTRY.requiresResource.
178
+ // help/completion are dispatched before this point.
179
+ // If --help is passed, show verb help instead of the resource error.
180
+ if (!resource && VERB_REQUIRES_RESOURCE.has(verb)) {
181
+ if (values.help) {
182
+ showVerbResources(verb);
935
183
  return;
936
184
  }
937
- // If resource not recognized for search, show available resources
938
- showVerbResources("search");
939
- return;
940
- }
941
- // Handle identity list commands
942
- if (verb === "list" && normalizedResource === "user") {
943
- await listUsers({
944
- profile: str(values.profile),
945
- sortBy: str(values.sortBy),
946
- sortOrder,
947
- limit,
948
- });
949
- return;
950
- }
951
- if (verb === "list" && normalizedResource === "role") {
952
- await listRoles({
953
- profile: str(values.profile),
954
- sortBy: str(values.sortBy),
955
- sortOrder,
956
- limit,
957
- });
958
- return;
959
- }
960
- if (verb === "list" && normalizedResource === "group") {
961
- await listGroups({
962
- profile: str(values.profile),
963
- sortBy: str(values.sortBy),
964
- sortOrder,
965
- limit,
966
- });
967
- return;
968
- }
969
- if (verb === "list" && normalizedResource === "tenant") {
970
- await listTenants({
971
- profile: str(values.profile),
972
- sortBy: str(values.sortBy),
973
- sortOrder,
974
- limit,
975
- });
976
- return;
977
- }
978
- if (verb === "list" && normalizedResource === "authorization") {
979
- await listAuthorizations({
980
- profile: str(values.profile),
981
- sortBy: str(values.sortBy),
982
- sortOrder,
983
- limit,
984
- });
985
- return;
986
- }
987
- if (verb === "list" && normalizedResource === "mapping-rule") {
988
- await listMappingRules({
989
- profile: str(values.profile),
990
- sortBy: str(values.sortBy),
991
- sortOrder,
992
- limit,
993
- });
994
- return;
995
- }
996
- // Handle identity get commands
997
- if (verb === "get" && normalizedResource === "user") {
998
- if (!args[0]) {
999
- logger.error("Username required. Usage: c8 get user <username>");
1000
- process.exit(1);
1001
- }
1002
- await getIdentityUser(args[0], { profile: str(values.profile) });
1003
- return;
1004
- }
1005
- if (verb === "get" && normalizedResource === "role") {
1006
- if (!args[0]) {
1007
- logger.error("Role ID required. Usage: c8 get role <roleId>");
1008
- process.exit(1);
1009
- }
1010
- await getIdentityRole(args[0], { profile: str(values.profile) });
1011
- return;
1012
- }
1013
- if (verb === "get" && normalizedResource === "group") {
1014
- if (!args[0]) {
1015
- logger.error("Group ID required. Usage: c8 get group <groupId>");
1016
- process.exit(1);
1017
- }
1018
- await getIdentityGroup(args[0], { profile: str(values.profile) });
1019
- return;
1020
- }
1021
- if (verb === "get" && normalizedResource === "tenant") {
1022
- if (!args[0]) {
1023
- logger.error("Tenant ID required. Usage: c8 get tenant <tenantId>");
1024
- process.exit(1);
1025
- }
1026
- await getIdentityTenant(args[0], { profile: str(values.profile) });
1027
- return;
1028
- }
1029
- if (verb === "get" && normalizedResource === "authorization") {
1030
- if (!args[0]) {
1031
- logger.error("Authorization key required. Usage: c8 get auth <key>");
1032
- process.exit(1);
1033
- }
1034
- await getIdentityAuthorization(args[0], { profile: str(values.profile) });
1035
- return;
1036
- }
1037
- if (verb === "get" && normalizedResource === "mapping-rule") {
1038
- if (!args[0]) {
1039
- logger.error("Mapping rule ID required. Usage: c8 get mapping-rule <id>");
1040
- process.exit(1);
1041
- }
1042
- await getIdentityMappingRule(args[0], { profile: str(values.profile) });
1043
- return;
1044
- }
1045
- // Handle identity create commands
1046
- if (verb === "create" && normalizedResource === "user") {
1047
- await createIdentityUser({
1048
- profile: str(values.profile),
1049
- username: str(values.username),
1050
- name: str(values.name),
1051
- email: str(values.email),
1052
- password: str(values.password),
1053
- });
1054
- return;
1055
- }
1056
- if (verb === "create" && normalizedResource === "role") {
1057
- await createIdentityRole({
1058
- profile: str(values.profile),
1059
- roleId: str(values.roleId),
1060
- name: str(values.name),
1061
- });
1062
- return;
1063
- }
1064
- if (verb === "create" && normalizedResource === "group") {
1065
- await createIdentityGroup({
1066
- profile: str(values.profile),
1067
- groupId: str(values.groupId),
1068
- name: str(values.name),
1069
- });
1070
- return;
1071
- }
1072
- if (verb === "create" && normalizedResource === "tenant") {
1073
- await createIdentityTenant({
1074
- profile: str(values.profile),
1075
- tenantId: str(values.tenantId),
1076
- name: str(values.name),
1077
- });
1078
- return;
1079
- }
1080
- if (verb === "create" && normalizedResource === "authorization") {
1081
- const validated = validateCreateAuthorizationOptions({
1082
- profile: str(values.profile),
1083
- ownerId: str(values.ownerId),
1084
- ownerType: str(values.ownerType),
1085
- resourceType: str(values.resourceType),
1086
- resourceId: str(values.resourceId),
1087
- permissions: str(values.permissions),
1088
- });
1089
- await createIdentityAuthorization(validated);
1090
- return;
1091
- }
1092
- if (verb === "create" && normalizedResource === "mapping-rule") {
1093
- await createIdentityMappingRule({
1094
- profile: str(values.profile),
1095
- mappingRuleId: str(values.mappingRuleId),
1096
- name: str(values.name),
1097
- claimName: str(values.claimName),
1098
- claimValue: str(values.claimValue),
1099
- });
1100
- return;
1101
- }
1102
- // Handle identity delete commands
1103
- if (verb === "delete" && normalizedResource === "user") {
1104
- if (!args[0]) {
1105
- logger.error("Username required. Usage: c8 delete user <username>");
1106
- process.exit(1);
1107
- }
1108
- await deleteIdentityUser(args[0], { profile: str(values.profile) });
1109
- return;
1110
- }
1111
- if (verb === "delete" && normalizedResource === "role") {
1112
- if (!args[0]) {
1113
- logger.error("Role ID required. Usage: c8 delete role <roleId>");
1114
- process.exit(1);
1115
- }
1116
- await deleteIdentityRole(args[0], { profile: str(values.profile) });
1117
- return;
1118
- }
1119
- if (verb === "delete" && normalizedResource === "group") {
1120
- if (!args[0]) {
1121
- logger.error("Group ID required. Usage: c8 delete group <groupId>");
1122
- process.exit(1);
1123
- }
1124
- await deleteIdentityGroup(args[0], { profile: str(values.profile) });
1125
- return;
1126
- }
1127
- if (verb === "delete" && normalizedResource === "tenant") {
1128
- if (!args[0]) {
1129
- logger.error("Tenant ID required. Usage: c8 delete tenant <tenantId>");
1130
- process.exit(1);
1131
- }
1132
- await deleteIdentityTenant(args[0], { profile: str(values.profile) });
1133
- return;
1134
- }
1135
- if (verb === "delete" && normalizedResource === "authorization") {
1136
- if (!args[0]) {
1137
- logger.error("Authorization key required. Usage: c8 delete auth <key>");
1138
- process.exit(1);
1139
- }
1140
- await deleteIdentityAuthorization(args[0], {
1141
- profile: str(values.profile),
1142
- });
1143
- return;
1144
- }
1145
- if (verb === "delete" && normalizedResource === "mapping-rule") {
1146
- if (!args[0]) {
1147
- logger.error("Mapping rule ID required. Usage: c8 delete mapping-rule <id>");
1148
- process.exit(1);
1149
- }
1150
- await deleteIdentityMappingRule(args[0], { profile: str(values.profile) });
1151
- return;
185
+ showVerbResources(verb);
186
+ process.exit(1);
1152
187
  }
1153
- // Handle assign/unassign commands
188
+ // Flag validation — run all registered validators before dispatch.
189
+ // Validators throw on invalid input; validateFlags catches and exits.
190
+ const commandDef = getCommandDef(verb);
191
+ if (commandDef) {
192
+ validateFlags(values, commandDef.flags);
193
+ }
194
+ // Unknown flag detection — warn about flags not recognised for this verb × resource.
195
+ // Derived from COMMAND_REGISTRY; resource-scoped for search/list.
196
+ const unknownFlags = detectUnknownFlags(verb, normalizedResource, values);
197
+ warnUnknownFlags(logger, unknownFlags, verb, resource);
198
+ // ── Assign / unassign — legacy delegation (not yet migrated to defineCommand) ──
1154
199
  if (verb === "assign") {
1155
- if (!normalizedResource) {
1156
- showVerbResources("assign");
1157
- return;
1158
- }
1159
200
  if (!args[0]) {
1160
201
  logger.error(`ID required. Usage: c8 assign ${normalizedResource} <id> --to-<target>=<targetId>`);
1161
202
  process.exit(1);
@@ -1166,10 +207,6 @@ async function main() {
1166
207
  return;
1167
208
  }
1168
209
  if (verb === "unassign") {
1169
- if (!normalizedResource) {
1170
- showVerbResources("unassign");
1171
- return;
1172
- }
1173
210
  if (!args[0]) {
1174
211
  logger.error(`ID required. Usage: c8 unassign ${normalizedResource} <id> --from-<target>=<targetId>`);
1175
212
  process.exit(1);
@@ -1179,17 +216,66 @@ async function main() {
1179
216
  });
1180
217
  return;
1181
218
  }
1182
- // Try to execute plugin command (before verb-only check)
1183
- if (await executePluginCommand(verb, resource ? [resource, ...args] : args)) {
219
+ // ── Registry-driven dispatch ───────────────────────────────────────────
220
+ // For verbs with enumerated resources (e.g. `list process-instance`),
221
+ // the dispatch key includes the normalised resource.
222
+ // For verbs without enumerated resources (e.g. `deploy`, `run`, `watch`),
223
+ // the resource slot holds the first positional argument (a file path, etc.)
224
+ // and the dispatch key uses an empty resource suffix.
225
+ const hasEnumeratedResources = commandDef !== undefined &&
226
+ commandDef.resources !== undefined &&
227
+ commandDef.resources.length > 0;
228
+ const useResourceKey = VERB_REQUIRES_RESOURCE.has(verb) && hasEnumeratedResources;
229
+ const dispatchKey = useResourceKey
230
+ ? `${verb}:${normalizedResource}`
231
+ : `${verb}:`;
232
+ // For verbs with enumerated resources, fall back to "verb:" when the
233
+ // specific resource key isn't found (lets the handler validate/reject).
234
+ const handler = COMMAND_DISPATCH.get(dispatchKey) ??
235
+ (useResourceKey ? COMMAND_DISPATCH.get(`${verb}:`) : undefined);
236
+ if (handler) {
237
+ const profile = str(values.profile);
238
+ // Lazy config access: createClient() and resolveTenantId() are deferred
239
+ // until first access, so commands that never touch ctx.client or
240
+ // ctx.tenantId (e.g. session/profile management) skip config resolution.
241
+ let _client;
242
+ let _tenantId;
243
+ let _tenantResolved = false;
244
+ const ctx = {
245
+ get client() {
246
+ if (!_client)
247
+ _client = createClient(profile);
248
+ return _client;
249
+ },
250
+ logger,
251
+ get tenantId() {
252
+ if (!_tenantResolved) {
253
+ _tenantId = resolveTenantId(profile);
254
+ _tenantResolved = true;
255
+ }
256
+ return _tenantId;
257
+ },
258
+ resource: useResourceKey ? normalizedResource : resource || "",
259
+ positionals: args,
260
+ sortOrder,
261
+ sortBy: str(values.sortBy),
262
+ limit,
263
+ all: bool(values.all),
264
+ between: str(values.between),
265
+ dateField: str(values.dateField),
266
+ version: parseVersionFlag(values),
267
+ dryRun: c8ctl.dryRun,
268
+ profile,
269
+ };
270
+ await handler.execute(ctx, values, args);
1184
271
  return;
1185
272
  }
1186
- // Handle verb-only invocations (show available resources)
1187
- if (!resource) {
1188
- showVerbResources(verb);
273
+ // Try to execute plugin command (before unknown-command error)
274
+ if (await executePluginCommand(verb, resource ? [resource, ...args] : args)) {
1189
275
  return;
1190
276
  }
1191
277
  // Unknown command
1192
- logger.error(`Unknown command: ${verb} ${resource}`);
278
+ logger.error(`Unknown command: ${verb}${resource ? ` ${resource}` : ""}`);
1193
279
  logger.info('Run "c8 help" for usage information');
1194
280
  process.exit(1);
1195
281
  }
@@ -1197,7 +283,9 @@ async function main() {
1197
283
  // Use realpathSync to resolve symlinks (e.g. when installed globally via npm link)
1198
284
  try {
1199
285
  if (realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
1200
- main().catch((error) => {
286
+ main()
287
+ .then(() => printUpdateNotification())
288
+ .catch((error) => {
1201
289
  if (c8ctl.verbose) {
1202
290
  throw error;
1203
291
  }