toolcraft-openapi 0.0.24 → 0.0.26
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/bin/generate.d.ts +4 -3
- package/dist/bin/generate.js +12 -3
- package/node_modules/@poe-code/design-system/dist/components/browser.js +1 -1
- package/node_modules/@poe-code/design-system/dist/explorer/actions.js +1 -1
- package/node_modules/@poe-code/design-system/dist/explorer/keymap.js +11 -1
- package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +64 -8
- package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +9 -11
- package/node_modules/@poe-code/design-system/dist/explorer/render/footer.js +18 -8
- package/node_modules/@poe-code/design-system/dist/explorer/render/header.js +11 -18
- package/node_modules/@poe-code/design-system/dist/explorer/render/index.js +2 -10
- package/node_modules/@poe-code/design-system/dist/explorer/render/list.js +32 -22
- package/node_modules/@poe-code/design-system/dist/explorer/render/modal.js +5 -9
- package/node_modules/@poe-code/design-system/dist/explorer/render/text.d.ts +12 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/text.js +81 -0
- package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/explorer/state.js +2 -0
- package/node_modules/@poe-code/design-system/dist/prompts/index.js +3 -3
- package/node_modules/auth-store/dist/encrypted-file-store.d.ts +1 -0
- package/node_modules/auth-store/dist/encrypted-file-store.js +36 -2
- package/node_modules/auth-store/dist/keychain-store.js +20 -1
- package/package.json +2 -2
package/dist/bin/generate.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
interface GenerateCliFileSystem {
|
|
3
|
+
lstat(targetPath: string): Promise<{
|
|
4
|
+
isDirectory(): boolean;
|
|
5
|
+
isSymbolicLink(): boolean;
|
|
6
|
+
}>;
|
|
3
7
|
mkdir(directoryPath: string, options?: {
|
|
4
8
|
recursive?: boolean;
|
|
5
9
|
}): Promise<unknown>;
|
|
@@ -9,9 +13,6 @@ interface GenerateCliFileSystem {
|
|
|
9
13
|
force?: boolean;
|
|
10
14
|
}): Promise<void>;
|
|
11
15
|
realpath(targetPath: string): Promise<string>;
|
|
12
|
-
stat(targetPath: string): Promise<{
|
|
13
|
-
isDirectory(): boolean;
|
|
14
|
-
}>;
|
|
15
16
|
writeFile(filePath: string, contents: string, encoding: BufferEncoding): Promise<void>;
|
|
16
17
|
}
|
|
17
18
|
interface GenerateCliWriter {
|
package/dist/bin/generate.js
CHANGED
|
@@ -81,7 +81,7 @@ export async function syncGeneratedClient(options, services) {
|
|
|
81
81
|
if (!options.check && drifted) {
|
|
82
82
|
try {
|
|
83
83
|
await writeGeneratedFiles(services.fs, outputDir, updatedFiles);
|
|
84
|
-
await deleteGeneratedFiles(services.fs, deletedFiles);
|
|
84
|
+
await deleteGeneratedFiles(services.fs, outputDir, deletedFiles);
|
|
85
85
|
}
|
|
86
86
|
catch (error) {
|
|
87
87
|
await restoreGeneratedFiles(services.fs, outputDir, currentFiles, updatedFiles, deletedFiles);
|
|
@@ -170,10 +170,17 @@ function tryParseUrl(input) {
|
|
|
170
170
|
async function readGeneratedFiles(fs, directoryPath) {
|
|
171
171
|
const files = new Map();
|
|
172
172
|
try {
|
|
173
|
+
const directoryStats = await fs.lstat(directoryPath);
|
|
174
|
+
if (directoryStats.isSymbolicLink()) {
|
|
175
|
+
throw new Error("Generated output must remain inside the output directory.");
|
|
176
|
+
}
|
|
173
177
|
const entries = await fs.readdir(directoryPath);
|
|
174
178
|
for (const entry of entries) {
|
|
175
179
|
const entryPath = path.resolve(directoryPath, entry);
|
|
176
|
-
const stats = await fs.
|
|
180
|
+
const stats = await fs.lstat(entryPath);
|
|
181
|
+
if (stats.isSymbolicLink()) {
|
|
182
|
+
throw new Error("Generated output must remain inside the output directory.");
|
|
183
|
+
}
|
|
177
184
|
if (stats.isDirectory()) {
|
|
178
185
|
for (const [nestedPath, nestedContents] of await readGeneratedFiles(fs, entryPath)) {
|
|
179
186
|
files.set(nestedPath, nestedContents);
|
|
@@ -228,8 +235,9 @@ async function assertSafeOutputPath(fs, outputDir, filePath) {
|
|
|
228
235
|
throw new Error("Generated output must remain inside the output directory.");
|
|
229
236
|
}
|
|
230
237
|
}
|
|
231
|
-
async function deleteGeneratedFiles(fs, filePaths) {
|
|
238
|
+
async function deleteGeneratedFiles(fs, outputDir, filePaths) {
|
|
232
239
|
for (const filePath of filePaths) {
|
|
240
|
+
await assertSafeOutputPath(fs, outputDir, filePath);
|
|
233
241
|
await fs.rm(filePath, { force: true });
|
|
234
242
|
}
|
|
235
243
|
}
|
|
@@ -237,6 +245,7 @@ async function restoreGeneratedFiles(fs, outputDir, currentFiles, updatedFiles,
|
|
|
237
245
|
for (const file of updatedFiles) {
|
|
238
246
|
const previousContents = currentFiles.get(file.path);
|
|
239
247
|
if (previousContents === undefined) {
|
|
248
|
+
await assertSafeOutputPath(fs, outputDir, file.path);
|
|
240
249
|
await fs.rm(file.path, { force: true });
|
|
241
250
|
continue;
|
|
242
251
|
}
|
|
@@ -10,7 +10,7 @@ function browserCommand(url, platform) {
|
|
|
10
10
|
return { command: "open", args: [url] };
|
|
11
11
|
}
|
|
12
12
|
if (platform === "win32") {
|
|
13
|
-
return { command: "
|
|
13
|
+
return { command: "rundll32.exe", args: ["url.dll,FileProtocolHandler", url] };
|
|
14
14
|
}
|
|
15
15
|
return { command: "xdg-open", args: [url] };
|
|
16
16
|
}
|
|
@@ -32,7 +32,7 @@ function currentDetailItem(state) {
|
|
|
32
32
|
return state.detail.items?.[state.detail.cursor];
|
|
33
33
|
}
|
|
34
34
|
function selectedRows(state, fallback) {
|
|
35
|
-
if (state.selected.size === 0) {
|
|
35
|
+
if (!state.multiSelect || state.selected.size === 0) {
|
|
36
36
|
return fallback.id === "" ? [] : [fallback];
|
|
37
37
|
}
|
|
38
38
|
return state.rows.filter((row) => state.selected.has(row.id));
|
|
@@ -45,14 +45,24 @@ const baseBuiltinCommands = [
|
|
|
45
45
|
"extendSelectionDown"
|
|
46
46
|
];
|
|
47
47
|
const reorderCommands = ["reorderUp", "reorderDown"];
|
|
48
|
+
const selectionCommands = new Set([
|
|
49
|
+
"toggleSelect",
|
|
50
|
+
"selectAll",
|
|
51
|
+
"clearSelection",
|
|
52
|
+
"extendSelectionUp",
|
|
53
|
+
"extendSelectionDown"
|
|
54
|
+
]);
|
|
48
55
|
const reservedActionIds = new Set(["quit"]);
|
|
49
56
|
export function resolveBindings(config, defaults = {}) {
|
|
50
|
-
const
|
|
57
|
+
const baseCommands = config.reorder === undefined
|
|
51
58
|
? baseBuiltinCommands
|
|
52
59
|
: [
|
|
53
60
|
...baseBuiltinCommands.filter((command) => command !== "extendSelectionUp" && command !== "extendSelectionDown"),
|
|
54
61
|
...reorderCommands
|
|
55
62
|
];
|
|
63
|
+
const commands = config.multiSelect === false
|
|
64
|
+
? baseCommands.filter((command) => !selectionCommands.has(command))
|
|
65
|
+
: baseCommands;
|
|
56
66
|
const commandBindings = new Map();
|
|
57
67
|
const flatBindings = new Map();
|
|
58
68
|
const targetKeys = new Map();
|
|
@@ -101,6 +101,9 @@ function stepKey(state, key, runtimeHandles) {
|
|
|
101
101
|
if (isBackspace(key)) {
|
|
102
102
|
return updateFilter(state, state.filter.slice(0, -1));
|
|
103
103
|
}
|
|
104
|
+
if (!state.multiSelect && isSelectionSpace(key)) {
|
|
105
|
+
return mark(state, 0);
|
|
106
|
+
}
|
|
104
107
|
if (isPrintable(key)) {
|
|
105
108
|
return updateFilter(state, `${state.filter}${key.ch}`);
|
|
106
109
|
}
|
|
@@ -170,7 +173,7 @@ function resize(state, cols, rows) {
|
|
|
170
173
|
return mark(state, 0);
|
|
171
174
|
}
|
|
172
175
|
return {
|
|
173
|
-
state: { ...state, size, layout, dirty: REGION_ALL },
|
|
176
|
+
state: clampDetailScroll({ ...state, size, layout, dirty: REGION_ALL }),
|
|
174
177
|
effects: NO_EFFECTS
|
|
175
178
|
};
|
|
176
179
|
}
|
|
@@ -186,7 +189,7 @@ function rowsLoaded(state, rows) {
|
|
|
186
189
|
const filtered = matches.map((match) => match.index);
|
|
187
190
|
const matchPositions = createMatchPositions(matches);
|
|
188
191
|
const cursor = clamp(state.cursor, 0, Math.max(0, filtered.length - 1));
|
|
189
|
-
const selected = pruneSelection(state.selected, rows);
|
|
192
|
+
const selected = state.multiSelect ? pruneSelection(state.selected, rows) : new Set();
|
|
190
193
|
const detail = resetDetailForCursor(state, rows, filtered, cursor);
|
|
191
194
|
const modal = modalStillValid(state.modal, rows);
|
|
192
195
|
if (state.modal?.kind === "confirm" && modal === null) {
|
|
@@ -239,8 +242,9 @@ function detailItemRendered(state, rowId, token, itemIndex, content) {
|
|
|
239
242
|
return mark(state, 0);
|
|
240
243
|
}
|
|
241
244
|
const items = state.detail.items.map((item, index) => index === itemIndex ? { ...item, renderedContent: content } : item);
|
|
245
|
+
const detail = { ...state.detail, items };
|
|
242
246
|
return {
|
|
243
|
-
state: { ...state, detail
|
|
247
|
+
state: clampDetailScroll({ ...state, detail, dirty: REGION_DETAIL }),
|
|
244
248
|
effects: NO_EFFECTS
|
|
245
249
|
};
|
|
246
250
|
}
|
|
@@ -400,6 +404,9 @@ function confirmKey(state, runtimeHandles) {
|
|
|
400
404
|
return dispatchPrimary(state, runtimeHandles);
|
|
401
405
|
}
|
|
402
406
|
function toggleSelect(state) {
|
|
407
|
+
if (!state.multiSelect) {
|
|
408
|
+
return mark(state, 0);
|
|
409
|
+
}
|
|
403
410
|
const row = currentRow(state);
|
|
404
411
|
if (row === undefined) {
|
|
405
412
|
return mark(state, 0);
|
|
@@ -414,6 +421,9 @@ function toggleSelect(state) {
|
|
|
414
421
|
return selectionChanged(state, selected);
|
|
415
422
|
}
|
|
416
423
|
function selectAll(state) {
|
|
424
|
+
if (!state.multiSelect) {
|
|
425
|
+
return mark(state, 0);
|
|
426
|
+
}
|
|
417
427
|
const selected = new Set(state.selected);
|
|
418
428
|
for (const index of state.filtered) {
|
|
419
429
|
const row = state.rows[index];
|
|
@@ -430,13 +440,14 @@ function clearSelection(state) {
|
|
|
430
440
|
return selectionChanged(state, new Set());
|
|
431
441
|
}
|
|
432
442
|
function selectionChanged(state, selected) {
|
|
433
|
-
|
|
443
|
+
const normalized = state.multiSelect ? selected : new Set();
|
|
444
|
+
if (setsEqual(state.selected, normalized)) {
|
|
434
445
|
return mark(state, 0);
|
|
435
446
|
}
|
|
436
447
|
const next = {
|
|
437
448
|
...state,
|
|
438
|
-
selected,
|
|
439
|
-
actionState: recomputeActionState({ ...state, selected }),
|
|
449
|
+
selected: normalized,
|
|
450
|
+
actionState: recomputeActionState({ ...state, selected: normalized }),
|
|
440
451
|
dirty: REGION_LIST | REGION_FOOTER
|
|
441
452
|
};
|
|
442
453
|
return { state: next, effects: NO_EFFECTS };
|
|
@@ -445,7 +456,7 @@ function detailScroll(state, delta) {
|
|
|
445
456
|
if (state.focused !== "detail") {
|
|
446
457
|
return mark(state, 0);
|
|
447
458
|
}
|
|
448
|
-
const scroll =
|
|
459
|
+
const scroll = clamp(state.detail.scroll + delta, 0, maxDetailScroll(state));
|
|
449
460
|
if (scroll === state.detail.scroll) {
|
|
450
461
|
return mark(state, 0);
|
|
451
462
|
}
|
|
@@ -454,7 +465,49 @@ function detailScroll(state, delta) {
|
|
|
454
465
|
effects: NO_EFFECTS
|
|
455
466
|
};
|
|
456
467
|
}
|
|
468
|
+
function clampDetailScroll(state) {
|
|
469
|
+
const scroll = clamp(state.detail.scroll, 0, maxDetailScroll(state));
|
|
470
|
+
if (scroll === state.detail.scroll) {
|
|
471
|
+
return state;
|
|
472
|
+
}
|
|
473
|
+
return { ...state, detail: { ...state.detail, scroll } };
|
|
474
|
+
}
|
|
475
|
+
function maxDetailScroll(state) {
|
|
476
|
+
const items = state.detail.items;
|
|
477
|
+
if (items === null || items.length === 0) {
|
|
478
|
+
return 0;
|
|
479
|
+
}
|
|
480
|
+
if (items.length === 1 && items[0]?.title === undefined) {
|
|
481
|
+
const visibleHeight = detailBodyHeight(state);
|
|
482
|
+
if (visibleHeight <= 0) {
|
|
483
|
+
return 0;
|
|
484
|
+
}
|
|
485
|
+
return Math.max(0, detailContentLineCount(items[0]) - visibleHeight);
|
|
486
|
+
}
|
|
487
|
+
return Math.max(0, items.length - 1);
|
|
488
|
+
}
|
|
489
|
+
function detailContentLineCount(item) {
|
|
490
|
+
return (item.renderedContent ?? "").split("\n").length;
|
|
491
|
+
}
|
|
492
|
+
function detailBodyHeight(state) {
|
|
493
|
+
if (state.layout === "too-narrow" || state.layout === "narrow-list-only") {
|
|
494
|
+
return 0;
|
|
495
|
+
}
|
|
496
|
+
const rows = normalizeSize(state.size.rows);
|
|
497
|
+
const footerHeight = rows > 0 ? Math.min(1, rows) : 0;
|
|
498
|
+
const headerHeight = Math.min(3, Math.max(0, rows - footerHeight));
|
|
499
|
+
const contentHeight = Math.max(0, rows - headerHeight - footerHeight);
|
|
500
|
+
if (state.layout === "narrow-vertical") {
|
|
501
|
+
const listHeight = Math.ceil(contentHeight / 2);
|
|
502
|
+
const detailHeight = contentHeight - listHeight;
|
|
503
|
+
return Math.max(0, detailHeight - 1);
|
|
504
|
+
}
|
|
505
|
+
return contentHeight;
|
|
506
|
+
}
|
|
457
507
|
function extendSelection(state, delta) {
|
|
508
|
+
if (!state.multiSelect) {
|
|
509
|
+
return moveCursor(state, delta);
|
|
510
|
+
}
|
|
458
511
|
const moved = moveCursor(state, delta);
|
|
459
512
|
const row = currentRow(moved.state);
|
|
460
513
|
if (row === undefined) {
|
|
@@ -646,7 +699,7 @@ function actionSource(state, action) {
|
|
|
646
699
|
return state.actionState.get(action.id)?.source ?? "row";
|
|
647
700
|
}
|
|
648
701
|
function selectedRows(state) {
|
|
649
|
-
if (state.selected.size === 0) {
|
|
702
|
+
if (!state.multiSelect || state.selected.size === 0) {
|
|
650
703
|
const row = currentRow(state);
|
|
651
704
|
return row === undefined ? [] : [row];
|
|
652
705
|
}
|
|
@@ -718,6 +771,9 @@ function isPrintable(key) {
|
|
|
718
771
|
function isBackspace(key) {
|
|
719
772
|
return key.name === "backspace" || key.name === "delete";
|
|
720
773
|
}
|
|
774
|
+
function isSelectionSpace(key) {
|
|
775
|
+
return key.name === "space" || key.ch === " ";
|
|
776
|
+
}
|
|
721
777
|
function isConfirmYes(key) {
|
|
722
778
|
return key.ch === "y" || key.ch === "Y";
|
|
723
779
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getExplorerStyles } from "../theme.js";
|
|
2
|
+
import { fitToWidth } from "./text.js";
|
|
2
3
|
export function renderDetail(state, screen, layout) {
|
|
3
4
|
const rect = layout.detail;
|
|
4
5
|
const styles = getExplorerStyles();
|
|
@@ -35,7 +36,8 @@ function renderDetailBody(state, screen, rect, row) {
|
|
|
35
36
|
function renderListMode(state, screen, rect, items, row) {
|
|
36
37
|
const styles = getExplorerStyles();
|
|
37
38
|
let y = 0;
|
|
38
|
-
|
|
39
|
+
const start = clamp(state.detail.scroll, 0, Math.max(0, items.length - 1));
|
|
40
|
+
for (let index = start; index < items.length && y < rect.height; index += 1) {
|
|
39
41
|
const item = items[index];
|
|
40
42
|
const cursor = index === state.detail.cursor;
|
|
41
43
|
const title = item.title ?? item.id;
|
|
@@ -60,7 +62,9 @@ function renderListMode(state, screen, rect, items, row) {
|
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
64
|
function renderBlob(screen, rect, text, scroll) {
|
|
63
|
-
const
|
|
65
|
+
const allLines = text.split("\n");
|
|
66
|
+
const start = clamp(scroll, 0, Math.max(0, allLines.length - rect.height));
|
|
67
|
+
const lines = allLines.slice(start);
|
|
64
68
|
for (let row = 0; row < rect.height; row += 1) {
|
|
65
69
|
writeLine(screen, rect, row, lines[row] ?? "");
|
|
66
70
|
}
|
|
@@ -86,14 +90,8 @@ function writeLine(screen, rect, row, text, style = {}) {
|
|
|
86
90
|
if (row < 0 || row >= rect.height) {
|
|
87
91
|
return;
|
|
88
92
|
}
|
|
89
|
-
screen.put(rect.x, rect.y + row,
|
|
93
|
+
screen.put(rect.x, rect.y + row, fitToWidth(text, rect.width, rect.x), style);
|
|
90
94
|
}
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
return "";
|
|
94
|
-
}
|
|
95
|
-
if (text.length <= width) {
|
|
96
|
-
return text;
|
|
97
|
-
}
|
|
98
|
-
return width <= 1 ? text.slice(0, width) : `${text.slice(0, width - 1)}…`;
|
|
95
|
+
function clamp(value, min, max) {
|
|
96
|
+
return Math.min(max, Math.max(min, value));
|
|
99
97
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getExplorerStyles } from "../theme.js";
|
|
2
|
+
import { cellWidth, fitToWidth } from "./text.js";
|
|
2
3
|
export function renderFooter(state, screen, layout) {
|
|
3
4
|
const rect = layout.footer;
|
|
4
5
|
const styles = getExplorerStyles();
|
|
@@ -9,22 +10,31 @@ export function renderFooter(state, screen, layout) {
|
|
|
9
10
|
const hints = footerHints(state);
|
|
10
11
|
let x = rect.x + 2;
|
|
11
12
|
const y = rect.y;
|
|
13
|
+
const endX = rect.x + rect.width;
|
|
12
14
|
for (const hint of hints) {
|
|
13
|
-
if (x >=
|
|
15
|
+
if (x >= endX) {
|
|
14
16
|
break;
|
|
15
17
|
}
|
|
16
18
|
if (hint.bracketed === false) {
|
|
17
19
|
const text = `${hint.key} ${hint.label}`;
|
|
18
|
-
screen
|
|
19
|
-
x += text.length + 2;
|
|
20
|
+
x += putFooterText(screen, x, y, endX, text, hint.running ? styles.muted : {}) + 2;
|
|
20
21
|
continue;
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
-
x
|
|
24
|
-
|
|
25
|
-
x
|
|
23
|
+
const keyText = `[${hint.key}]`;
|
|
24
|
+
const keyWidth = putFooterText(screen, x, y, endX, keyText, hint.running ? styles.muted : styles.accent);
|
|
25
|
+
x += keyWidth;
|
|
26
|
+
if (keyWidth < cellWidth(keyText) || x >= endX) {
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
x += putFooterText(screen, x, y, endX, ` ${hint.label}`, hint.running ? styles.muted : {}) + 2;
|
|
26
30
|
}
|
|
27
31
|
}
|
|
32
|
+
function putFooterText(screen, x, y, endX, text, style = {}) {
|
|
33
|
+
const remaining = Math.max(0, endX - x);
|
|
34
|
+
const fitted = fitToWidth(text, remaining, x);
|
|
35
|
+
screen.put(x, y, fitted, style);
|
|
36
|
+
return cellWidth(fitted, x);
|
|
37
|
+
}
|
|
28
38
|
function footerHints(state) {
|
|
29
39
|
const hints = [];
|
|
30
40
|
if (state.focused === "detail") {
|
|
@@ -36,7 +46,7 @@ function footerHints(state) {
|
|
|
36
46
|
continue;
|
|
37
47
|
}
|
|
38
48
|
const key = actionKey(entry, id);
|
|
39
|
-
const label = state.selected.size > 0 && entry.source === "row"
|
|
49
|
+
const label = state.multiSelect && state.selected.size > 0 && entry.source === "row"
|
|
40
50
|
? `${entry.label} ${state.selected.size}`
|
|
41
51
|
: entry.label;
|
|
42
52
|
hints.push({ key, label, running: entry.running === true });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getExplorerStyles } from "../theme.js";
|
|
2
|
+
import { cellWidth, fitToWidth, padEndCells } from "./text.js";
|
|
2
3
|
export function renderHeader(state, screen, layout) {
|
|
3
4
|
const rect = layout.header;
|
|
4
5
|
const styles = getExplorerStyles();
|
|
@@ -7,7 +8,7 @@ export function renderHeader(state, screen, layout) {
|
|
|
7
8
|
return;
|
|
8
9
|
}
|
|
9
10
|
if (layout.mode === "too-narrow") {
|
|
10
|
-
screen.put(0, 0,
|
|
11
|
+
screen.put(0, 0, fitToWidth("Terminal too narrow", rect.width), styles.borderFocused);
|
|
11
12
|
return;
|
|
12
13
|
}
|
|
13
14
|
drawTopBorder(screen, state.title, rect.width, styles.border);
|
|
@@ -15,13 +16,15 @@ export function renderHeader(state, screen, layout) {
|
|
|
15
16
|
const prompt = `${state.title.toLocaleLowerCase()}>`;
|
|
16
17
|
const filter = state.filter.length > 0 ? ` ${state.filter}` : "";
|
|
17
18
|
const count = `${state.filtered.length}/${state.rows.length}`;
|
|
18
|
-
const selected = state.selected.size > 0 ? ` (${state.selected.size} selected)` : "";
|
|
19
|
+
const selected = state.multiSelect && state.selected.size > 0 ? ` (${state.selected.size} selected)` : "";
|
|
19
20
|
const spinner = state.detail.loading ? " *" : "";
|
|
20
21
|
const right = `${count}${selected}${spinner}`;
|
|
21
22
|
screen.put(0, 1, "│", styles.border);
|
|
22
23
|
screen.put(Math.max(0, rect.width - 1), 1, "│", styles.border);
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
const rightWidth = cellWidth(right);
|
|
25
|
+
const promptWidth = Math.max(0, rect.width - rightWidth - 5);
|
|
26
|
+
screen.put(2, 1, fitToWidth(`${prompt}${filter}`, promptWidth, 2), styles.accent);
|
|
27
|
+
screen.put(Math.max(2, rect.width - rightWidth - 2), 1, right, styles.muted);
|
|
25
28
|
}
|
|
26
29
|
if (rect.height > 2) {
|
|
27
30
|
drawHorizontal(screen, 2, rect.width, styles.border);
|
|
@@ -32,11 +35,10 @@ function drawTopBorder(screen, title, width, style) {
|
|
|
32
35
|
screen.put(0, 0, "┌", style);
|
|
33
36
|
return;
|
|
34
37
|
}
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
screen.put(0, 0, `┌${middle.slice(0, Math.max(0, width - 2))}┐`, style);
|
|
38
|
+
const innerWidth = Math.max(0, width - 2);
|
|
39
|
+
const label = fitToWidth(`─ ${title} `, innerWidth, 1);
|
|
40
|
+
const middle = padEndCells(label, innerWidth, "─", 1);
|
|
41
|
+
screen.put(0, 0, `┌${middle}┐`, style);
|
|
40
42
|
}
|
|
41
43
|
function drawHorizontal(screen, y, width, style) {
|
|
42
44
|
if (width === 1) {
|
|
@@ -45,12 +47,3 @@ function drawHorizontal(screen, y, width, style) {
|
|
|
45
47
|
}
|
|
46
48
|
screen.put(0, y, `├${"─".repeat(Math.max(0, width - 2))}┤`, style);
|
|
47
49
|
}
|
|
48
|
-
function fit(text, width) {
|
|
49
|
-
if (width <= 0) {
|
|
50
|
-
return "";
|
|
51
|
-
}
|
|
52
|
-
if (text.length <= width) {
|
|
53
|
-
return text;
|
|
54
|
-
}
|
|
55
|
-
return width <= 1 ? text.slice(0, width) : `${text.slice(0, width - 1)}…`;
|
|
56
|
-
}
|
|
@@ -6,6 +6,7 @@ import { renderFooter } from "./footer.js";
|
|
|
6
6
|
import { renderHeader } from "./header.js";
|
|
7
7
|
import { renderList } from "./list.js";
|
|
8
8
|
import { renderModal } from "./modal.js";
|
|
9
|
+
import { fitToWidth } from "./text.js";
|
|
9
10
|
const REGION_RENDERERS = [
|
|
10
11
|
[REGION_HEADER, renderHeader],
|
|
11
12
|
[REGION_LIST, renderList],
|
|
@@ -42,18 +43,9 @@ function renderToast(state, screen) {
|
|
|
42
43
|
return;
|
|
43
44
|
}
|
|
44
45
|
const styles = getExplorerStyles();
|
|
45
|
-
const message =
|
|
46
|
+
const message = fitToWidth(` ${state.toast.message} `, screen.width);
|
|
46
47
|
screen.put(0, y, message, styles.accent);
|
|
47
48
|
}
|
|
48
|
-
function fit(text, width) {
|
|
49
|
-
if (width <= 0) {
|
|
50
|
-
return "";
|
|
51
|
-
}
|
|
52
|
-
if (text.length <= width) {
|
|
53
|
-
return text;
|
|
54
|
-
}
|
|
55
|
-
return width <= 1 ? text.slice(0, width) : `${text.slice(0, width - 1)}…`;
|
|
56
|
-
}
|
|
57
49
|
export { renderDetail } from "./detail.js";
|
|
58
50
|
export { renderFooter } from "./footer.js";
|
|
59
51
|
export { renderHeader } from "./header.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getExplorerStyles } from "../theme.js";
|
|
2
|
+
import { cellWidth, centerCells, fitToWidth, splitGraphemeCells, stripAnsi } from "./text.js";
|
|
2
3
|
const listLineCache = new WeakMap();
|
|
3
4
|
export function renderList(state, screen, layout) {
|
|
4
5
|
const styles = getExplorerStyles();
|
|
@@ -17,7 +18,7 @@ export function renderList(state, screen, layout) {
|
|
|
17
18
|
listLineCache.set(screen, { rectKey, lines: cache });
|
|
18
19
|
if (state.filtered.length === 0) {
|
|
19
20
|
const hint = state.emptyHint;
|
|
20
|
-
writeLine(screen, rect, Math.floor(rect.height / 2),
|
|
21
|
+
writeLine(screen, rect, Math.floor(rect.height / 2), centerCells(hint, rect.width, rect.x), styles.muted);
|
|
21
22
|
cache.clear();
|
|
22
23
|
return;
|
|
23
24
|
}
|
|
@@ -43,7 +44,7 @@ export function renderList(state, screen, layout) {
|
|
|
43
44
|
if (y >= rect.height) {
|
|
44
45
|
break;
|
|
45
46
|
}
|
|
46
|
-
const selected = state.selected.has(row.id);
|
|
47
|
+
const selected = state.multiSelect && state.selected.has(row.id);
|
|
47
48
|
const cursor = rowIndex === state.filtered[state.cursor];
|
|
48
49
|
const positions = state.matchPositions.get(rowIndex) ?? [];
|
|
49
50
|
const hash = lineHash(row, selected, cursor, positions);
|
|
@@ -67,40 +68,49 @@ function renderRow(screen, rect, rowY, row, opts) {
|
|
|
67
68
|
const marker = opts.selected ? "┃" : " ";
|
|
68
69
|
const cursor = opts.cursor ? "●" : "◌";
|
|
69
70
|
const focus = opts.cursor && opts.focused ? " ▌" : "";
|
|
70
|
-
const badge = row.badge ? ` ${row.badge.text}` : "";
|
|
71
71
|
const prefix = `${marker} ${cursor} `;
|
|
72
|
-
const
|
|
73
|
-
const
|
|
72
|
+
const prefixWidth = cellWidth(prefix, rect.x);
|
|
73
|
+
const focusWidth = cellWidth(focus);
|
|
74
|
+
const badge = row.badge
|
|
75
|
+
? fitToWidth(` ${row.badge.text}`, Math.max(0, rect.width - prefixWidth - focusWidth), rect.x + prefixWidth)
|
|
76
|
+
: "";
|
|
77
|
+
const badgeWidth = cellWidth(badge);
|
|
78
|
+
const available = Math.max(0, rect.width - prefixWidth - focusWidth - badgeWidth);
|
|
79
|
+
const rawTitle = stripAnsi(row.title);
|
|
80
|
+
const titleX = rect.x + prefixWidth;
|
|
81
|
+
const title = fitToWidth(rawTitle, available, titleX);
|
|
82
|
+
const titleWasTruncated = cellWidth(rawTitle, titleX) > available;
|
|
83
|
+
const positions = new Set(opts.positions);
|
|
74
84
|
let x = rect.x;
|
|
75
85
|
const y = rect.y + rowY;
|
|
76
86
|
screen.put(x, y, prefix, opts.cursor ? styles.accent : styles.muted);
|
|
77
|
-
x +=
|
|
78
|
-
for (
|
|
79
|
-
const
|
|
80
|
-
|
|
87
|
+
x += prefixWidth;
|
|
88
|
+
for (const segment of splitGraphemeCells(title, x)) {
|
|
89
|
+
const isTruncationMarker = titleWasTruncated && segment.end === title.length && segment.value === "…";
|
|
90
|
+
const style = !isTruncationMarker && hasMatchPosition(segment.start, segment.end, positions)
|
|
91
|
+
? styles.matchHighlight
|
|
92
|
+
: {};
|
|
93
|
+
screen.put(x, y, segment.value, style);
|
|
94
|
+
x += segment.width;
|
|
81
95
|
}
|
|
82
96
|
if (row.badge) {
|
|
83
|
-
screen.put(rect.x + rect.width -
|
|
97
|
+
screen.put(rect.x + rect.width - badgeWidth - focusWidth, y, badge, styles.tones[row.badge.tone ?? "muted"]);
|
|
84
98
|
}
|
|
85
99
|
if (focus) {
|
|
86
|
-
screen.put(rect.x + rect.width -
|
|
100
|
+
screen.put(rect.x + rect.width - focusWidth, y, focus, styles.borderFocused);
|
|
87
101
|
}
|
|
88
102
|
}
|
|
89
103
|
function writeLine(screen, rect, row, text, style = {}) {
|
|
90
|
-
screen.put(rect.x, rect.y + row,
|
|
104
|
+
screen.put(rect.x, rect.y + row, fitToWidth(text, rect.width, rect.x), style);
|
|
91
105
|
}
|
|
92
106
|
function lineHash(row, selected, cursor, positions) {
|
|
93
107
|
return `${row.id}:${selected ? 1 : 0}:${cursor ? 1 : 0}:${positions.join(",")}`;
|
|
94
108
|
}
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return text;
|
|
109
|
+
function hasMatchPosition(start, end, positions) {
|
|
110
|
+
for (let position = start; position < end; position += 1) {
|
|
111
|
+
if (positions.has(position)) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
101
114
|
}
|
|
102
|
-
return
|
|
103
|
-
}
|
|
104
|
-
function stripAnsi(value) {
|
|
105
|
-
return value.replace(/\u001b\[[0-9;]*m/g, "");
|
|
115
|
+
return false;
|
|
106
116
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getExplorerStyles } from "../theme.js";
|
|
2
|
+
import { fitToWidth, padEndCells } from "./text.js";
|
|
2
3
|
export function renderModal(state, screen) {
|
|
3
4
|
if (state.modal === null || screen.width <= 0 || screen.height <= 0) {
|
|
4
5
|
return;
|
|
@@ -11,7 +12,7 @@ export function renderModal(state, screen) {
|
|
|
11
12
|
drawBox(screen, x, y, width, height, title(state), styles.borderFocused);
|
|
12
13
|
const lines = modalLines(state);
|
|
13
14
|
for (let row = 0; row < Math.min(lines.length, height - 2); row += 1) {
|
|
14
|
-
screen.put(x + 2, y + 1 + row,
|
|
15
|
+
screen.put(x + 2, y + 1 + row, fitToWidth(lines[row], width - 4, x + 2), row === 1 ? styles.accent : {});
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
function modalLines(state) {
|
|
@@ -54,8 +55,9 @@ function modalHeight(state) {
|
|
|
54
55
|
}
|
|
55
56
|
function drawBox(screen, x, y, width, height, boxTitle, style) {
|
|
56
57
|
screen.clearRect({ x, y, width, height });
|
|
57
|
-
const
|
|
58
|
-
|
|
58
|
+
const innerWidth = Math.max(0, width - 2);
|
|
59
|
+
const titleSegment = fitToWidth(`─ ${boxTitle} `, innerWidth, x + 1);
|
|
60
|
+
screen.put(x, y, `╭${padEndCells(titleSegment, innerWidth, "─", x + 1)}╮`, style);
|
|
59
61
|
for (let row = 1; row < height - 1; row += 1) {
|
|
60
62
|
screen.put(x, y + row, "│", style);
|
|
61
63
|
screen.put(x + width - 1, y + row, "│", style);
|
|
@@ -83,9 +85,3 @@ function paletteLines(state) {
|
|
|
83
85
|
function labelFor(action) {
|
|
84
86
|
return typeof action.label === "function" ? action.label() : action.label;
|
|
85
87
|
}
|
|
86
|
-
function fit(text, width) {
|
|
87
|
-
if (text.length <= width) {
|
|
88
|
-
return text;
|
|
89
|
-
}
|
|
90
|
-
return width <= 1 ? text.slice(0, width) : `${text.slice(0, width - 1)}…`;
|
|
91
|
-
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface GraphemeCell {
|
|
2
|
+
value: string;
|
|
3
|
+
start: number;
|
|
4
|
+
end: number;
|
|
5
|
+
width: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function cellWidth(value: string, startColumn?: number): number;
|
|
8
|
+
export declare function fitToWidth(text: string, width: number, startColumn?: number): string;
|
|
9
|
+
export declare function centerCells(text: string, width: number, startColumn?: number): string;
|
|
10
|
+
export declare function padEndCells(text: string, width: number, fill?: string, startColumn?: number): string;
|
|
11
|
+
export declare function splitGraphemeCells(value: string, startColumn?: number): GraphemeCell[];
|
|
12
|
+
export declare function stripAnsi(value: string): string;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { displayWidth, graphemes } from "../../dashboard/terminal-width.js";
|
|
2
|
+
const ELLIPSIS = "…";
|
|
3
|
+
export function cellWidth(value, startColumn = 0) {
|
|
4
|
+
return displayWidth(value, startColumn);
|
|
5
|
+
}
|
|
6
|
+
export function fitToWidth(text, width, startColumn = 0) {
|
|
7
|
+
if (width <= 0) {
|
|
8
|
+
return "";
|
|
9
|
+
}
|
|
10
|
+
if (cellWidth(text, startColumn) <= width) {
|
|
11
|
+
return text;
|
|
12
|
+
}
|
|
13
|
+
const ellipsisWidth = cellWidth(ELLIPSIS, startColumn);
|
|
14
|
+
if (ellipsisWidth > width) {
|
|
15
|
+
return takeCells(text, width, startColumn);
|
|
16
|
+
}
|
|
17
|
+
const prefix = takeCells(text, width - ellipsisWidth, startColumn);
|
|
18
|
+
return `${prefix}${ELLIPSIS}`;
|
|
19
|
+
}
|
|
20
|
+
export function centerCells(text, width, startColumn = 0) {
|
|
21
|
+
const fitted = fitToWidth(text, width, startColumn);
|
|
22
|
+
const padding = Math.max(0, Math.floor((width - cellWidth(fitted, startColumn)) / 2));
|
|
23
|
+
return `${" ".repeat(padding)}${fitted}`;
|
|
24
|
+
}
|
|
25
|
+
export function padEndCells(text, width, fill = " ", startColumn = 0) {
|
|
26
|
+
let output = takeCells(text, width, startColumn);
|
|
27
|
+
let used = cellWidth(output, startColumn);
|
|
28
|
+
let column = startColumn + used;
|
|
29
|
+
while (used < width) {
|
|
30
|
+
const fillWidth = cellWidth(fill, column);
|
|
31
|
+
if (fill.length === 0 || fillWidth <= 0 || used + fillWidth > width) {
|
|
32
|
+
const spaces = width - used;
|
|
33
|
+
output += " ".repeat(spaces);
|
|
34
|
+
used += spaces;
|
|
35
|
+
column += spaces;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
output += fill;
|
|
39
|
+
used += fillWidth;
|
|
40
|
+
column += fillWidth;
|
|
41
|
+
}
|
|
42
|
+
return output;
|
|
43
|
+
}
|
|
44
|
+
export function splitGraphemeCells(value, startColumn = 0) {
|
|
45
|
+
const cells = [];
|
|
46
|
+
let offset = 0;
|
|
47
|
+
let column = startColumn;
|
|
48
|
+
for (const segment of graphemes(value)) {
|
|
49
|
+
const width = cellWidth(segment, column);
|
|
50
|
+
cells.push({
|
|
51
|
+
value: segment,
|
|
52
|
+
start: offset,
|
|
53
|
+
end: offset + segment.length,
|
|
54
|
+
width
|
|
55
|
+
});
|
|
56
|
+
offset += segment.length;
|
|
57
|
+
column += width;
|
|
58
|
+
}
|
|
59
|
+
return cells;
|
|
60
|
+
}
|
|
61
|
+
export function stripAnsi(value) {
|
|
62
|
+
return value.replace(/\u001b\[[0-9;]*m/g, "");
|
|
63
|
+
}
|
|
64
|
+
function takeCells(text, width, startColumn) {
|
|
65
|
+
if (width <= 0) {
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
let output = "";
|
|
69
|
+
let used = 0;
|
|
70
|
+
let column = startColumn;
|
|
71
|
+
for (const segment of graphemes(text)) {
|
|
72
|
+
const segmentWidth = cellWidth(segment, column);
|
|
73
|
+
if (used + segmentWidth > width) {
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
output += segment;
|
|
77
|
+
used += segmentWidth;
|
|
78
|
+
column += segmentWidth;
|
|
79
|
+
}
|
|
80
|
+
return output;
|
|
81
|
+
}
|
|
@@ -16,6 +16,7 @@ export function createInitialState(config, size) {
|
|
|
16
16
|
cols: normalizeSize(size.cols),
|
|
17
17
|
rows: normalizeSize(size.rows)
|
|
18
18
|
};
|
|
19
|
+
const multiSelect = config.multiSelect ?? true;
|
|
19
20
|
return {
|
|
20
21
|
title: config.title,
|
|
21
22
|
emptyHint: config.emptyHint ?? "No detail",
|
|
@@ -35,6 +36,7 @@ export function createInitialState(config, size) {
|
|
|
35
36
|
loading: false
|
|
36
37
|
},
|
|
37
38
|
selected: new Set(),
|
|
39
|
+
multiSelect,
|
|
38
40
|
modal: null,
|
|
39
41
|
toast: null,
|
|
40
42
|
dirty: REGION_ALL,
|
|
@@ -95,12 +95,12 @@ export async function withSpinner(options) {
|
|
|
95
95
|
const result = await fn();
|
|
96
96
|
const msg = stopMessage ? stopMessage(result) : undefined;
|
|
97
97
|
if (msg) {
|
|
98
|
-
process.stdout.write(
|
|
98
|
+
process.stdout.write(`${color.green("◆")} ${msg}\n`);
|
|
99
99
|
}
|
|
100
100
|
const sub = subtext ? subtext(result) : undefined;
|
|
101
101
|
if (sub) {
|
|
102
102
|
for (const line of sub.split("\n")) {
|
|
103
|
-
process.stdout.write(
|
|
103
|
+
process.stdout.write(`${color.gray("│")} ${line}\n`);
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
return result;
|
|
@@ -120,7 +120,7 @@ export async function withSpinner(options) {
|
|
|
120
120
|
const sub = subtext ? subtext(result) : undefined;
|
|
121
121
|
if (sub) {
|
|
122
122
|
for (const line of sub.split("\n")) {
|
|
123
|
-
process.stdout.write(
|
|
123
|
+
process.stdout.write(`${color.gray("│")} ${line}\n`);
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
return result;
|
|
@@ -33,6 +33,7 @@ export interface EncryptedFileStoreInput {
|
|
|
33
33
|
export declare class EncryptedFileStore implements SecretStore {
|
|
34
34
|
private readonly fs;
|
|
35
35
|
private readonly filePath;
|
|
36
|
+
private readonly symbolicLinkCheckStartPath;
|
|
36
37
|
private readonly salt;
|
|
37
38
|
private readonly getMachineIdentity;
|
|
38
39
|
private readonly getRandomBytes;
|
|
@@ -13,6 +13,7 @@ let temporaryFileSequence = 0;
|
|
|
13
13
|
export class EncryptedFileStore {
|
|
14
14
|
fs;
|
|
15
15
|
filePath;
|
|
16
|
+
symbolicLinkCheckStartPath;
|
|
16
17
|
salt;
|
|
17
18
|
getMachineIdentity;
|
|
18
19
|
getRandomBytes;
|
|
@@ -20,7 +21,16 @@ export class EncryptedFileStore {
|
|
|
20
21
|
constructor(input) {
|
|
21
22
|
this.fs = input.fs ?? fs;
|
|
22
23
|
this.salt = input.salt;
|
|
23
|
-
|
|
24
|
+
if (input.filePath === undefined) {
|
|
25
|
+
const homeDirectory = (input.getHomeDirectory ?? homedir)();
|
|
26
|
+
const defaultDirectory = input.defaultDirectory ?? ".auth-store";
|
|
27
|
+
this.filePath = path.join(homeDirectory, defaultDirectory, input.defaultFileName ?? "credentials.enc");
|
|
28
|
+
this.symbolicLinkCheckStartPath = resolveDefaultDirectoryCheckStart(homeDirectory, defaultDirectory);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
this.filePath = input.filePath;
|
|
32
|
+
this.symbolicLinkCheckStartPath = null;
|
|
33
|
+
}
|
|
24
34
|
this.getMachineIdentity = input.getMachineIdentity ?? defaultMachineIdentity;
|
|
25
35
|
this.getRandomBytes = input.getRandomBytes ?? randomBytes;
|
|
26
36
|
}
|
|
@@ -106,7 +116,7 @@ export class EncryptedFileStore {
|
|
|
106
116
|
}
|
|
107
117
|
async assertRegularCredentialPath() {
|
|
108
118
|
const resolvedPath = path.resolve(this.filePath);
|
|
109
|
-
const protectedPaths =
|
|
119
|
+
const protectedPaths = getProtectedCredentialPaths(resolvedPath, this.symbolicLinkCheckStartPath);
|
|
110
120
|
for (const currentPath of protectedPaths) {
|
|
111
121
|
try {
|
|
112
122
|
const stats = await this.fs.lstat(currentPath);
|
|
@@ -135,6 +145,30 @@ export class EncryptedFileStore {
|
|
|
135
145
|
return this.keyPromise;
|
|
136
146
|
}
|
|
137
147
|
}
|
|
148
|
+
function resolveDefaultDirectoryCheckStart(homeDirectory, defaultDirectory) {
|
|
149
|
+
const [firstSegment] = defaultDirectory.split(/[\\/]+/).filter(Boolean);
|
|
150
|
+
return path.resolve(homeDirectory, firstSegment ?? ".");
|
|
151
|
+
}
|
|
152
|
+
function getProtectedCredentialPaths(resolvedPath, symbolicLinkCheckStartPath) {
|
|
153
|
+
if (symbolicLinkCheckStartPath === null) {
|
|
154
|
+
return [path.dirname(resolvedPath), resolvedPath];
|
|
155
|
+
}
|
|
156
|
+
const resolvedStartPath = path.resolve(symbolicLinkCheckStartPath);
|
|
157
|
+
if (!isPathInsideOrEqual(resolvedPath, resolvedStartPath)) {
|
|
158
|
+
return [path.dirname(resolvedPath), resolvedPath];
|
|
159
|
+
}
|
|
160
|
+
const protectedPaths = [resolvedStartPath];
|
|
161
|
+
let currentPath = resolvedStartPath;
|
|
162
|
+
for (const segment of path.relative(resolvedStartPath, resolvedPath).split(path.sep).filter(Boolean)) {
|
|
163
|
+
currentPath = path.join(currentPath, segment);
|
|
164
|
+
protectedPaths.push(currentPath);
|
|
165
|
+
}
|
|
166
|
+
return protectedPaths;
|
|
167
|
+
}
|
|
168
|
+
function isPathInsideOrEqual(childPath, parentPath) {
|
|
169
|
+
const relativePath = path.relative(parentPath, childPath);
|
|
170
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
171
|
+
}
|
|
138
172
|
async function removeIfPresent(fileSystem, filePath) {
|
|
139
173
|
try {
|
|
140
174
|
await fileSystem.unlink(filePath);
|
|
@@ -64,6 +64,19 @@ function runSecurityCommand(command, args, options) {
|
|
|
64
64
|
});
|
|
65
65
|
let stdout = "";
|
|
66
66
|
let stderr = "";
|
|
67
|
+
let stdinErrorMessage;
|
|
68
|
+
const appendStderr = (message) => {
|
|
69
|
+
stderr = stderr.length === 0
|
|
70
|
+
? message
|
|
71
|
+
: `${stderr}${stderr.endsWith("\n") ? "" : "\n"}${message}`;
|
|
72
|
+
};
|
|
73
|
+
const appendStdinError = () => {
|
|
74
|
+
if (stdinErrorMessage === undefined) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
appendStderr(stdinErrorMessage);
|
|
78
|
+
stdinErrorMessage = undefined;
|
|
79
|
+
};
|
|
67
80
|
child.stdout?.setEncoding("utf8");
|
|
68
81
|
child.stdout?.on("data", (chunk) => {
|
|
69
82
|
stdout += chunk.toString();
|
|
@@ -73,17 +86,23 @@ function runSecurityCommand(command, args, options) {
|
|
|
73
86
|
stderr += chunk.toString();
|
|
74
87
|
});
|
|
75
88
|
if (options?.stdin !== undefined) {
|
|
89
|
+
child.stdin?.once("error", (error) => {
|
|
90
|
+
stdinErrorMessage = error instanceof Error ? error.message : String(error);
|
|
91
|
+
});
|
|
76
92
|
child.stdin?.end(options.stdin);
|
|
77
93
|
}
|
|
78
94
|
child.on("error", (error) => {
|
|
79
95
|
const message = error instanceof Error ? error.message : String(error ?? "Unknown error");
|
|
96
|
+
appendStdinError();
|
|
97
|
+
appendStderr(message);
|
|
80
98
|
resolve({
|
|
81
99
|
stdout,
|
|
82
|
-
stderr
|
|
100
|
+
stderr,
|
|
83
101
|
exitCode: 127
|
|
84
102
|
});
|
|
85
103
|
});
|
|
86
104
|
child.on("close", (code) => {
|
|
105
|
+
appendStdinError();
|
|
87
106
|
resolve({
|
|
88
107
|
stdout,
|
|
89
108
|
stderr,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolcraft-openapi",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@clack/core": "^1.0.0",
|
|
32
32
|
"@clack/prompts": "^1.0.0",
|
|
33
33
|
"@poe-code/design-system": "^0.0.2",
|
|
34
|
-
"toolcraft": "0.0.
|
|
34
|
+
"toolcraft": "0.0.26",
|
|
35
35
|
"auth-store": "^0.0.1",
|
|
36
36
|
"yaml": "^2.8.2"
|
|
37
37
|
},
|