@agentuity/cli 0.0.48 → 0.0.50

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.
Files changed (282) hide show
  1. package/bin/cli.ts +26 -5
  2. package/dist/banner.d.ts +1 -1
  3. package/dist/banner.d.ts.map +1 -1
  4. package/dist/cli-logger.d.ts +27 -0
  5. package/dist/cli-logger.d.ts.map +1 -0
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cmd/agents/index.d.ts +2 -0
  8. package/dist/cmd/agents/index.d.ts.map +1 -0
  9. package/dist/cmd/auth/index.d.ts.map +1 -1
  10. package/dist/cmd/auth/login.d.ts.map +1 -1
  11. package/dist/cmd/auth/logout.d.ts.map +1 -1
  12. package/dist/cmd/auth/signup.d.ts.map +1 -1
  13. package/dist/cmd/auth/ssh/add.d.ts.map +1 -1
  14. package/dist/cmd/auth/ssh/delete.d.ts.map +1 -1
  15. package/dist/cmd/auth/ssh/index.d.ts +1 -2
  16. package/dist/cmd/auth/ssh/index.d.ts.map +1 -1
  17. package/dist/cmd/auth/ssh/list.d.ts.map +1 -1
  18. package/dist/cmd/auth/whoami.d.ts.map +1 -1
  19. package/dist/cmd/bundle/ast.d.ts +3 -1
  20. package/dist/cmd/bundle/ast.d.ts.map +1 -1
  21. package/dist/cmd/bundle/index.d.ts.map +1 -1
  22. package/dist/cmd/bundle/plugin.d.ts.map +1 -1
  23. package/dist/cmd/capabilities/index.d.ts +4 -0
  24. package/dist/cmd/capabilities/index.d.ts.map +1 -0
  25. package/dist/cmd/capabilities/show.d.ts +20 -0
  26. package/dist/cmd/capabilities/show.d.ts.map +1 -0
  27. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  28. package/dist/cmd/cloud/deployment/index.d.ts.map +1 -1
  29. package/dist/cmd/cloud/deployment/list.d.ts.map +1 -1
  30. package/dist/cmd/cloud/deployment/remove.d.ts.map +1 -1
  31. package/dist/cmd/cloud/deployment/rollback.d.ts.map +1 -1
  32. package/dist/cmd/cloud/deployment/show.d.ts.map +1 -1
  33. package/dist/cmd/cloud/deployment/undeploy.d.ts.map +1 -1
  34. package/dist/cmd/cloud/deployment/utils.d.ts +4 -2
  35. package/dist/cmd/cloud/deployment/utils.d.ts.map +1 -1
  36. package/dist/cmd/cloud/domain.d.ts.map +1 -1
  37. package/dist/cmd/cloud/index.d.ts.map +1 -1
  38. package/dist/cmd/cloud/resource/add.d.ts.map +1 -1
  39. package/dist/cmd/cloud/resource/delete.d.ts.map +1 -1
  40. package/dist/cmd/cloud/resource/index.d.ts +1 -2
  41. package/dist/cmd/cloud/resource/index.d.ts.map +1 -1
  42. package/dist/cmd/cloud/resource/list.d.ts.map +1 -1
  43. package/dist/cmd/cloud/scp/download.d.ts.map +1 -1
  44. package/dist/cmd/cloud/scp/index.d.ts +1 -2
  45. package/dist/cmd/cloud/scp/index.d.ts.map +1 -1
  46. package/dist/cmd/cloud/scp/upload.d.ts.map +1 -1
  47. package/dist/cmd/cloud/session/get.d.ts +2 -0
  48. package/dist/cmd/cloud/session/get.d.ts.map +1 -0
  49. package/dist/cmd/cloud/session/index.d.ts +2 -0
  50. package/dist/cmd/cloud/session/index.d.ts.map +1 -0
  51. package/dist/cmd/cloud/session/list.d.ts +2 -0
  52. package/dist/cmd/cloud/session/list.d.ts.map +1 -0
  53. package/dist/cmd/cloud/session/logs.d.ts +2 -0
  54. package/dist/cmd/cloud/session/logs.d.ts.map +1 -0
  55. package/dist/cmd/cloud/ssh.d.ts.map +1 -1
  56. package/dist/cmd/dev/agents.d.ts +2 -0
  57. package/dist/cmd/dev/agents.d.ts.map +1 -0
  58. package/dist/cmd/dev/index.d.ts.map +1 -1
  59. package/dist/cmd/dev/sync.d.ts +12 -0
  60. package/dist/cmd/dev/sync.d.ts.map +1 -0
  61. package/dist/cmd/env/delete.d.ts.map +1 -1
  62. package/dist/cmd/env/get.d.ts.map +1 -1
  63. package/dist/cmd/env/import.d.ts.map +1 -1
  64. package/dist/cmd/env/index.d.ts.map +1 -1
  65. package/dist/cmd/env/list.d.ts.map +1 -1
  66. package/dist/cmd/env/pull.d.ts.map +1 -1
  67. package/dist/cmd/env/push.d.ts.map +1 -1
  68. package/dist/cmd/env/set.d.ts.map +1 -1
  69. package/dist/cmd/index.d.ts.map +1 -1
  70. package/dist/cmd/kv/create-namespace.d.ts +3 -0
  71. package/dist/cmd/kv/create-namespace.d.ts.map +1 -0
  72. package/dist/cmd/kv/delete-namespace.d.ts +3 -0
  73. package/dist/cmd/kv/delete-namespace.d.ts.map +1 -0
  74. package/dist/cmd/kv/delete.d.ts +3 -0
  75. package/dist/cmd/kv/delete.d.ts.map +1 -0
  76. package/dist/cmd/kv/get.d.ts +3 -0
  77. package/dist/cmd/kv/get.d.ts.map +1 -0
  78. package/dist/cmd/kv/index.d.ts +2 -0
  79. package/dist/cmd/kv/index.d.ts.map +1 -0
  80. package/dist/cmd/kv/keys.d.ts +3 -0
  81. package/dist/cmd/kv/keys.d.ts.map +1 -0
  82. package/dist/cmd/kv/list-namespaces.d.ts +3 -0
  83. package/dist/cmd/kv/list-namespaces.d.ts.map +1 -0
  84. package/dist/cmd/kv/repl.d.ts +3 -0
  85. package/dist/cmd/kv/repl.d.ts.map +1 -0
  86. package/dist/cmd/kv/search.d.ts +3 -0
  87. package/dist/cmd/kv/search.d.ts.map +1 -0
  88. package/dist/cmd/kv/set.d.ts +3 -0
  89. package/dist/cmd/kv/set.d.ts.map +1 -0
  90. package/dist/cmd/kv/stats.d.ts +3 -0
  91. package/dist/cmd/kv/stats.d.ts.map +1 -0
  92. package/dist/cmd/kv/util.d.ts +8 -0
  93. package/dist/cmd/kv/util.d.ts.map +1 -0
  94. package/dist/cmd/objectstore/delete-bucket.d.ts +3 -0
  95. package/dist/cmd/objectstore/delete-bucket.d.ts.map +1 -0
  96. package/dist/cmd/objectstore/delete.d.ts +3 -0
  97. package/dist/cmd/objectstore/delete.d.ts.map +1 -0
  98. package/dist/cmd/objectstore/get.d.ts +3 -0
  99. package/dist/cmd/objectstore/get.d.ts.map +1 -0
  100. package/dist/cmd/objectstore/index.d.ts +2 -0
  101. package/dist/cmd/objectstore/index.d.ts.map +1 -0
  102. package/dist/cmd/objectstore/list-buckets.d.ts +3 -0
  103. package/dist/cmd/objectstore/list-buckets.d.ts.map +1 -0
  104. package/dist/cmd/objectstore/list-keys.d.ts +3 -0
  105. package/dist/cmd/objectstore/list-keys.d.ts.map +1 -0
  106. package/dist/cmd/objectstore/put.d.ts +3 -0
  107. package/dist/cmd/objectstore/put.d.ts.map +1 -0
  108. package/dist/cmd/objectstore/repl.d.ts +3 -0
  109. package/dist/cmd/objectstore/repl.d.ts.map +1 -0
  110. package/dist/cmd/objectstore/url.d.ts +3 -0
  111. package/dist/cmd/objectstore/url.d.ts.map +1 -0
  112. package/dist/cmd/objectstore/util.d.ts +8 -0
  113. package/dist/cmd/objectstore/util.d.ts.map +1 -0
  114. package/dist/cmd/profile/create.d.ts.map +1 -1
  115. package/dist/cmd/profile/delete.d.ts.map +1 -1
  116. package/dist/cmd/profile/index.d.ts.map +1 -1
  117. package/dist/cmd/profile/list.d.ts +1 -2
  118. package/dist/cmd/profile/list.d.ts.map +1 -1
  119. package/dist/cmd/profile/show.d.ts.map +1 -1
  120. package/dist/cmd/profile/use.d.ts.map +1 -1
  121. package/dist/cmd/project/create.d.ts.map +1 -1
  122. package/dist/cmd/project/delete.d.ts.map +1 -1
  123. package/dist/cmd/project/index.d.ts.map +1 -1
  124. package/dist/cmd/project/list.d.ts.map +1 -1
  125. package/dist/cmd/project/show.d.ts.map +1 -1
  126. package/dist/cmd/project/template-flow.d.ts +1 -1
  127. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  128. package/dist/cmd/prompt/index.d.ts +4 -0
  129. package/dist/cmd/prompt/index.d.ts.map +1 -0
  130. package/dist/cmd/prompt/llm.d.ts +3 -0
  131. package/dist/cmd/prompt/llm.d.ts.map +1 -0
  132. package/dist/cmd/repl/index.d.ts +3 -0
  133. package/dist/cmd/repl/index.d.ts.map +1 -0
  134. package/dist/cmd/schema/index.d.ts +4 -0
  135. package/dist/cmd/schema/index.d.ts.map +1 -0
  136. package/dist/cmd/schema/show.d.ts +3 -0
  137. package/dist/cmd/schema/show.d.ts.map +1 -0
  138. package/dist/cmd/secret/delete.d.ts.map +1 -1
  139. package/dist/cmd/secret/get.d.ts.map +1 -1
  140. package/dist/cmd/secret/import.d.ts.map +1 -1
  141. package/dist/cmd/secret/index.d.ts.map +1 -1
  142. package/dist/cmd/secret/list.d.ts.map +1 -1
  143. package/dist/cmd/secret/pull.d.ts.map +1 -1
  144. package/dist/cmd/secret/push.d.ts.map +1 -1
  145. package/dist/cmd/secret/set.d.ts.map +1 -1
  146. package/dist/cmd/version/index.d.ts.map +1 -1
  147. package/dist/config.d.ts +1 -1
  148. package/dist/config.d.ts.map +1 -1
  149. package/dist/errors.d.ts +83 -0
  150. package/dist/errors.d.ts.map +1 -0
  151. package/dist/explain.d.ts +47 -0
  152. package/dist/explain.d.ts.map +1 -0
  153. package/dist/index.d.ts +6 -0
  154. package/dist/index.d.ts.map +1 -1
  155. package/dist/json.d.ts +3 -0
  156. package/dist/json.d.ts.map +1 -0
  157. package/dist/output.d.ts +136 -0
  158. package/dist/output.d.ts.map +1 -0
  159. package/dist/repl.d.ts +120 -0
  160. package/dist/repl.d.ts.map +1 -0
  161. package/dist/schema-generator.d.ts +67 -0
  162. package/dist/schema-generator.d.ts.map +1 -0
  163. package/dist/tui.d.ts +35 -1
  164. package/dist/tui.d.ts.map +1 -1
  165. package/dist/types.d.ts +77 -6
  166. package/dist/types.d.ts.map +1 -1
  167. package/dist/utils/format.d.ts +9 -0
  168. package/dist/utils/format.d.ts.map +1 -0
  169. package/package.json +12 -4
  170. package/src/banner.ts +7 -7
  171. package/src/cli-logger.ts +80 -0
  172. package/src/cli.ts +192 -58
  173. package/src/cmd/agents/index.ts +147 -0
  174. package/src/cmd/auth/index.ts +1 -0
  175. package/src/cmd/auth/login.ts +7 -2
  176. package/src/cmd/auth/logout.ts +4 -0
  177. package/src/cmd/auth/signup.ts +7 -2
  178. package/src/cmd/auth/ssh/add.ts +20 -3
  179. package/src/cmd/auth/ssh/delete.ts +57 -4
  180. package/src/cmd/auth/ssh/index.ts +4 -3
  181. package/src/cmd/auth/ssh/list.ts +38 -27
  182. package/src/cmd/auth/whoami.ts +32 -21
  183. package/src/cmd/bundle/ast.test.ts +2 -2
  184. package/src/cmd/bundle/ast.ts +112 -22
  185. package/src/cmd/bundle/index.ts +20 -0
  186. package/src/cmd/bundle/plugin.ts +60 -14
  187. package/src/cmd/capabilities/index.ts +12 -0
  188. package/src/cmd/capabilities/show.ts +256 -0
  189. package/src/cmd/cloud/deploy.ts +54 -0
  190. package/src/cmd/cloud/deployment/index.ts +1 -0
  191. package/src/cmd/cloud/deployment/list.ts +66 -25
  192. package/src/cmd/cloud/deployment/remove.ts +26 -2
  193. package/src/cmd/cloud/deployment/rollback.ts +35 -4
  194. package/src/cmd/cloud/deployment/show.ts +37 -2
  195. package/src/cmd/cloud/deployment/undeploy.ts +12 -1
  196. package/src/cmd/cloud/deployment/utils.ts +5 -2
  197. package/src/cmd/cloud/domain.ts +3 -2
  198. package/src/cmd/cloud/index.ts +10 -1
  199. package/src/cmd/cloud/resource/add.ts +19 -0
  200. package/src/cmd/cloud/resource/delete.ts +24 -3
  201. package/src/cmd/cloud/resource/index.ts +4 -3
  202. package/src/cmd/cloud/resource/list.ts +36 -10
  203. package/src/cmd/cloud/scp/download.ts +27 -1
  204. package/src/cmd/cloud/scp/index.ts +4 -3
  205. package/src/cmd/cloud/scp/upload.ts +27 -1
  206. package/src/cmd/cloud/session/get.ts +164 -0
  207. package/src/cmd/cloud/session/index.ts +11 -0
  208. package/src/cmd/cloud/session/list.ts +145 -0
  209. package/src/cmd/cloud/session/logs.ts +68 -0
  210. package/src/cmd/cloud/ssh.ts +12 -0
  211. package/src/cmd/dev/agents.ts +122 -0
  212. package/src/cmd/dev/index.ts +106 -8
  213. package/src/cmd/dev/sync.ts +414 -0
  214. package/src/cmd/dev/templates.ts +1 -1
  215. package/src/cmd/env/delete.ts +17 -0
  216. package/src/cmd/env/get.ts +17 -1
  217. package/src/cmd/env/import.ts +47 -3
  218. package/src/cmd/env/index.ts +1 -0
  219. package/src/cmd/env/list.ts +13 -1
  220. package/src/cmd/env/pull.ts +20 -0
  221. package/src/cmd/env/push.ts +33 -1
  222. package/src/cmd/env/set.ts +25 -1
  223. package/src/cmd/index.ts +9 -2
  224. package/src/cmd/kv/create-namespace.ts +45 -0
  225. package/src/cmd/kv/delete-namespace.ts +73 -0
  226. package/src/cmd/kv/delete.ts +51 -0
  227. package/src/cmd/kv/get.ts +65 -0
  228. package/src/cmd/kv/index.ts +31 -0
  229. package/src/cmd/kv/keys.ts +57 -0
  230. package/src/cmd/kv/list-namespaces.ts +43 -0
  231. package/src/cmd/kv/repl.ts +284 -0
  232. package/src/cmd/kv/search.ts +80 -0
  233. package/src/cmd/kv/set.ts +63 -0
  234. package/src/cmd/kv/stats.ts +96 -0
  235. package/src/cmd/kv/util.ts +32 -0
  236. package/src/cmd/objectstore/delete-bucket.ts +72 -0
  237. package/src/cmd/objectstore/delete.ts +59 -0
  238. package/src/cmd/objectstore/get.ts +64 -0
  239. package/src/cmd/objectstore/index.ts +27 -0
  240. package/src/cmd/objectstore/list-buckets.ts +45 -0
  241. package/src/cmd/objectstore/list-keys.ts +60 -0
  242. package/src/cmd/objectstore/put.ts +62 -0
  243. package/src/cmd/objectstore/repl.ts +235 -0
  244. package/src/cmd/objectstore/url.ts +59 -0
  245. package/src/cmd/objectstore/util.ts +28 -0
  246. package/src/cmd/profile/create.ts +28 -2
  247. package/src/cmd/profile/delete.ts +17 -2
  248. package/src/cmd/profile/index.ts +1 -0
  249. package/src/cmd/profile/list.ts +7 -3
  250. package/src/cmd/profile/show.ts +20 -5
  251. package/src/cmd/profile/use.ts +8 -0
  252. package/src/cmd/project/create.ts +31 -0
  253. package/src/cmd/project/delete.ts +24 -2
  254. package/src/cmd/project/index.ts +1 -0
  255. package/src/cmd/project/list.ts +24 -10
  256. package/src/cmd/project/show.ts +28 -9
  257. package/src/cmd/project/template-flow.ts +10 -6
  258. package/src/cmd/prompt/index.ts +12 -0
  259. package/src/cmd/prompt/llm.ts +368 -0
  260. package/src/cmd/repl/index.ts +477 -0
  261. package/src/cmd/schema/index.ts +12 -0
  262. package/src/cmd/schema/show.ts +27 -0
  263. package/src/cmd/secret/delete.ts +17 -0
  264. package/src/cmd/secret/get.ts +20 -1
  265. package/src/cmd/secret/import.ts +45 -2
  266. package/src/cmd/secret/index.ts +1 -0
  267. package/src/cmd/secret/list.ts +10 -1
  268. package/src/cmd/secret/pull.ts +20 -0
  269. package/src/cmd/secret/push.ts +33 -1
  270. package/src/cmd/secret/set.ts +20 -0
  271. package/src/cmd/version/index.ts +15 -2
  272. package/src/config.ts +17 -4
  273. package/src/errors.ts +222 -0
  274. package/src/explain.ts +126 -0
  275. package/src/index.ts +51 -0
  276. package/src/json.ts +28 -0
  277. package/src/output.ts +307 -0
  278. package/src/repl.ts +1507 -0
  279. package/src/schema-generator.ts +389 -0
  280. package/src/tui.ts +178 -13
  281. package/src/types.ts +75 -22
  282. package/src/utils/format.ts +17 -0
package/src/repl.ts ADDED
@@ -0,0 +1,1507 @@
1
+ /**
2
+ * Reusable REPL (Read-Eval-Print Loop) component for building interactive CLI tools
3
+ */
4
+
5
+ import * as tui from './tui';
6
+ import { getDefaultConfigDir } from './config';
7
+ import { join } from 'node:path';
8
+ import { mkdir } from 'node:fs/promises';
9
+ import { z } from 'zod';
10
+ import { colorize } from 'json-colorizer';
11
+
12
+ /**
13
+ * Result of parsing a REPL command
14
+ */
15
+ export interface ParsedCommand {
16
+ /** The command name */
17
+ command: string;
18
+ /** Command arguments (positional) */
19
+ args: string[];
20
+ /** Command options (flags/named parameters) */
21
+ options: Record<string, string | boolean>;
22
+ }
23
+
24
+ /**
25
+ * Table column definition
26
+ */
27
+ export type TableColumn = tui.TableColumn;
28
+
29
+ /**
30
+ * Context provided to command handlers
31
+ */
32
+ export interface ReplContext {
33
+ /** The parsed command */
34
+ parsed: ParsedCommand;
35
+ /** Raw input line */
36
+ raw: string;
37
+ /** Write output to the REPL */
38
+ write: (message: string) => void;
39
+ /** Write error output to the REPL */
40
+ error: (message: string) => void;
41
+ /** Write success output to the REPL */
42
+ success: (message: string) => void;
43
+ /** Write info output to the REPL */
44
+ info: (message: string) => void;
45
+ /** Write warning output to the REPL */
46
+ warning: (message: string) => void;
47
+ /** Write debug output to the REPL */
48
+ debug: (message: string) => void;
49
+ /** Update the progress/activity message */
50
+ setProgress: (message: string) => void;
51
+ /** Abort signal for cancelling long-running operations */
52
+ signal: AbortSignal;
53
+ /** Exit the REPL */
54
+ exit: () => void;
55
+ /** Render a table with columns and data */
56
+ table: (columns: TableColumn[], data: Record<string, unknown>[]) => void;
57
+ /** Render colorized JSON output */
58
+ json: (value: unknown) => void;
59
+ }
60
+
61
+ /**
62
+ * Command handler function - can return void, Promise<void>, or an async generator for streaming
63
+ */
64
+ export type CommandHandler = (
65
+ ctx: ReplContext
66
+ ) => void | Promise<void> | AsyncGenerator<string, void, unknown>;
67
+
68
+ /**
69
+ * Command schema for validation and autocomplete
70
+ */
71
+ export interface ReplCommandSchema {
72
+ /** Zod schema for arguments (positional) */
73
+ args?: z.ZodTuple<[z.ZodTypeAny, ...z.ZodTypeAny[]]> | z.ZodArray<z.ZodTypeAny>;
74
+ /** Zod schema for options (flags) */
75
+ options?: z.ZodObject<z.ZodRawShape>;
76
+ /** Argument names for display (e.g., ['message', 'count']) */
77
+ argNames?: string[];
78
+ }
79
+
80
+ /**
81
+ * Subcommand definition
82
+ */
83
+ export interface ReplSubcommand {
84
+ /** Subcommand name */
85
+ name: string;
86
+ /** Subcommand description */
87
+ description?: string;
88
+ /** Subcommand handler */
89
+ handler: CommandHandler;
90
+ /** Aliases for this subcommand */
91
+ aliases?: string[];
92
+ /** Schema for validation and autocomplete hints */
93
+ schema?: ReplCommandSchema;
94
+ }
95
+
96
+ /**
97
+ * Command definition for the REPL
98
+ */
99
+ export interface ReplCommand {
100
+ /** Command name */
101
+ name: string;
102
+ /** Command description (shown in help) */
103
+ description?: string;
104
+ /** Command handler (not used if subcommands provided) */
105
+ handler?: CommandHandler;
106
+ /** Aliases for this command */
107
+ aliases?: string[];
108
+ /** Schema for validation and autocomplete hints */
109
+ schema?: ReplCommandSchema;
110
+ /** Subcommands for this command group */
111
+ subcommands?: ReplSubcommand[];
112
+ }
113
+
114
+ /**
115
+ * REPL configuration
116
+ */
117
+ export interface ReplConfig {
118
+ /** REPL prompt (default: "> ") */
119
+ prompt?: string;
120
+ /** Welcome message shown on startup */
121
+ welcome?: string;
122
+ /** Exit message shown on exit */
123
+ exitMessage?: string;
124
+ /** Commands to register */
125
+ commands: ReplCommand[];
126
+ /** Show help command automatically (default: true) */
127
+ showHelp?: boolean;
128
+ /** Name for history file (saved as ~/.config/agentuity/history/<name>.txt) */
129
+ name?: string;
130
+ }
131
+
132
+ /**
133
+ * Parse a command line into command, args, and options
134
+ */
135
+ function parseCommandLine(line: string): ParsedCommand {
136
+ const tokens: string[] = [];
137
+ let current = '';
138
+ let inQuotes = false;
139
+ let quoteChar = '';
140
+ let braceDepth = 0;
141
+ let bracketDepth = 0;
142
+
143
+ // Tokenize the input, respecting quotes and JSON objects/arrays
144
+ for (let i = 0; i < line.length; i++) {
145
+ const char = line[i];
146
+
147
+ if ((char === '"' || char === "'") && !inQuotes) {
148
+ inQuotes = true;
149
+ quoteChar = char;
150
+ current += char;
151
+ } else if (char === quoteChar && inQuotes) {
152
+ inQuotes = false;
153
+ quoteChar = '';
154
+ current += char;
155
+ } else if (char === '{' && !inQuotes) {
156
+ braceDepth++;
157
+ current += char;
158
+ } else if (char === '}' && !inQuotes) {
159
+ braceDepth--;
160
+ current += char;
161
+ } else if (char === '[' && !inQuotes) {
162
+ bracketDepth++;
163
+ current += char;
164
+ } else if (char === ']' && !inQuotes) {
165
+ bracketDepth--;
166
+ current += char;
167
+ } else if (char === ' ' && !inQuotes && braceDepth === 0 && bracketDepth === 0) {
168
+ if (current) {
169
+ tokens.push(current);
170
+ current = '';
171
+ }
172
+ } else {
173
+ current += char;
174
+ }
175
+ }
176
+ if (current) {
177
+ tokens.push(current);
178
+ }
179
+
180
+ const command = tokens[0] || '';
181
+ const args: string[] = [];
182
+ const options: Record<string, string | boolean> = {};
183
+
184
+ // Parse remaining tokens into args and options
185
+ for (let i = 1; i < tokens.length; i++) {
186
+ const token = tokens[i];
187
+
188
+ if (token.startsWith('--')) {
189
+ // Long option: --name=value or --flag
190
+ const name = token.slice(2);
191
+ const eqIndex = name.indexOf('=');
192
+
193
+ if (eqIndex > 0) {
194
+ options[name.slice(0, eqIndex)] = name.slice(eqIndex + 1);
195
+ } else {
196
+ // Check if next token is a value
197
+ if (i + 1 < tokens.length && !tokens[i + 1].startsWith('-')) {
198
+ options[name] = tokens[i + 1];
199
+ i++;
200
+ } else {
201
+ options[name] = true;
202
+ }
203
+ }
204
+ } else if (token.startsWith('-') && token.length > 1) {
205
+ // Short option: -f or -n value
206
+ const name = token.slice(1);
207
+
208
+ // Check if next token is a value
209
+ if (i + 1 < tokens.length && !tokens[i + 1].startsWith('-')) {
210
+ options[name] = tokens[i + 1];
211
+ i++;
212
+ } else {
213
+ options[name] = true;
214
+ }
215
+ } else {
216
+ args.push(token);
217
+ }
218
+ }
219
+
220
+ return { command, args, options };
221
+ }
222
+
223
+ /**
224
+ * Load history from file
225
+ */
226
+ async function loadHistory(name: string): Promise<string[]> {
227
+ if (!name) return [];
228
+
229
+ try {
230
+ const historyDir = join(getDefaultConfigDir(), 'history');
231
+ const historyFile = join(historyDir, `${name}.txt`);
232
+
233
+ if (!(await Bun.file(historyFile).exists())) {
234
+ return [];
235
+ }
236
+
237
+ const content = await Bun.file(historyFile).text();
238
+ return content.split('\n').filter((line) => line.trim());
239
+ } catch (_err) {
240
+ return [];
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Save history to file
246
+ */
247
+ async function saveHistory(name: string, history: string[]): Promise<void> {
248
+ if (!name) return;
249
+
250
+ try {
251
+ const historyDir = join(getDefaultConfigDir(), 'history');
252
+ await mkdir(historyDir, { recursive: true });
253
+
254
+ const historyFile = join(historyDir, `${name}.txt`);
255
+ await Bun.write(historyFile, history.join('\n'));
256
+ } catch (_err) {
257
+ // Silently fail - history is not critical
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Create and run a REPL
263
+ */
264
+ export async function createRepl(config: ReplConfig): Promise<void> {
265
+ const prompt = config.prompt || '> ';
266
+ const showHelp = config.showHelp !== false;
267
+ const historyName = config.name || '';
268
+
269
+ // Load command history
270
+ const history = await loadHistory(historyName);
271
+ let historyIndex = history.length;
272
+
273
+ // Build command map with aliases
274
+ const commandMap = new Map<string, ReplCommand>();
275
+ for (const cmd of config.commands) {
276
+ commandMap.set(cmd.name.toLowerCase(), cmd);
277
+ if (cmd.aliases) {
278
+ for (const alias of cmd.aliases) {
279
+ commandMap.set(alias.toLowerCase(), cmd);
280
+ }
281
+ }
282
+ }
283
+
284
+ // Add built-in help command
285
+ if (showHelp) {
286
+ const helpCommand: ReplCommand = {
287
+ name: 'help',
288
+ description: 'Show available commands',
289
+ aliases: ['?'],
290
+ handler: (ctx) => {
291
+ ctx.info('Available commands:');
292
+ tui.newline();
293
+
294
+ for (const cmd of config.commands) {
295
+ const aliases = cmd.aliases ? ` (${cmd.aliases.join(', ')})` : '';
296
+ const desc = cmd.description || 'No description';
297
+ console.log(` ${tui.bold(cmd.name)}${tui.muted(aliases)}`);
298
+ console.log(` ${desc}`);
299
+ }
300
+
301
+ tui.newline();
302
+ console.log(` ${tui.bold('exit')} ${tui.muted('(quit, q)')}`);
303
+ console.log(` Exit the REPL`);
304
+ },
305
+ };
306
+ commandMap.set('help', helpCommand);
307
+ commandMap.set('?', helpCommand);
308
+ }
309
+
310
+ // Show welcome message
311
+ if (config.welcome) {
312
+ console.log(tui.bold(config.welcome));
313
+ tui.newline();
314
+ }
315
+
316
+ // REPL state
317
+ let running = true;
318
+ const ctrlCState = { lastTime: 0 };
319
+ let commandAbortController: AbortController | null = null;
320
+
321
+ const exitRepl = () => {
322
+ running = false;
323
+ };
324
+
325
+ // Build list of all command names for autocomplete
326
+ const commandNames = Array.from(commandMap.keys());
327
+
328
+ // Remove any existing SIGINT handlers to prevent default exit
329
+ process.removeAllListeners('SIGINT');
330
+
331
+ // Setup global SIGINT handler to prevent default exit
332
+ const globalSigintHandler = () => {
333
+ // If command is running, abort it
334
+ if (commandAbortController && !commandAbortController.signal.aborted) {
335
+ commandAbortController.abort();
336
+ // Don't exit - just abort the command
337
+ return;
338
+ }
339
+ // If not running command, ignore - let raw mode handler deal with it
340
+ };
341
+ process.on('SIGINT', globalSigintHandler);
342
+
343
+ // Main REPL loop
344
+ while (running) {
345
+ // Reset Ctrl+C timer when starting new command
346
+ ctrlCState.lastTime = 0;
347
+
348
+ // Read input with history support
349
+ process.stdout.write(prompt);
350
+
351
+ const input = await readLine(
352
+ history,
353
+ historyIndex,
354
+ prompt,
355
+ commandNames,
356
+ commandMap,
357
+ ctrlCState
358
+ );
359
+ if (input === null) {
360
+ // EOF (Ctrl+D)
361
+ break;
362
+ }
363
+
364
+ const { line: rawInput, newHistoryIndex } = input;
365
+ historyIndex = newHistoryIndex;
366
+
367
+ const line = rawInput.trim();
368
+ if (!line) {
369
+ continue;
370
+ }
371
+
372
+ // Parse command
373
+ const parsed = parseCommandLine(line);
374
+
375
+ // Check for exit commands
376
+ if (['exit', 'quit', 'q'].includes(parsed.command.toLowerCase())) {
377
+ break;
378
+ }
379
+
380
+ // Find and execute command
381
+ const cmd = commandMap.get(parsed.command.toLowerCase());
382
+ if (!cmd) {
383
+ tui.error(`Unknown command: ${parsed.command}`);
384
+ console.log(`Type ${tui.bold('help')} for available commands`);
385
+ continue;
386
+ }
387
+
388
+ // Check if command has subcommands
389
+ let actualHandler = cmd.handler;
390
+ let actualSchema = cmd.schema;
391
+
392
+ if (cmd.subcommands && cmd.subcommands.length > 0) {
393
+ // Parse subcommand from first arg
394
+ const subcommandName = parsed.args[0]?.toLowerCase();
395
+
396
+ if (!subcommandName) {
397
+ tui.error(`Missing subcommand for ${parsed.command}`);
398
+ console.log('Available subcommands:');
399
+ for (const sub of cmd.subcommands) {
400
+ const argHint = sub.schema?.argNames?.map((n) => `<${n}>`).join(' ') || '';
401
+ console.log(
402
+ ` ${tui.bold(sub.name)} ${argHint} ${tui.muted(sub.description || '')}`
403
+ );
404
+ }
405
+ continue;
406
+ }
407
+
408
+ const subcommand = cmd.subcommands.find(
409
+ (sub) =>
410
+ sub.name.toLowerCase() === subcommandName || sub.aliases?.includes(subcommandName)
411
+ );
412
+
413
+ if (!subcommand) {
414
+ tui.error(`Unknown subcommand: ${parsed.command} ${subcommandName}`);
415
+ console.log('Available subcommands:');
416
+ for (const sub of cmd.subcommands) {
417
+ const argHint = sub.schema?.argNames?.map((n) => `<${n}>`).join(' ') || '';
418
+ console.log(
419
+ ` ${tui.bold(sub.name)} ${argHint} ${tui.muted(sub.description || '')}`
420
+ );
421
+ }
422
+ continue;
423
+ }
424
+
425
+ // Use subcommand handler and schema, remove subcommand from args
426
+ actualHandler = subcommand.handler;
427
+ actualSchema = subcommand.schema;
428
+ parsed.args = parsed.args.slice(1);
429
+ }
430
+
431
+ if (!actualHandler) {
432
+ tui.error(`Command ${parsed.command} requires a subcommand`);
433
+ continue;
434
+ }
435
+
436
+ // Validate against schema if provided
437
+ if (actualSchema) {
438
+ try {
439
+ if (actualSchema.args) {
440
+ parsed.args = actualSchema.args.parse(parsed.args) as string[];
441
+ }
442
+
443
+ if (actualSchema.options) {
444
+ parsed.options = actualSchema.options.parse(parsed.options) as Record<
445
+ string,
446
+ string | boolean
447
+ >;
448
+ }
449
+ } catch (err) {
450
+ if (err instanceof z.ZodError) {
451
+ tui.error('Invalid arguments:');
452
+ for (const issue of err.issues) {
453
+ const path = issue.path.join('.');
454
+ console.log(
455
+ ` ${tui.colorError('•')} ${path ? `${path}: ` : ''}${issue.message}`
456
+ );
457
+ }
458
+ continue;
459
+ }
460
+ throw err;
461
+ }
462
+ }
463
+
464
+ // Create context with output buffering for paging
465
+ const outputBuffer: string[] = [];
466
+
467
+ const bufferWrite = (msg: string) => {
468
+ outputBuffer.push(...msg.split('\n'));
469
+ };
470
+
471
+ // Helper to format messages with icons and colors (without printing)
472
+ const formatMessage = (
473
+ type: 'success' | 'error' | 'info' | 'warning' | 'debug',
474
+ msg: string
475
+ ): string => {
476
+ const icons = {
477
+ success: '✓',
478
+ error: '✗',
479
+ warning: '⚠',
480
+ info: 'ℹ',
481
+ debug: '',
482
+ };
483
+
484
+ const colorFormatters = {
485
+ success: tui.colorSuccess,
486
+ error: tui.colorError,
487
+ warning: tui.colorWarning,
488
+ info: tui.colorInfo,
489
+ debug: tui.colorMuted,
490
+ };
491
+
492
+ const icon = icons[type];
493
+ const formatter = colorFormatters[type];
494
+
495
+ if (!icon) {
496
+ return formatter(msg); // debug messages have no icon
497
+ }
498
+
499
+ return `${formatter(icon + ' ' + msg)}`;
500
+ };
501
+
502
+ // Create abort controller for this command
503
+ const abortController = new AbortController();
504
+ commandAbortController = abortController;
505
+
506
+ const ctx: ReplContext = {
507
+ parsed,
508
+ raw: line,
509
+ write: bufferWrite,
510
+ error: (msg: string) => outputBuffer.push(formatMessage('error', msg)),
511
+ success: (msg: string) => outputBuffer.push(formatMessage('success', msg)),
512
+ info: (msg: string) => outputBuffer.push(formatMessage('info', msg)),
513
+ warning: (msg: string) => outputBuffer.push(formatMessage('warning', msg)),
514
+ debug: (msg: string) => outputBuffer.push(formatMessage('debug', msg)),
515
+ setProgress: (msg: string) => spinner.updateMessage(msg),
516
+ signal: abortController.signal,
517
+ exit: exitRepl,
518
+ table: (columns: TableColumn[], data: Record<string, unknown>[]) => {
519
+ // Capture table output to buffer instead of direct stdout
520
+ const tableOutput = tui.table(data, columns, { render: true }) || '';
521
+ outputBuffer.push(...tableOutput.split('\n'));
522
+ },
523
+ json: (value: unknown) => {
524
+ // Use util.inspect for colorized output if colors are enabled
525
+ const stringValue = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
526
+ const output = colorize(stringValue);
527
+ outputBuffer.push(output);
528
+ },
529
+ };
530
+
531
+ // Execute command handler with activity indicator
532
+ const spinner = new ActivityIndicator(parsed.command);
533
+ spinner.start();
534
+
535
+ try {
536
+ const result = actualHandler(ctx);
537
+
538
+ // Handle async generator (streaming)
539
+ if (result && typeof result === 'object' && Symbol.asyncIterator in result) {
540
+ for await (const chunk of result as AsyncGenerator<string, void, unknown>) {
541
+ if (abortController.signal.aborted) {
542
+ break;
543
+ }
544
+ outputBuffer.push(...chunk.split('\n'));
545
+ }
546
+ } else if (result && typeof result === 'object' && 'then' in result) {
547
+ // Handle promise
548
+ await result;
549
+ }
550
+
551
+ spinner.stop();
552
+ commandAbortController = null;
553
+
554
+ // Check if aborted
555
+ if (abortController.signal.aborted) {
556
+ tui.warning('Command aborted');
557
+ continue;
558
+ }
559
+
560
+ // Display output with paging
561
+ if (outputBuffer.length > 0) {
562
+ await displayWithPaging(outputBuffer);
563
+ }
564
+
565
+ // Add successful command to history
566
+ if (history[history.length - 1] !== line) {
567
+ history.push(line);
568
+ historyIndex = history.length;
569
+
570
+ // Save history asynchronously (don't await to avoid blocking)
571
+ saveHistory(historyName, history);
572
+ }
573
+ } catch (err) {
574
+ spinner.stop();
575
+ commandAbortController = null;
576
+
577
+ // Check if it was an abort
578
+ if (err instanceof Error && err.name === 'AbortError') {
579
+ tui.warning('Command aborted');
580
+ } else {
581
+ tui.error(`Command failed: ${err instanceof Error ? err.message : String(err)}`);
582
+ }
583
+ }
584
+ }
585
+
586
+ // Cleanup global handler
587
+ process.off('SIGINT', globalSigintHandler);
588
+
589
+ // Show exit message
590
+ if (config.exitMessage) {
591
+ tui.info(config.exitMessage);
592
+ }
593
+ }
594
+
595
+ /**
596
+ * Get all autocomplete matches based on current input
597
+ */
598
+ function getAutocompleteMatches(
599
+ buffer: string,
600
+ commands: string[],
601
+ commandMap?: Map<string, ReplCommand>
602
+ ): string[] {
603
+ if (!buffer.trim()) return [];
604
+
605
+ const tokens = buffer.trim().split(/\s+/);
606
+ const firstToken = tokens[0].toLowerCase();
607
+
608
+ // If we're typing the first word (no trailing space), suggest commands
609
+ if (tokens.length === 1 && buffer === buffer.trimEnd()) {
610
+ return commands.filter((cmd) => cmd.startsWith(firstToken) && cmd !== firstToken);
611
+ }
612
+
613
+ // If we have a command + space, suggest subcommands
614
+ if (tokens.length === 1 && buffer !== buffer.trimEnd() && commandMap) {
615
+ const cmd = commandMap.get(firstToken);
616
+ if (cmd?.subcommands) {
617
+ return cmd.subcommands.map((sub) => sub.name);
618
+ }
619
+ return [];
620
+ }
621
+
622
+ // If we're typing a subcommand, filter matches
623
+ if (tokens.length === 2 && buffer === buffer.trimEnd() && commandMap) {
624
+ const cmd = commandMap.get(firstToken);
625
+ if (cmd?.subcommands) {
626
+ const subToken = tokens[1].toLowerCase();
627
+ return cmd.subcommands
628
+ .filter((sub) => sub.name.startsWith(subToken) && sub.name !== subToken)
629
+ .map((sub) => sub.name);
630
+ }
631
+ return [];
632
+ }
633
+
634
+ return [];
635
+ }
636
+
637
+ /**
638
+ * Get autocomplete suggestion based on current input and cycle index
639
+ */
640
+ function getAutocompleteSuggestion(
641
+ buffer: string,
642
+ commands: string[],
643
+ commandMap: Map<string, ReplCommand>,
644
+ cycleIndex: number = 0
645
+ ): string {
646
+ const matches = getAutocompleteMatches(buffer, commands, commandMap);
647
+ if (matches.length === 0) return '';
648
+
649
+ const selectedMatch = matches[cycleIndex % matches.length];
650
+ const tokens = buffer.trim().split(/\s+/);
651
+ const firstToken = tokens[0].toLowerCase();
652
+
653
+ // Typing first word (command name)
654
+ if (tokens.length === 1 && buffer === buffer.trimEnd()) {
655
+ const cmd = commandMap.get(selectedMatch.toLowerCase());
656
+ let suggestion = selectedMatch.slice(firstToken.length);
657
+
658
+ // Add argument placeholders if schema exists
659
+ if (cmd?.schema?.argNames) {
660
+ suggestion += ' ' + cmd.schema.argNames.map((name) => `<${name}>`).join(' ');
661
+ }
662
+ // Add subcommand hint if this command has subcommands
663
+ else if (cmd?.subcommands && cmd.subcommands.length > 0) {
664
+ suggestion += ' <subcommand>';
665
+ }
666
+
667
+ return suggestion;
668
+ }
669
+
670
+ // After command + space, suggesting subcommands
671
+ if (tokens.length === 1 && buffer !== buffer.trimEnd()) {
672
+ return selectedMatch;
673
+ }
674
+
675
+ // Typing subcommand name
676
+ if (tokens.length === 2 && buffer === buffer.trimEnd()) {
677
+ const cmd = commandMap.get(firstToken);
678
+ const subToken = tokens[1];
679
+ const subcommand = cmd?.subcommands?.find((sub) => sub.name === selectedMatch);
680
+
681
+ let suggestion = selectedMatch.slice(subToken.length);
682
+
683
+ // Add argument placeholders for subcommand
684
+ if (subcommand?.schema?.argNames) {
685
+ suggestion += ' ' + subcommand.schema.argNames.map((name) => `<${name}>`).join(' ');
686
+ }
687
+
688
+ return suggestion;
689
+ }
690
+
691
+ return '';
692
+ }
693
+
694
+ /**
695
+ * Display output with paging if it's too long
696
+ */
697
+ async function displayWithPaging(lines: string[]): Promise<void> {
698
+ const terminalHeight = process.stdout.rows || 24;
699
+ const pageSize = terminalHeight - 2; // Leave room for prompt
700
+
701
+ if (lines.length <= pageSize) {
702
+ // Short output, just display it
703
+ for (const line of lines) {
704
+ console.log(`\x1b[2m│\x1b[0m ${line}`);
705
+ }
706
+ return;
707
+ }
708
+
709
+ // Long output, page it
710
+ let currentLine = 0;
711
+
712
+ while (currentLine < lines.length) {
713
+ // Clear screen and show current page
714
+ const endLine = Math.min(currentLine + pageSize, lines.length);
715
+
716
+ for (let i = currentLine; i < endLine; i++) {
717
+ console.log(`${tui.colorMuted('│')} ${lines[i]}`);
718
+ }
719
+
720
+ // Check if there's more
721
+ if (endLine < lines.length) {
722
+ const remaining = lines.length - endLine;
723
+ process.stdout.write(tui.bold(`-- More (${remaining} lines) -- [Space=next, q=quit]`));
724
+
725
+ // Wait for keypress
726
+ const key = await waitForKey();
727
+ process.stdout.write('\r\x1b[K'); // Clear the "More" line
728
+
729
+ if (key === 'q' || key === '\x03') {
730
+ // Quit
731
+ console.log(`${tui.colorMuted('│')} (output truncated)`);
732
+ break;
733
+ } else if (key === ' ' || key === '\r' || key === '\n') {
734
+ // Continue to next page
735
+ currentLine = endLine;
736
+ } else {
737
+ // Any other key, continue
738
+ currentLine = endLine;
739
+ }
740
+ } else {
741
+ break;
742
+ }
743
+ }
744
+ }
745
+
746
+ /**
747
+ * Wait for a single keypress
748
+ */
749
+ async function waitForKey(): Promise<string> {
750
+ return new Promise((resolve) => {
751
+ process.stdin.setRawMode(true);
752
+ process.stdin.resume();
753
+
754
+ const onData = (chunk: Buffer) => {
755
+ const key = chunk.toString();
756
+ process.stdin.setRawMode(false);
757
+ process.stdin.removeListener('data', onData);
758
+ process.stdin.pause();
759
+ resolve(key);
760
+ };
761
+
762
+ process.stdin.on('data', onData);
763
+ });
764
+ }
765
+
766
+ /**
767
+ * Activity indicator that shows a spinner while command is executing
768
+ */
769
+ class ActivityIndicator {
770
+ private frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
771
+ private currentFrame = 0;
772
+ private intervalId: Timer | null = null;
773
+ private message: string;
774
+
775
+ constructor(message: string = 'Running') {
776
+ this.message = message;
777
+ }
778
+
779
+ start() {
780
+ // Hide cursor
781
+ process.stdout.write('\x1b[?25l');
782
+
783
+ // Show initial spinner on current line
784
+ this.draw();
785
+
786
+ // Update spinner every 80ms
787
+ this.intervalId = setInterval(() => {
788
+ this.currentFrame = (this.currentFrame + 1) % this.frames.length;
789
+ this.draw();
790
+ }, 80);
791
+ }
792
+
793
+ private draw() {
794
+ const frame = this.frames[this.currentFrame];
795
+ // Clear line, draw spinner, stay on same line
796
+ process.stdout.write('\r\x1b[K'); // Clear line from cursor
797
+ process.stdout.write(`${tui.muted(frame)} ${tui.muted(this.message)}...`);
798
+ }
799
+
800
+ stop() {
801
+ if (this.intervalId) {
802
+ clearInterval(this.intervalId);
803
+ this.intervalId = null;
804
+ }
805
+
806
+ // Clear the spinner line - cursor stays at start of line
807
+ process.stdout.write('\r\x1b[K'); // Clear current line, cursor at start
808
+
809
+ // Show cursor again
810
+ process.stdout.write('\x1b[?25h');
811
+ }
812
+
813
+ updateMessage(message: string) {
814
+ this.message = message;
815
+ }
816
+ }
817
+
818
+ /**
819
+ * Show command picker popup and return selected command
820
+ */
821
+ async function showCommandPicker(
822
+ commandMap: Map<string, ReplCommand>,
823
+ prompt: string
824
+ ): Promise<string | null> {
825
+ // Build list of commands
826
+ const commands = Array.from(commandMap.entries())
827
+ .filter(([name, cmd]) => name === cmd.name.toLowerCase())
828
+ .sort(([a], [b]) => a.localeCompare(b))
829
+ .map(([name, cmd]) => ({
830
+ name,
831
+ description: cmd.description || 'No description',
832
+ argHint: cmd.schema?.argNames?.map((n) => `<${n}>`).join(' ') || '',
833
+ }));
834
+
835
+ let selectedIndex = 0;
836
+ const menuHeight = commands.length + 2; // +2 for header and blank line
837
+
838
+ // Calculate max command text length for padding
839
+ const maxCmdLength = Math.max(
840
+ ...commands.map((cmd) => {
841
+ const cmdText = `${cmd.name}${cmd.argHint ? ' ' + cmd.argHint : ''}`;
842
+ return cmdText.length;
843
+ })
844
+ );
845
+
846
+ const drawPicker = () => {
847
+ // Save cursor position, move down to draw menu
848
+ process.stdout.write('\n'); // Move to next line
849
+
850
+ // Draw header
851
+ console.log(
852
+ tui.bold('Command Picker') + ' ' + tui.muted('(↑/↓ navigate, Enter select, Esc cancel)')
853
+ );
854
+
855
+ // Draw commands
856
+ for (let i = 0; i < commands.length; i++) {
857
+ const cmd = commands[i];
858
+ const isSelected = i === selectedIndex;
859
+ const prefix = isSelected ? '▶ ' : ' ';
860
+ const style = isSelected ? '\x1b[7m' : ''; // Reverse video for selected
861
+ const reset = isSelected ? '\x1b[0m' : '';
862
+
863
+ const cmdText = `${cmd.name}${cmd.argHint ? ' ' + cmd.argHint : ''}`;
864
+ const paddedCmdText = cmdText.padEnd(maxCmdLength);
865
+ const description = tui.muted(cmd.description);
866
+
867
+ console.log(`${prefix}${style}${tui.bold(paddedCmdText)}${reset} ${description}`);
868
+ }
869
+
870
+ // Move cursor back to prompt line
871
+ process.stdout.write(`\x1b[${menuHeight}A`); // Move up N lines
872
+ process.stdout.write(`\r${prompt}/`); // Redraw prompt with /
873
+ };
874
+
875
+ const clearPicker = () => {
876
+ // Move down to menu area
877
+ process.stdout.write(`\x1b[${menuHeight}B`); // Move down to after menu
878
+ // Clear all menu lines by moving up and clearing
879
+ for (let i = 0; i < menuHeight; i++) {
880
+ process.stdout.write('\x1b[A'); // Move up
881
+ process.stdout.write('\r\x1b[K'); // Clear line
882
+ }
883
+ // Back to prompt - clear the line completely
884
+ process.stdout.write('\r\x1b[K');
885
+ };
886
+
887
+ drawPicker();
888
+
889
+ return new Promise((resolve) => {
890
+ process.stdin.setRawMode(true);
891
+ process.stdin.resume();
892
+
893
+ const onData = (chunk: Buffer) => {
894
+ const bytes = Array.from(chunk);
895
+
896
+ // Escape or Ctrl+C - cancel
897
+ if (bytes[0] === 0x1b && bytes.length === 1) {
898
+ cleanup();
899
+ clearPicker();
900
+ resolve(null);
901
+ return;
902
+ }
903
+
904
+ if (bytes[0] === 0x03) {
905
+ cleanup();
906
+ clearPicker();
907
+ resolve(null);
908
+ return;
909
+ }
910
+
911
+ // Enter - select
912
+ if (bytes[0] === 0x0d || bytes[0] === 0x0a) {
913
+ const selected = commands[selectedIndex];
914
+ cleanup();
915
+ clearPicker();
916
+ resolve(selected.name + (selected.argHint ? ' ' : ''));
917
+ return;
918
+ }
919
+
920
+ // Arrow keys
921
+ if (bytes[0] === 0x1b && bytes[1] === 0x5b) {
922
+ // Up arrow
923
+ if (bytes[2] === 0x41) {
924
+ selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : commands.length - 1;
925
+ drawPicker();
926
+ return;
927
+ }
928
+
929
+ // Down arrow
930
+ if (bytes[2] === 0x42) {
931
+ selectedIndex = (selectedIndex + 1) % commands.length;
932
+ drawPicker();
933
+ return;
934
+ }
935
+ }
936
+ };
937
+
938
+ const cleanup = () => {
939
+ process.stdin.setRawMode(false);
940
+ process.stdin.removeListener('data', onData);
941
+ process.stdin.pause();
942
+ };
943
+
944
+ process.stdin.on('data', onData);
945
+ });
946
+ }
947
+
948
+ /**
949
+ * Apply syntax highlighting to buffer
950
+ */
951
+ function applySyntaxHighlighting(buffer: string, commands: string[]): string {
952
+ if (!buffer.trim()) return buffer;
953
+
954
+ const tokens = buffer.split(/(\s+)/); // Split but keep whitespace
955
+ let result = '';
956
+ let isFirstToken = true;
957
+
958
+ for (let i = 0; i < tokens.length; i++) {
959
+ const token = tokens[i];
960
+
961
+ // Skip whitespace
962
+ if (/^\s+$/.test(token)) {
963
+ result += token;
964
+ continue;
965
+ }
966
+
967
+ // First token is the command
968
+ if (isFirstToken) {
969
+ const isValid = commands.includes(token.toLowerCase());
970
+ if (isValid) {
971
+ result += tui.colorSuccess(token); // Green for valid command
972
+ } else {
973
+ result += tui.colorError(token); // Red for invalid command
974
+ }
975
+ isFirstToken = false;
976
+ }
977
+ // Options (start with - or --)
978
+ else if (token.startsWith('-')) {
979
+ result += tui.colorInfo(token); // Cyan for options
980
+ }
981
+ // Regular arguments
982
+ else {
983
+ result += tui.colorMuted(token); // Gray for arguments
984
+ }
985
+ }
986
+
987
+ return result;
988
+ }
989
+
990
+ /**
991
+ * Read a line from stdin with arrow key history support and autocomplete
992
+ */
993
+ async function readLine(
994
+ history: string[],
995
+ startIndex: number,
996
+ prompt: string = '',
997
+ commands: string[] = [],
998
+ commandMap?: Map<string, ReplCommand>,
999
+ ctrlCState?: { lastTime: number }
1000
+ ): Promise<{ line: string; newHistoryIndex: number } | null> {
1001
+ return new Promise((resolve) => {
1002
+ process.stdin.setRawMode(true);
1003
+ process.stdin.resume();
1004
+
1005
+ // Enable vertical bar cursor
1006
+ process.stdout.write('\x1b[6 q');
1007
+
1008
+ let buffer = '';
1009
+ let cursorPos = 0;
1010
+ let historyIndex = startIndex;
1011
+ let searchMode = false;
1012
+ let searchQuery = '';
1013
+ let searchResultIndex = -1;
1014
+ let autocompleteCycleIndex = 0;
1015
+ let lastAutocompleteBuffer = '';
1016
+ const lines: string[] = [''];
1017
+ let currentLineIndex = 0;
1018
+
1019
+ const searchHistory = (query: string, startFrom: number): number => {
1020
+ for (let i = startFrom - 1; i >= 0; i--) {
1021
+ if (history[i].toLowerCase().includes(query.toLowerCase())) {
1022
+ return i;
1023
+ }
1024
+ }
1025
+ return -1;
1026
+ };
1027
+
1028
+ const redraw = (cmdMap?: Map<string, ReplCommand>) => {
1029
+ if (searchMode) {
1030
+ // Search mode display
1031
+ process.stdout.write('\r\x1b[K');
1032
+ const foundEntry = searchResultIndex >= 0 ? history[searchResultIndex] : '';
1033
+ const searchPrompt = `(reverse-i-search)\`${searchQuery}': `;
1034
+ process.stdout.write(searchPrompt + foundEntry);
1035
+ } else if (lines.length > 1) {
1036
+ // Multi-line mode - redraw all lines
1037
+ // Move to start of first line
1038
+ for (let i = 0; i < currentLineIndex; i++) {
1039
+ process.stdout.write('\x1b[A'); // Move up
1040
+ }
1041
+ process.stdout.write('\r');
1042
+
1043
+ // Redraw all lines
1044
+ for (let i = 0; i < lines.length; i++) {
1045
+ process.stdout.write('\x1b[K'); // Clear line
1046
+ const linePrompt = i === 0 ? prompt : '... ';
1047
+ process.stdout.write(linePrompt + lines[i]);
1048
+ if (i < lines.length - 1) {
1049
+ process.stdout.write('\n');
1050
+ }
1051
+ }
1052
+
1053
+ // Position cursor on current line at cursor position
1054
+ const linesToMove = lines.length - 1 - currentLineIndex;
1055
+ for (let i = 0; i < linesToMove; i++) {
1056
+ process.stdout.write('\x1b[A'); // Move up to current line
1057
+ }
1058
+ const linePrompt = currentLineIndex === 0 ? prompt : '... ';
1059
+ process.stdout.write('\r');
1060
+ process.stdout.write(linePrompt + lines[currentLineIndex].slice(0, cursorPos));
1061
+ } else {
1062
+ // Single-line mode (original behavior)
1063
+ process.stdout.write('\r\x1b[K');
1064
+ const suggestion = cmdMap
1065
+ ? getAutocompleteSuggestion(buffer, commands, cmdMap, autocompleteCycleIndex)
1066
+ : '';
1067
+ const matches = cmdMap ? getAutocompleteMatches(buffer, commands, cmdMap) : [];
1068
+
1069
+ // Apply syntax highlighting to buffer
1070
+ const highlightedBuffer = applySyntaxHighlighting(buffer, commands);
1071
+ process.stdout.write(prompt + highlightedBuffer);
1072
+
1073
+ // Show suggestion in dark gray (only when cursor is at end)
1074
+ const showSuggestion = suggestion && cursorPos === buffer.length;
1075
+ if (showSuggestion) {
1076
+ process.stdout.write(`\x1b[90m${suggestion}\x1b[0m`);
1077
+
1078
+ // Show match count if multiple matches
1079
+ if (matches.length > 1) {
1080
+ process.stdout.write(
1081
+ ` \x1b[90m[${autocompleteCycleIndex + 1}/${matches.length}]\x1b[0m`
1082
+ );
1083
+ }
1084
+ }
1085
+
1086
+ // Move cursor to correct position (accounting for prompt length and suggestion)
1087
+ const suggestionLength = showSuggestion ? suggestion.length : 0;
1088
+ const counterLength =
1089
+ showSuggestion && matches.length > 1
1090
+ ? ` [${autocompleteCycleIndex + 1}/${matches.length}]`.length
1091
+ : 0;
1092
+ const totalLength = buffer.length + suggestionLength + counterLength;
1093
+ const diff = totalLength - cursorPos;
1094
+ if (diff > 0) {
1095
+ process.stdout.write(`\x1b[${diff}D`);
1096
+ }
1097
+ }
1098
+ };
1099
+
1100
+ const onData = async (chunk: Buffer) => {
1101
+ const bytes = Array.from(chunk);
1102
+
1103
+ // Check for / key - show command picker
1104
+ if (bytes[0] === 0x2f && buffer.length === 0 && commandMap) {
1105
+ // '/' key at start of line
1106
+
1107
+ // Temporarily remove our listener to avoid conflicts
1108
+ process.stdin.removeListener('data', onData);
1109
+
1110
+ const selected = await showCommandPicker(commandMap, prompt);
1111
+
1112
+ // Re-attach our listener and restore state
1113
+ process.stdin.setRawMode(true);
1114
+ process.stdin.resume();
1115
+ process.stdin.on('data', onData);
1116
+
1117
+ if (selected) {
1118
+ buffer = selected;
1119
+ cursorPos = buffer.length;
1120
+ lines[currentLineIndex] = buffer;
1121
+ // Reset autocomplete state
1122
+ autocompleteCycleIndex = 0;
1123
+ lastAutocompleteBuffer = '';
1124
+ // Force full redraw after command picker
1125
+ redraw(commandMap);
1126
+ }
1127
+ return;
1128
+ }
1129
+
1130
+ // Check for Ctrl+C - double press to exit
1131
+ if (bytes[0] === 0x03 && ctrlCState) {
1132
+ const now = Date.now();
1133
+ const timeSinceLastCtrlC = now - ctrlCState.lastTime;
1134
+
1135
+ // If pressed within 2 seconds, exit
1136
+ if (timeSinceLastCtrlC < 2000 && ctrlCState.lastTime > 0) {
1137
+ cleanup();
1138
+ console.log(''); // Newline before exit
1139
+ process.exit(0);
1140
+ }
1141
+
1142
+ // First Ctrl+C - show message below, keep prompt in place
1143
+ ctrlCState.lastTime = now;
1144
+
1145
+ // Save cursor position
1146
+ const promptWithBuffer = prompt + buffer;
1147
+
1148
+ // Move to new line and show message
1149
+ process.stdout.write('\n');
1150
+ process.stdout.write(tui.muted('Press Ctrl+C again to exit'));
1151
+
1152
+ // Move back up to prompt line
1153
+ process.stdout.write('\x1b[A'); // Move up one line
1154
+ process.stdout.write(`\r`); // Go to start of line
1155
+ process.stdout.write(promptWithBuffer); // Redraw prompt
1156
+
1157
+ // Position cursor at correct location
1158
+ if (cursorPos < buffer.length) {
1159
+ const diff = buffer.length - cursorPos;
1160
+ process.stdout.write(`\x1b[${diff}D`);
1161
+ }
1162
+
1163
+ return;
1164
+ }
1165
+
1166
+ // Check for Ctrl+D (EOF)
1167
+ if (bytes[0] === 0x04 && buffer.length === 0) {
1168
+ cleanup();
1169
+ resolve(null);
1170
+ return;
1171
+ }
1172
+
1173
+ // Check for Ctrl+A (jump to start)
1174
+ if (bytes[0] === 0x01) {
1175
+ cursorPos = 0;
1176
+ redraw(commandMap);
1177
+ return;
1178
+ }
1179
+
1180
+ // Check for Ctrl+E (jump to end)
1181
+ if (bytes[0] === 0x05) {
1182
+ cursorPos = buffer.length;
1183
+ redraw(commandMap);
1184
+ return;
1185
+ }
1186
+
1187
+ // Check for Ctrl+K (delete to end of line)
1188
+ if (bytes[0] === 0x0b) {
1189
+ buffer = buffer.slice(0, cursorPos);
1190
+ redraw(commandMap);
1191
+ return;
1192
+ }
1193
+
1194
+ // Check for Ctrl+U (delete entire line)
1195
+ if (bytes[0] === 0x15) {
1196
+ buffer = '';
1197
+ cursorPos = 0;
1198
+ redraw(commandMap);
1199
+ return;
1200
+ }
1201
+
1202
+ // Check for Ctrl+W (delete word backward)
1203
+ if (bytes[0] === 0x17) {
1204
+ const beforeCursor = buffer.slice(0, cursorPos);
1205
+ const match = beforeCursor.match(/\s*\S+\s*$/);
1206
+ if (match) {
1207
+ const deleteCount = match[0].length;
1208
+ buffer = buffer.slice(0, cursorPos - deleteCount) + buffer.slice(cursorPos);
1209
+ cursorPos -= deleteCount;
1210
+ redraw(commandMap);
1211
+ }
1212
+ return;
1213
+ }
1214
+
1215
+ // Check for Ctrl+L (clear screen)
1216
+ if (bytes[0] === 0x0c) {
1217
+ process.stdout.write('\x1b[2J\x1b[H');
1218
+ redraw(commandMap);
1219
+ return;
1220
+ }
1221
+
1222
+ // Check for Ctrl+R (reverse search)
1223
+ if (bytes[0] === 0x12) {
1224
+ if (!searchMode) {
1225
+ // Enter search mode
1226
+ searchMode = true;
1227
+ searchQuery = '';
1228
+ searchResultIndex = searchHistory('', history.length);
1229
+ redraw(commandMap);
1230
+ } else {
1231
+ // Find next match
1232
+ if (searchResultIndex > 0) {
1233
+ searchResultIndex = searchHistory(searchQuery, searchResultIndex);
1234
+ redraw(commandMap);
1235
+ }
1236
+ }
1237
+ return;
1238
+ }
1239
+
1240
+ // Check for Tab (autocomplete - cycle only)
1241
+ if (bytes[0] === 0x09 && commandMap) {
1242
+ const matches = getAutocompleteMatches(buffer, commands, commandMap);
1243
+
1244
+ if (matches.length === 0) {
1245
+ // No matches, do nothing
1246
+ return;
1247
+ }
1248
+
1249
+ // Check if we're cycling through the same buffer
1250
+ if (buffer === lastAutocompleteBuffer && matches.length > 0) {
1251
+ // Continue cycling
1252
+ autocompleteCycleIndex = (autocompleteCycleIndex + 1) % matches.length;
1253
+ redraw(commandMap);
1254
+ } else {
1255
+ // Start new cycle - show first suggestion
1256
+ autocompleteCycleIndex = 0;
1257
+ lastAutocompleteBuffer = buffer;
1258
+ redraw(commandMap);
1259
+ }
1260
+ return;
1261
+ }
1262
+
1263
+ // Check for Shift+Enter (newline without submit)
1264
+ // Different terminals send different sequences:
1265
+ // - iTerm2/Terminal.app: ESC + Enter (0x1b, 0x0d or 0x1b, 0x0a)
1266
+ // - Some terminals: ESC[27;2;13~
1267
+ // - VSCode: ESC[13;2u
1268
+ if (
1269
+ (bytes[0] === 0x1b && bytes.length === 2 && (bytes[1] === 0x0d || bytes[1] === 0x0a)) || // ESC + Enter
1270
+ (bytes[0] === 0x1b &&
1271
+ bytes[1] === 0x5b &&
1272
+ bytes.includes(0x3b) &&
1273
+ bytes.includes(0x32)) || // ESC[...;2;...
1274
+ (bytes[0] === 0x1b &&
1275
+ bytes[1] === 0x5b &&
1276
+ bytes[2] === 0x31 &&
1277
+ bytes[3] === 0x33 &&
1278
+ bytes[4] === 0x3b &&
1279
+ bytes[5] === 0x32) // ESC[13;2u
1280
+ ) {
1281
+ // Shift+Enter detected - add newline
1282
+ lines.push('');
1283
+ currentLineIndex++;
1284
+ cursorPos = 0;
1285
+ buffer = lines[currentLineIndex];
1286
+ process.stdout.write('\n');
1287
+ process.stdout.write('... ');
1288
+ return;
1289
+ }
1290
+
1291
+ // Check for Enter
1292
+ if (bytes[0] === 0x0d || bytes[0] === 0x0a) {
1293
+ if (searchMode) {
1294
+ // Accept search result
1295
+ if (searchResultIndex >= 0) {
1296
+ buffer = history[searchResultIndex];
1297
+ }
1298
+ searchMode = false;
1299
+ process.stdout.write('\n');
1300
+ cleanup();
1301
+ resolve({ line: buffer, newHistoryIndex: history.length });
1302
+ return;
1303
+ }
1304
+
1305
+ // Check if line ends with backslash (continuation)
1306
+ if (buffer.endsWith('\\')) {
1307
+ // Remove backslash and continue to next line
1308
+ lines[currentLineIndex] = buffer.slice(0, -1);
1309
+ lines.push('');
1310
+ currentLineIndex++;
1311
+ cursorPos = 0;
1312
+ buffer = '';
1313
+ process.stdout.write('\n');
1314
+ process.stdout.write('... ');
1315
+ return;
1316
+ }
1317
+
1318
+ // Check for unclosed quotes or brackets
1319
+ const hasUnclosedQuote =
1320
+ (buffer.match(/"/g) || []).length % 2 !== 0 ||
1321
+ (buffer.match(/'/g) || []).length % 2 !== 0;
1322
+ const openBrackets = (buffer.match(/[{[(]/g) || []).length;
1323
+ const closeBrackets = (buffer.match(/[}\])]/g) || []).length;
1324
+
1325
+ if (hasUnclosedQuote || openBrackets > closeBrackets) {
1326
+ // Auto-continue to next line
1327
+ lines[currentLineIndex] = buffer;
1328
+ lines.push('');
1329
+ currentLineIndex++;
1330
+ cursorPos = 0;
1331
+ buffer = '';
1332
+ process.stdout.write('\n');
1333
+ process.stdout.write('... ');
1334
+ return;
1335
+ }
1336
+
1337
+ // Submit the command
1338
+ process.stdout.write('\n');
1339
+ const finalBuffer = lines.length > 1 ? lines.join('\n') : buffer;
1340
+ cleanup();
1341
+ resolve({ line: finalBuffer, newHistoryIndex: history.length });
1342
+ return;
1343
+ }
1344
+
1345
+ // Check for Esc key (cancel search mode)
1346
+ if (bytes[0] === 0x1b && bytes.length === 1) {
1347
+ if (searchMode) {
1348
+ searchMode = false;
1349
+ searchQuery = '';
1350
+ searchResultIndex = -1;
1351
+ redraw(commandMap);
1352
+ }
1353
+ return;
1354
+ }
1355
+
1356
+ // Check for escape sequences (arrow keys, delete, home, end)
1357
+ if (bytes[0] === 0x1b && bytes[1] === 0x5b) {
1358
+ // Up arrow
1359
+ if (bytes[2] === 0x41) {
1360
+ if (historyIndex > 0) {
1361
+ historyIndex--;
1362
+ buffer = history[historyIndex] || '';
1363
+ cursorPos = buffer.length;
1364
+ redraw(commandMap);
1365
+ }
1366
+ return;
1367
+ }
1368
+
1369
+ // Down arrow
1370
+ if (bytes[2] === 0x42) {
1371
+ if (historyIndex < history.length) {
1372
+ historyIndex++;
1373
+ buffer = history[historyIndex] || '';
1374
+ cursorPos = buffer.length;
1375
+ redraw(commandMap);
1376
+ }
1377
+ return;
1378
+ }
1379
+
1380
+ // Right arrow
1381
+ if (bytes[2] === 0x43) {
1382
+ // Check if we're at the end and have a suggestion to accept
1383
+ if (cursorPos === buffer.length && commandMap) {
1384
+ const suggestion = getAutocompleteSuggestion(
1385
+ buffer,
1386
+ commands,
1387
+ commandMap,
1388
+ autocompleteCycleIndex
1389
+ );
1390
+ if (suggestion) {
1391
+ // Accept the autocomplete suggestion (without argument placeholders)
1392
+ // Remove argument placeholders like <key> <value> from suggestion
1393
+ let completionOnly = suggestion.replace(/<[^>]+>/g, '').trimEnd();
1394
+
1395
+ // Add trailing space if there were argument placeholders
1396
+ if (suggestion.includes('<')) {
1397
+ completionOnly += ' ';
1398
+ }
1399
+
1400
+ buffer += completionOnly;
1401
+ cursorPos = buffer.length;
1402
+ lines[currentLineIndex] = buffer;
1403
+ autocompleteCycleIndex = 0;
1404
+ lastAutocompleteBuffer = '';
1405
+ redraw(commandMap);
1406
+ return;
1407
+ }
1408
+ }
1409
+
1410
+ // Normal cursor movement
1411
+ if (cursorPos < buffer.length) {
1412
+ cursorPos++;
1413
+ process.stdout.write('\x1b[C');
1414
+ }
1415
+ return;
1416
+ }
1417
+
1418
+ // Left arrow
1419
+ if (bytes[2] === 0x44) {
1420
+ if (cursorPos > 0) {
1421
+ cursorPos--;
1422
+ process.stdout.write('\x1b[D');
1423
+ }
1424
+ return;
1425
+ }
1426
+
1427
+ // Delete key (ESC[3~)
1428
+ if (bytes[2] === 0x33 && bytes.length > 3 && bytes[3] === 0x7e) {
1429
+ if (cursorPos < buffer.length) {
1430
+ buffer = buffer.slice(0, cursorPos) + buffer.slice(cursorPos + 1);
1431
+ redraw(commandMap);
1432
+ }
1433
+ return;
1434
+ }
1435
+
1436
+ // Home key (ESC[H or ESC[1~)
1437
+ if (bytes[2] === 0x48 || (bytes[2] === 0x31 && bytes[3] === 0x7e)) {
1438
+ cursorPos = 0;
1439
+ redraw(commandMap);
1440
+ return;
1441
+ }
1442
+
1443
+ // End key (ESC[F or ESC[4~)
1444
+ if (bytes[2] === 0x46 || (bytes[2] === 0x34 && bytes[3] === 0x7e)) {
1445
+ cursorPos = buffer.length;
1446
+ redraw(commandMap);
1447
+ return;
1448
+ }
1449
+ }
1450
+
1451
+ // Backspace
1452
+ if (bytes[0] === 0x7f || bytes[0] === 0x08) {
1453
+ if (searchMode) {
1454
+ // In search mode, delete from search query
1455
+ if (searchQuery.length > 0) {
1456
+ searchQuery = searchQuery.slice(0, -1);
1457
+ searchResultIndex = searchHistory(searchQuery, history.length);
1458
+ redraw(commandMap);
1459
+ }
1460
+ } else {
1461
+ if (cursorPos > 0) {
1462
+ buffer = buffer.slice(0, cursorPos - 1) + buffer.slice(cursorPos);
1463
+ cursorPos--;
1464
+ redraw(commandMap);
1465
+ }
1466
+ }
1467
+ return;
1468
+ }
1469
+
1470
+ // Regular character input
1471
+ if (searchMode) {
1472
+ // In search mode, add to search query
1473
+ const char = chunk.toString();
1474
+ if (char.match(/^[\x20-\x7E]$/)) {
1475
+ // Printable ASCII
1476
+ searchQuery += char;
1477
+ searchResultIndex = searchHistory(searchQuery, history.length);
1478
+ redraw(commandMap);
1479
+ }
1480
+ } else {
1481
+ const char = chunk.toString();
1482
+ buffer = buffer.slice(0, cursorPos) + char + buffer.slice(cursorPos);
1483
+ cursorPos += char.length;
1484
+
1485
+ // Update current line
1486
+ lines[currentLineIndex] = buffer;
1487
+
1488
+ // Reset autocomplete cycle on new input
1489
+ autocompleteCycleIndex = 0;
1490
+ lastAutocompleteBuffer = '';
1491
+
1492
+ // Always redraw to show autocomplete suggestion
1493
+ redraw(commandMap);
1494
+ }
1495
+ };
1496
+
1497
+ const cleanup = () => {
1498
+ // Restore default block cursor
1499
+ process.stdout.write('\x1b[0 q');
1500
+ process.stdin.setRawMode(false);
1501
+ process.stdin.removeListener('data', onData);
1502
+ process.stdin.pause();
1503
+ };
1504
+
1505
+ process.stdin.on('data', onData);
1506
+ });
1507
+ }