@emesoft/tokenhub 0.8.2 → 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/bin/tokenhub.js +4 -1
- package/lib/banner.d.ts +1 -0
- package/lib/banner.js +14 -17
- package/lib/pair.d.ts +13 -1
- package/lib/pair.js +121 -4
- package/package.json +1 -1
package/bin/tokenhub.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { adminConfigPath, removeAdminConfig } from "../lib/admin.js";
|
|
4
|
-
import { showBanner } from "../lib/banner.js";
|
|
4
|
+
import { isInteractive, showBanner } from "../lib/banner.js";
|
|
5
5
|
import { parseDashboardArgs, runDashboard } from "../lib/dashboard.js";
|
|
6
6
|
import { runOverlayInstall } from "../lib/overlay.js";
|
|
7
7
|
import { promptTenant, runConnect, TENANT_DOMAIN } from "../lib/pair.js";
|
|
@@ -53,6 +53,9 @@ async function main(argv) {
|
|
|
53
53
|
}
|
|
54
54
|
const file = settingsPath();
|
|
55
55
|
if (command === "connect") {
|
|
56
|
+
if (isInteractive()) {
|
|
57
|
+
await showBanner({ showActions: false });
|
|
58
|
+
}
|
|
56
59
|
const controller = new AbortController();
|
|
57
60
|
process.once("SIGINT", () => controller.abort());
|
|
58
61
|
let poolUrl = arg;
|
package/lib/banner.d.ts
CHANGED
package/lib/banner.js
CHANGED
|
@@ -6,7 +6,6 @@ const TOKEN_COLOR = "\x1b[38;5;39;1m"; // Electric cyan bold
|
|
|
6
6
|
const HUB_COLOR = "\x1b[38;5;208;1m"; // TokenHub warm amber/clay bold
|
|
7
7
|
const CYAN = "\x1b[38;5;39m";
|
|
8
8
|
const MUTED = "\x1b[38;5;244m";
|
|
9
|
-
const GREEN = "\x1b[38;5;114m";
|
|
10
9
|
const RESET = "\x1b[0m";
|
|
11
10
|
const BOLD = "\x1b[1m";
|
|
12
11
|
|
|
@@ -25,15 +24,15 @@ export const LOGO_PARTS = [
|
|
|
25
24
|
},
|
|
26
25
|
{
|
|
27
26
|
token: " ██║ ██║ ██║██╔═██╗ ██╔══╝ ██║╚██╗██║",
|
|
28
|
-
hub: "██╔══██║██║
|
|
27
|
+
hub: "██╔══██║██║ ██║██╔══██╗",
|
|
29
28
|
},
|
|
30
29
|
{
|
|
31
30
|
token: " ██║ ╚██████╔╝██║ ██╗███████╗██║ ╚████║",
|
|
32
|
-
hub: "██║
|
|
31
|
+
hub: "██║ ██║╚██████╔╝██████╔╝",
|
|
33
32
|
},
|
|
34
33
|
{
|
|
35
34
|
token: " ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝",
|
|
36
|
-
hub: "╚═╝ ╚═╝ ╚═════╝
|
|
35
|
+
hub: "╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ",
|
|
37
36
|
},
|
|
38
37
|
];
|
|
39
38
|
|
|
@@ -72,9 +71,10 @@ export function isInteractive(stream = process.stdout) {
|
|
|
72
71
|
* @param {object} [options]
|
|
73
72
|
* @param {NodeJS.WriteStream} [options.stream]
|
|
74
73
|
* @param {boolean} [options.animate]
|
|
74
|
+
* @param {boolean} [options.showActions]
|
|
75
75
|
* @returns {Promise<void>}
|
|
76
76
|
*/
|
|
77
|
-
export async function showBanner({ stream = process.stdout, animate } = {}) {
|
|
77
|
+
export async function showBanner({ stream = process.stdout, animate, showActions = true } = {}) {
|
|
78
78
|
const interactive = animate ?? isInteractive(stream);
|
|
79
79
|
const color = !process.env.NO_COLOR && (interactive || Boolean(stream?.isTTY));
|
|
80
80
|
|
|
@@ -91,8 +91,10 @@ export async function showBanner({ stream = process.stdout, animate } = {}) {
|
|
|
91
91
|
stream.write("\n");
|
|
92
92
|
stream.write(color ? `${CYAN}${SUBTITLE}${RESET}\n` : `${SUBTITLE}\n`);
|
|
93
93
|
stream.write(color ? `${MUTED}${DIVIDER}${RESET}\n` : `${DIVIDER}\n`);
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
if (showActions) {
|
|
95
|
+
for (const action of ACTIONS) {
|
|
96
|
+
stream.write(color ? `${action}\n` : `${action.replace(/\x1b\[[0-9;]*m/g, "")}\n`);
|
|
97
|
+
}
|
|
96
98
|
}
|
|
97
99
|
stream.write("\n");
|
|
98
100
|
return;
|
|
@@ -103,12 +105,6 @@ export async function showBanner({ stream = process.stdout, animate } = {}) {
|
|
|
103
105
|
|
|
104
106
|
// Draw the logo lines progressively with a smooth typewriter scan
|
|
105
107
|
for (const row of LOGO_PARTS) {
|
|
106
|
-
const tokenPart = `${TOKEN_COLOR}${row.token}`;
|
|
107
|
-
const hubPart = `${HUB_COLOR}${row.hub}${RESET}\n`;
|
|
108
|
-
|
|
109
|
-
// Progressive character scan per line
|
|
110
|
-
const fullLine = `${tokenPart}${hubPart}`;
|
|
111
|
-
// Split into 4 chunks per line for high-tech sweep effect
|
|
112
108
|
const chunkSize = 16;
|
|
113
109
|
let idx = 0;
|
|
114
110
|
while (idx < row.token.length) {
|
|
@@ -146,12 +142,13 @@ export async function showBanner({ stream = process.stdout, animate } = {}) {
|
|
|
146
142
|
await sleep(8);
|
|
147
143
|
}
|
|
148
144
|
stream.write(`${RESET}\n`);
|
|
149
|
-
await sleep(40);
|
|
150
145
|
|
|
151
|
-
|
|
152
|
-
for (const action of ACTIONS) {
|
|
153
|
-
stream.write(`${action}\n`);
|
|
146
|
+
if (showActions) {
|
|
154
147
|
await sleep(40);
|
|
148
|
+
for (const action of ACTIONS) {
|
|
149
|
+
stream.write(`${action}\n`);
|
|
150
|
+
await sleep(40);
|
|
151
|
+
}
|
|
155
152
|
}
|
|
156
153
|
|
|
157
154
|
stream.write("\n");
|
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}): `);
|