@theholocron/kyber 0.0.0-stage → 5.0.0-alpha.338
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/LICENSE +21 -0
- package/README.md +43 -2
- package/dist/flags.d.mts +38 -0
- package/dist/flags.mjs +56 -0
- package/dist/non-interactive-error-Mm_Mn79M.mjs +12 -0
- package/dist/notifier.d.mts +33 -0
- package/dist/notifier.mjs +121 -0
- package/dist/prompts.d.mts +214 -0
- package/dist/prompts.mjs +263 -0
- package/dist/secrets.d.mts +191 -0
- package/dist/secrets.mjs +329 -0
- package/dist/testing.d.mts +83 -0
- package/dist/testing.mjs +139 -0
- package/dist/token-args-BgrCq_kK.mjs +38 -0
- package/dist/token-args.type-arNIDKsQ.d.mts +7 -0
- package/dist/ui.d.mts +151 -0
- package/dist/ui.mjs +391 -0
- package/package.json +126 -3
package/dist/ui.d.mts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/** A JSON-pointer-ish path: `""` is the root, children append `/<key>`. */
|
|
2
|
+
type Path = string;
|
|
3
|
+
/** One rendered line of the viewer. */
|
|
4
|
+
interface Line {
|
|
5
|
+
/** Where this line's value sits in the document. */
|
|
6
|
+
path: Path;
|
|
7
|
+
/** The line as drawn, indent included. */
|
|
8
|
+
text: string;
|
|
9
|
+
/** Object/array with at least one child — the only lines that fold. */
|
|
10
|
+
foldable: boolean;
|
|
11
|
+
/** Whether this line's children are shown. */
|
|
12
|
+
expanded: boolean;
|
|
13
|
+
/** Number of leading indent levels, so the TTY layer can jump to a parent. */
|
|
14
|
+
level: number;
|
|
15
|
+
}
|
|
16
|
+
/** What {@link renderTree} needs to draw the document. */
|
|
17
|
+
interface RenderOptions {
|
|
18
|
+
/** Paths currently expanded. */
|
|
19
|
+
expanded: ReadonlySet<Path>;
|
|
20
|
+
}
|
|
21
|
+
/** A value that can have children: an object or an array. */
|
|
22
|
+
type Container = Record<string, unknown> | unknown[];
|
|
23
|
+
/** How {@link viewJson} shows a document. */
|
|
24
|
+
interface ViewJsonOptions {
|
|
25
|
+
/** Collapse containers deeper than this many levels (static mode and the interactive start state). */
|
|
26
|
+
depth?: number;
|
|
27
|
+
/**
|
|
28
|
+
* `true` forces the folding viewer (throws without a TTY), `false` forces
|
|
29
|
+
* static output. Left unset it auto-enables when stdin and stdout are both
|
|
30
|
+
* TTYs and no `depth` was asked for (an explicit depth means static output).
|
|
31
|
+
*/
|
|
32
|
+
interactive?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** Where the interactive viewer is: the selected line, the first visible line and the open containers. */
|
|
35
|
+
interface ViewState {
|
|
36
|
+
/** The index of the selected line. */
|
|
37
|
+
cursor: number;
|
|
38
|
+
/** The index of the first line on screen. */
|
|
39
|
+
top: number;
|
|
40
|
+
/** The paths currently open. */
|
|
41
|
+
expanded: Set<Path>;
|
|
42
|
+
}
|
|
43
|
+
/** A key the interactive viewer reacts to, already mapped from the terminal's escape sequences. */
|
|
44
|
+
type Key = "up" | "down" | "left" | "right" | "expandAll" | "collapseAll" | "quit";
|
|
45
|
+
/**
|
|
46
|
+
* Paths of every container deeper than `depth` levels are left out, so those
|
|
47
|
+
* render collapsed. `depth: 0` collapses the root; `Infinity` expands all.
|
|
48
|
+
*/
|
|
49
|
+
export declare function expandToDepth(value: unknown, depth: number): Set<Path>;
|
|
50
|
+
/** Flatten `value` into the lines visible under `expanded`. Pure — no I/O. */
|
|
51
|
+
export declare function renderTree(value: unknown, { expanded }: RenderOptions): Line[];
|
|
52
|
+
/** Pure key reducer: `(state, key) -> state`. `rows` is the viewport height. */
|
|
53
|
+
export declare function reduceKey(state: ViewState, key: Key, value: unknown, rows: number): ViewState;
|
|
54
|
+
/**
|
|
55
|
+
* Print `value` as JSON. Non-TTY stdout always gets plain, valid
|
|
56
|
+
* `JSON.stringify` output (so `| jq` and redirects keep working); a TTY gets a
|
|
57
|
+
* coloured tree, collapsed past `depth`. `interactive` opens a folding viewer (the default on a TTY).
|
|
58
|
+
*/
|
|
59
|
+
export declare function viewJson(value: unknown, options?: ViewJsonOptions): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Raised when a prompt is needed but stdin isn't a TTY. A non-TTY run (CI, a
|
|
62
|
+
* script, a pipe) throws this instead of hanging on `@inquirer/prompts`, which
|
|
63
|
+
* otherwise throws its own `ExitPromptError` the moment stdin closes. `cli.ts`'s
|
|
64
|
+
* `USER_FACING_ERRORS` catch prints the message cleanly.
|
|
65
|
+
*/
|
|
66
|
+
export declare class NonInteractiveError extends Error {
|
|
67
|
+
/** The error's name, `NonInteractiveError`, so a catch can tell it from any other error. */
|
|
68
|
+
name: string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Open `path` in the OS's default file browser (Finder / Explorer /
|
|
72
|
+
* whatever `xdg-open` resolves to on Linux). Fire-and-forget — detached so
|
|
73
|
+
* it outlives this process, and never throws when the opener binary isn't
|
|
74
|
+
* on PATH (e.g. no `xdg-open` in a minimal container).
|
|
75
|
+
*/
|
|
76
|
+
export declare function openPath(path: string): void;
|
|
77
|
+
/** A spinner for one step at a time, which the step can print through without breaking it. */
|
|
78
|
+
interface Progress {
|
|
79
|
+
/** Run `fn` under a spinner named `label`. The spinner is cleared, not kept, so the step's own result line stands alone. */
|
|
80
|
+
run<T>(label: string, fn: () => Promise<T>): Promise<T>;
|
|
81
|
+
/** Print a line without breaking the spinner: it is cleared, the line printed, and the spinner drawn again. */
|
|
82
|
+
print(line: string): void;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Hooks something into a running step, such as a retry notice. It is given the step's label and a way to change the
|
|
86
|
+
* spinner's text, and returns the function that unhooks it, called when the step ends.
|
|
87
|
+
*/
|
|
88
|
+
type ProgressListener = (label: string, setText: (text: string) => void) => () => void;
|
|
89
|
+
/**
|
|
90
|
+
* Runs `fn`, showing an ora spinner for its duration in TTY environments.
|
|
91
|
+
* In non-TTY environments (CI, pipes, tests) the spinner is skipped entirely.
|
|
92
|
+
*/
|
|
93
|
+
export declare function withSpinner<T>(label: string, fn: () => Promise<T>): Promise<T>;
|
|
94
|
+
/** Turn progress off for the run (`--quiet`, `--log`), so nothing extra shares the terminal with what those print. */
|
|
95
|
+
export declare function allowProgress(on: boolean): void;
|
|
96
|
+
/**
|
|
97
|
+
* Whether to draw a progress line: only when a person is watching a terminal (stdout and stderr both), not in CI, and not
|
|
98
|
+
* switched off. Everywhere else the output stays the plain line-per-step text that scripts and the docs rely on.
|
|
99
|
+
*/
|
|
100
|
+
export declare function progressEnabled(): boolean;
|
|
101
|
+
/**
|
|
102
|
+
* A spinner for one step at a time that a step can print through. Steps that only return a result use {@link
|
|
103
|
+
* withProgress}; a step that prints its own result line while it runs prints through `print`. A `listen` hook can change the
|
|
104
|
+
* spinner's text while the step runs, for instance to say that a request is being sent again.
|
|
105
|
+
*/
|
|
106
|
+
export declare function createProgress(print: (line: string) => void, enabled?: () => boolean, listen?: ProgressListener): Progress;
|
|
107
|
+
/** `fn` under a cleared-when-done spinner named `label`, on a terminal only. */
|
|
108
|
+
export declare function withProgress<T>(label: string, fn: () => Promise<T>, listen?: ProgressListener): Promise<T>;
|
|
109
|
+
/** How a row of a check or setup run came out: it passed, it failed, or it was skipped. */
|
|
110
|
+
type RowStatus = "ok" | "fail" | "skip";
|
|
111
|
+
/** Right-pads `s` to `width` with spaces — never truncates. */
|
|
112
|
+
export declare function pad(s: string, width: number): string;
|
|
113
|
+
/**
|
|
114
|
+
* One row of a status report: a 2-space indent, the status icon (✓ / ✗ / ·),
|
|
115
|
+
* then `columns` joined with a two-space gap (blank columns dropped, so a
|
|
116
|
+
* row that has nothing for its last column doesn't trail whitespace).
|
|
117
|
+
* Column padding (`pad`) is the caller's job — `doctor` pads capability and
|
|
118
|
+
* provider to a fixed width; `clone`'s repo names and paths don't, since
|
|
119
|
+
* they vary too widely for fixed-width alignment to read cleanly.
|
|
120
|
+
*
|
|
121
|
+
* Shared by `doctor` (capability / provider / message) and `clone` (repo /
|
|
122
|
+
* destination / reason) so every status table in the CLI reads the same way.
|
|
123
|
+
*/
|
|
124
|
+
export declare function statusRow(status: RowStatus, columns: string[]): string;
|
|
125
|
+
/** Formats a message with a status marker and colour: one helper per kind of line a command prints. */
|
|
126
|
+
export declare const style: {
|
|
127
|
+
/** A green `✓` for something that worked. */
|
|
128
|
+
success: (msg: string) => string;
|
|
129
|
+
/** A yellow `⚠` for something worth a second look. */
|
|
130
|
+
warn: (msg: string) => string;
|
|
131
|
+
/** A red `✗` for something that failed. */
|
|
132
|
+
fail: (msg: string) => string;
|
|
133
|
+
/** A cyan `→` for a step being taken. */
|
|
134
|
+
step: (msg: string) => string;
|
|
135
|
+
/** A dim `·` marker for a row that's neither a pass nor a failure — loaded but inactive, or skipped. */
|
|
136
|
+
skip: (msg: string) => string;
|
|
137
|
+
/** Dimmed text for a suggestion or a next step. */
|
|
138
|
+
hint: (msg: string) => string;
|
|
139
|
+
/** Dimmed text for detail that should recede. */
|
|
140
|
+
dim: (msg: string) => string;
|
|
141
|
+
/** Bold text for a heading. */
|
|
142
|
+
header: (msg: string) => string;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Print `message` and wait until Enter is pressed, the input ends, or Ctrl-C is pressed, whichever comes first, and
|
|
146
|
+
* return which: `enter`, or `closed` for the other two. It never hangs on a closed input. It reads from stdin and
|
|
147
|
+
* writes to stderr, so a pipe on stdout is never touched; the caller checks there is a terminal to answer it. The
|
|
148
|
+
* streams are parameters so a test can answer for the user.
|
|
149
|
+
*/
|
|
150
|
+
export declare function waitForEnter(message: string, input?: NodeJS.ReadableStream, output?: NodeJS.WritableStream): Promise<"enter" | "closed">;
|
|
151
|
+
export type { Container, Key, Line, Path, Progress, ProgressListener, RenderOptions, RowStatus, ViewJsonOptions, ViewState };
|
package/dist/ui.mjs
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { t as NonInteractiveError } from "./non-interactive-error-Mm_Mn79M.mjs";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
6
|
+
// src/ui/json-view/json-view.ts
|
|
7
|
+
function isContainer(value) {
|
|
8
|
+
return typeof value === "object" && value !== null;
|
|
9
|
+
}
|
|
10
|
+
function childrenOf(value) {
|
|
11
|
+
return Array.isArray(value) ? value.map((v, i) => [String(i), v]) : Object.entries(value);
|
|
12
|
+
}
|
|
13
|
+
function scalar(value) {
|
|
14
|
+
if (typeof value === "string") return chalk.green(JSON.stringify(value));
|
|
15
|
+
if (typeof value === "number") return chalk.yellow(String(value));
|
|
16
|
+
if (typeof value === "boolean") return chalk.magenta(String(value));
|
|
17
|
+
return chalk.dim(String(value === void 0 ? null : value));
|
|
18
|
+
}
|
|
19
|
+
function summary(value) {
|
|
20
|
+
const n = childrenOf(value).length;
|
|
21
|
+
const unit = Array.isArray(value) ? n === 1 ? "item" : "items" : n === 1 ? "key" : "keys";
|
|
22
|
+
return chalk.dim(Array.isArray(value) ? `[… ${n} ${unit}]` : `{… ${n} ${unit}}`);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Paths of every container deeper than `depth` levels are left out, so those
|
|
26
|
+
* render collapsed. `depth: 0` collapses the root; `Infinity` expands all.
|
|
27
|
+
*/
|
|
28
|
+
function expandToDepth(value, depth) {
|
|
29
|
+
const out = /* @__PURE__ */ new Set();
|
|
30
|
+
const walk = (v, path, level) => {
|
|
31
|
+
if (!isContainer(v) || level >= depth) return;
|
|
32
|
+
out.add(path);
|
|
33
|
+
for (const [k, child] of childrenOf(v)) walk(child, `${path}/${k}`, level + 1);
|
|
34
|
+
};
|
|
35
|
+
walk(value, "", 0);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
/** Flatten `value` into the lines visible under `expanded`. Pure — no I/O. */
|
|
39
|
+
function renderTree(value, { expanded }) {
|
|
40
|
+
const lines = [];
|
|
41
|
+
const walk = (v, path, level, label, trailing) => {
|
|
42
|
+
const indent = " ".repeat(level);
|
|
43
|
+
const prefix = `${indent}${label}`;
|
|
44
|
+
if (!isContainer(v)) {
|
|
45
|
+
lines.push({
|
|
46
|
+
path,
|
|
47
|
+
text: `${prefix}${scalar(v)}${trailing}`,
|
|
48
|
+
foldable: false,
|
|
49
|
+
expanded: false,
|
|
50
|
+
level
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const kids = childrenOf(v);
|
|
55
|
+
const [open, close] = Array.isArray(v) ? ["[", "]"] : ["{", "}"];
|
|
56
|
+
if (kids.length === 0) {
|
|
57
|
+
lines.push({
|
|
58
|
+
path,
|
|
59
|
+
text: `${prefix}${open}${close}${trailing}`,
|
|
60
|
+
foldable: false,
|
|
61
|
+
expanded: false,
|
|
62
|
+
level
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!expanded.has(path)) {
|
|
67
|
+
lines.push({
|
|
68
|
+
path,
|
|
69
|
+
text: `${prefix}${summary(v)}${trailing}`,
|
|
70
|
+
foldable: true,
|
|
71
|
+
expanded: false,
|
|
72
|
+
level
|
|
73
|
+
});
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
lines.push({
|
|
77
|
+
path,
|
|
78
|
+
text: `${prefix}${open}`,
|
|
79
|
+
foldable: true,
|
|
80
|
+
expanded: true,
|
|
81
|
+
level
|
|
82
|
+
});
|
|
83
|
+
kids.forEach(([k, child], i) => {
|
|
84
|
+
const comma = i < kids.length - 1 ? "," : "";
|
|
85
|
+
const childLabel = Array.isArray(v) ? "" : `${chalk.cyan(JSON.stringify(k))}: `;
|
|
86
|
+
walk(child, `${path}/${k}`, level + 1, childLabel, comma);
|
|
87
|
+
});
|
|
88
|
+
lines.push({
|
|
89
|
+
path: `${path}#close`,
|
|
90
|
+
text: `${indent}${close}${trailing}`,
|
|
91
|
+
foldable: false,
|
|
92
|
+
expanded: false,
|
|
93
|
+
level
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
walk(value, "", 0, "", "");
|
|
97
|
+
return lines;
|
|
98
|
+
}
|
|
99
|
+
/** Pure key reducer: `(state, key) -> state`. `rows` is the viewport height. */
|
|
100
|
+
function reduceKey(state, key, value, rows) {
|
|
101
|
+
const lines = renderTree(value, { expanded: state.expanded });
|
|
102
|
+
const cur = lines[state.cursor];
|
|
103
|
+
let { cursor } = state;
|
|
104
|
+
const expanded = new Set(state.expanded);
|
|
105
|
+
switch (key) {
|
|
106
|
+
case "up":
|
|
107
|
+
cursor = Math.max(0, cursor - 1);
|
|
108
|
+
break;
|
|
109
|
+
case "down":
|
|
110
|
+
cursor = Math.min(lines.length - 1, cursor + 1);
|
|
111
|
+
break;
|
|
112
|
+
case "right":
|
|
113
|
+
if (cur?.foldable) expanded.add(cur.path);
|
|
114
|
+
break;
|
|
115
|
+
case "left":
|
|
116
|
+
if (cur?.foldable && cur.expanded) expanded.delete(cur.path);
|
|
117
|
+
else if (cur) {
|
|
118
|
+
const parent = lines.findLastIndex((l, i) => i < cursor && l.foldable && l.expanded && l.level < cur.level);
|
|
119
|
+
if (parent >= 0) cursor = parent;
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
case "expandAll": return {
|
|
123
|
+
cursor,
|
|
124
|
+
top: state.top,
|
|
125
|
+
expanded: expandToDepth(value, Infinity)
|
|
126
|
+
};
|
|
127
|
+
case "collapseAll": return {
|
|
128
|
+
cursor: 0,
|
|
129
|
+
top: 0,
|
|
130
|
+
expanded: /* @__PURE__ */ new Set()
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const count = renderTree(value, { expanded }).length;
|
|
134
|
+
cursor = Math.min(cursor, count - 1);
|
|
135
|
+
let top = state.top;
|
|
136
|
+
if (cursor < top) top = cursor;
|
|
137
|
+
if (cursor >= top + rows) top = cursor - rows + 1;
|
|
138
|
+
return {
|
|
139
|
+
cursor,
|
|
140
|
+
top: Math.max(0, top),
|
|
141
|
+
expanded
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const KEYMAP = {
|
|
145
|
+
"\x1B[A": "up",
|
|
146
|
+
k: "up",
|
|
147
|
+
"\x1B[B": "down",
|
|
148
|
+
j: "down",
|
|
149
|
+
"\x1B[C": "right",
|
|
150
|
+
l: "right",
|
|
151
|
+
"\r": "right",
|
|
152
|
+
" ": "right",
|
|
153
|
+
"\x1B[D": "left",
|
|
154
|
+
h: "left",
|
|
155
|
+
E: "expandAll",
|
|
156
|
+
C: "collapseAll",
|
|
157
|
+
q: "quit",
|
|
158
|
+
"\x1B": "quit",
|
|
159
|
+
"": "quit"
|
|
160
|
+
};
|
|
161
|
+
function draw(out, value, state, rows) {
|
|
162
|
+
const lines = renderTree(value, { expanded: state.expanded });
|
|
163
|
+
const view = lines.slice(state.top, state.top + rows).map((l, i) => {
|
|
164
|
+
const row = `${l.foldable ? l.expanded ? "▾ " : "▸ " : " "}${l.text}`;
|
|
165
|
+
return state.top + i === state.cursor ? chalk.inverse(row) : row;
|
|
166
|
+
});
|
|
167
|
+
const footer = chalk.dim(`${state.cursor + 1}/${lines.length} ↑↓ move →/⏎ expand ← collapse E/C all q quit`);
|
|
168
|
+
out.write(`\u001b[H\u001b[J${view.join("\n")}\n${footer}`);
|
|
169
|
+
}
|
|
170
|
+
async function runInteractive(value, depth) {
|
|
171
|
+
const { stdin, stdout } = process;
|
|
172
|
+
if (!stdin.isTTY || !stdout.isTTY) throw new NonInteractiveError("--interactive needs a TTY on stdin and stdout.");
|
|
173
|
+
const rows = () => Math.max(3, (stdout.rows ?? 24) - 1);
|
|
174
|
+
let state = {
|
|
175
|
+
cursor: 0,
|
|
176
|
+
top: 0,
|
|
177
|
+
expanded: expandToDepth(value, depth)
|
|
178
|
+
};
|
|
179
|
+
const wasRaw = stdin.isRaw;
|
|
180
|
+
stdin.setRawMode(true);
|
|
181
|
+
stdin.resume();
|
|
182
|
+
stdout.write("\x1B[?1049h\x1B[?25l");
|
|
183
|
+
const redraw = () => draw(stdout, value, state, rows());
|
|
184
|
+
stdout.on("resize", redraw);
|
|
185
|
+
try {
|
|
186
|
+
redraw();
|
|
187
|
+
await new Promise((resolve) => {
|
|
188
|
+
const onData = (buf) => {
|
|
189
|
+
const key = KEYMAP[buf.toString()];
|
|
190
|
+
if (key === "quit") {
|
|
191
|
+
stdin.off("data", onData);
|
|
192
|
+
resolve();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (key) {
|
|
196
|
+
state = reduceKey(state, key, value, rows());
|
|
197
|
+
redraw();
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
stdin.on("data", onData);
|
|
201
|
+
});
|
|
202
|
+
} finally {
|
|
203
|
+
stdout.off("resize", redraw);
|
|
204
|
+
stdout.write("\x1B[?25h\x1B[?1049l");
|
|
205
|
+
stdin.setRawMode(wasRaw);
|
|
206
|
+
stdin.pause();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Print `value` as JSON. Non-TTY stdout always gets plain, valid
|
|
211
|
+
* `JSON.stringify` output (so `| jq` and redirects keep working); a TTY gets a
|
|
212
|
+
* coloured tree, collapsed past `depth`. `interactive` opens a folding viewer (the default on a TTY).
|
|
213
|
+
*/
|
|
214
|
+
async function viewJson(value, options = {}) {
|
|
215
|
+
const depth = options.depth ?? Infinity;
|
|
216
|
+
if (options.interactive ?? (options.depth === void 0 && Boolean(process.stdin.isTTY && process.stdout.isTTY))) return runInteractive(value, options.depth ?? 1);
|
|
217
|
+
if (!process.stdout.isTTY) {
|
|
218
|
+
console.log(JSON.stringify(value, null, 2));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const lines = renderTree(value, { expanded: expandToDepth(value, depth) });
|
|
222
|
+
console.log(lines.map((l) => l.text).join("\n"));
|
|
223
|
+
}
|
|
224
|
+
// src/ui/open-path/open-path.ts
|
|
225
|
+
/** Platform-appropriate command to open a path in the OS's file browser. */
|
|
226
|
+
function openCommand(path) {
|
|
227
|
+
if (process.platform === "darwin") return {
|
|
228
|
+
cmd: "open",
|
|
229
|
+
args: [path]
|
|
230
|
+
};
|
|
231
|
+
if (process.platform === "win32") return {
|
|
232
|
+
cmd: "cmd",
|
|
233
|
+
args: [
|
|
234
|
+
"/c",
|
|
235
|
+
"start",
|
|
236
|
+
"",
|
|
237
|
+
path
|
|
238
|
+
]
|
|
239
|
+
};
|
|
240
|
+
return {
|
|
241
|
+
cmd: "xdg-open",
|
|
242
|
+
args: [path]
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Open `path` in the OS's default file browser (Finder / Explorer /
|
|
247
|
+
* whatever `xdg-open` resolves to on Linux). Fire-and-forget — detached so
|
|
248
|
+
* it outlives this process, and never throws when the opener binary isn't
|
|
249
|
+
* on PATH (e.g. no `xdg-open` in a minimal container).
|
|
250
|
+
*/
|
|
251
|
+
function openPath(path) {
|
|
252
|
+
const { cmd, args } = openCommand(path);
|
|
253
|
+
const child = spawn(cmd, args, {
|
|
254
|
+
stdio: "ignore",
|
|
255
|
+
detached: true
|
|
256
|
+
});
|
|
257
|
+
child.on("error", () => {});
|
|
258
|
+
child.unref();
|
|
259
|
+
}
|
|
260
|
+
// src/ui/progress/progress.ts
|
|
261
|
+
/**
|
|
262
|
+
* Runs `fn`, showing an ora spinner for its duration in TTY environments.
|
|
263
|
+
* In non-TTY environments (CI, pipes, tests) the spinner is skipped entirely.
|
|
264
|
+
*/
|
|
265
|
+
async function withSpinner(label, fn) {
|
|
266
|
+
if (!process.stdout.isTTY) return fn();
|
|
267
|
+
const spinner = ora(label).start();
|
|
268
|
+
try {
|
|
269
|
+
const result = await fn();
|
|
270
|
+
spinner.succeed();
|
|
271
|
+
return result;
|
|
272
|
+
} catch (err) {
|
|
273
|
+
spinner.fail();
|
|
274
|
+
throw err;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
let allowed = true;
|
|
278
|
+
/** Turn progress off for the run (`--quiet`, `--log`), so nothing extra shares the terminal with what those print. */
|
|
279
|
+
function allowProgress(on) {
|
|
280
|
+
allowed = on;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Whether to draw a progress line: only when a person is watching a terminal (stdout and stderr both), not in CI, and not
|
|
284
|
+
* switched off. Everywhere else the output stays the plain line-per-step text that scripts and the docs rely on.
|
|
285
|
+
*/
|
|
286
|
+
function progressEnabled() {
|
|
287
|
+
return allowed && Boolean(process.stdout.isTTY) && Boolean(process.stderr.isTTY) && !process.env["CI"];
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* A spinner for one step at a time that a step can print through. Steps that only return a result use {@link
|
|
291
|
+
* withProgress}; a step that prints its own result line while it runs prints through `print`. A `listen` hook can change the
|
|
292
|
+
* spinner's text while the step runs, for instance to say that a request is being sent again.
|
|
293
|
+
*/
|
|
294
|
+
function createProgress(print, enabled = progressEnabled, listen) {
|
|
295
|
+
let active;
|
|
296
|
+
return {
|
|
297
|
+
async run(label, fn) {
|
|
298
|
+
if (!enabled()) return fn();
|
|
299
|
+
const spinner = ora({
|
|
300
|
+
text: label,
|
|
301
|
+
stream: process.stderr,
|
|
302
|
+
discardStdin: false
|
|
303
|
+
}).start();
|
|
304
|
+
active = spinner;
|
|
305
|
+
const stopListening = listen?.(label, (text) => {
|
|
306
|
+
spinner.text = text;
|
|
307
|
+
});
|
|
308
|
+
try {
|
|
309
|
+
return await fn();
|
|
310
|
+
} finally {
|
|
311
|
+
stopListening?.();
|
|
312
|
+
spinner.stop();
|
|
313
|
+
active = void 0;
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
print(line) {
|
|
317
|
+
const spinner = active;
|
|
318
|
+
if (!spinner) return print(line);
|
|
319
|
+
spinner.stop();
|
|
320
|
+
print(line);
|
|
321
|
+
spinner.start();
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
/** `fn` under a cleared-when-done spinner named `label`, on a terminal only. */
|
|
326
|
+
function withProgress(label, fn, listen) {
|
|
327
|
+
return createProgress(() => {}, progressEnabled, listen).run(label, fn);
|
|
328
|
+
}
|
|
329
|
+
// src/ui/style/style.ts
|
|
330
|
+
/** Formats a message with a status marker and colour: one helper per kind of line a command prints. */
|
|
331
|
+
const style = {
|
|
332
|
+
/** A green `✓` for something that worked. */
|
|
333
|
+
success: (msg) => `${chalk.green("✓")} ${msg}`,
|
|
334
|
+
/** A yellow `⚠` for something worth a second look. */
|
|
335
|
+
warn: (msg) => `${chalk.yellow("⚠")} ${msg}`,
|
|
336
|
+
/** A red `✗` for something that failed. */
|
|
337
|
+
fail: (msg) => `${chalk.red("✗")} ${msg}`,
|
|
338
|
+
/** A cyan `→` for a step being taken. */
|
|
339
|
+
step: (msg) => `${chalk.cyan("→")} ${msg}`,
|
|
340
|
+
/** A dim `·` marker for a row that's neither a pass nor a failure — loaded but inactive, or skipped. */
|
|
341
|
+
skip: (msg) => chalk.dim(`· ${msg}`),
|
|
342
|
+
/** Dimmed text for a suggestion or a next step. */
|
|
343
|
+
hint: (msg) => chalk.dim(msg),
|
|
344
|
+
/** Dimmed text for detail that should recede. */
|
|
345
|
+
dim: (msg) => chalk.dim(msg),
|
|
346
|
+
/** Bold text for a heading. */
|
|
347
|
+
header: (msg) => chalk.bold(msg)
|
|
348
|
+
};
|
|
349
|
+
// src/ui/status-row/status-row.ts
|
|
350
|
+
/** Right-pads `s` to `width` with spaces — never truncates. */
|
|
351
|
+
function pad(s, width) {
|
|
352
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* One row of a status report: a 2-space indent, the status icon (✓ / ✗ / ·),
|
|
356
|
+
* then `columns` joined with a two-space gap (blank columns dropped, so a
|
|
357
|
+
* row that has nothing for its last column doesn't trail whitespace).
|
|
358
|
+
* Column padding (`pad`) is the caller's job — `doctor` pads capability and
|
|
359
|
+
* provider to a fixed width; `clone`'s repo names and paths don't, since
|
|
360
|
+
* they vary too widely for fixed-width alignment to read cleanly.
|
|
361
|
+
*
|
|
362
|
+
* Shared by `doctor` (capability / provider / message) and `clone` (repo /
|
|
363
|
+
* destination / reason) so every status table in the CLI reads the same way.
|
|
364
|
+
*/
|
|
365
|
+
function statusRow(status, columns) {
|
|
366
|
+
const joined = columns.filter((c) => c.length > 0).join(" ");
|
|
367
|
+
return ` ${status === "ok" ? style.success(joined) : status === "fail" ? style.fail(joined) : style.skip(joined)}`;
|
|
368
|
+
}
|
|
369
|
+
// src/ui/wait-for-enter/wait-for-enter.ts
|
|
370
|
+
/**
|
|
371
|
+
* Print `message` and wait until Enter is pressed, the input ends, or Ctrl-C is pressed, whichever comes first, and
|
|
372
|
+
* return which: `enter`, or `closed` for the other two. It never hangs on a closed input. It reads from stdin and
|
|
373
|
+
* writes to stderr, so a pipe on stdout is never touched; the caller checks there is a terminal to answer it. The
|
|
374
|
+
* streams are parameters so a test can answer for the user.
|
|
375
|
+
*/
|
|
376
|
+
function waitForEnter(message, input = process.stdin, output = process.stderr) {
|
|
377
|
+
return new Promise((resolve) => {
|
|
378
|
+
const rl = createInterface({
|
|
379
|
+
input,
|
|
380
|
+
output
|
|
381
|
+
});
|
|
382
|
+
let answered = false;
|
|
383
|
+
rl.on("close", () => resolve(answered ? "enter" : "closed"));
|
|
384
|
+
rl.on("SIGINT", () => rl.close());
|
|
385
|
+
rl.question(message, () => {
|
|
386
|
+
answered = true;
|
|
387
|
+
rl.close();
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
export { NonInteractiveError, allowProgress, createProgress, expandToDepth, openPath, pad, progressEnabled, reduceKey, renderTree, statusRow, style, viewJson, waitForEnter, withProgress, withSpinner };
|
package/package.json
CHANGED
|
@@ -1,6 +1,129 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theholocron/kyber",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "5.0.0-alpha.338",
|
|
4
|
+
"description": "The reusable parts of a command-line tool: terminal output, prompts, the update notifier, safe handling of tokens, a test harness and the global flags.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"holocron",
|
|
8
|
+
"kyber",
|
|
9
|
+
"theholocron"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://docs.theholocron.dev/holocron/kyber/",
|
|
12
|
+
"bugs": "https://github.com/theholocron/holocron/issues",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/theholocron/holocron.git",
|
|
16
|
+
"directory": "cli/kyber"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "Newton Koumantzelis",
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"type": "module",
|
|
22
|
+
"exports": {
|
|
23
|
+
"./flags": {
|
|
24
|
+
"types": "./dist/flags.d.mts",
|
|
25
|
+
"import": "./dist/flags.mjs",
|
|
26
|
+
"default": "./dist/flags.mjs"
|
|
27
|
+
},
|
|
28
|
+
"./notifier": {
|
|
29
|
+
"types": "./dist/notifier.d.mts",
|
|
30
|
+
"import": "./dist/notifier.mjs",
|
|
31
|
+
"default": "./dist/notifier.mjs"
|
|
32
|
+
},
|
|
33
|
+
"./prompts": {
|
|
34
|
+
"types": "./dist/prompts.d.mts",
|
|
35
|
+
"import": "./dist/prompts.mjs",
|
|
36
|
+
"default": "./dist/prompts.mjs"
|
|
37
|
+
},
|
|
38
|
+
"./secrets": {
|
|
39
|
+
"types": "./dist/secrets.d.mts",
|
|
40
|
+
"import": "./dist/secrets.mjs",
|
|
41
|
+
"default": "./dist/secrets.mjs"
|
|
42
|
+
},
|
|
43
|
+
"./testing": {
|
|
44
|
+
"types": "./dist/testing.d.mts",
|
|
45
|
+
"import": "./dist/testing.mjs",
|
|
46
|
+
"default": "./dist/testing.mjs"
|
|
47
|
+
},
|
|
48
|
+
"./ui": {
|
|
49
|
+
"types": "./dist/ui.d.mts",
|
|
50
|
+
"import": "./dist/ui.mjs",
|
|
51
|
+
"default": "./dist/ui.mjs"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
"dist"
|
|
56
|
+
],
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@inquirer/prompts": "^8.7.5",
|
|
59
|
+
"@inquirer/search": "^4.3.6",
|
|
60
|
+
"@types/node": "^26",
|
|
61
|
+
"@types/yargs": "^17.0.35",
|
|
62
|
+
"@vitest/coverage-v8": "^5.0.5",
|
|
63
|
+
"@vitest/eslint-plugin": "^1.6.28",
|
|
64
|
+
"chalk": "^6.0.1",
|
|
65
|
+
"clipboardy": "^5.3.2",
|
|
66
|
+
"dotenv": "^17.4.2",
|
|
67
|
+
"eslint": "^10.14.0",
|
|
68
|
+
"eslint-plugin-n": "^18.5.0",
|
|
69
|
+
"globals": "^17.14.0",
|
|
70
|
+
"ora": "^9.4.1",
|
|
71
|
+
"tsdown": "^0.23.4",
|
|
72
|
+
"typescript": "^5.9.3",
|
|
73
|
+
"vitest": "^5.0.5",
|
|
74
|
+
"yargs": "^18.1.0",
|
|
75
|
+
"@theholocron/eslint-config": "^8.9.0-alpha.8",
|
|
76
|
+
"@theholocron/tsconfig": "^8.9.0-alpha.8",
|
|
77
|
+
"@theholocron/tsdown-config": "^8.9.0-alpha.8",
|
|
78
|
+
"@theholocron/vitest-config": "^8.9.0-alpha.8"
|
|
79
|
+
},
|
|
80
|
+
"peerDependencies": {
|
|
81
|
+
"@inquirer/prompts": "^8.0.0",
|
|
82
|
+
"@inquirer/search": "^4.0.0",
|
|
83
|
+
"@napi-rs/keyring": "^1.0.0",
|
|
84
|
+
"chalk": "^6.0.0",
|
|
85
|
+
"clipboardy": "^5.0.0",
|
|
86
|
+
"dotenv": "^17.0.0",
|
|
87
|
+
"ora": "^9.0.0",
|
|
88
|
+
"yargs": "^18.0.0"
|
|
89
|
+
},
|
|
90
|
+
"peerDependenciesMeta": {
|
|
91
|
+
"@inquirer/prompts": {
|
|
92
|
+
"optional": true
|
|
93
|
+
},
|
|
94
|
+
"@inquirer/search": {
|
|
95
|
+
"optional": true
|
|
96
|
+
},
|
|
97
|
+
"@napi-rs/keyring": {
|
|
98
|
+
"optional": true
|
|
99
|
+
},
|
|
100
|
+
"chalk": {
|
|
101
|
+
"optional": true
|
|
102
|
+
},
|
|
103
|
+
"clipboardy": {
|
|
104
|
+
"optional": true
|
|
105
|
+
},
|
|
106
|
+
"dotenv": {
|
|
107
|
+
"optional": true
|
|
108
|
+
},
|
|
109
|
+
"ora": {
|
|
110
|
+
"optional": true
|
|
111
|
+
},
|
|
112
|
+
"yargs": {
|
|
113
|
+
"optional": true
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"engines": {
|
|
117
|
+
"node": ">=22"
|
|
118
|
+
},
|
|
119
|
+
"publishConfig": {
|
|
120
|
+
"access": "public"
|
|
121
|
+
},
|
|
122
|
+
"scripts": {
|
|
123
|
+
"build": "tsdown",
|
|
124
|
+
"lint:code": "eslint .",
|
|
125
|
+
"test:unit": "vitest run --coverage",
|
|
126
|
+
"test:watch": "vitest",
|
|
127
|
+
"typecheck": "tsc --noEmit"
|
|
128
|
+
}
|
|
6
129
|
}
|