@bpmnkit/cli 0.0.13 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/ask.js +413 -0
- package/dist/commands/index.js +40 -5
- package/dist/run.js +25 -2
- package/dist/tui.js +332 -14
- package/package.json +2 -2
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
// ─── Spinner ──────────────────────────────────────────────────────────────────
|
|
3
|
+
class Spinner {
|
|
4
|
+
frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
5
|
+
i = 0;
|
|
6
|
+
timer = null;
|
|
7
|
+
msg = "";
|
|
8
|
+
isTTY = process.stderr.isTTY === true;
|
|
9
|
+
start(msg) {
|
|
10
|
+
this.msg = msg;
|
|
11
|
+
if (!this.isTTY) {
|
|
12
|
+
process.stderr.write(`${msg}\n`);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
process.stderr.write(`${this.frames[0]} ${msg}`);
|
|
16
|
+
this.timer = setInterval(() => {
|
|
17
|
+
this.i++;
|
|
18
|
+
process.stderr.write(`\r${this.frames[this.i % this.frames.length]} ${this.msg}`);
|
|
19
|
+
}, 80);
|
|
20
|
+
}
|
|
21
|
+
update(msg) {
|
|
22
|
+
this.msg = msg;
|
|
23
|
+
if (!this.isTTY) {
|
|
24
|
+
process.stderr.write(`${msg}\n`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
process.stderr.write(`\r${this.frames[this.i % this.frames.length]} ${msg}`);
|
|
28
|
+
}
|
|
29
|
+
stop() {
|
|
30
|
+
if (this.timer) {
|
|
31
|
+
clearInterval(this.timer);
|
|
32
|
+
this.timer = null;
|
|
33
|
+
}
|
|
34
|
+
if (this.isTTY) {
|
|
35
|
+
process.stderr.write("\r\x1b[K"); // CR + erase to end of line
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// ─── Compact AI adapter ───────────────────────────────────────────────────────
|
|
40
|
+
/** Try claude → copilot → gemini. Return the first available binary name. */
|
|
41
|
+
async function detectAi() {
|
|
42
|
+
for (const bin of ["claude", "copilot", "gemini"]) {
|
|
43
|
+
const ok = await Promise.race([
|
|
44
|
+
new Promise((res) => {
|
|
45
|
+
const p = spawn(bin, ["--version"], { stdio: "ignore" });
|
|
46
|
+
p.on("error", () => res(false));
|
|
47
|
+
p.on("close", (code) => res(code === 0));
|
|
48
|
+
}),
|
|
49
|
+
new Promise((res) => setTimeout(() => res(false), 3000)),
|
|
50
|
+
]);
|
|
51
|
+
if (ok)
|
|
52
|
+
return bin;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
/** Run an AI binary with the given prompt and collect the full text output. */
|
|
57
|
+
async function runAi(bin, prompt) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
let args;
|
|
60
|
+
if (bin === "claude") {
|
|
61
|
+
args = [
|
|
62
|
+
"-p",
|
|
63
|
+
prompt,
|
|
64
|
+
"--output-format",
|
|
65
|
+
"stream-json",
|
|
66
|
+
"--verbose",
|
|
67
|
+
"--dangerously-skip-permissions",
|
|
68
|
+
"--permission-mode",
|
|
69
|
+
"bypassPermissions",
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
else if (bin === "copilot") {
|
|
73
|
+
args = ["-p", prompt, "--yolo"];
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
args = ["--prompt", prompt, "--yolo"];
|
|
77
|
+
}
|
|
78
|
+
const env = { ...process.env, CLAUDECODE: undefined };
|
|
79
|
+
const proc = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"], env });
|
|
80
|
+
// Drain stderr to prevent the pipe buffer from filling and stalling the child.
|
|
81
|
+
proc.stderr?.resume();
|
|
82
|
+
let out = "";
|
|
83
|
+
let buf = "";
|
|
84
|
+
if (bin === "claude") {
|
|
85
|
+
proc.stdout?.on("data", (chunk) => {
|
|
86
|
+
buf += chunk.toString();
|
|
87
|
+
const lines = buf.split("\n");
|
|
88
|
+
buf = lines.pop() ?? "";
|
|
89
|
+
for (const line of lines) {
|
|
90
|
+
if (!line.trim())
|
|
91
|
+
continue;
|
|
92
|
+
try {
|
|
93
|
+
const ev = JSON.parse(line);
|
|
94
|
+
if (ev.type === "assistant" && ev.message?.content) {
|
|
95
|
+
for (const block of ev.message.content) {
|
|
96
|
+
if (block.type === "text" && block.text)
|
|
97
|
+
out += block.text;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
/* skip non-JSON */
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
proc.stdout?.on("data", (chunk) => {
|
|
109
|
+
out += chunk.toString();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
proc.on("error", reject);
|
|
113
|
+
proc.on("close", (code) => {
|
|
114
|
+
if (code === 0)
|
|
115
|
+
resolve(out);
|
|
116
|
+
else
|
|
117
|
+
reject(new Error(`${bin} exited with code ${code}`));
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
// ─── System prompt ────────────────────────────────────────────────────────────
|
|
122
|
+
const SYSTEM_PROMPT = [
|
|
123
|
+
"Convert the user query into a Camunda 8 search spec. Output ONLY a valid JSON object — no prose, no markdown.",
|
|
124
|
+
"",
|
|
125
|
+
'Schema: { "resource": <resource>, "filter": { ... } }',
|
|
126
|
+
"",
|
|
127
|
+
"Resources and filter fields:",
|
|
128
|
+
' "processInstances": state("ACTIVE"|"COMPLETED"|"TERMINATED"), hasIncident(bool), processDefinitionId(str), processInstanceKey(str)',
|
|
129
|
+
' "incidents": state("ACTIVE"|"RESOLVED"), type(str), processInstanceKey(str), processDefinitionKey(str)',
|
|
130
|
+
' "userTasks": state("CREATED"|"COMPLETED"|"CANCELED"), processInstanceKey(str), assignee(str)',
|
|
131
|
+
' "variables": name(str), value(str — JSON-serialized: 3355→"3355", true→"true", hello→"\\"hello\\""), processInstanceKey(str)',
|
|
132
|
+
' "jobs": state("ACTIVATABLE"|"ACTIVE"|"FAILED"|"COMPLETED"|"CANCELED"|"ERROR_THROWN"), type(str), processInstanceKey(str)',
|
|
133
|
+
' "decisionInstances": decisionDefinitionId(str), state("EVALUATED"|"FAILED"), processInstanceKey(str)',
|
|
134
|
+
"",
|
|
135
|
+
'Use "variables" whenever a variable name or value is mentioned.',
|
|
136
|
+
"Omit filter fields that are not relevant. Output ONLY the JSON object.",
|
|
137
|
+
].join("\n");
|
|
138
|
+
async function fetchSpec(spec, getClient) {
|
|
139
|
+
const client = await getClient();
|
|
140
|
+
const filter = spec.filter;
|
|
141
|
+
const body = { filter };
|
|
142
|
+
switch (spec.resource) {
|
|
143
|
+
case "processInstances": {
|
|
144
|
+
const result = await client.processInstance.searchProcessInstances(body);
|
|
145
|
+
const items = result.items ?? [];
|
|
146
|
+
const total = result.page?.totalItems ?? items.length;
|
|
147
|
+
return {
|
|
148
|
+
resource: spec.resource,
|
|
149
|
+
filter,
|
|
150
|
+
items,
|
|
151
|
+
total,
|
|
152
|
+
columns: [
|
|
153
|
+
{ key: "processInstanceKey", header: "PROCESS INSTANCE KEY", maxWidth: 22 },
|
|
154
|
+
{ key: "processDefinitionId", header: "PROCESS", maxWidth: 30 },
|
|
155
|
+
{ key: "state", header: "STATE", maxWidth: 12 },
|
|
156
|
+
{ key: "startDate", header: "STARTED", maxWidth: 20, transform: relTime },
|
|
157
|
+
{ key: "hasIncident", header: "INCIDENT", maxWidth: 8 },
|
|
158
|
+
],
|
|
159
|
+
relations: [
|
|
160
|
+
{
|
|
161
|
+
groupName: "process-instance",
|
|
162
|
+
commandName: "get",
|
|
163
|
+
description: "View process instance",
|
|
164
|
+
params: [{ field: "processInstanceKey", param: "processInstanceKey" }],
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
case "incidents": {
|
|
170
|
+
const result = await client.incident.searchIncidents(body);
|
|
171
|
+
const items = result.items ?? [];
|
|
172
|
+
const total = result.page?.totalItems ?? items.length;
|
|
173
|
+
return {
|
|
174
|
+
resource: spec.resource,
|
|
175
|
+
filter,
|
|
176
|
+
items,
|
|
177
|
+
total,
|
|
178
|
+
columns: [
|
|
179
|
+
{ key: "incidentKey", header: "INCIDENT KEY", maxWidth: 22 },
|
|
180
|
+
{ key: "processInstanceKey", header: "PROCESS INSTANCE KEY", maxWidth: 22 },
|
|
181
|
+
{ key: "type", header: "TYPE", maxWidth: 28 },
|
|
182
|
+
{ key: "state", header: "STATE", maxWidth: 10 },
|
|
183
|
+
{ key: "message", header: "MESSAGE", maxWidth: 50 },
|
|
184
|
+
],
|
|
185
|
+
relations: [
|
|
186
|
+
{
|
|
187
|
+
groupName: "process-instance",
|
|
188
|
+
commandName: "get",
|
|
189
|
+
description: "View process instance",
|
|
190
|
+
params: [{ field: "processInstanceKey", param: "processInstanceKey" }],
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
case "userTasks": {
|
|
196
|
+
const result = await client.userTask.searchUserTasks(body);
|
|
197
|
+
const items = result.items ?? [];
|
|
198
|
+
const total = result.page?.totalItems ?? items.length;
|
|
199
|
+
return {
|
|
200
|
+
resource: spec.resource,
|
|
201
|
+
filter,
|
|
202
|
+
items,
|
|
203
|
+
total,
|
|
204
|
+
columns: [
|
|
205
|
+
{ key: "userTaskKey", header: "USER TASK KEY", maxWidth: 22 },
|
|
206
|
+
{ key: "processInstanceKey", header: "PROCESS INSTANCE KEY", maxWidth: 22 },
|
|
207
|
+
{ key: "elementId", header: "ELEMENT", maxWidth: 24 },
|
|
208
|
+
{ key: "assignee", header: "ASSIGNEE", maxWidth: 20 },
|
|
209
|
+
{ key: "state", header: "STATE", maxWidth: 12 },
|
|
210
|
+
],
|
|
211
|
+
relations: [
|
|
212
|
+
{
|
|
213
|
+
groupName: "user-task",
|
|
214
|
+
commandName: "get",
|
|
215
|
+
description: "View user task",
|
|
216
|
+
params: [{ field: "userTaskKey", param: "userTaskKey" }],
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
groupName: "process-instance",
|
|
220
|
+
commandName: "get",
|
|
221
|
+
description: "View process instance",
|
|
222
|
+
params: [{ field: "processInstanceKey", param: "processInstanceKey" }],
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
case "variables": {
|
|
228
|
+
const result = await client.variable.searchVariables(body);
|
|
229
|
+
const items = result.items ?? [];
|
|
230
|
+
const total = result.page?.totalItems ?? items.length;
|
|
231
|
+
return {
|
|
232
|
+
resource: spec.resource,
|
|
233
|
+
filter,
|
|
234
|
+
items,
|
|
235
|
+
total,
|
|
236
|
+
columns: [
|
|
237
|
+
{ key: "variableKey", header: "VARIABLE KEY", maxWidth: 22 },
|
|
238
|
+
{ key: "processInstanceKey", header: "PROCESS INSTANCE KEY", maxWidth: 22 },
|
|
239
|
+
{ key: "name", header: "NAME", maxWidth: 24 },
|
|
240
|
+
{ key: "value", header: "VALUE", maxWidth: 50 },
|
|
241
|
+
],
|
|
242
|
+
relations: [
|
|
243
|
+
{
|
|
244
|
+
groupName: "variable",
|
|
245
|
+
commandName: "get",
|
|
246
|
+
description: "View variable",
|
|
247
|
+
params: [{ field: "variableKey", param: "variableKey" }],
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
groupName: "process-instance",
|
|
251
|
+
commandName: "get",
|
|
252
|
+
description: "View process instance",
|
|
253
|
+
params: [{ field: "processInstanceKey", param: "processInstanceKey" }],
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
case "jobs": {
|
|
259
|
+
const result = await client.job.searchJobs(body);
|
|
260
|
+
const items = result.items ?? [];
|
|
261
|
+
const total = result.page?.totalItems ?? items.length;
|
|
262
|
+
return {
|
|
263
|
+
resource: spec.resource,
|
|
264
|
+
filter,
|
|
265
|
+
items,
|
|
266
|
+
total,
|
|
267
|
+
columns: [
|
|
268
|
+
{ key: "jobKey", header: "JOB KEY", maxWidth: 22 },
|
|
269
|
+
{ key: "processInstanceKey", header: "PROCESS INSTANCE KEY", maxWidth: 22 },
|
|
270
|
+
{ key: "type", header: "TYPE", maxWidth: 28 },
|
|
271
|
+
{ key: "state", header: "STATE", maxWidth: 12 },
|
|
272
|
+
{ key: "retries", header: "RETRIES", maxWidth: 8 },
|
|
273
|
+
],
|
|
274
|
+
relations: [
|
|
275
|
+
{
|
|
276
|
+
groupName: "process-instance",
|
|
277
|
+
commandName: "get",
|
|
278
|
+
description: "View process instance",
|
|
279
|
+
params: [{ field: "processInstanceKey", param: "processInstanceKey" }],
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
case "decisionInstances": {
|
|
285
|
+
const result = await client.decisionInstance.searchDecisionInstances(body);
|
|
286
|
+
const items = result.items ?? [];
|
|
287
|
+
const total = result.page?.totalItems ?? items.length;
|
|
288
|
+
return {
|
|
289
|
+
resource: spec.resource,
|
|
290
|
+
filter,
|
|
291
|
+
items,
|
|
292
|
+
total,
|
|
293
|
+
columns: [
|
|
294
|
+
{ key: "decisionInstanceKey", header: "DECISION INSTANCE KEY", maxWidth: 22 },
|
|
295
|
+
{ key: "processInstanceKey", header: "PROCESS INSTANCE KEY", maxWidth: 22 },
|
|
296
|
+
{ key: "decisionDefinitionId", header: "DECISION", maxWidth: 30 },
|
|
297
|
+
{ key: "state", header: "STATE", maxWidth: 12 },
|
|
298
|
+
],
|
|
299
|
+
relations: [
|
|
300
|
+
{
|
|
301
|
+
groupName: "process-instance",
|
|
302
|
+
commandName: "get",
|
|
303
|
+
description: "View process instance",
|
|
304
|
+
params: [{ field: "processInstanceKey", param: "processInstanceKey" }],
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
default:
|
|
310
|
+
throw new Error(`Unknown resource: "${spec.resource}"`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function relTime(value) {
|
|
314
|
+
if (typeof value !== "string" || !value)
|
|
315
|
+
return "—";
|
|
316
|
+
const d = new Date(value);
|
|
317
|
+
const diff = Date.now() - d.getTime();
|
|
318
|
+
const m = Math.floor(diff / 60_000);
|
|
319
|
+
if (m < 1)
|
|
320
|
+
return "just now";
|
|
321
|
+
if (m < 60)
|
|
322
|
+
return `${m}m ago`;
|
|
323
|
+
const h = Math.floor(m / 60);
|
|
324
|
+
if (h < 24)
|
|
325
|
+
return `${h}h ${m % 60}m ago`;
|
|
326
|
+
return d.toLocaleString(undefined, {
|
|
327
|
+
month: "short",
|
|
328
|
+
day: "numeric",
|
|
329
|
+
hour: "2-digit",
|
|
330
|
+
minute: "2-digit",
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
// ─── Quick-parse bypass (no AI needed) ───────────────────────────────────────
|
|
334
|
+
function quickParse(query) {
|
|
335
|
+
const q = query.trim().toLowerCase();
|
|
336
|
+
// Pure numeric → process instance key lookup
|
|
337
|
+
if (/^\d+$/.test(query.trim())) {
|
|
338
|
+
return { resource: "processInstances", filter: { processInstanceKey: query.trim() } };
|
|
339
|
+
}
|
|
340
|
+
// Single state keyword
|
|
341
|
+
const stateMap = {
|
|
342
|
+
active: { resource: "processInstances", filter: { state: "ACTIVE" } },
|
|
343
|
+
completed: { resource: "processInstances", filter: { state: "COMPLETED" } },
|
|
344
|
+
terminated: { resource: "processInstances", filter: { state: "TERMINATED" } },
|
|
345
|
+
incidents: { resource: "incidents", filter: {} },
|
|
346
|
+
};
|
|
347
|
+
if (q in stateMap)
|
|
348
|
+
return stateMap[q] ?? null;
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
// ─── Exported query runner ────────────────────────────────────────────────────
|
|
352
|
+
export async function runAskQuery(query, getClient, onStatus) {
|
|
353
|
+
// Try quick-parse first (no AI token cost)
|
|
354
|
+
const quick = quickParse(query);
|
|
355
|
+
if (quick) {
|
|
356
|
+
return fetchSpec(quick, getClient);
|
|
357
|
+
}
|
|
358
|
+
// Detect AI binary
|
|
359
|
+
onStatus("Detecting AI…");
|
|
360
|
+
const bin = await detectAi();
|
|
361
|
+
if (!bin) {
|
|
362
|
+
throw new Error("No AI CLI found. Install claude, copilot, or gemini and ensure it is in PATH.");
|
|
363
|
+
}
|
|
364
|
+
// Ask AI to translate query → search spec
|
|
365
|
+
onStatus(`Asking ${bin}…`);
|
|
366
|
+
const fullPrompt = `${SYSTEM_PROMPT}\n\nQuery: ${query}\n\nJSON:`;
|
|
367
|
+
const raw = await runAi(bin, fullPrompt);
|
|
368
|
+
// Extract JSON from the response (strip any prose/markdown fences)
|
|
369
|
+
const jsonMatch = raw.match(/\{[\s\S]*\}/);
|
|
370
|
+
if (!jsonMatch) {
|
|
371
|
+
throw new Error(`AI returned no JSON.\n\nRaw output:\n${raw}`);
|
|
372
|
+
}
|
|
373
|
+
let spec;
|
|
374
|
+
try {
|
|
375
|
+
spec = JSON.parse(jsonMatch[0]);
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
throw new Error(`AI returned invalid JSON.\n\nRaw output:\n${raw}`);
|
|
379
|
+
}
|
|
380
|
+
// Execute the spec against the API
|
|
381
|
+
onStatus(`Searching ${spec.resource}…`);
|
|
382
|
+
return fetchSpec(spec, getClient);
|
|
383
|
+
}
|
|
384
|
+
// ─── Command group ────────────────────────────────────────────────────────────
|
|
385
|
+
export const askGroup = {
|
|
386
|
+
name: "ask",
|
|
387
|
+
description: "Natural language search using a local AI (claude/copilot/gemini)",
|
|
388
|
+
commands: [
|
|
389
|
+
{
|
|
390
|
+
name: "query",
|
|
391
|
+
description: "Ask in plain language; the AI translates it to a Camunda API search",
|
|
392
|
+
args: [{ name: "query", description: "Natural language query", required: true }],
|
|
393
|
+
async run(ctx) {
|
|
394
|
+
const query = ctx.positional.join(" ").trim();
|
|
395
|
+
if (!query)
|
|
396
|
+
throw new Error("Provide a query, e.g.: casen ask active incidents");
|
|
397
|
+
const spinner = new Spinner();
|
|
398
|
+
try {
|
|
399
|
+
spinner.start("Detecting AI…");
|
|
400
|
+
const result = await runAskQuery(query, ctx.getClient, (msg) => spinner.update(msg));
|
|
401
|
+
spinner.stop();
|
|
402
|
+
ctx.output.info(`${result.resource} ${JSON.stringify(result.filter)}`);
|
|
403
|
+
ctx.output.printList({ items: result.items, page: { totalItems: result.total } }, result.columns);
|
|
404
|
+
}
|
|
405
|
+
catch (err) {
|
|
406
|
+
spinner.stop();
|
|
407
|
+
throw err;
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
],
|
|
412
|
+
};
|
|
413
|
+
//# sourceMappingURL=ask.js.map
|
package/dist/commands/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { adminCommandGroups } from "../generated/admin-commands.js";
|
|
2
2
|
import { decisionDefinitionGroup, decisionRequirementsGroup, generatedCommandGroups, jobGroup, processDefinitionGroup, userTaskGroup, } from "../generated/commands.js";
|
|
3
|
+
import { askGroup } from "./ask.js";
|
|
3
4
|
import { getDmnReqsXmlCmd, getDmnXmlCmd, getStartFormCmd, getUserTaskFormCmd, getXmlCmd, renderBpmnCmd, } from "./bpmn.js";
|
|
4
5
|
import { completionGroup } from "./completion.js";
|
|
5
6
|
import { connectorGroup } from "./connector.js";
|
|
@@ -32,16 +33,50 @@ const customisedGroups = generatedCommandGroups.map((g) => {
|
|
|
32
33
|
}
|
|
33
34
|
return g;
|
|
34
35
|
});
|
|
35
|
-
const
|
|
36
|
-
profileGroup,
|
|
37
|
-
settingsGroup,
|
|
36
|
+
const sortedOtherGroups = [
|
|
38
37
|
connectorGroup,
|
|
39
38
|
...customisedGroups,
|
|
40
39
|
...adminCommandGroups,
|
|
41
40
|
completionGroup,
|
|
41
|
+
].sort((a, b) => a.name.localeCompare(b.name));
|
|
42
|
+
export const commandGroups = [
|
|
43
|
+
askGroup,
|
|
44
|
+
settingsGroup,
|
|
45
|
+
profileGroup,
|
|
46
|
+
...sortedOtherGroups,
|
|
42
47
|
];
|
|
43
|
-
// Sort alphabetically by name for the main menu
|
|
44
|
-
export const commandGroups = allGroups.sort((a, b) => a.name.localeCompare(b.name));
|
|
45
48
|
// Compute follow-up relations between commands based on shared field/arg names
|
|
46
49
|
computeRelations(commandGroups);
|
|
50
|
+
// Manually inject relations on GET commands (they return a single object, not a
|
|
51
|
+
// list, so they have no `columns` and are skipped by computeRelations).
|
|
52
|
+
const piGroup = commandGroups.find((g) => g.name === "process-instance");
|
|
53
|
+
const pdGroup = commandGroups.find((g) => g.name === "process-definition");
|
|
54
|
+
const piGetCmd = piGroup?.commands.find((c) => c.name === "get");
|
|
55
|
+
const pdGetCmd = pdGroup?.commands.find((c) => c.name === "get");
|
|
56
|
+
const pdRelations = [
|
|
57
|
+
{
|
|
58
|
+
groupName: "process-definition",
|
|
59
|
+
commandName: "get",
|
|
60
|
+
description: "View process definition",
|
|
61
|
+
params: [{ field: "processDefinitionKey", param: "processDefinitionKey" }],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
groupName: "process-definition",
|
|
65
|
+
commandName: "render",
|
|
66
|
+
description: "Render BPMN diagram",
|
|
67
|
+
params: [{ field: "processDefinitionKey", param: "processDefinitionKey" }],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
groupName: "process-definition",
|
|
71
|
+
commandName: "get-xml",
|
|
72
|
+
description: "Get BPMN XML",
|
|
73
|
+
params: [{ field: "processDefinitionKey", param: "processDefinitionKey" }],
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
if (piGetCmd) {
|
|
77
|
+
piGetCmd.relations = pdRelations;
|
|
78
|
+
}
|
|
79
|
+
if (pdGetCmd) {
|
|
80
|
+
pdGetCmd.relations = pdRelations.filter((r) => r.commandName !== "get");
|
|
81
|
+
}
|
|
47
82
|
//# sourceMappingURL=index.js.map
|
package/dist/run.js
CHANGED
|
@@ -6,7 +6,7 @@ import { printCommandHelp, printGlobalHelp, printGroupHelp, printVersion } from
|
|
|
6
6
|
import { createNullWriter, createOutputWriter, printRawResponse } from "./output.js";
|
|
7
7
|
import { runProfileManager } from "./profile-tui.js";
|
|
8
8
|
import { runSettingsManager } from "./settings-tui.js";
|
|
9
|
-
import { runGroupTui, runMainTui } from "./tui.js";
|
|
9
|
+
import { runAskTui, runGroupTui, runMainTui } from "./tui.js";
|
|
10
10
|
// ─── Profile info ─────────────────────────────────────────────────────────────
|
|
11
11
|
function buildProfileInfo(profileName) {
|
|
12
12
|
const effectiveName = profileName ?? getActiveName() ?? "none";
|
|
@@ -91,9 +91,32 @@ export async function run(argv) {
|
|
|
91
91
|
process.exitCode = 1;
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
|
+
// ── ask: join remaining positionals as the query ─────────────────────────
|
|
95
|
+
if (group.name === "ask" && positional.length >= 2 && !wantHelp) {
|
|
96
|
+
const queryWords = positional.slice(1);
|
|
97
|
+
const output = createOutputWriter((flags.output ?? flags.o ?? "table"), flags["no-color"] === true);
|
|
98
|
+
const ctx = {
|
|
99
|
+
positional: queryWords,
|
|
100
|
+
flags,
|
|
101
|
+
output,
|
|
102
|
+
getClient: () => Promise.resolve(createClientFromProfile(profileName)),
|
|
103
|
+
getAdminClient: () => Promise.resolve(createAdminClientFromProfile(profileName)),
|
|
104
|
+
};
|
|
105
|
+
const cmd = group.commands[0];
|
|
106
|
+
if (cmd)
|
|
107
|
+
await cmd.run(ctx);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
94
110
|
// ── TUI (no subcommand, no --help) ───────────────────────────────────────
|
|
95
111
|
if (positional.length === 1 && !wantHelp) {
|
|
96
|
-
if (group.name === "
|
|
112
|
+
if (group.name === "ask") {
|
|
113
|
+
const { name: pName, info: pInfo } = buildProfileInfo(profileName);
|
|
114
|
+
await runAskTui(commandGroups, getClient, getAdminClient, {
|
|
115
|
+
profile: pName,
|
|
116
|
+
profileInfo: pInfo,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
else if (group.name === "profile") {
|
|
97
120
|
await runProfileManager();
|
|
98
121
|
}
|
|
99
122
|
else if (group.name === "settings") {
|
package/dist/tui.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { renderBpmnAscii } from "@bpmnkit/ascii";
|
|
3
3
|
import { appendAuditEntry, getAuditLog, getSettings, saveSettings } from "@bpmnkit/profiles";
|
|
4
|
+
import { runAskQuery } from "./commands/ask.js";
|
|
4
5
|
// ─── ANSI helpers ─────────────────────────────────────────────────────────────
|
|
5
6
|
const CSI = "\x1b[";
|
|
6
7
|
const HIDE = `${CSI}?25l`;
|
|
@@ -375,6 +376,10 @@ function renderMain(state, screen) {
|
|
|
375
376
|
const desc = dim(fit(g.description, cols - nameW - 8));
|
|
376
377
|
const line = ` ${name} ${desc}`;
|
|
377
378
|
lines.push(isCursor ? inv(line.padEnd(cols - 1)) : line);
|
|
379
|
+
// Add separator after the pinned top section (ask, settings, profile)
|
|
380
|
+
if (!searching && g.name === "profile" && i < groups.length - 1) {
|
|
381
|
+
lines.push(` ${dim("─".repeat(cols - 4))}`);
|
|
382
|
+
}
|
|
378
383
|
}
|
|
379
384
|
if (groups.length > viewH) {
|
|
380
385
|
const hi = Math.min(screen.scroll + viewH, groups.length);
|
|
@@ -386,6 +391,33 @@ function renderMain(state, screen) {
|
|
|
386
391
|
lines.push(`\n${hint}`);
|
|
387
392
|
return lines;
|
|
388
393
|
}
|
|
394
|
+
const ASK_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
395
|
+
let askSpinnerFrame = 0;
|
|
396
|
+
function renderAsk(state, screen) {
|
|
397
|
+
const { cols } = termSize();
|
|
398
|
+
const lines = [
|
|
399
|
+
renderHeader(["casen", "ask"], cols, state.profile),
|
|
400
|
+
`\n ${dim("Natural language search using a local AI (claude/copilot/gemini)")}\n`,
|
|
401
|
+
];
|
|
402
|
+
const inputLine = ` > ${renderText(screen.query, screen.cursor, cols - 6, true)}`;
|
|
403
|
+
lines.push(inputLine);
|
|
404
|
+
lines.push("");
|
|
405
|
+
if (screen.status === "running" && screen.statusMsg) {
|
|
406
|
+
const frame = ASK_SPINNER_FRAMES[askSpinnerFrame % ASK_SPINNER_FRAMES.length] ?? "⠋";
|
|
407
|
+
lines.push(` ${frame} ${dim(screen.statusMsg)}`);
|
|
408
|
+
}
|
|
409
|
+
else if (screen.error) {
|
|
410
|
+
lines.push(` ${red("error:")} ${screen.error}`);
|
|
411
|
+
}
|
|
412
|
+
lines.push("");
|
|
413
|
+
if (screen.status === "running") {
|
|
414
|
+
lines.push(` ${dim("type query")} ${cyan("enter")} run ${cyan("esc")} back ${cyan("^C")} abort`);
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
lines.push(` ${dim("type query")} ${cyan("enter")} run ${cyan("esc")} back ${cyan("^C")} quit`);
|
|
418
|
+
}
|
|
419
|
+
return lines;
|
|
420
|
+
}
|
|
389
421
|
function renderCommands(state, screen) {
|
|
390
422
|
const { cols, rows } = termSize();
|
|
391
423
|
const searching = screen.search.length > 0;
|
|
@@ -688,9 +720,10 @@ function renderResults(state, screen) {
|
|
|
688
720
|
lines.push(isCursor ? inv(line.padEnd(cols - 1)) : line);
|
|
689
721
|
}
|
|
690
722
|
const spaceHint = entries.some(([, v]) => v instanceof ArrayValue)
|
|
691
|
-
? ` ${cyan("space")} expand
|
|
723
|
+
? ` ${cyan("space")} expand`
|
|
692
724
|
: "";
|
|
693
|
-
|
|
725
|
+
const fHint = screen.cmd.relations ? ` ${cyan("f")} follow-up` : "";
|
|
726
|
+
lines.push(`\n ${dim("↑↓")} navigate ${cyan("enter")} navigate field${spaceHint}${fHint}${rawToggle}${curlToggle} ${cyan("m")} main ${cyan("esc")} back ${cyan("q")} quit`);
|
|
694
727
|
}
|
|
695
728
|
else {
|
|
696
729
|
lines.push(renderHeader([screen.group.name, screen.cmd.name], cols, state.profile));
|
|
@@ -746,7 +779,8 @@ function renderDetail(state, screen) {
|
|
|
746
779
|
const spaceHint = entries.some(([, v]) => v instanceof ArrayValue)
|
|
747
780
|
? ` ${cyan("space/→")} expand ${cyan("←")} back`
|
|
748
781
|
: ` ${cyan("←")} back`;
|
|
749
|
-
|
|
782
|
+
const fHint = screen.cmd?.relations ? ` ${cyan("f")} follow-up` : "";
|
|
783
|
+
lines.push(` ${dim("↑↓")} navigate ${cyan("enter")} navigate field${spaceHint}${fHint} ${cyan("m")} main ${cyan("esc")} back ${cyan("q")} quit`);
|
|
750
784
|
return lines;
|
|
751
785
|
}
|
|
752
786
|
function renderFollowup(state, screen) {
|
|
@@ -1171,6 +1205,160 @@ function launchWorkerView(inputScreen, state) {
|
|
|
1171
1205
|
stopWorkerScreen(ws);
|
|
1172
1206
|
});
|
|
1173
1207
|
}
|
|
1208
|
+
// ─── Ask screen helpers ───────────────────────────────────────────────────────
|
|
1209
|
+
async function runAskInTui(screen, state) {
|
|
1210
|
+
let frame = 0;
|
|
1211
|
+
screen._timer = setInterval(() => {
|
|
1212
|
+
frame++;
|
|
1213
|
+
askSpinnerFrame = frame;
|
|
1214
|
+
if (!state.quitting && state.stack.includes(screen)) {
|
|
1215
|
+
render(state);
|
|
1216
|
+
}
|
|
1217
|
+
else {
|
|
1218
|
+
if (screen._timer !== null) {
|
|
1219
|
+
clearInterval(screen._timer);
|
|
1220
|
+
screen._timer = null;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}, 80);
|
|
1224
|
+
let result;
|
|
1225
|
+
try {
|
|
1226
|
+
result = await runAskQuery(screen.query, state.getClient, (msg) => {
|
|
1227
|
+
screen.statusMsg = msg;
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
catch (err) {
|
|
1231
|
+
if (screen._timer !== null) {
|
|
1232
|
+
clearInterval(screen._timer);
|
|
1233
|
+
screen._timer = null;
|
|
1234
|
+
}
|
|
1235
|
+
screen.error = err instanceof Error ? err.message : String(err);
|
|
1236
|
+
screen.status = "idle";
|
|
1237
|
+
screen.statusMsg = "";
|
|
1238
|
+
render(state);
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
if (screen._timer !== null) {
|
|
1242
|
+
clearInterval(screen._timer);
|
|
1243
|
+
screen._timer = null;
|
|
1244
|
+
}
|
|
1245
|
+
const askGrp = state.groups.find((g) => g.name === "ask") ?? {
|
|
1246
|
+
name: "ask",
|
|
1247
|
+
description: "",
|
|
1248
|
+
commands: [],
|
|
1249
|
+
};
|
|
1250
|
+
const syntheticCmd = {
|
|
1251
|
+
name: result.resource,
|
|
1252
|
+
description: "AI search results",
|
|
1253
|
+
run: async () => { },
|
|
1254
|
+
relations: result.relations,
|
|
1255
|
+
};
|
|
1256
|
+
state.stack.push({
|
|
1257
|
+
kind: "results",
|
|
1258
|
+
group: askGrp,
|
|
1259
|
+
cmd: syntheticCmd,
|
|
1260
|
+
output: {
|
|
1261
|
+
type: "list",
|
|
1262
|
+
items: result.items,
|
|
1263
|
+
columns: result.columns,
|
|
1264
|
+
total: result.total,
|
|
1265
|
+
},
|
|
1266
|
+
raw: null,
|
|
1267
|
+
rawView: false,
|
|
1268
|
+
curlView: false,
|
|
1269
|
+
altView: false,
|
|
1270
|
+
cursor: 0,
|
|
1271
|
+
scroll: 0,
|
|
1272
|
+
});
|
|
1273
|
+
render(state);
|
|
1274
|
+
}
|
|
1275
|
+
async function handleAskKey(key, screen, state, done) {
|
|
1276
|
+
// Ctrl+C always quits
|
|
1277
|
+
if (key === "\x03") {
|
|
1278
|
+
done();
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
// ESC: if running do nothing; otherwise pop
|
|
1282
|
+
if (key === "\x1b") {
|
|
1283
|
+
if (screen.status === "running") {
|
|
1284
|
+
render(state);
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
if (screen._timer !== null) {
|
|
1288
|
+
clearInterval(screen._timer);
|
|
1289
|
+
screen._timer = null;
|
|
1290
|
+
}
|
|
1291
|
+
state.stack.pop();
|
|
1292
|
+
render(state);
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
// Enter: run if not running and has query
|
|
1296
|
+
if (key === "\r" || key === "\n") {
|
|
1297
|
+
if (screen.status !== "running" && screen.query.trim()) {
|
|
1298
|
+
screen.status = "running";
|
|
1299
|
+
screen.statusMsg = "";
|
|
1300
|
+
screen.error = "";
|
|
1301
|
+
render(state);
|
|
1302
|
+
runAskInTui(screen, state).catch((err) => {
|
|
1303
|
+
if (screen._timer !== null) {
|
|
1304
|
+
clearInterval(screen._timer);
|
|
1305
|
+
screen._timer = null;
|
|
1306
|
+
}
|
|
1307
|
+
screen.error = err instanceof Error ? err.message : String(err);
|
|
1308
|
+
screen.status = "idle";
|
|
1309
|
+
render(state);
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
// Text editing (only when idle)
|
|
1315
|
+
if (screen.status === "running") {
|
|
1316
|
+
render(state);
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
switch (key) {
|
|
1320
|
+
case "\x7f": // backspace
|
|
1321
|
+
case "\x08":
|
|
1322
|
+
if (screen.cursor > 0) {
|
|
1323
|
+
screen.query = screen.query.slice(0, screen.cursor - 1) + screen.query.slice(screen.cursor);
|
|
1324
|
+
screen.cursor--;
|
|
1325
|
+
}
|
|
1326
|
+
break;
|
|
1327
|
+
case "\x01": // Ctrl+A
|
|
1328
|
+
case "\x1b[H":
|
|
1329
|
+
screen.cursor = 0;
|
|
1330
|
+
break;
|
|
1331
|
+
case "\x05": // Ctrl+E
|
|
1332
|
+
case "\x1b[F":
|
|
1333
|
+
screen.cursor = screen.query.length;
|
|
1334
|
+
break;
|
|
1335
|
+
case "\x0b": // Ctrl+K — clear to end
|
|
1336
|
+
screen.query = screen.query.slice(0, screen.cursor);
|
|
1337
|
+
break;
|
|
1338
|
+
case "\x15": // Ctrl+U — clear line
|
|
1339
|
+
screen.query = "";
|
|
1340
|
+
screen.cursor = 0;
|
|
1341
|
+
break;
|
|
1342
|
+
case "\x1b[D": // left arrow
|
|
1343
|
+
if (screen.cursor > 0)
|
|
1344
|
+
screen.cursor--;
|
|
1345
|
+
break;
|
|
1346
|
+
case "\x1b[C": // right arrow
|
|
1347
|
+
if (screen.cursor < screen.query.length)
|
|
1348
|
+
screen.cursor++;
|
|
1349
|
+
break;
|
|
1350
|
+
default: {
|
|
1351
|
+
const printable = [...key].filter((ch) => ch >= " ").join("");
|
|
1352
|
+
if (printable) {
|
|
1353
|
+
screen.query =
|
|
1354
|
+
screen.query.slice(0, screen.cursor) + printable + screen.query.slice(screen.cursor);
|
|
1355
|
+
screen.cursor += printable.length;
|
|
1356
|
+
}
|
|
1357
|
+
break;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
render(state);
|
|
1361
|
+
}
|
|
1174
1362
|
function render(state) {
|
|
1175
1363
|
if (state.quitting)
|
|
1176
1364
|
return;
|
|
@@ -1182,6 +1370,9 @@ function render(state) {
|
|
|
1182
1370
|
case "main":
|
|
1183
1371
|
lines = renderMain(state, screen);
|
|
1184
1372
|
break;
|
|
1373
|
+
case "ask":
|
|
1374
|
+
lines = renderAsk(state, screen);
|
|
1375
|
+
break;
|
|
1185
1376
|
case "commands":
|
|
1186
1377
|
lines = renderCommands(state, screen);
|
|
1187
1378
|
break;
|
|
@@ -1283,6 +1474,17 @@ function handleMainKey(key, screen, state, done) {
|
|
|
1283
1474
|
message: "",
|
|
1284
1475
|
});
|
|
1285
1476
|
}
|
|
1477
|
+
else if (group?.name === "ask") {
|
|
1478
|
+
state.stack.push({
|
|
1479
|
+
kind: "ask",
|
|
1480
|
+
query: "",
|
|
1481
|
+
cursor: 0,
|
|
1482
|
+
status: "idle",
|
|
1483
|
+
statusMsg: "",
|
|
1484
|
+
error: "",
|
|
1485
|
+
_timer: null,
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1286
1488
|
else if (group) {
|
|
1287
1489
|
state.stack.push({ kind: "commands", group, cursor: 0, search: "" });
|
|
1288
1490
|
}
|
|
@@ -1762,6 +1964,41 @@ function handleResultsKey(key, screen, state, done) {
|
|
|
1762
1964
|
screen.cursor++;
|
|
1763
1965
|
else if (key === " " || key === "\x1b[C")
|
|
1764
1966
|
expandArray();
|
|
1967
|
+
else if (key === "\r" || key === "\n") {
|
|
1968
|
+
const entry = entries[screen.cursor];
|
|
1969
|
+
if (entry) {
|
|
1970
|
+
const [fieldKey, v] = entry;
|
|
1971
|
+
if (v instanceof ArrayValue) {
|
|
1972
|
+
expandArray();
|
|
1973
|
+
}
|
|
1974
|
+
else {
|
|
1975
|
+
const resolved = resolveRelationsForField(fieldKey, screen.cmd.relations, state.groups);
|
|
1976
|
+
if (resolved.length > 0) {
|
|
1977
|
+
state.stack.push({
|
|
1978
|
+
kind: "followup",
|
|
1979
|
+
sourceGroup: screen.group,
|
|
1980
|
+
item: screen.output.data,
|
|
1981
|
+
relations: resolved,
|
|
1982
|
+
cursor: 0,
|
|
1983
|
+
scroll: 0,
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
else if (key === "f" || key === "F") {
|
|
1990
|
+
const resolved = resolveAllRelations(screen.cmd.relations, state.groups);
|
|
1991
|
+
if (resolved.length > 0) {
|
|
1992
|
+
state.stack.push({
|
|
1993
|
+
kind: "followup",
|
|
1994
|
+
sourceGroup: screen.group,
|
|
1995
|
+
item: screen.output.data,
|
|
1996
|
+
relations: resolved,
|
|
1997
|
+
cursor: 0,
|
|
1998
|
+
scroll: 0,
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
1765
2002
|
else if (key === "\x1b[D")
|
|
1766
2003
|
state.stack.pop();
|
|
1767
2004
|
else if (key === "\x1b")
|
|
@@ -1851,6 +2088,7 @@ function handleResultsKey(key, screen, state, done) {
|
|
|
1851
2088
|
state.stack.push({
|
|
1852
2089
|
kind: "detail",
|
|
1853
2090
|
group: screen.group,
|
|
2091
|
+
cmd: screen.cmd,
|
|
1854
2092
|
item,
|
|
1855
2093
|
label: "detail",
|
|
1856
2094
|
cursor: 0,
|
|
@@ -1862,17 +2100,8 @@ function handleResultsKey(key, screen, state, done) {
|
|
|
1862
2100
|
case "f":
|
|
1863
2101
|
case "F": {
|
|
1864
2102
|
const item = screen.output.items[screen.cursor];
|
|
1865
|
-
if (item
|
|
1866
|
-
const resolved =
|
|
1867
|
-
for (const rel of screen.cmd.relations) {
|
|
1868
|
-
const tGroup = state.groups.find((g) => g.name === rel.groupName);
|
|
1869
|
-
if (!tGroup)
|
|
1870
|
-
continue;
|
|
1871
|
-
const tCmd = tGroup.commands.find((c) => c.name === rel.commandName);
|
|
1872
|
-
if (!tCmd)
|
|
1873
|
-
continue;
|
|
1874
|
-
resolved.push({ group: tGroup, cmd: tCmd, params: rel.params });
|
|
1875
|
-
}
|
|
2103
|
+
if (item) {
|
|
2104
|
+
const resolved = resolveAllRelations(screen.cmd.relations, state.groups);
|
|
1876
2105
|
if (resolved.length > 0) {
|
|
1877
2106
|
state.stack.push({
|
|
1878
2107
|
kind: "followup",
|
|
@@ -1900,6 +2129,25 @@ function handleResultsKey(key, screen, state, done) {
|
|
|
1900
2129
|
}
|
|
1901
2130
|
render(state);
|
|
1902
2131
|
}
|
|
2132
|
+
function resolveAllRelations(relations, groups) {
|
|
2133
|
+
if (!relations)
|
|
2134
|
+
return [];
|
|
2135
|
+
const out = [];
|
|
2136
|
+
for (const rel of relations) {
|
|
2137
|
+
const tGroup = groups.find((g) => g.name === rel.groupName);
|
|
2138
|
+
if (!tGroup)
|
|
2139
|
+
continue;
|
|
2140
|
+
const tCmd = tGroup.commands.find((c) => c.name === rel.commandName);
|
|
2141
|
+
if (!tCmd)
|
|
2142
|
+
continue;
|
|
2143
|
+
out.push({ group: tGroup, cmd: tCmd, params: rel.params });
|
|
2144
|
+
}
|
|
2145
|
+
return out;
|
|
2146
|
+
}
|
|
2147
|
+
function resolveRelationsForField(fieldName, relations, groups) {
|
|
2148
|
+
const matching = (relations ?? []).filter((rel) => rel.params.some((p) => p.field === fieldName));
|
|
2149
|
+
return resolveAllRelations(matching, groups);
|
|
2150
|
+
}
|
|
1903
2151
|
function handleDetailKey(key, screen, state, done) {
|
|
1904
2152
|
const { rows } = termSize();
|
|
1905
2153
|
const viewH = Math.max(3, rows - 7);
|
|
@@ -1946,9 +2194,59 @@ function handleDetailKey(key, screen, state, done) {
|
|
|
1946
2194
|
}
|
|
1947
2195
|
break;
|
|
1948
2196
|
}
|
|
2197
|
+
case "\r":
|
|
2198
|
+
case "\n": {
|
|
2199
|
+
const entry = entries[screen.cursor];
|
|
2200
|
+
if (entry && typeof screen.item === "object" && screen.item !== null) {
|
|
2201
|
+
const [fieldKey, v] = entry;
|
|
2202
|
+
if (v instanceof ArrayValue) {
|
|
2203
|
+
state.stack.push({
|
|
2204
|
+
kind: "detail",
|
|
2205
|
+
group: screen.group,
|
|
2206
|
+
item: v.items,
|
|
2207
|
+
label: `${fieldKey} (${v.items.length})`,
|
|
2208
|
+
cursor: 0,
|
|
2209
|
+
scroll: 0,
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
else {
|
|
2213
|
+
const item = screen.item;
|
|
2214
|
+
const resolved = resolveRelationsForField(fieldKey, screen.cmd?.relations, state.groups);
|
|
2215
|
+
if (resolved.length > 0) {
|
|
2216
|
+
state.stack.push({
|
|
2217
|
+
kind: "followup",
|
|
2218
|
+
sourceGroup: screen.group,
|
|
2219
|
+
item,
|
|
2220
|
+
relations: resolved,
|
|
2221
|
+
cursor: 0,
|
|
2222
|
+
scroll: 0,
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
break;
|
|
2228
|
+
}
|
|
1949
2229
|
case "\x1b[D":
|
|
1950
2230
|
state.stack.pop();
|
|
1951
2231
|
break;
|
|
2232
|
+
case "f":
|
|
2233
|
+
case "F": {
|
|
2234
|
+
if (typeof screen.item === "object" && screen.item !== null) {
|
|
2235
|
+
const item = screen.item;
|
|
2236
|
+
const resolved = resolveAllRelations(screen.cmd?.relations, state.groups);
|
|
2237
|
+
if (resolved.length > 0) {
|
|
2238
|
+
state.stack.push({
|
|
2239
|
+
kind: "followup",
|
|
2240
|
+
sourceGroup: screen.group,
|
|
2241
|
+
item,
|
|
2242
|
+
relations: resolved,
|
|
2243
|
+
cursor: 0,
|
|
2244
|
+
scroll: 0,
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
break;
|
|
2249
|
+
}
|
|
1952
2250
|
case "m":
|
|
1953
2251
|
case "M":
|
|
1954
2252
|
popToMain(state);
|
|
@@ -2421,6 +2719,9 @@ async function handleKey(key, state, done) {
|
|
|
2421
2719
|
case "main":
|
|
2422
2720
|
handleMainKey(key, screen, state, done);
|
|
2423
2721
|
break;
|
|
2722
|
+
case "ask":
|
|
2723
|
+
await handleAskKey(key, screen, state, done);
|
|
2724
|
+
break;
|
|
2424
2725
|
case "commands":
|
|
2425
2726
|
handleCommandsKey(key, screen, state, done);
|
|
2426
2727
|
break;
|
|
@@ -2520,6 +2821,23 @@ export async function runMainTui(groups, getClient, getAdminClient, opts) {
|
|
|
2520
2821
|
profileInfo: opts?.profileInfo ?? [],
|
|
2521
2822
|
});
|
|
2522
2823
|
}
|
|
2824
|
+
/** Open the TUI directly at the ask screen (natural language search). */
|
|
2825
|
+
export async function runAskTui(groups, getClient, getAdminClient, opts) {
|
|
2826
|
+
return startTui({
|
|
2827
|
+
groups,
|
|
2828
|
+
stack: [
|
|
2829
|
+
{ kind: "main", cursor: 0, scroll: 0, search: "" },
|
|
2830
|
+
{ kind: "ask", query: "", cursor: 0, status: "idle", statusMsg: "", error: "", _timer: null },
|
|
2831
|
+
],
|
|
2832
|
+
getClient,
|
|
2833
|
+
getAdminClient: getAdminClient ??
|
|
2834
|
+
opts?.getAdminClient ??
|
|
2835
|
+
(() => Promise.reject(new Error("No admin client"))),
|
|
2836
|
+
quitting: false,
|
|
2837
|
+
profile: opts?.profile ?? "",
|
|
2838
|
+
profileInfo: opts?.profileInfo ?? [],
|
|
2839
|
+
});
|
|
2840
|
+
}
|
|
2523
2841
|
/**
|
|
2524
2842
|
* Open the TUI directly at a specific group's command list.
|
|
2525
2843
|
* The main menu is placed at the bottom of the stack so `m` always works.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bpmnkit/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
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,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@bpmnkit/api": "0.0.12",
|
|
19
|
-
"@bpmnkit/ascii": "0.0.
|
|
19
|
+
"@bpmnkit/ascii": "0.0.13",
|
|
20
20
|
"@bpmnkit/connector-gen": "0.0.6",
|
|
21
21
|
"@bpmnkit/profiles": "0.0.9"
|
|
22
22
|
},
|