archbyte 0.3.3 → 0.3.5

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/cli/ui.d.ts CHANGED
@@ -8,6 +8,8 @@ export declare function spinner(label: string): Spinner;
8
8
  /**
9
9
  * Arrow-key selection menu. Returns the selected index.
10
10
  * Non-TTY fallback: returns 0 (first option).
11
+ *
12
+ * Navigation: ↑/↓ arrows to move, Enter to confirm, Ctrl+C to exit.
11
13
  */
12
14
  export declare function select(prompt: string, options: string[]): Promise<number>;
13
15
  interface ProgressBar {
@@ -23,6 +25,9 @@ export declare function progressBar(totalSteps: number): ProgressBar;
23
25
  /**
24
26
  * Y/n confirmation prompt. Returns true for y/Enter, false for n.
25
27
  * Non-TTY fallback: returns true.
28
+ *
29
+ * Only responds to explicit y/n/Enter/Ctrl+C. Ignores escape sequences
30
+ * (arrow keys, etc.) to prevent accidental confirmation.
26
31
  */
27
32
  export declare function confirm(prompt: string): Promise<boolean>;
28
33
  export {};
package/dist/cli/ui.js CHANGED
@@ -1,5 +1,26 @@
1
1
  import chalk from "chalk";
2
2
  const BRAILLE_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
3
+ // ─── Cursor Safety ───
4
+ // Ensure the terminal cursor is always restored, even on unhandled crashes.
5
+ let cursorHidden = false;
6
+ function hideCursor() {
7
+ if (!cursorHidden) {
8
+ process.stdout.write("\x1b[?25l");
9
+ cursorHidden = true;
10
+ }
11
+ }
12
+ function showCursor() {
13
+ if (cursorHidden) {
14
+ process.stdout.write("\x1b[?25h");
15
+ cursorHidden = false;
16
+ }
17
+ }
18
+ // Restore cursor on any exit path
19
+ for (const event of ["exit", "SIGINT", "SIGTERM", "uncaughtException", "unhandledRejection"]) {
20
+ process.on(event, () => {
21
+ showCursor();
22
+ });
23
+ }
3
24
  /**
4
25
  * Animated braille spinner. Falls back to static console.log when not a TTY.
5
26
  */
@@ -30,6 +51,8 @@ export function spinner(label) {
30
51
  /**
31
52
  * Arrow-key selection menu. Returns the selected index.
32
53
  * Non-TTY fallback: returns 0 (first option).
54
+ *
55
+ * Navigation: ↑/↓ arrows to move, Enter to confirm, Ctrl+C to exit.
33
56
  */
34
57
  export function select(prompt, options) {
35
58
  if (!process.stdout.isTTY || options.length === 0) {
@@ -45,10 +68,9 @@ export function select(prompt, options) {
45
68
  stdin.resume();
46
69
  stdin.setEncoding("utf8");
47
70
  let selected = 0;
48
- // Hide cursor
49
- process.stdout.write("\x1b[?25l");
71
+ hideCursor();
50
72
  function render() {
51
- // Move up to clear previous render (except first time)
73
+ // Move up to clear previous render
52
74
  const lines = options.length + 1; // prompt + options
53
75
  process.stdout.write(`\x1b[${lines}A`);
54
76
  process.stdout.write(`\x1b[K ${chalk.bold(prompt)}\n`);
@@ -74,13 +96,13 @@ export function select(prompt, options) {
74
96
  }
75
97
  }
76
98
  const onData = (data) => {
77
- if (data === "\x1b[A") {
78
- // Up arrow
99
+ if (data === "\x1b[A" || data === "k") {
100
+ // Up arrow or k (vim-style)
79
101
  selected = (selected - 1 + options.length) % options.length;
80
102
  render();
81
103
  }
82
- else if (data === "\x1b[B") {
83
- // Down arrow
104
+ else if (data === "\x1b[B" || data === "j") {
105
+ // Down arrow or j (vim-style)
84
106
  selected = (selected + 1) % options.length;
85
107
  render();
86
108
  }
@@ -89,18 +111,19 @@ export function select(prompt, options) {
89
111
  cleanup();
90
112
  resolve(selected);
91
113
  }
92
- else if (data === "\x03" || data === "q" || data === "Q") {
93
- // Ctrl+C or q to quit
114
+ else if (data === "\x03") {
115
+ // Ctrl+C only clean exit
94
116
  cleanup();
117
+ process.stdout.write("\n");
95
118
  process.exit(0);
96
119
  }
120
+ // All other keys (including q, arrows, etc.) are ignored
97
121
  };
98
122
  function cleanup() {
99
123
  stdin.removeListener("data", onData);
100
124
  stdin.setRawMode(wasRaw ?? false);
101
125
  stdin.pause();
102
- // Show cursor
103
- process.stdout.write("\x1b[?25h");
126
+ showCursor();
104
127
  }
105
128
  stdin.on("data", onData);
106
129
  });
@@ -152,6 +175,9 @@ export function progressBar(totalSteps) {
152
175
  /**
153
176
  * Y/n confirmation prompt. Returns true for y/Enter, false for n.
154
177
  * Non-TTY fallback: returns true.
178
+ *
179
+ * Only responds to explicit y/n/Enter/Ctrl+C. Ignores escape sequences
180
+ * (arrow keys, etc.) to prevent accidental confirmation.
155
181
  */
156
182
  export function confirm(prompt) {
157
183
  if (!process.stdout.isTTY) {
@@ -166,6 +192,9 @@ export function confirm(prompt) {
166
192
  stdin.resume();
167
193
  stdin.setEncoding("utf8");
168
194
  const onData = (data) => {
195
+ // Ignore escape sequences (arrow keys, function keys, etc.)
196
+ if (data.startsWith("\x1b"))
197
+ return;
169
198
  stdin.removeListener("data", onData);
170
199
  stdin.setRawMode(wasRaw ?? false);
171
200
  stdin.pause();
@@ -173,15 +202,16 @@ export function confirm(prompt) {
173
202
  process.stdout.write("n\n");
174
203
  resolve(false);
175
204
  }
176
- else if (data === "\x03" || data === "q" || data === "Q") {
205
+ else if (data === "\x03") {
206
+ // Ctrl+C only
177
207
  process.stdout.write("\n");
178
208
  process.exit(0);
179
209
  }
180
- else {
181
- // y, Y, Enter — all true
210
+ else if (data === "y" || data === "Y" || data === "\r" || data === "\n") {
182
211
  process.stdout.write("y\n");
183
212
  resolve(true);
184
213
  }
214
+ // Ignore any other single keypresses — wait for y/n/Enter
185
215
  };
186
216
  stdin.on("data", onData);
187
217
  });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Check if a command exists in the system PATH.
3
+ * Uses `which` (Unix) or `where` (Windows).
4
+ */
5
+ export declare function isInPath(cmd: string): boolean;
6
+ /**
7
+ * Mask an API key for safe display.
8
+ * Shows first 6 + last 4 characters: `sk-ant-...7x9z`
9
+ */
10
+ export declare function maskKey(key: string): string;
11
+ /**
12
+ * Whether stdin/stdout support interactive terminal features.
13
+ */
14
+ export declare function isTTY(): boolean;
15
+ /**
16
+ * Resolve the user's home directory, or throw if unresolvable.
17
+ * Prefers $HOME (Unix), falls back to $USERPROFILE (Windows).
18
+ */
19
+ export declare function resolveHome(): string;
20
+ /**
21
+ * Basic email format check. Not exhaustive — just catches obvious typos.
22
+ */
23
+ export declare function isValidEmail(email: string): boolean;
@@ -0,0 +1,52 @@
1
+ // Shared CLI utilities.
2
+ // Single source of truth for helpers used across multiple CLI modules.
3
+ import { execSync } from "child_process";
4
+ /**
5
+ * Check if a command exists in the system PATH.
6
+ * Uses `which` (Unix) or `where` (Windows).
7
+ */
8
+ export function isInPath(cmd) {
9
+ try {
10
+ // Only allow simple command names (no spaces, slashes, or shell metacharacters)
11
+ if (!/^[a-zA-Z0-9._-]+$/.test(cmd))
12
+ return false;
13
+ const which = process.platform === "win32" ? "where" : "which";
14
+ execSync(`${which} ${cmd}`, { stdio: "ignore" });
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ /**
22
+ * Mask an API key for safe display.
23
+ * Shows first 6 + last 4 characters: `sk-ant-...7x9z`
24
+ */
25
+ export function maskKey(key) {
26
+ if (key.length <= 10)
27
+ return "****";
28
+ return key.slice(0, 6) + "..." + key.slice(-4);
29
+ }
30
+ /**
31
+ * Whether stdin/stdout support interactive terminal features.
32
+ */
33
+ export function isTTY() {
34
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
35
+ }
36
+ /**
37
+ * Resolve the user's home directory, or throw if unresolvable.
38
+ * Prefers $HOME (Unix), falls back to $USERPROFILE (Windows).
39
+ */
40
+ export function resolveHome() {
41
+ const home = process.env.HOME ?? process.env.USERPROFILE;
42
+ if (!home) {
43
+ throw new Error("Cannot determine home directory. Set the HOME environment variable.");
44
+ }
45
+ return home;
46
+ }
47
+ /**
48
+ * Basic email format check. Not exhaustive — just catches obvious typos.
49
+ */
50
+ export function isValidEmail(email) {
51
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
52
+ }
@@ -150,7 +150,7 @@ function createHttpServer() {
150
150
  // Read current architecture file
151
151
  const content = await readFile(config.diagramPath, "utf-8");
152
152
  const arch = JSON.parse(content);
153
- // Apply position/dimension updates
153
+ // Apply position/dimension updates only to moved nodes
154
154
  for (const update of updates) {
155
155
  const node = arch.nodes.find((n) => n.id === update.id);
156
156
  if (node) {
@@ -158,10 +158,9 @@ function createHttpServer() {
158
158
  node.y = update.y;
159
159
  node.width = update.width;
160
160
  node.height = update.height;
161
+ node.userPositioned = true;
161
162
  }
162
163
  }
163
- // Mark layout as user-saved so UI uses these positions
164
- arch.layoutSaved = true;
165
164
  // Write back
166
165
  await writeFile(config.diagramPath, JSON.stringify(arch, null, 2), "utf-8");
167
166
  currentArchitecture = arch;
@@ -726,6 +725,7 @@ function createHttpServer() {
726
725
  node.width = update.width;
727
726
  if (update.height)
728
727
  node.height = update.height;
728
+ node.userPositioned = true;
729
729
  }
730
730
  }
731
731
  // Update timestamp
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archbyte",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "ArchByte - See what agents build. As they build it.",
5
5
  "type": "module",
6
6
  "bin": {