@bpmnkit/cli 0.0.17 → 0.0.18

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.
@@ -1,10 +1,11 @@
1
- import { readFile } from "node:fs/promises";
2
- import { Bpmn, optimize } from "@bpmnkit/core";
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { Bpmn, compactify, optimize } from "@bpmnkit/core";
3
3
  const SEVERITY_SYMBOL = {
4
4
  error: "✖",
5
5
  warning: "⚠",
6
6
  info: "ℹ",
7
7
  };
8
+ const DEFAULT_SERVER = "http://localhost:3033";
8
9
  const lintCmd = {
9
10
  name: "lint",
10
11
  description: "Lint a BPMN file — run all static analysis and pattern checks",
@@ -20,6 +21,11 @@ const lintCmd = {
20
21
  description: "Output format: text (default) or json",
21
22
  type: "string",
22
23
  },
24
+ {
25
+ name: "fix",
26
+ description: "Auto-apply all fixable findings and write the result back to the file",
27
+ type: "boolean",
28
+ },
23
29
  ],
24
30
  async run(ctx) {
25
31
  const filePath = ctx.positional[0];
@@ -33,6 +39,20 @@ const lintCmd = {
33
39
  : undefined;
34
40
  const report = optimize(defs, categories !== undefined ? { categories } : undefined);
35
41
  const { findings } = report;
42
+ // --fix: apply all auto-fixable findings and write back
43
+ if (ctx.flags.fix) {
44
+ const fixable = findings.filter((f) => f.applyFix);
45
+ for (const f of fixable)
46
+ f.applyFix?.(defs);
47
+ if (fixable.length === 0) {
48
+ ctx.output.ok("No auto-fixable issues found.");
49
+ }
50
+ else {
51
+ await writeFile(filePath, Bpmn.export(defs), "utf-8");
52
+ ctx.output.ok(`Fixed ${fixable.length} issue${fixable.length === 1 ? "" : "s"} and wrote ${filePath}`);
53
+ }
54
+ return;
55
+ }
36
56
  const formatFlag = ctx.flags.format;
37
57
  if (formatFlag === "json") {
38
58
  ctx.output.print(findings);
@@ -57,9 +77,143 @@ const lintCmd = {
57
77
  }
58
78
  },
59
79
  };
80
+ function describeOp(op) {
81
+ switch (op.op) {
82
+ case "rename":
83
+ return `Rename to "${op.name}"`;
84
+ case "update":
85
+ return "Update element properties";
86
+ case "delete":
87
+ return "Remove element";
88
+ case "insert":
89
+ return `Add ${op.element.type}${op.element.name ? ` "${op.element.name}"` : ""}`;
90
+ case "add_flow":
91
+ return `Add flow: ${op.from} → ${op.to}`;
92
+ case "delete_flow":
93
+ return "Remove flow";
94
+ case "redirect_flow":
95
+ return "Redirect flow";
96
+ }
97
+ }
98
+ const improveCmd = {
99
+ name: "improve",
100
+ description: "AI-assisted BPMN improvement — analyzes and suggests fixes using an AI model",
101
+ args: [{ name: "file", description: "Path to the .bpmn file", required: true }],
102
+ flags: [
103
+ {
104
+ name: "auto",
105
+ description: "Apply the AI-suggested improvements and write the result back to the file",
106
+ type: "boolean",
107
+ },
108
+ {
109
+ name: "server",
110
+ description: `AI proxy server URL (default: ${DEFAULT_SERVER})`,
111
+ type: "string",
112
+ },
113
+ ],
114
+ async run(ctx) {
115
+ const filePath = ctx.positional[0];
116
+ if (!filePath)
117
+ throw new Error("Missing required argument: <file>");
118
+ const serverUrl = typeof ctx.flags.server === "string" && ctx.flags.server.length > 0
119
+ ? ctx.flags.server
120
+ : DEFAULT_SERVER;
121
+ const xml = await readFile(filePath, "utf-8");
122
+ const defs = Bpmn.parse(xml);
123
+ const compactDiagram = compactify(defs);
124
+ let res;
125
+ try {
126
+ res = await fetch(`${serverUrl}/improve`, {
127
+ method: "POST",
128
+ headers: { "Content-Type": "application/json" },
129
+ body: JSON.stringify({ context: compactDiagram, instruction: null, backend: null }),
130
+ });
131
+ }
132
+ catch (err) {
133
+ throw new Error(`Cannot reach AI server at ${serverUrl}. Start it with: pnpx @bpmnkit/ai-server\n${String(err)}`);
134
+ }
135
+ if (!res.ok || !res.body) {
136
+ throw new Error(`AI server returned ${res.status}`);
137
+ }
138
+ let capturedOps = [];
139
+ let capturedAutoFixCount = 0;
140
+ let capturedXml;
141
+ let hadOutput = false;
142
+ const reader = res.body.getReader();
143
+ const decoder = new TextDecoder();
144
+ let buf = "";
145
+ try {
146
+ while (true) {
147
+ const { done, value } = await reader.read();
148
+ if (done)
149
+ break;
150
+ buf += decoder.decode(value, { stream: true });
151
+ const parts = buf.split("\n\n");
152
+ buf = parts.pop() ?? "";
153
+ for (const part of parts) {
154
+ const line = part.startsWith("data: ") ? part.slice(6) : part;
155
+ const trimmed = line.trim();
156
+ if (!trimmed)
157
+ continue;
158
+ try {
159
+ const event = JSON.parse(trimmed);
160
+ if (event.type === "token" && event.text) {
161
+ process.stdout.write(event.text);
162
+ hadOutput = true;
163
+ }
164
+ if (event.type === "ops") {
165
+ capturedOps = event.ops ?? [];
166
+ capturedAutoFixCount = event.autoFixCount ?? 0;
167
+ }
168
+ if (event.type === "xml" && event.xml)
169
+ capturedXml = event.xml;
170
+ if (event.type === "error")
171
+ throw new Error(event.message ?? "AI error");
172
+ }
173
+ catch (e) {
174
+ if (e instanceof SyntaxError)
175
+ continue;
176
+ throw e;
177
+ }
178
+ }
179
+ }
180
+ }
181
+ finally {
182
+ reader.releaseLock();
183
+ }
184
+ if (hadOutput)
185
+ process.stdout.write("\n\n");
186
+ // Print diff summary
187
+ if (capturedAutoFixCount > 0) {
188
+ ctx.output.info(`✓ ${capturedAutoFixCount} issue${capturedAutoFixCount === 1 ? "" : "s"} auto-fixed`);
189
+ }
190
+ if (capturedOps.length > 0) {
191
+ ctx.output.info("AI-suggested changes:");
192
+ for (const op of capturedOps) {
193
+ const prefix = op.op === "insert" || op.op === "add_flow"
194
+ ? "+"
195
+ : op.op === "delete" || op.op === "delete_flow"
196
+ ? "−"
197
+ : "~";
198
+ ctx.output.info(` ${prefix} ${describeOp(op)}`);
199
+ }
200
+ }
201
+ if (capturedXml === undefined) {
202
+ ctx.output.info("No changes suggested.");
203
+ return;
204
+ }
205
+ if (ctx.flags.auto) {
206
+ await writeFile(filePath, capturedXml, "utf-8");
207
+ ctx.output.ok(`Improvements applied and written to ${filePath}`);
208
+ }
209
+ else {
210
+ ctx.output.info("\nRun with --auto to apply these changes.");
211
+ }
212
+ },
213
+ };
60
214
  export const lintGroup = {
61
215
  name: "lint",
62
216
  description: "Lint BPMN files using the static analyzer",
63
- commands: [lintCmd],
217
+ commands: [lintCmd, improveCmd],
64
218
  };
65
219
  //# sourceMappingURL=lint.js.map
@@ -1,12 +1,27 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { deleteProfile, getActiveName, getActiveProfile, getConfigFilePath, getProfile, listProfiles, saveProfile, useProfile, } from "@bpmnkit/profiles";
2
+ import { deleteProfile, getActiveName, getActiveProfile, getConfigFilePath, getProfile, listProfiles, saveProfile, setProfileMeta, useProfile, } from "@bpmnkit/profiles";
3
3
  const API_TYPE_FLAG = {
4
4
  name: "api-type",
5
5
  description: "API type: c8 (default) or admin",
6
6
  type: "string",
7
7
  default: "c8",
8
8
  placeholder: "TYPE",
9
+ enum: ["c8", "admin"],
9
10
  };
11
+ const META_FLAGS = [
12
+ {
13
+ name: "description",
14
+ description: "Short description of the profile",
15
+ type: "string",
16
+ placeholder: "TEXT",
17
+ },
18
+ {
19
+ name: "tags",
20
+ description: "Comma-separated tags (predefined: dev, stage, prod)",
21
+ type: "string",
22
+ placeholder: "TAGS",
23
+ },
24
+ ];
10
25
  const AUTH_FLAGS = [
11
26
  {
12
27
  name: "base-url",
@@ -21,6 +36,7 @@ const AUTH_FLAGS = [
21
36
  type: "string",
22
37
  required: true,
23
38
  placeholder: "TYPE",
39
+ enum: ["bearer", "oauth2", "basic", "none"],
24
40
  },
25
41
  {
26
42
  name: "token",
@@ -165,7 +181,7 @@ export const profileGroup = {
165
181
  name: "create",
166
182
  description: "Create or update a profile",
167
183
  args: [{ name: "name", description: "Profile name", required: true }],
168
- flags: [API_TYPE_FLAG, ...AUTH_FLAGS],
184
+ flags: [API_TYPE_FLAG, ...AUTH_FLAGS, ...META_FLAGS],
169
185
  examples: [
170
186
  {
171
187
  description: "Bearer token profile",
@@ -191,7 +207,15 @@ export const profileGroup = {
191
207
  const rawApiType = ctx.flags["api-type"] ?? "c8";
192
208
  const apiType = rawApiType === "admin" ? "admin" : "c8";
193
209
  const config = { baseUrl, auth };
194
- saveProfile(name, config, apiType);
210
+ const description = ctx.flags.description;
211
+ const tagsRaw = ctx.flags.tags;
212
+ const tags = tagsRaw
213
+ ? tagsRaw
214
+ .split(",")
215
+ .map((t) => t.trim())
216
+ .filter(Boolean)
217
+ : undefined;
218
+ saveProfile(name, config, apiType, { description, tags });
195
219
  ctx.output.ok(`Profile "${name}" saved [${apiType}] (${getConfigFilePath()})`);
196
220
  },
197
221
  },
@@ -214,6 +238,7 @@ export const profileGroup = {
214
238
  apiType: p.apiType,
215
239
  baseUrl: p.config.baseUrl ?? "(from env/file)",
216
240
  authType: p.config.auth?.type ?? "—",
241
+ tags: p.tags && p.tags.length > 0 ? p.tags.join(", ") : "—",
217
242
  })),
218
243
  }, [
219
244
  { key: "active", header: " " },
@@ -221,6 +246,7 @@ export const profileGroup = {
221
246
  { key: "apiType", header: "API" },
222
247
  { key: "baseUrl", header: "BASE URL", maxWidth: 50 },
223
248
  { key: "authType", header: "AUTH TYPE" },
249
+ { key: "tags", header: "TAGS" },
224
250
  ]);
225
251
  },
226
252
  },
@@ -260,6 +286,10 @@ export const profileGroup = {
260
286
  const active = getActiveName();
261
287
  const isActive = profile.name === active;
262
288
  ctx.output.info(`Profile: ${profile.name}${isActive ? " (active)" : ""}`);
289
+ if (profile.description)
290
+ ctx.output.info(`Description: ${profile.description}`);
291
+ if (profile.tags && profile.tags.length > 0)
292
+ ctx.output.info(`Tags: ${profile.tags.join(", ")}`);
263
293
  ctx.output.printItem(profile.config);
264
294
  },
265
295
  },
@@ -304,6 +334,35 @@ export const profileGroup = {
304
334
  ctx.output.info(`baseUrl: ${config.baseUrl ?? ""}`);
305
335
  },
306
336
  },
337
+ {
338
+ name: "meta",
339
+ description: "Update description or tags on an existing profile",
340
+ args: [{ name: "name", description: "Profile name", required: true }],
341
+ flags: META_FLAGS,
342
+ examples: [
343
+ {
344
+ description: "Set description and tags",
345
+ command: 'casen profile meta prod --description "Production cluster" --tags prod',
346
+ },
347
+ ],
348
+ async run(ctx) {
349
+ const name = ctx.positional[0];
350
+ if (!name)
351
+ throw new Error("Missing required argument: <name>");
352
+ const description = ctx.flags.description;
353
+ const tagsRaw = ctx.flags.tags;
354
+ const tags = tagsRaw
355
+ ? tagsRaw
356
+ .split(",")
357
+ .map((t) => t.trim())
358
+ .filter(Boolean)
359
+ : undefined;
360
+ if (!setProfileMeta(name, { description, tags })) {
361
+ throw new Error(`Profile "${name}" not found.`);
362
+ }
363
+ ctx.output.ok(`Profile "${name}" updated`);
364
+ },
365
+ },
307
366
  {
308
367
  name: "delete",
309
368
  aliases: ["rm"],
@@ -94,7 +94,6 @@ export const workerCmd = {
94
94
  worker: "casen-worker",
95
95
  timeout,
96
96
  maxJobsToActivate: maxJobs,
97
- requestTimeout: 20000, // 20 s long poll
98
97
  }));
99
98
  }
100
99
  catch (err) {
@@ -109,24 +108,27 @@ export const workerCmd = {
109
108
  for (const job of jobs) {
110
109
  if (!running)
111
110
  break;
112
- ctx.output.info(`Activated job ${job.jobKey} process=${job.processDefinitionId} element=${job.elementId} instance=${job.processInstanceKey}`);
111
+ // Some Zeebe-compatible engines return `key` instead of `jobKey`
112
+ const jobKey = job.jobKey ?? job.key;
113
+ if (!jobKey) {
114
+ ctx.output.info("Activated job has no key — skipping");
115
+ continue;
116
+ }
117
+ ctx.output.info(`Activated job ${jobKey} process=${job.processDefinitionId} element=${job.elementId} instance=${job.processInstanceKey}`);
113
118
  if (Object.keys(job.variables ?? {}).length > 0) {
114
119
  ctx.output.info(` Input variables: ${JSON.stringify(job.variables)}`);
115
120
  }
116
121
  try {
117
- await client.job.completeJob(job.jobKey, { variables });
122
+ await client.job.completeJob(jobKey, { variables });
118
123
  completed++;
119
- ctx.output.ok(`Completed job ${job.jobKey} (total: ${completed})`);
124
+ ctx.output.ok(`Completed job ${jobKey} (total: ${completed})`);
120
125
  }
121
126
  catch (err) {
122
- ctx.output.info(`Failed to complete job ${job.jobKey}: ${err instanceof Error ? err.message : String(err)}`);
127
+ ctx.output.info(`Failed to complete job ${jobKey}: ${err instanceof Error ? err.message : String(err)}`);
123
128
  }
124
129
  }
125
- // When no jobs were returned, the long-poll already waited; loop immediately.
126
- // When jobs were found, yield the event loop briefly before the next poll.
127
- if (jobs.length > 0) {
128
- await delay(100);
129
- }
130
+ // Sleep between polls: brief yield after processing jobs, longer pause when idle
131
+ await delay(jobs.length > 0 ? 100 : 2000);
130
132
  }
131
133
  ctx.output.info(`\nWorker stopped. Completed ${completed} job(s).`);
132
134
  },
@@ -53,6 +53,7 @@ export const authorizationGroup = {
53
53
  makeCreateCmd({
54
54
  description: "Create authorization",
55
55
  create: (client, body) => client.authorization.createAuthorization(body),
56
+ bodyFields: [{ name: "ownerId", type: "string", description: "The ID of the owner of the permissions.", required: true }, { name: "ownerType", type: "string", description: "The type of the owner of permissions.", required: true, enum: ["USER", "CLIENT", "ROLE", "GROUP", "MAPPING_RULE", "UNSPECIFIED"] }, { name: "resourceId", type: "string", description: "The ID of the resource to add permissions to.", required: true }, { name: "resourceType", type: "string", description: "The type of resource to add permissions to.", required: true }, { name: "permissionTypes", type: "array", description: "The permission types to add.", required: true }, { name: "resourcePropertyName", type: "string", description: "The name of the resource property on which this authorization is based.", required: true }],
56
57
  }),
57
58
  makeListCmd({
58
59
  description: "Search authorizations",
@@ -77,6 +78,7 @@ export const authorizationGroup = {
77
78
  description: "Update authorization",
78
79
  argName: "authorizationKey",
79
80
  update: (client, key, body) => client.authorization.updateAuthorization(key, body),
81
+ bodyFields: [{ name: "ownerId", type: "string", description: "The ID of the owner of the permissions.", required: true }, { name: "ownerType", type: "string", description: "The type of the owner of permissions.", required: true, enum: ["USER", "CLIENT", "ROLE", "GROUP", "MAPPING_RULE", "UNSPECIFIED"] }, { name: "resourceId", type: "string", description: "The ID of the resource to add permissions to.", required: true }, { name: "resourceType", type: "string", description: "The type of resource to add permissions to.", required: true }, { name: "permissionTypes", type: "array", description: "The permission types to add.", required: true }, { name: "resourcePropertyName", type: "string", description: "The name of the resource property on which this authorization is based.", required: true }],
80
82
  }),
81
83
  makeDeleteCmd({
82
84
  description: "Delete authorization",
@@ -354,6 +356,7 @@ export const decisionDefinitionGroup = {
354
356
  name: "evaluate",
355
357
  description: "Evaluate decision",
356
358
  create: (client, body) => client.decisionDefinition.evaluateDecision(body),
359
+ bodyFields: [{ name: "decisionDefinitionId", type: "string", description: " The ID of the decision to be evaluated.\n When using the decision ID, the latest\n deployed version of the decision is used.", required: true }, { name: "variables", type: "object", description: "The message variables as JSON document." }, { name: "tenantId", type: "string", description: "The tenant ID of the decision." }, { name: "decisionDefinitionKey", type: "string", description: "System-generated key for a decision definition.", required: true }],
357
360
  }),
358
361
  makeListCmd({
359
362
  description: "Search decision definitions",
@@ -1158,6 +1161,7 @@ export const processInstanceGroup = {
1158
1161
  makeCreateCmd({
1159
1162
  description: "Create process instance",
1160
1163
  create: (client, body) => client.processInstance.createProcessInstance(body),
1164
+ bodyFields: [{ name: "processDefinitionKey", type: "string", description: " The unique key identifying the process definition, for example, returned for a process in the\n deploy resources endpoint.", required: true }, { name: "processDefinitionVersion", type: "string", description: " As the version is already identified by the `processDefinitionKey`, the value of this field is ignored.\n It's here for backwards-compatibility only as previous releases accepted it in request bodies." }, { name: "variables", type: "object", description: " Set of variables as JSON object to instantiate in the root variable scope of the process\n instance. Can include nested complex objects." }, { name: "startInstructions", type: "array", description: " List of start instructions. By default, the process instance will start at\n the start event. If provided, the process instance will apply start instructions\n after it has been created." }, { name: "runtimeInstructions", type: "array", description: " Runtime instructions (alpha). List of instructions that affect the runtime behavior of\n the process instance. Refer to specific instruction types for more details.\n\n This parameter is an alpha feature and may be subject to change\n in future releases." }, { name: "tenantId", type: "string", description: " The tenant id of the process definition.\n If multi-tenancy is enabled, provide the tenant id of the process definition to start a\n process instance of. If multi-tenancy is disabled, don't provide this parameter." }, { name: "operationReference", type: "string", description: " A reference key chosen by the user that will be part of all records resulting from this operation.\n Must be > 0 if provided." }, { name: "awaitCompletion", type: "boolean", description: " Wait for the process instance to complete. If the process instance does not complete\n within the request timeout limit, a 504 response status will be returned. The process\n instance will continue to run in the background regardless of the timeout. Disabled by\n default." }, { name: "requestTimeout", type: "string", description: " Timeout (in ms) the request waits for the process to complete. By default or\n when set to 0, the generic request timeout configured in the cluster is applied." }, { name: "fetchVariables", type: "array", description: " List of variables by name to be included in the response when awaitCompletion is set to true.\n If empty, all visible variables in the root scope will be returned." }, { name: "tags", type: "array", description: "List of tags. Tags need to start with a letter; then alphanumerics, `_`, `-`, `:`, or `.`; length ≤ 100." }, { name: "businessId", type: "string", description: " An optional, user-defined string identifier that identifies the process instance\n within the scope of a process definition (scoped by tenant). If provided and uniqueness\n enforcement is enabled, the engine will reject creation if another root process instance\n with the same business id is already active for the same process definition.\n Note that any active child process instances with the same business id are not taken into account." }, { name: "processDefinitionId", type: "string", description: " The BPMN process id of the process definition to start an instance of.", required: true }],
1161
1165
  }),
1162
1166
  makeCreateCmd({
1163
1167
  name: "cancel-batch-operation",
package/dist/output.js CHANGED
@@ -259,4 +259,29 @@ export function printRawResponse(raw, noColor) {
259
259
  process.stdout.write(`${raw.body}\n`);
260
260
  }
261
261
  }
262
+ /** Print HTTP error details (request + response) to stderr. */
263
+ export function printHttpErrorDetails(raw, noColor) {
264
+ const colors = shouldUseColor(noColor);
265
+ process.stderr.write(`\n${dim("→", colors)} ${bold(`${raw.method} ${raw.url}`, colors)}\n`);
266
+ for (const [k, v] of Object.entries(raw.requestHeaders)) {
267
+ process.stderr.write(` ${dim(k, colors)}: ${v}\n`);
268
+ }
269
+ if (raw.requestBody) {
270
+ process.stderr.write(`\n${dim(raw.requestBody, colors)}\n`);
271
+ }
272
+ process.stderr.write(`\n${red(`HTTP ${raw.status}`, colors)}\n`);
273
+ for (const [k, v] of Object.entries(raw.headers)) {
274
+ process.stderr.write(` ${dim(k, colors)}: ${v}\n`);
275
+ }
276
+ if (raw.body) {
277
+ process.stderr.write("\n");
278
+ try {
279
+ const parsed = JSON.parse(raw.body);
280
+ process.stderr.write(`${JSON.stringify(parsed, null, 2)}\n`);
281
+ }
282
+ catch {
283
+ process.stderr.write(`${raw.body}\n`);
284
+ }
285
+ }
286
+ }
262
287
  //# sourceMappingURL=output.js.map
package/dist/run.js CHANGED
@@ -3,7 +3,7 @@ import { parseArgs } from "./args.js";
3
3
  import { apiGroups, commandGroups, pinnedGroups, pluginGroup, profileGroup, } from "./commands/index.js";
4
4
  import { getRuntimeCompletions } from "./completion.js";
5
5
  import { printCommandHelp, printGlobalHelp, printGroupHelp, printVersion } from "./help.js";
6
- import { createNullWriter, createOutputWriter, printRawResponse } from "./output.js";
6
+ import { createNullWriter, createOutputWriter, printHttpErrorDetails, printRawResponse, } from "./output.js";
7
7
  import { loadPlugins } from "./plugin-loader.js";
8
8
  import { runProfileManager } from "./profile-tui.js";
9
9
  import { runSettingsManager } from "./settings-tui.js";
@@ -240,6 +240,9 @@ export async function run(argv) {
240
240
  if (isRaw && capture) {
241
241
  printRawResponse(capture, noColor);
242
242
  }
243
+ else if (capture) {
244
+ printHttpErrorDetails(capture, noColor);
245
+ }
243
246
  const msg = err instanceof Error ? err.message : String(err);
244
247
  appendAuditEntry(effectiveProfile, {
245
248
  group: group.name,
package/dist/tui.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { renderBpmnAscii } from "@bpmnkit/ascii";
3
- import { appendAuditEntry, getAuditLog, getSettings, saveSettings } from "@bpmnkit/profiles";
3
+ import { appendAuditEntry, deleteProfile, getActiveName, getAuditLog, getProfile, getSettings, listProfiles, saveSettings, useProfile, } from "@bpmnkit/profiles";
4
4
  import { runAskQuery } from "./commands/ask.js";
5
5
  import { pluginGroup, searchNpmRegistry } from "./commands/plugin.js";
6
6
  import { profileGroup } from "./commands/profile.js";
@@ -193,8 +193,9 @@ function entriesToJson(entries) {
193
193
  }
194
194
  /**
195
195
  * Build initial entries for the JSON editor.
196
- * If fieldSpecs are provided, pre-populate with all known fields (in spec order),
197
- * merging in any existing values. Extra keys from existing JSON are appended at the end.
196
+ * If fieldSpecs are provided, pre-populate only required fields (in spec order),
197
+ * merging in any existing values. Optional fields are available via the add-row picker.
198
+ * Extra keys from existing JSON that are not in the spec are appended at the end.
198
199
  */
199
200
  function buildInitialEntries(existingJson, fieldSpecs) {
200
201
  const existing = {};
@@ -213,7 +214,10 @@ function buildInitialEntries(existingJson, fieldSpecs) {
213
214
  return parseJsonToEntries(existingJson);
214
215
  const seenKeys = new Set();
215
216
  const entries = [];
217
+ // Only pre-populate required fields; optional ones are added on demand
216
218
  for (const spec of fieldSpecs) {
219
+ if (!spec.required && !(spec.name in existing))
220
+ continue;
217
221
  seenKeys.add(spec.name);
218
222
  const val = existing[spec.name] ?? "";
219
223
  entries.push({ key: spec.name, keyCursor: spec.name.length, val, valCursor: val.length });
@@ -230,6 +234,57 @@ function buildInitialEntries(existingJson, fieldSpecs) {
230
234
  function getFieldSpec(key, fieldSpecs) {
231
235
  return fieldSpecs?.find((s) => s.name === key);
232
236
  }
237
+ /**
238
+ * Return specs not already used as keys by OTHER entries (not the one at currentIndex).
239
+ * Pass null for currentIndex when adding a brand-new entry.
240
+ */
241
+ function getAvailableFieldSpecs(currentIndex, entries, fieldSpecs) {
242
+ const usedKeys = new Set(entries
243
+ .filter((_, i) => i !== currentIndex)
244
+ .map((e) => e.key)
245
+ .filter(Boolean));
246
+ return fieldSpecs.filter((s) => !usedKeys.has(s.name));
247
+ }
248
+ /** Build profile info entries (key/value pairs with secrets redacted). */
249
+ function buildProfileInfoEntries(profileName) {
250
+ const p = getProfile(profileName);
251
+ if (!p)
252
+ return [{ key: "status", value: "profile not found" }];
253
+ const info = [
254
+ { key: "name", value: p.name },
255
+ { key: "apiType", value: p.apiType },
256
+ { key: "baseUrl", value: p.config.baseUrl ?? "(default)" },
257
+ ];
258
+ const auth = p.config.auth;
259
+ if (auth) {
260
+ info.push({ key: "auth.type", value: auth.type });
261
+ if (auth.type === "bearer") {
262
+ info.push({ key: "auth.token", value: "***" });
263
+ }
264
+ else if (auth.type === "oauth2") {
265
+ info.push({ key: "auth.clientId", value: auth.clientId });
266
+ info.push({ key: "auth.clientSecret", value: "***" });
267
+ info.push({ key: "auth.tokenUrl", value: auth.tokenUrl });
268
+ }
269
+ else if (auth.type === "basic") {
270
+ info.push({ key: "auth.username", value: auth.username });
271
+ info.push({ key: "auth.password", value: "***" });
272
+ }
273
+ }
274
+ return info;
275
+ }
276
+ /** Rebuild profile list items after use/delete. */
277
+ function rebuildProfileListItems() {
278
+ const profiles = listProfiles();
279
+ const active = getActiveName();
280
+ return profiles.map((p) => ({
281
+ active: p.name === active ? "●" : " ",
282
+ name: p.name,
283
+ apiType: p.apiType,
284
+ baseUrl: p.config.baseUrl ?? "(from env/file)",
285
+ authType: p.config.auth?.type ?? "—",
286
+ }));
287
+ }
233
288
  // ─── Table helpers ────────────────────────────────────────────────────────────
234
289
  function getCellStr(item, col) {
235
290
  let val = item;
@@ -515,6 +570,22 @@ function renderInput(state, screen) {
515
570
  lines.push("");
516
571
  if (screen.error) {
517
572
  lines.push(` ${red("error:")} ${screen.error}`);
573
+ if (screen.errorRaw) {
574
+ const raw = screen.errorRaw;
575
+ lines.push(` ${dim(`${raw.method} ${raw.url}`)}`);
576
+ lines.push(` ${red(`HTTP ${raw.status}`)}`);
577
+ if (raw.body) {
578
+ try {
579
+ const parsed = JSON.parse(raw.body);
580
+ for (const l of JSON.stringify(parsed, null, 2).split("\n")) {
581
+ lines.push(` ${dim(l)}`);
582
+ }
583
+ }
584
+ catch {
585
+ lines.push(` ${dim(raw.body)}`);
586
+ }
587
+ }
588
+ }
518
589
  }
519
590
  else if (screen.running) {
520
591
  lines.push(` ${dim("running…")}`);
@@ -707,7 +778,12 @@ function renderResults(state, screen) {
707
778
  lines.push(` ${dim(`${screen.scroll + 1}–${hi} of ${out.total}`)}`);
708
779
  }
709
780
  const followupHint = screen.cmd.relations ? ` ${cyan("f")} follow-up` : "";
710
- lines.push(` ${dim("↑↓")} navigate ${cyan("enter")} detail${followupHint} ${dim("pgup/pgdn")} page${rawToggle}${curlToggle} ${cyan("m")} main menu ${cyan("esc")} back ${cyan("q")} quit`);
781
+ const profileListHints = screen.group.name === "profile" && screen.cmd.name === "list"
782
+ ? ` ${cyan("u")} use ${cyan("s")} show ${cyan("d")} delete`
783
+ : "";
784
+ if (screen.message)
785
+ lines.push(` ${green(screen.message)}`);
786
+ lines.push(` ${dim("↑↓")} navigate ${cyan("enter")} detail${followupHint}${profileListHints} ${dim("pgup/pgdn")} page${rawToggle}${curlToggle} ${cyan("m")} main menu ${cyan("esc")} back ${cyan("q")} quit`);
711
787
  }
712
788
  else if (out.type === "item") {
713
789
  lines.push(renderHeader([screen.group.name, screen.cmd.name], cols, state.profile));
@@ -1032,12 +1108,15 @@ function renderJsonEditor(state, screen) {
1032
1108
  const editKey = isCursor && screen.col === "key" && screen.editing;
1033
1109
  const editVal = isCursor && screen.col === "val" && screen.editing;
1034
1110
  const keyStr = renderText(entry.key, entry.keyCursor, keyW - (isKnown ? 0 : 0), editKey);
1035
- // For known enum fields in nav mode, show cycling hint instead of raw value
1111
+ // For known enum fields in nav mode, show cycling hint instead of raw value.
1112
+ // For non-enum known fields, show a type placeholder when value is empty.
1036
1113
  const valDisplay = !editVal && hasEnum && entry.val
1037
1114
  ? `${entry.val} ${dim("↑↓")}`
1038
1115
  : !editVal && hasEnum && !entry.val
1039
1116
  ? dim("<pick ↑↓>")
1040
- : renderText(entry.val, entry.valCursor, valW - 4, editVal);
1117
+ : !editVal && !hasEnum && spec && !entry.val
1118
+ ? dim(`<${spec.type}>`)
1119
+ : renderText(entry.val, entry.valCursor, valW - 4, editVal);
1041
1120
  const valStr = valDisplay;
1042
1121
  const keyPart = isCursor && screen.col === "key" && !editKey
1043
1122
  ? cyan(padEnd(keyStr, keyW))
@@ -1079,7 +1158,9 @@ function renderJsonEditor(state, screen) {
1079
1158
  : undefined;
1080
1159
  const editHint = editSpec?.enum
1081
1160
  ? `${dim("↑↓")} pick value ${cyan("enter")} confirm ${cyan("esc")} cancel`
1082
- : `${dim("←→")} cursor ${cyan("tab")} switch col ${cyan("enter")} confirm ${cyan("esc")} cancel`;
1161
+ : screen.col === "key" && screen.fieldSpecs && screen.fieldSpecs.length > 0
1162
+ ? `${dim("↑↓")} pick field ${dim("←→")} cursor ${cyan("tab")} switch col ${cyan("enter")} confirm ${cyan("esc")} cancel`
1163
+ : `${dim("←→")} cursor ${cyan("tab")} switch col ${cyan("enter")} confirm ${cyan("esc")} cancel`;
1083
1164
  lines.push(` ${editHint}`);
1084
1165
  }
1085
1166
  else {
@@ -1140,7 +1221,6 @@ async function runWorkerLoop(ws, state, variables, jobTimeout, maxJobs) {
1140
1221
  worker: "casen-worker",
1141
1222
  timeout: jobTimeout,
1142
1223
  maxJobsToActivate: maxJobs,
1143
- requestTimeout: 20000,
1144
1224
  }));
1145
1225
  }
1146
1226
  catch (err) {
@@ -1154,13 +1234,19 @@ async function runWorkerLoop(ws, state, variables, jobTimeout, maxJobs) {
1154
1234
  for (const job of jobs) {
1155
1235
  if (!running)
1156
1236
  break;
1237
+ // Some Zeebe-compatible engines return `key` instead of `jobKey`
1238
+ const jobKey = job.jobKey ?? job.key;
1239
+ if (!jobKey) {
1240
+ addWorkerLog(ws, "err", "Activated job has no key — skipping");
1241
+ continue;
1242
+ }
1157
1243
  ws.stats.activated++;
1158
- addWorkerLog(ws, "info", `Activated ${job.jobKey} process=${job.processDefinitionId} element=${job.elementId}`);
1244
+ addWorkerLog(ws, "info", `Activated ${jobKey} process=${job.processDefinitionId} element=${job.elementId}`);
1159
1245
  let jobResult = { outcome: "complete", variables };
1160
1246
  const processJob = ws.cmd._worker?.processJob;
1161
1247
  if (processJob) {
1162
1248
  try {
1163
- jobResult = await processJob(job);
1249
+ jobResult = await processJob({ ...job, jobKey });
1164
1250
  }
1165
1251
  catch (err) {
1166
1252
  addWorkerLog(ws, "err", `Handler error: ${err instanceof Error ? err.message : String(err)} — using defaults`);
@@ -1168,36 +1254,36 @@ async function runWorkerLoop(ws, state, variables, jobTimeout, maxJobs) {
1168
1254
  }
1169
1255
  try {
1170
1256
  if (jobResult.outcome === "complete") {
1171
- await client.job.completeJob(job.jobKey, { variables: jobResult.variables });
1257
+ await client.job.completeJob(jobKey, { variables: jobResult.variables });
1172
1258
  ws.stats.completed++;
1173
- addWorkerLog(ws, "ok", `Completed ${job.jobKey}`);
1259
+ addWorkerLog(ws, "ok", `Completed ${jobKey}`);
1174
1260
  }
1175
1261
  else if (jobResult.outcome === "fail") {
1176
- await client.job.failJob(job.jobKey, {
1262
+ await client.job.failJob(jobKey, {
1177
1263
  errorMessage: jobResult.errorMessage,
1178
1264
  retries: jobResult.retries,
1179
1265
  retryBackOff: jobResult.retryBackOff,
1180
1266
  });
1181
1267
  ws.stats.failed++;
1182
- addWorkerLog(ws, "err", `Failed ${job.jobKey}: ${jobResult.errorMessage}`);
1268
+ addWorkerLog(ws, "err", `Failed ${jobKey}: ${jobResult.errorMessage}`);
1183
1269
  }
1184
1270
  else {
1185
- await client.job.throwJobError(job.jobKey, {
1271
+ await client.job.throwJobError(jobKey, {
1186
1272
  errorCode: jobResult.errorCode,
1187
1273
  errorMessage: jobResult.errorMessage,
1188
1274
  variables: jobResult.variables,
1189
1275
  });
1190
1276
  ws.stats.failed++;
1191
- addWorkerLog(ws, "err", `Error ${job.jobKey} [${jobResult.errorCode}]: ${jobResult.errorMessage ?? ""}`);
1277
+ addWorkerLog(ws, "err", `Error ${jobKey} [${jobResult.errorCode}]: ${jobResult.errorMessage ?? ""}`);
1192
1278
  }
1193
1279
  }
1194
1280
  catch (err) {
1195
1281
  ws.stats.failed++;
1196
- addWorkerLog(ws, "err", `Failed to settle ${job.jobKey}: ${err instanceof Error ? err.message : String(err)}`);
1282
+ addWorkerLog(ws, "err", `Failed to settle ${jobKey}: ${err instanceof Error ? err.message : String(err)}`);
1197
1283
  }
1198
1284
  }
1199
- if (jobs.length > 0)
1200
- await new Promise((r) => setTimeout(r, 100));
1285
+ // Sleep between polls: brief yield after processing jobs, longer pause when idle
1286
+ await new Promise((r) => setTimeout(r, jobs.length > 0 ? 100 : 2000));
1201
1287
  }
1202
1288
  ws.status = "stopped";
1203
1289
  addWorkerLog(ws, "info", `Stopped. Activated: ${ws.stats.activated} Completed: ${ws.stats.completed} Failed: ${ws.stats.failed}`);
@@ -1321,6 +1407,7 @@ async function runAskInTui(screen, state) {
1321
1407
  },
1322
1408
  raw: null,
1323
1409
  rawView: false,
1410
+ message: "",
1324
1411
  curlView: false,
1325
1412
  altView: false,
1326
1413
  cursor: 0,
@@ -1559,6 +1646,7 @@ function handleMainKey(key, screen, state, done) {
1559
1646
  scroll: 0,
1560
1647
  editing: false,
1561
1648
  error: "",
1649
+ errorRaw: null,
1562
1650
  running: false,
1563
1651
  });
1564
1652
  }
@@ -1626,6 +1714,7 @@ function handleCommandsKey(key, screen, state, done) {
1626
1714
  scroll: 0,
1627
1715
  editing: false,
1628
1716
  error: "",
1717
+ errorRaw: null,
1629
1718
  running: false,
1630
1719
  });
1631
1720
  }
@@ -1655,6 +1744,7 @@ async function executeCommand(screen, state) {
1655
1744
  }
1656
1745
  screen.running = true;
1657
1746
  screen.error = "";
1747
+ screen.errorRaw = null;
1658
1748
  render(state);
1659
1749
  const { writer, get } = makeCapturingWriter();
1660
1750
  // Wrap client factories to capture the last raw HTTP response
@@ -1713,6 +1803,7 @@ async function executeCommand(screen, state) {
1713
1803
  altView: false,
1714
1804
  cursor: 0,
1715
1805
  scroll: 0,
1806
+ message: "",
1716
1807
  });
1717
1808
  }
1718
1809
  catch (err) {
@@ -1726,6 +1817,7 @@ async function executeCommand(screen, state) {
1726
1817
  error: msg,
1727
1818
  });
1728
1819
  screen.error = msg;
1820
+ screen.errorRaw = rawCapture;
1729
1821
  }
1730
1822
  finally {
1731
1823
  screen.running = false;
@@ -1941,6 +2033,60 @@ async function handleInputKey(key, screen, state, done) {
1941
2033
  function handleResultsKey(key, screen, state, done) {
1942
2034
  const { rows } = termSize();
1943
2035
  const viewH = Math.max(3, rows - 10);
2036
+ // Profile list inline shortcuts: u=use, s=show, d=delete
2037
+ if (screen.group.name === "profile" &&
2038
+ screen.cmd.name === "list" &&
2039
+ screen.output.type === "list") {
2040
+ // Clear stale message on any key in this context
2041
+ screen.message = "";
2042
+ if (key === "u" || key === "U") {
2043
+ const item = screen.output.items[screen.cursor];
2044
+ const name = item?.name;
2045
+ if (name && useProfile(name)) {
2046
+ state.profile = name;
2047
+ state.profileInfo = buildProfileInfoEntries(name);
2048
+ const items = rebuildProfileListItems();
2049
+ screen.output = { type: "list", items, columns: screen.output.columns, total: items.length };
2050
+ screen.message = `✓ Active profile: ${name}`;
2051
+ }
2052
+ else if (name) {
2053
+ screen.message = `Profile "${name}" not found`;
2054
+ }
2055
+ render(state);
2056
+ return;
2057
+ }
2058
+ if (key === "s" || key === "S") {
2059
+ const item = screen.output.items[screen.cursor];
2060
+ if (item) {
2061
+ state.stack.push({
2062
+ kind: "detail",
2063
+ group: screen.group,
2064
+ cmd: screen.cmd,
2065
+ item,
2066
+ label: String(item.name ?? "profile"),
2067
+ cursor: 0,
2068
+ scroll: 0,
2069
+ });
2070
+ }
2071
+ render(state);
2072
+ return;
2073
+ }
2074
+ if (key === "d" || key === "D") {
2075
+ const item = screen.output.items[screen.cursor];
2076
+ const name = item?.name;
2077
+ if (name && deleteProfile(name)) {
2078
+ const items = rebuildProfileListItems();
2079
+ screen.output = { type: "list", items, columns: screen.output.columns, total: items.length };
2080
+ screen.cursor = Math.min(screen.cursor, Math.max(0, items.length - 1));
2081
+ screen.message = `✓ Deleted profile "${name}"`;
2082
+ }
2083
+ else if (name) {
2084
+ screen.message = `Cannot delete "${name}" (modeler profiles are read-only)`;
2085
+ }
2086
+ render(state);
2087
+ return;
2088
+ }
2089
+ }
1944
2090
  // r/R toggles raw view; u/U toggles curl view — mutually exclusive
1945
2091
  if (key === "r" || key === "R") {
1946
2092
  screen.rawView = !screen.rawView;
@@ -2381,6 +2527,7 @@ function handleFollowupKey(key, screen, state, done) {
2381
2527
  scroll: 0,
2382
2528
  editing: false,
2383
2529
  error: "",
2530
+ errorRaw: null,
2384
2531
  running: false,
2385
2532
  });
2386
2533
  }
@@ -2431,6 +2578,29 @@ function handleJsonEditorKey(key, screen, state, done) {
2431
2578
  entry.valCursor = cur;
2432
2579
  }
2433
2580
  };
2581
+ // Key cycling for key column when fieldSpecs are available
2582
+ if (activeIsKey && screen.fieldSpecs && screen.fieldSpecs.length > 0) {
2583
+ const available = getAvailableFieldSpecs(screen.cursor, screen.entries, screen.fieldSpecs);
2584
+ if (available.length > 0) {
2585
+ if (key === "\x1b[A" || key === "\x1b[B") {
2586
+ const curIdx = available.findIndex((s) => s.name === entry.key);
2587
+ let nextIdx;
2588
+ if (key === "\x1b[A") {
2589
+ nextIdx = curIdx <= 0 ? available.length - 1 : curIdx - 1;
2590
+ }
2591
+ else {
2592
+ nextIdx = curIdx < 0 || curIdx >= available.length - 1 ? 0 : curIdx + 1;
2593
+ }
2594
+ const newSpec = available[nextIdx];
2595
+ if (newSpec) {
2596
+ entry.key = newSpec.name;
2597
+ entry.keyCursor = entry.key.length;
2598
+ }
2599
+ render(state);
2600
+ return;
2601
+ }
2602
+ }
2603
+ }
2434
2604
  // Enum cycling for value column of a known enum field
2435
2605
  const valSpec = !activeIsKey ? getFieldSpec(entry.key, screen.fieldSpecs) : undefined;
2436
2606
  const enumVals = valSpec?.enum;
@@ -2553,8 +2723,17 @@ function handleJsonEditorKey(key, screen, state, done) {
2553
2723
  case "\r":
2554
2724
  case "\n":
2555
2725
  if (isAddRow) {
2556
- // Add a new entry and start editing its key
2557
- screen.entries.push({ key: "", keyCursor: 0, val: "", valCursor: 0 });
2726
+ // Add a new entry; pre-seed key from first available field spec if available
2727
+ const newEntry = { key: "", keyCursor: 0, val: "", valCursor: 0 };
2728
+ if (screen.fieldSpecs && screen.fieldSpecs.length > 0) {
2729
+ const available = getAvailableFieldSpecs(null, screen.entries, screen.fieldSpecs);
2730
+ const firstSpec = available[0];
2731
+ if (firstSpec) {
2732
+ newEntry.key = firstSpec.name;
2733
+ newEntry.keyCursor = firstSpec.name.length;
2734
+ }
2735
+ }
2736
+ screen.entries.push(newEntry);
2558
2737
  screen.cursor = screen.entries.length - 1;
2559
2738
  screen.col = "key";
2560
2739
  screen.editing = true;
@@ -2572,18 +2751,23 @@ function handleJsonEditorKey(key, screen, state, done) {
2572
2751
  }
2573
2752
  break;
2574
2753
  case "a":
2575
- case "A":
2576
- // Insert new entry after cursor and start editing
2577
- screen.entries.splice(screen.cursor + 1, 0, {
2578
- key: "",
2579
- keyCursor: 0,
2580
- val: "",
2581
- valCursor: 0,
2582
- });
2754
+ case "A": {
2755
+ // Insert new entry after cursor; pre-seed key from first available field spec
2756
+ const addEntry = { key: "", keyCursor: 0, val: "", valCursor: 0 };
2757
+ if (screen.fieldSpecs && screen.fieldSpecs.length > 0) {
2758
+ const available = getAvailableFieldSpecs(null, screen.entries, screen.fieldSpecs);
2759
+ const firstSpec = available[0];
2760
+ if (firstSpec) {
2761
+ addEntry.key = firstSpec.name;
2762
+ addEntry.keyCursor = firstSpec.name.length;
2763
+ }
2764
+ }
2765
+ screen.entries.splice(screen.cursor + 1, 0, addEntry);
2583
2766
  screen.cursor = Math.min(screen.cursor + 1, screen.entries.length - 1);
2584
2767
  screen.col = "key";
2585
2768
  screen.editing = true;
2586
2769
  break;
2770
+ }
2587
2771
  case "d":
2588
2772
  case "D":
2589
2773
  if (!isAddRow && screen.entries.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bpmnkit/cli",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
4
4
  "description": "Command-line interface for Camunda 8 — deploy, manage, and monitor processes from the terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,11 +16,11 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@bpmnkit/api": "0.0.13",
19
- "@bpmnkit/ascii": "0.0.15",
19
+ "@bpmnkit/ascii": "0.0.16",
20
20
  "@bpmnkit/connector-gen": "0.0.8",
21
- "@bpmnkit/core": "0.0.15",
22
- "@bpmnkit/engine": "0.1.14",
23
- "@bpmnkit/profiles": "0.0.10"
21
+ "@bpmnkit/core": "0.0.16",
22
+ "@bpmnkit/engine": "0.1.15",
23
+ "@bpmnkit/profiles": "0.0.11"
24
24
  },
25
25
  "publishConfig": {
26
26
  "access": "public"