toolcraft-openapi 0.0.151 → 0.0.153

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 (55) hide show
  1. package/dist/composition.json +2 -2
  2. package/node_modules/toolcraft-design/README.md +30 -0
  3. package/node_modules/toolcraft-design/dist/dashboard/terminal-width.d.ts +1 -0
  4. package/node_modules/toolcraft-design/dist/dashboard/terminal-width.js +18 -1
  5. package/node_modules/toolcraft-design/dist/explorer/actions.d.ts +1 -1
  6. package/node_modules/toolcraft-design/dist/explorer/actions.js +22 -4
  7. package/node_modules/toolcraft-design/dist/explorer/filter.js +1 -1
  8. package/node_modules/toolcraft-design/dist/explorer/index.d.ts +2 -4
  9. package/node_modules/toolcraft-design/dist/explorer/index.js +1 -2
  10. package/node_modules/toolcraft-design/dist/explorer/jobs.d.ts +2 -2
  11. package/node_modules/toolcraft-design/dist/explorer/jobs.js +3 -5
  12. package/node_modules/toolcraft-design/dist/explorer/keymap.d.ts +13 -3
  13. package/node_modules/toolcraft-design/dist/explorer/keymap.js +89 -348
  14. package/node_modules/toolcraft-design/dist/explorer/layout.d.ts +1 -0
  15. package/node_modules/toolcraft-design/dist/explorer/layout.js +14 -4
  16. package/node_modules/toolcraft-design/dist/explorer/reducer.js +58 -9
  17. package/node_modules/toolcraft-design/dist/explorer/render/detail.d.ts +1 -1
  18. package/node_modules/toolcraft-design/dist/explorer/render/detail.js +43 -7
  19. package/node_modules/toolcraft-design/dist/explorer/render/footer.d.ts +1 -1
  20. package/node_modules/toolcraft-design/dist/explorer/render/footer.js +6 -3
  21. package/node_modules/toolcraft-design/dist/explorer/render/header.d.ts +1 -1
  22. package/node_modules/toolcraft-design/dist/explorer/render/header.js +6 -2
  23. package/node_modules/toolcraft-design/dist/explorer/render/index.d.ts +1 -1
  24. package/node_modules/toolcraft-design/dist/explorer/render/index.js +2 -1
  25. package/node_modules/toolcraft-design/dist/explorer/render/list.d.ts +16 -2
  26. package/node_modules/toolcraft-design/dist/explorer/render/list.js +13 -34
  27. package/node_modules/toolcraft-design/dist/explorer/render/modal.d.ts +1 -1
  28. package/node_modules/toolcraft-design/dist/explorer/render/pane.d.ts +5 -2
  29. package/node_modules/toolcraft-design/dist/explorer/render/pane.js +9 -6
  30. package/node_modules/toolcraft-design/dist/explorer/render/test-fixtures.js +2 -3
  31. package/node_modules/toolcraft-design/dist/explorer/runtime.js +68 -63
  32. package/node_modules/toolcraft-design/dist/explorer/runtime.test-helpers.d.ts +23 -37
  33. package/node_modules/toolcraft-design/dist/explorer/runtime.test-helpers.js +32 -83
  34. package/node_modules/toolcraft-design/dist/explorer/state.d.ts +47 -5
  35. package/node_modules/toolcraft-design/dist/explorer/state.js +56 -12
  36. package/node_modules/toolcraft-design/dist/index.d.ts +8 -2
  37. package/node_modules/toolcraft-design/dist/index.js +5 -1
  38. package/node_modules/toolcraft-design/dist/screen/ansi-text.d.ts +2 -0
  39. package/node_modules/toolcraft-design/dist/screen/ansi-text.js +35 -0
  40. package/node_modules/toolcraft-design/dist/screen/screen.d.ts +40 -0
  41. package/node_modules/toolcraft-design/dist/screen/screen.js +122 -0
  42. package/node_modules/toolcraft-design/dist/screen/style.d.ts +16 -0
  43. package/node_modules/toolcraft-design/dist/screen/style.js +55 -0
  44. package/node_modules/toolcraft-design/dist/terminal/driver.d.ts +33 -0
  45. package/node_modules/toolcraft-design/dist/terminal/driver.js +61 -0
  46. package/node_modules/toolcraft-design/dist/terminal/input.d.ts +25 -0
  47. package/node_modules/toolcraft-design/dist/terminal/input.js +181 -0
  48. package/node_modules/toolcraft-design/dist/terminal/output.d.ts +8 -0
  49. package/node_modules/toolcraft-design/dist/terminal/output.js +42 -0
  50. package/node_modules/toolcraft-schema/package.json +1 -1
  51. package/package.json +3 -3
  52. package/node_modules/toolcraft-design/dist/explorer/two-pane.d.ts +0 -102
  53. package/node_modules/toolcraft-design/dist/explorer/two-pane.js +0 -510
  54. package/node_modules/toolcraft-design/dist/run-two-pane-explorer.d.ts +0 -2
  55. package/node_modules/toolcraft-design/dist/run-two-pane-explorer.js +0 -1
@@ -1,510 +0,0 @@
1
- import { ScreenBuffer, cellToAnsi } from "../dashboard/buffer.js";
2
- import { createTerminalDriver } from "../dashboard/terminal.js";
3
- const TOAST_MS = 2500;
4
- export async function runTwoPaneExplorer(config) {
5
- if (process.stdout.isTTY !== true) {
6
- throw new Error("two-pane explorer requires a TTY");
7
- }
8
- return new TwoPaneExplorerRuntime(config, createTerminalDriver()).run();
9
- }
10
- export class TwoPaneExplorerRuntime {
11
- config;
12
- driver;
13
- state;
14
- unsubscribeKeypress;
15
- unsubscribeResize;
16
- toastTimer;
17
- stopped = false;
18
- rowsRequestToken = 0;
19
- settle;
20
- constructor(config, driver) {
21
- this.config = config;
22
- this.driver = driver;
23
- const size = normalizeSize(driver.getSize());
24
- this.state = {
25
- title: config.title,
26
- panes: [
27
- initialPaneState(config.panes[0]),
28
- initialPaneState(config.panes[1])
29
- ],
30
- activePaneIndex: 0,
31
- filterFocused: false,
32
- toast: null,
33
- size
34
- };
35
- }
36
- run() {
37
- return new Promise((resolve, reject) => {
38
- this.settle = { resolve, reject };
39
- try {
40
- this.startTerminal();
41
- this.render();
42
- this.loadRows().catch((error) => this.fail(error));
43
- }
44
- catch (error) {
45
- this.fail(error);
46
- }
47
- });
48
- }
49
- startTerminal() {
50
- this.driver.enterRawMode();
51
- this.driver.enterAltScreen();
52
- this.driver.disableLineWrap();
53
- this.driver.hideCursor();
54
- this.subscribeKeypress();
55
- this.unsubscribeResize = this.driver.onResize(() => {
56
- this.state = { ...this.state, size: normalizeSize(this.driver.getSize()) };
57
- this.render();
58
- });
59
- }
60
- subscribeKeypress() {
61
- this.unsubscribeKeypress = this.driver.onKeypress((key) => {
62
- this.dispatchKey(key);
63
- });
64
- }
65
- pauseKeypress() {
66
- this.unsubscribeKeypress?.();
67
- this.unsubscribeKeypress = undefined;
68
- }
69
- async loadRows(requestToken = ++this.rowsRequestToken) {
70
- const [leftRows, rightRows] = await Promise.all([
71
- this.config.panes[0].rows(),
72
- this.config.panes[1].rows()
73
- ]);
74
- if (requestToken !== this.rowsRequestToken) {
75
- return;
76
- }
77
- this.state = {
78
- ...this.state,
79
- panes: [
80
- rowsLoaded(this.state.panes[0], leftRows),
81
- rowsLoaded(this.state.panes[1], rightRows)
82
- ]
83
- };
84
- this.render();
85
- }
86
- async refresh() {
87
- await this.config.refresh?.();
88
- await this.loadRows(++this.rowsRequestToken);
89
- }
90
- dispatchKey(key) {
91
- if (this.stopped) {
92
- return;
93
- }
94
- this.trace("key", {
95
- key: traceKey(key),
96
- filterFocused: this.state.filterFocused,
97
- pane: this.activePane().id,
98
- cursor: this.activePane().cursor,
99
- selected: this.activePane().selected.size,
100
- filter: this.activePane().filter
101
- });
102
- if (this.state.filterFocused) {
103
- this.dispatchFilterKey(key);
104
- return;
105
- }
106
- if (isQuitKey(key)) {
107
- this.exit(null);
108
- return;
109
- }
110
- if (isTabKey(key)) {
111
- this.state = {
112
- ...this.state,
113
- activePaneIndex: this.state.activePaneIndex === 0 ? 1 : 0
114
- };
115
- this.render();
116
- return;
117
- }
118
- if (key.name === "up" || key.name === "down") {
119
- this.moveCursor(key.name === "up" ? -1 : 1);
120
- return;
121
- }
122
- if (key.name === "home") {
123
- this.setCursor(0);
124
- return;
125
- }
126
- if (key.name === "end") {
127
- this.setCursor(filteredRows(this.activePane()).length - 1);
128
- return;
129
- }
130
- if (key.ch === " " || key.name === "space") {
131
- this.toggleSelection();
132
- return;
133
- }
134
- if (key.ch === "/") {
135
- this.state = {
136
- ...this.state,
137
- filterFocused: true,
138
- panes: updateActivePane(this.state, (pane) => ({ ...pane, filter: "", cursor: 0 }))
139
- };
140
- this.render();
141
- return;
142
- }
143
- const action = this.config.actions.find((candidate) => actionMatchesKey(candidate, key));
144
- if (action !== undefined) {
145
- this.runAction(action);
146
- }
147
- }
148
- dispatchFilterKey(key) {
149
- if (key.name === "escape" || key.name === "return" || key.name === "enter") {
150
- this.state = { ...this.state, filterFocused: false };
151
- this.trace("filter.submit", {
152
- key: traceKey(key),
153
- pane: this.activePane().id,
154
- filter: this.activePane().filter,
155
- rows: filteredRows(this.activePane()).length
156
- });
157
- this.render();
158
- return;
159
- }
160
- if (key.name === "backspace") {
161
- this.state = {
162
- ...this.state,
163
- panes: updateActivePane(this.state, (pane) => ({
164
- ...pane,
165
- filter: Array.from(pane.filter).slice(0, -1).join(""),
166
- cursor: 0
167
- }))
168
- };
169
- this.trace("filter.update", {
170
- key: traceKey(key),
171
- pane: this.activePane().id,
172
- filter: this.activePane().filter,
173
- rows: filteredRows(this.activePane()).length
174
- });
175
- this.render();
176
- return;
177
- }
178
- if (key.ch !== undefined && key.ch.length > 0 && key.ch !== "/") {
179
- this.state = {
180
- ...this.state,
181
- panes: updateActivePane(this.state, (pane) => ({
182
- ...pane,
183
- filter: `${pane.filter}${key.ch}`,
184
- cursor: 0
185
- }))
186
- };
187
- this.trace("filter.update", {
188
- key: traceKey(key),
189
- pane: this.activePane().id,
190
- filter: this.activePane().filter,
191
- rows: filteredRows(this.activePane()).length
192
- });
193
- this.render();
194
- }
195
- }
196
- moveCursor(delta) {
197
- const pane = this.activePane();
198
- const rows = filteredRows(pane);
199
- if (rows.length === 0) {
200
- return;
201
- }
202
- this.setCursor(Math.max(0, Math.min(rows.length - 1, pane.cursor + delta)));
203
- }
204
- setCursor(cursor) {
205
- this.state = {
206
- ...this.state,
207
- panes: updateActivePane(this.state, (pane) => ({
208
- ...pane,
209
- cursor: Math.max(0, cursor)
210
- }))
211
- };
212
- this.render();
213
- }
214
- toggleSelection() {
215
- const pane = this.activePane();
216
- const row = currentRow(pane);
217
- if (row === undefined) {
218
- return;
219
- }
220
- const selected = new Set(pane.selected);
221
- if (selected.has(row.id)) {
222
- selected.delete(row.id);
223
- }
224
- else {
225
- selected.add(row.id);
226
- }
227
- this.state = {
228
- ...this.state,
229
- panes: updateActivePane(this.state, (candidate) => ({ ...candidate, selected }))
230
- };
231
- this.trace("selection.toggle", {
232
- pane: pane.id,
233
- row: row.id,
234
- selected: selected.size,
235
- checked: selected.has(row.id),
236
- filter: pane.filter,
237
- cursor: pane.cursor
238
- });
239
- this.render();
240
- }
241
- runAction(action) {
242
- const activePane = this.activePane();
243
- const inactivePane = this.inactivePane();
244
- const row = currentRow(activePane);
245
- if (row === undefined) {
246
- this.showToast("Select an item first", "warning");
247
- return;
248
- }
249
- const selectedRows = activePane.rows.filter((candidate) => activePane.selected.has(candidate.id));
250
- const context = {
251
- activePane,
252
- inactivePane,
253
- row,
254
- rows: selectedRows.length > 0 ? selectedRows : [row],
255
- refresh: async () => this.refresh(),
256
- suspendAnd: async (fn) => this.suspendAnd(fn),
257
- toast: (message, tone) => this.showToast(message, tone),
258
- exit: (result) => this.exit(result ?? null)
259
- };
260
- Promise.resolve()
261
- .then(() => action.handler(context))
262
- .catch((error) => {
263
- this.showToast(error instanceof Error ? error.message : "Action failed", "error");
264
- });
265
- }
266
- async suspendAnd(fn) {
267
- this.pauseKeypress();
268
- this.driver.exitAltScreen();
269
- this.driver.enableLineWrap();
270
- this.driver.showCursor();
271
- this.driver.exitRawMode();
272
- try {
273
- return await fn();
274
- }
275
- finally {
276
- if (!this.stopped) {
277
- this.driver.enterRawMode();
278
- this.driver.enterAltScreen();
279
- this.driver.disableLineWrap();
280
- this.driver.hideCursor();
281
- this.subscribeKeypress();
282
- this.state = { ...this.state, size: normalizeSize(this.driver.getSize()) };
283
- this.render();
284
- }
285
- }
286
- }
287
- showToast(message, tone = "info") {
288
- if (this.toastTimer !== undefined) {
289
- clearTimeout(this.toastTimer);
290
- }
291
- this.state = { ...this.state, toast: { message, tone } };
292
- this.render();
293
- this.toastTimer = setTimeout(() => {
294
- this.state = { ...this.state, toast: null };
295
- this.render();
296
- }, TOAST_MS);
297
- }
298
- activePane() {
299
- return this.state.panes[this.state.activePaneIndex];
300
- }
301
- inactivePane() {
302
- return this.state.panes[this.state.activePaneIndex === 0 ? 1 : 0];
303
- }
304
- render() {
305
- if (this.stopped) {
306
- return;
307
- }
308
- const size = normalizeSize(this.driver.getSize());
309
- if (size.cols !== this.state.size.cols || size.rows !== this.state.size.rows) {
310
- this.state = { ...this.state, size };
311
- }
312
- const next = new ScreenBuffer(this.state.size.cols, this.state.size.rows);
313
- renderTwoPaneExplorer(this.state, this.config.actions, next);
314
- this.driver.write(screenToAnsi(next));
315
- }
316
- exit(result) {
317
- if (this.stopped) {
318
- return;
319
- }
320
- this.stopped = true;
321
- this.unsubscribeKeypress?.();
322
- this.unsubscribeResize?.();
323
- if (this.toastTimer !== undefined) {
324
- clearTimeout(this.toastTimer);
325
- }
326
- this.driver.destroy();
327
- this.settle?.resolve(result);
328
- }
329
- fail(error) {
330
- if (!this.stopped) {
331
- this.stopped = true;
332
- this.driver.destroy();
333
- }
334
- this.settle?.reject(error);
335
- }
336
- trace(event, fields = {}) {
337
- const trace = this.config.trace;
338
- if (trace === undefined) {
339
- return;
340
- }
341
- void Promise.resolve(trace({ event, ...fields })).catch(() => {
342
- // Diagnostic tracing must not interfere with TUI input handling.
343
- });
344
- }
345
- }
346
- export function renderTwoPaneExplorer(state, actions, screen) {
347
- screen.clear();
348
- if (state.size.cols < 60 || state.size.rows < 8) {
349
- screen.put(0, 0, fit(` ${state.title} `, state.size.cols), { bold: true });
350
- screen.put(0, 2, fit("Terminal is too small for two-pane view.", state.size.cols));
351
- screen.put(0, state.size.rows - 1, fit("q quit", state.size.cols), { dim: true });
352
- return;
353
- }
354
- const gap = 1;
355
- const paneWidth = Math.floor((state.size.cols - gap) / 2);
356
- const rightWidth = state.size.cols - paneWidth - gap;
357
- const paneHeight = Math.max(5, state.size.rows - 2);
358
- renderPane(screen, state.panes[0], {
359
- x: 0,
360
- y: 0,
361
- width: paneWidth,
362
- height: paneHeight,
363
- active: state.activePaneIndex === 0
364
- });
365
- renderPane(screen, state.panes[1], {
366
- x: paneWidth + gap,
367
- y: 0,
368
- width: rightWidth,
369
- height: paneHeight,
370
- active: state.activePaneIndex === 1
371
- });
372
- renderFooter(screen, state, actions, state.size.rows - 2);
373
- if (state.toast !== null) {
374
- screen.put(0, state.size.rows - 1, fit(` ${state.toast.message} `, state.size.cols), toneStyle(state.toast.tone));
375
- }
376
- }
377
- function renderPane(screen, pane, layout) {
378
- const borderStyle = layout.active ? { bold: true, fg: "cyan" } : { dim: true };
379
- const title = `${layout.active ? ">" : " "} ${pane.title}`;
380
- screen.put(layout.x, layout.y, `+${fit(title, layout.width - 2, "-")}+`, borderStyle);
381
- for (let y = 1; y < layout.height - 1; y += 1) {
382
- screen.put(layout.x, layout.y + y, "|", borderStyle);
383
- screen.put(layout.x + layout.width - 1, layout.y + y, "|", borderStyle);
384
- }
385
- const filterLabel = pane.filter.length > 0 ? ` /${pane.filter}` : "";
386
- screen.put(layout.x, layout.y + layout.height - 1, `+${fit(`${pane.selected.size} selected${filterLabel}`, layout.width - 2, "-")}+`, borderStyle);
387
- const visibleRows = filteredRows(pane);
388
- const bodyHeight = layout.height - 2;
389
- if (visibleRows.length === 0) {
390
- screen.put(layout.x + 2, layout.y + 2, fit(pane.emptyHint, Math.max(0, layout.width - 4)), { dim: true });
391
- return;
392
- }
393
- const start = Math.max(0, Math.min(pane.cursor - Math.floor(bodyHeight / 2), visibleRows.length - bodyHeight));
394
- for (let index = 0; index < bodyHeight; index += 1) {
395
- const row = visibleRows[start + index];
396
- if (row === undefined) {
397
- continue;
398
- }
399
- const selected = pane.selected.has(row.id);
400
- const cursor = start + index === pane.cursor;
401
- const marker = selected ? "*" : " ";
402
- const pointer = cursor && layout.active ? ">" : " ";
403
- const badge = row.badge ? ` [${row.badge.text}]` : "";
404
- const subtitle = row.subtitle ? ` ${row.subtitle}` : "";
405
- const style = cursor && layout.active ? { inverse: true } : {};
406
- screen.put(layout.x + 1, layout.y + 1 + index, fit(`${pointer}${marker} ${row.title}${badge}${subtitle}`, layout.width - 2), style);
407
- }
408
- }
409
- function renderFooter(screen, state, actions, y) {
410
- const actionHelp = actions.map((action) => `${firstKey(action.key)} ${action.label}`).join(" ");
411
- const filter = state.filterFocused ? "search: typing, enter done" : "/ search";
412
- const text = `tab pane ${filter} space select ${actionHelp} q quit`;
413
- screen.put(0, y, fit(text, state.size.cols), { dim: true });
414
- }
415
- function initialPaneState(definition) {
416
- return {
417
- id: definition.id,
418
- title: definition.title,
419
- rows: [],
420
- cursor: 0,
421
- selected: new Set(),
422
- filter: "",
423
- emptyHint: definition.emptyHint ?? "No items"
424
- };
425
- }
426
- function rowsLoaded(pane, rows) {
427
- const ids = new Set(rows.map((row) => row.id));
428
- return {
429
- ...pane,
430
- rows,
431
- selected: new Set([...pane.selected].filter((id) => ids.has(id))),
432
- cursor: Math.max(0, Math.min(pane.cursor, Math.max(0, filteredRows({ ...pane, rows }).length - 1)))
433
- };
434
- }
435
- function filteredRows(pane) {
436
- const query = pane.filter.trim().toLowerCase();
437
- if (query.length === 0) {
438
- return pane.rows;
439
- }
440
- return pane.rows.filter((row) => {
441
- const haystack = `${row.title} ${row.subtitle ?? ""} ${row.badge?.text ?? ""}`.toLowerCase();
442
- return haystack.includes(query);
443
- });
444
- }
445
- function currentRow(pane) {
446
- return filteredRows(pane)[pane.cursor];
447
- }
448
- function updateActivePane(state, update) {
449
- return state.activePaneIndex === 0
450
- ? [update(state.panes[0]), state.panes[1]]
451
- : [state.panes[0], update(state.panes[1])];
452
- }
453
- function actionMatchesKey(action, key) {
454
- const keys = Array.isArray(action.key) ? action.key : [action.key];
455
- return keys.some((candidate) => key.ch === candidate || key.name === candidate);
456
- }
457
- function isTabKey(key) {
458
- return key.name === "tab" || key.ch === "\t" || (key.name === "i" && key.ctrl);
459
- }
460
- function firstKey(key) {
461
- return Array.isArray(key) ? key[0] ?? "" : key;
462
- }
463
- function traceKey(key) {
464
- return {
465
- name: key.name,
466
- ch: key.ch,
467
- ctrl: key.ctrl,
468
- meta: key.meta,
469
- shift: key.shift
470
- };
471
- }
472
- function isQuitKey(key) {
473
- return key.ch === "q" || (key.name === "c" && key.ctrl);
474
- }
475
- function normalizeSize(size) {
476
- return {
477
- cols: Math.max(0, Math.floor(size.cols)),
478
- rows: Math.max(0, Math.floor(size.rows))
479
- };
480
- }
481
- function fit(text, width, pad = " ") {
482
- if (width <= 0) {
483
- return "";
484
- }
485
- const normalized = text.length > width ? text.slice(0, width) : text;
486
- return normalized.padEnd(width, pad);
487
- }
488
- function toneStyle(tone) {
489
- if (tone === "success") {
490
- return { fg: "green", bold: true };
491
- }
492
- if (tone === "warning") {
493
- return { fg: "yellow", bold: true };
494
- }
495
- if (tone === "error") {
496
- return { fg: "red", bold: true };
497
- }
498
- return tone === "muted" ? { dim: true } : { fg: "cyan", bold: true };
499
- }
500
- function screenToAnsi(screen) {
501
- let output = "";
502
- for (let y = 0; y < screen.height; y += 1) {
503
- output += `\u001b[${y + 1};1H`;
504
- for (let x = 0; x < screen.width; x += 1) {
505
- const cell = screen.get(x, y);
506
- output += cellToAnsi(cell);
507
- }
508
- }
509
- return output;
510
- }
@@ -1,2 +0,0 @@
1
- export { runTwoPaneExplorer } from "./explorer/index.js";
2
- export type { TwoPaneExplorerConfig } from "./explorer/index.js";
@@ -1 +0,0 @@
1
- export { runTwoPaneExplorer } from "./explorer/index.js";