@inceptionstack/roundhouse 0.3.17 → 0.3.19

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.
@@ -0,0 +1,142 @@
1
+ /**
2
+ * cli/setup-logger.ts — Structured logging for setup.
3
+ *
4
+ * Interactive mode: human-friendly text with step numbers and emoji.
5
+ * Headless mode: JSON lines for SSM/cloud-init/Docker log parsing.
6
+ */
7
+
8
+ export interface SetupLogger {
9
+ step(n: number, total: number, event: string, message: string, context?: Record<string, unknown>): void;
10
+ info(event: string, message: string, context?: Record<string, unknown>): void;
11
+ warn(event: string, message: string, context?: Record<string, unknown>): void;
12
+ error(event: string, message: string, context?: Record<string, unknown>): void;
13
+ ok(message: string): void;
14
+ fail(message: string): void;
15
+ }
16
+
17
+ const STEP_EMOJI = ["①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨", "⑩"];
18
+
19
+ function stepLabel(n: number): string {
20
+ return STEP_EMOJI[n - 1] ?? `(${n})`;
21
+ }
22
+
23
+ function ts(): string {
24
+ return new Date().toISOString();
25
+ }
26
+
27
+ /**
28
+ * Redact token-like strings from messages and context values.
29
+ * Catches patterns like "123456:AAH..." (Telegram bot tokens).
30
+ */
31
+ function redact(s: string): string {
32
+ return s.replace(/\d{8,}:[A-Za-z0-9_-]{20,}/g, (m) => {
33
+ const parts = m.split(":");
34
+ return `${parts[0].slice(0, 4)}...${parts[1]?.slice(-4) ?? ""}`;
35
+ });
36
+ }
37
+
38
+ function redactContext(ctx?: Record<string, unknown>): Record<string, unknown> | undefined {
39
+ if (!ctx) return undefined;
40
+ return JSON.parse(redact(JSON.stringify(ctx)));
41
+ }
42
+
43
+ export function createTextLogger(): SetupLogger {
44
+ return {
45
+ step(n, _total, _event, message) {
46
+ console.log(`\n${stepLabel(n)} ${redact(message)}`);
47
+ },
48
+ info(_event, message) {
49
+ console.log(` ${redact(message)}`);
50
+ },
51
+ warn(_event, message) {
52
+ console.log(` ⚠ ${redact(message)}`);
53
+ },
54
+ error(_event, message) {
55
+ console.error(` ❌ ${redact(message)}`);
56
+ },
57
+ ok(message) {
58
+ console.log(` ✓ ${redact(message)}`);
59
+ },
60
+ fail(message) {
61
+ console.error(` ✗ ${redact(message)}`);
62
+ },
63
+ };
64
+ }
65
+
66
+ export function createJsonLogger(): SetupLogger {
67
+ function emit(level: string, event: string, message: string, extra?: Record<string, unknown>) {
68
+ const line: Record<string, unknown> = {
69
+ ts: ts(),
70
+ level,
71
+ event,
72
+ message: redact(message),
73
+ ...redactContext(extra),
74
+ };
75
+ console.log(JSON.stringify(line));
76
+ }
77
+
78
+ return {
79
+ step(n, total, event, message, context) {
80
+ emit("info", event, message, { step: n, total, ...context });
81
+ },
82
+ info(event, message, context) {
83
+ emit("info", event, message, context);
84
+ },
85
+ warn(event, message, context) {
86
+ emit("warn", event, message, context);
87
+ },
88
+ error(event, message, context) {
89
+ emit("error", event, message, context);
90
+ },
91
+ ok(message) {
92
+ emit("info", "ok", message);
93
+ },
94
+ fail(message) {
95
+ emit("error", "fail", message);
96
+ },
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Collect diagnostic info for error output.
102
+ */
103
+ export interface SetupDiagnostics {
104
+ node: string;
105
+ platform: string;
106
+ arch: string;
107
+ cwd: string;
108
+ roundhouseDir: string;
109
+ configExists: boolean;
110
+ envExists: boolean;
111
+ pairingStatus: string;
112
+ serviceState: string;
113
+ error: { name: string; message: string; stack?: string };
114
+ }
115
+
116
+ export function printDiagnosticError(diag: SetupDiagnostics, headless: boolean): void {
117
+ if (headless) {
118
+ console.error(JSON.stringify({
119
+ ts: ts(),
120
+ level: "error",
121
+ event: "setup.failed",
122
+ diagnostics: {
123
+ ...diag,
124
+ error: {
125
+ ...diag.error,
126
+ message: redact(diag.error.message),
127
+ stack: diag.error.stack ? redact(diag.error.stack) : undefined,
128
+ },
129
+ },
130
+ }));
131
+ } else {
132
+ console.error(`\n━━━━━━━━━━━━━━━━━━━`);
133
+ console.error(`❌ Setup failed: ${redact(diag.error.message)}`);
134
+ console.error(`\nDiagnostics:`);
135
+ console.error(` Node: ${diag.node}`);
136
+ console.error(` Platform: ${diag.platform} ${diag.arch}`);
137
+ console.error(` Config: ${diag.configExists ? "exists" : "missing"}`);
138
+ console.error(` Env file: ${diag.envExists ? "exists" : "missing"}`);
139
+ console.error(` Pairing: ${diag.pairingStatus}`);
140
+ console.error(` Service: ${diag.serviceState}`);
141
+ }
142
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * cli/setup-prompts.ts — Interactive prompts using only Node built-in readline.
3
+ * No external dependencies.
4
+ */
5
+ import { createInterface, Interface } from "node:readline";
6
+
7
+ /**
8
+ * Prompt the user for text input with optional default.
9
+ */
10
+ export async function promptText(
11
+ question: string,
12
+ options?: { defaultValue?: string },
13
+ ): Promise<string> {
14
+ const suffix = options?.defaultValue ? ` (${options.defaultValue})` : "";
15
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
16
+ try {
17
+ return await new Promise<string>((resolve, reject) => {
18
+ let settled = false;
19
+ rl.question(`${question}${suffix}: `, (answer) => {
20
+ settled = true;
21
+ resolve(answer.trim() || options?.defaultValue || "");
22
+ });
23
+ rl.on("close", () => { if (!settled) reject(new Error("Input cancelled")); });
24
+ });
25
+ } finally {
26
+ rl.close();
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Prompt for secret input — characters are not echoed to the terminal.
32
+ * Uses readline with _writeToOutput override to suppress echo.
33
+ */
34
+ export async function promptMasked(question: string): Promise<string> {
35
+ const rl = createInterface({
36
+ input: process.stdin,
37
+ output: process.stdout,
38
+ terminal: true,
39
+ });
40
+
41
+ // Suppress character echo after the prompt is written
42
+ let prompted = false;
43
+ const originalWrite = (rl as unknown as { _writeToOutput: (s: string) => void })._writeToOutput;
44
+ (rl as unknown as { _writeToOutput: (s: string) => void })._writeToOutput = function (s: string) {
45
+ if (!prompted) {
46
+ originalWrite.call(rl, s);
47
+ if (s.includes(question)) prompted = true;
48
+ }
49
+ // After prompt is shown, swallow all output (no echo)
50
+ };
51
+
52
+ try {
53
+ return await new Promise<string>((resolve, reject) => {
54
+ let settled = false;
55
+ rl.question(`${question}: `, (answer) => {
56
+ settled = true;
57
+ process.stdout.write("\n");
58
+ resolve(answer.trim());
59
+ });
60
+ rl.on("close", () => { if (!settled) reject(new Error("Input cancelled")); });
61
+ });
62
+ } finally {
63
+ rl.close();
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Prompt for yes/no confirmation. Returns true for yes.
69
+ */
70
+ export async function promptConfirm(
71
+ question: string,
72
+ options?: { defaultYes?: boolean },
73
+ ): Promise<boolean> {
74
+ const hint = options?.defaultYes ? "[Y/n]" : "[y/N]";
75
+ const answer = await promptText(`${question} ${hint}`);
76
+ if (!answer) return !!options?.defaultYes;
77
+ return answer.toLowerCase().startsWith("y");
78
+ }
@@ -24,12 +24,12 @@ export interface PairResult {
24
24
 
25
25
  // ── API helper ───────────────────────────────────────
26
26
 
27
- function redactToken(token: string): string {
27
+ export function redactToken(token: string): string {
28
28
  if (token.length < 10) return "***";
29
29
  return token.slice(0, 4) + "..." + token.slice(-4);
30
30
  }
31
31
 
32
- async function telegramApi(
32
+ export async function telegramApi(
33
33
  token: string,
34
34
  method: string,
35
35
  params?: Record<string, unknown>,
@@ -78,6 +78,9 @@ export async function sendMessage(token: string, chatId: number, text: string):
78
78
  /**
79
79
  * Pair with a Telegram user — wait for them to send /start <nonce>.
80
80
  * Returns the user's chat ID and numeric user ID, or null on timeout.
81
+ *
82
+ * If opts.nonce is provided, use it instead of generating a new one.
83
+ * If opts.showLink is false, skip printing the pairing instructions.
81
84
  */
82
85
  export async function pairTelegram(
83
86
  token: string,
@@ -85,8 +88,9 @@ export async function pairTelegram(
85
88
  allowedUsers: string[],
86
89
  timeoutMs = 300_000,
87
90
  log: (msg: string) => void = console.log,
91
+ opts?: { nonce?: string; showLink?: boolean },
88
92
  ): Promise<PairResult | null> {
89
- const nonce = `rh-${randomBytes(3).toString("hex")}`;
93
+ const nonce = opts?.nonce ?? `rh-${randomBytes(8).toString("hex")}`;
90
94
  const normalizedUsers = allowedUsers.map((u) => u.replace(/^@/, "").toLowerCase());
91
95
 
92
96
  // Clear stale updates — advance offset past existing
@@ -101,8 +105,11 @@ export async function pairTelegram(
101
105
  // If getUpdates fails, start from 0
102
106
  }
103
107
 
104
- log(` Open https://t.me/${botUsername} and send: /start ${nonce}`);
105
- log(` Waiting... (Ctrl+C to skip)\n`);
108
+ if (opts?.showLink !== false) {
109
+ log(` Open https://t.me/${botUsername}?start=${nonce} and tap Start`);
110
+ log(` Or send /start ${nonce} to @${botUsername}`);
111
+ log(` Waiting... (Ctrl+C to skip)\n`);
112
+ }
106
113
 
107
114
  const deadline = Date.now() + timeoutMs;
108
115
  while (Date.now() < deadline) {