@emesoft/tokenhub 0.8.3 → 0.8.4
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/lib/pair.d.ts +13 -1
- package/lib/pair.js +121 -4
- package/package.json +1 -1
package/lib/pair.d.ts
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
export const TENANT_DOMAIN: string;
|
|
2
2
|
export function resolvePoolUrl(input: string | undefined): string;
|
|
3
|
-
export function
|
|
3
|
+
export function fetchTenants(platformUrl?: string): Promise<Array<{ slug: string; name: string }>>;
|
|
4
|
+
export function selectChoice(options: {
|
|
5
|
+
message: string;
|
|
6
|
+
choices: Array<{ label: string; value: string }>;
|
|
7
|
+
input?: NodeJS.ReadableStream & { isTTY?: boolean; setRawMode?: (mode: boolean) => void };
|
|
8
|
+
output?: NodeJS.WritableStream;
|
|
9
|
+
}): Promise<string>;
|
|
10
|
+
export function promptTenant(options?: {
|
|
11
|
+
input?: NodeJS.ReadableStream & { isTTY?: boolean; setRawMode?: (mode: boolean) => void };
|
|
12
|
+
output?: NodeJS.WritableStream;
|
|
13
|
+
fetchFn?: (platformUrl?: string) => Promise<Array<{ slug: string; name: string }>>;
|
|
14
|
+
tenantList?: Array<{ slug: string; name: string }>;
|
|
15
|
+
}): Promise<string>;
|
|
4
16
|
export function openBrowser(url: string, platform?: string): void;
|
|
5
17
|
export function runConnect(options: {
|
|
6
18
|
/** Tenant slug (`acme`) or pool URL; required. */
|
package/lib/pair.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { hostname } from "node:os";
|
|
3
|
+
import { emitKeypressEvents } from "node:readline";
|
|
3
4
|
import { createInterface } from "node:readline/promises";
|
|
4
5
|
import { writeAdminConfig } from "./admin.js";
|
|
5
6
|
import { connectSettings, normalizeUrl, overridingEnvVars, readSettings } from "./settings.js";
|
|
@@ -27,13 +28,129 @@ export function resolvePoolUrl(input) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
|
-
*
|
|
31
|
-
* @param {
|
|
32
|
-
* @returns {Promise<string
|
|
31
|
+
* Fetch available active tenants from the platform API.
|
|
32
|
+
* @param {string} [platformUrl] Address of the platform or tenant host.
|
|
33
|
+
* @returns {Promise<Array<{ slug: string, name: string }>>}
|
|
34
|
+
*/
|
|
35
|
+
export async function fetchTenants(platformUrl = `https://${TENANT_DOMAIN}`) {
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(`${platformUrl}/api/tenants`, { signal: AbortSignal.timeout(3000) });
|
|
38
|
+
if (!res.ok) return [{ slug: "emesoft", name: "Emesoft" }];
|
|
39
|
+
const data = await res.json().catch(() => null);
|
|
40
|
+
if (data?.ok && Array.isArray(data.tenants) && data.tenants.length > 0) {
|
|
41
|
+
return data.tenants;
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// offline or unreachable
|
|
45
|
+
}
|
|
46
|
+
return [{ slug: "emesoft", name: "Emesoft" }];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Interactive arrow-key select prompt in terminal.
|
|
51
|
+
* @param {object} options
|
|
52
|
+
* @param {string} options.message Question/prompt to display.
|
|
53
|
+
* @param {Array<{ label: string, value: string }>} options.choices List of choices.
|
|
54
|
+
* @param {NodeJS.ReadableStream & { isTTY?: boolean, setRawMode?: (mode: boolean) => void }} [options.input]
|
|
55
|
+
* @param {NodeJS.WritableStream} [options.output]
|
|
56
|
+
* @returns {Promise<string>} Selected value.
|
|
57
|
+
*/
|
|
58
|
+
export function selectChoice({ message, choices, input = process.stdin, output = process.stdout }) {
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
let selected = 0;
|
|
61
|
+
const isRaw = input.isRaw;
|
|
62
|
+
emitKeypressEvents(input);
|
|
63
|
+
if (typeof input.setRawMode === "function") input.setRawMode(true);
|
|
64
|
+
|
|
65
|
+
const CYAN = "\x1b[36;1m";
|
|
66
|
+
const RESET = "\x1b[0m";
|
|
67
|
+
const DIM = "\x1b[2m";
|
|
68
|
+
const HIDE_CURSOR = "\x1b[?25l";
|
|
69
|
+
const SHOW_CURSOR = "\x1b[?25h";
|
|
70
|
+
|
|
71
|
+
function render(first = false) {
|
|
72
|
+
if (!first) {
|
|
73
|
+
output.write(`\x1b[${choices.length}A`);
|
|
74
|
+
}
|
|
75
|
+
for (let i = 0; i < choices.length; i++) {
|
|
76
|
+
const isCurrent = i === selected;
|
|
77
|
+
const prefix = isCurrent ? `${CYAN}❯ ` : " ";
|
|
78
|
+
const label = isCurrent ? `${CYAN}${choices[i].label}${RESET}` : choices[i].label;
|
|
79
|
+
output.write(`\x1b[2K\r${prefix}${label}\n`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
output.write(`${HIDE_CURSOR}${message} ${DIM}(Use ↑/↓ arrows, Enter to select)${RESET}\n`);
|
|
84
|
+
render(true);
|
|
85
|
+
|
|
86
|
+
function cleanup() {
|
|
87
|
+
output.write(SHOW_CURSOR);
|
|
88
|
+
input.removeListener("keypress", onKeypress);
|
|
89
|
+
if (typeof input.setRawMode === "function") input.setRawMode(isRaw ?? false);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function onKeypress(str, key) {
|
|
93
|
+
if (key && key.name === "c" && key.ctrl) {
|
|
94
|
+
cleanup();
|
|
95
|
+
process.exit(130);
|
|
96
|
+
} else if (key && (key.name === "up" || key.name === "k")) {
|
|
97
|
+
selected = (selected - 1 + choices.length) % choices.length;
|
|
98
|
+
render();
|
|
99
|
+
} else if (key && (key.name === "down" || key.name === "j")) {
|
|
100
|
+
selected = (selected + 1) % choices.length;
|
|
101
|
+
render();
|
|
102
|
+
} else if (key && (key.name === "return" || key.name === "enter")) {
|
|
103
|
+
cleanup();
|
|
104
|
+
resolve(choices[selected].value);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
input.on("keypress", onKeypress);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Ask for the tenant slug on the terminal (interactive select or text input).
|
|
114
|
+
* @param {object} [options] Streams and options for prompting (tests).
|
|
115
|
+
* @param {NodeJS.ReadableStream & { isTTY?: boolean, setRawMode?: (mode: boolean) => void }} [options.input]
|
|
116
|
+
* @param {NodeJS.WritableStream} [options.output]
|
|
117
|
+
* @param {(platformUrl?: string) => Promise<Array<{ slug: string, name: string }>>} [options.fetchFn]
|
|
118
|
+
* @param {Array<{ slug: string, name: string }>} [options.tenantList]
|
|
119
|
+
* @returns {Promise<string>} What was selected or typed.
|
|
33
120
|
* @throws {Error} When stdin is not interactive.
|
|
34
121
|
*/
|
|
35
|
-
export async function promptTenant({
|
|
122
|
+
export async function promptTenant({
|
|
123
|
+
input = process.stdin,
|
|
124
|
+
output = process.stdout,
|
|
125
|
+
fetchFn = fetchTenants,
|
|
126
|
+
tenantList,
|
|
127
|
+
} = {}) {
|
|
36
128
|
if (!input.isTTY) throw new Error(`Name your tenant, e.g. \`tokenhub connect acme\` for https://acme.${TENANT_DOMAIN} (no terminal to ask on).`);
|
|
129
|
+
|
|
130
|
+
const canRaw = typeof input.setRawMode === "function";
|
|
131
|
+
if (canRaw) {
|
|
132
|
+
const list = tenantList ?? (await fetchFn());
|
|
133
|
+
const choices = list.map((t) => ({
|
|
134
|
+
label: `${t.slug} (${t.name})`,
|
|
135
|
+
value: t.slug,
|
|
136
|
+
}));
|
|
137
|
+
choices.push({
|
|
138
|
+
label: "Other... (enter custom tenant)",
|
|
139
|
+
value: "__custom__",
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const chosen = await selectChoice({
|
|
143
|
+
message: `? Select your TokenHub tenant:`,
|
|
144
|
+
choices,
|
|
145
|
+
input,
|
|
146
|
+
output,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (chosen !== "__custom__") {
|
|
150
|
+
return chosen;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
37
154
|
const rl = createInterface({ input, output });
|
|
38
155
|
try {
|
|
39
156
|
return await rl.question(`Tenant name (the part before .${TENANT_DOMAIN}): `);
|