@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.
- package/bin/cli.ts +26 -5
- package/dist/banner.d.ts +1 -1
- package/dist/banner.d.ts.map +1 -1
- package/dist/cli-logger.d.ts +27 -0
- package/dist/cli-logger.d.ts.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cmd/agents/index.d.ts +2 -0
- package/dist/cmd/agents/index.d.ts.map +1 -0
- package/dist/cmd/auth/index.d.ts.map +1 -1
- package/dist/cmd/auth/login.d.ts.map +1 -1
- package/dist/cmd/auth/logout.d.ts.map +1 -1
- package/dist/cmd/auth/signup.d.ts.map +1 -1
- package/dist/cmd/auth/ssh/add.d.ts.map +1 -1
- package/dist/cmd/auth/ssh/delete.d.ts.map +1 -1
- package/dist/cmd/auth/ssh/index.d.ts +1 -2
- package/dist/cmd/auth/ssh/index.d.ts.map +1 -1
- package/dist/cmd/auth/ssh/list.d.ts.map +1 -1
- package/dist/cmd/auth/whoami.d.ts.map +1 -1
- package/dist/cmd/bundle/ast.d.ts +3 -1
- package/dist/cmd/bundle/ast.d.ts.map +1 -1
- package/dist/cmd/bundle/index.d.ts.map +1 -1
- package/dist/cmd/bundle/plugin.d.ts.map +1 -1
- package/dist/cmd/capabilities/index.d.ts +4 -0
- package/dist/cmd/capabilities/index.d.ts.map +1 -0
- package/dist/cmd/capabilities/show.d.ts +20 -0
- package/dist/cmd/capabilities/show.d.ts.map +1 -0
- package/dist/cmd/cloud/deploy.d.ts.map +1 -1
- package/dist/cmd/cloud/deployment/index.d.ts.map +1 -1
- package/dist/cmd/cloud/deployment/list.d.ts.map +1 -1
- package/dist/cmd/cloud/deployment/remove.d.ts.map +1 -1
- package/dist/cmd/cloud/deployment/rollback.d.ts.map +1 -1
- package/dist/cmd/cloud/deployment/show.d.ts.map +1 -1
- package/dist/cmd/cloud/deployment/undeploy.d.ts.map +1 -1
- package/dist/cmd/cloud/deployment/utils.d.ts +4 -2
- package/dist/cmd/cloud/deployment/utils.d.ts.map +1 -1
- package/dist/cmd/cloud/domain.d.ts.map +1 -1
- package/dist/cmd/cloud/index.d.ts.map +1 -1
- package/dist/cmd/cloud/resource/add.d.ts.map +1 -1
- package/dist/cmd/cloud/resource/delete.d.ts.map +1 -1
- package/dist/cmd/cloud/resource/index.d.ts +1 -2
- package/dist/cmd/cloud/resource/index.d.ts.map +1 -1
- package/dist/cmd/cloud/resource/list.d.ts.map +1 -1
- package/dist/cmd/cloud/scp/download.d.ts.map +1 -1
- package/dist/cmd/cloud/scp/index.d.ts +1 -2
- package/dist/cmd/cloud/scp/index.d.ts.map +1 -1
- package/dist/cmd/cloud/scp/upload.d.ts.map +1 -1
- package/dist/cmd/cloud/session/get.d.ts +2 -0
- package/dist/cmd/cloud/session/get.d.ts.map +1 -0
- package/dist/cmd/cloud/session/index.d.ts +2 -0
- package/dist/cmd/cloud/session/index.d.ts.map +1 -0
- package/dist/cmd/cloud/session/list.d.ts +2 -0
- package/dist/cmd/cloud/session/list.d.ts.map +1 -0
- package/dist/cmd/cloud/session/logs.d.ts +2 -0
- package/dist/cmd/cloud/session/logs.d.ts.map +1 -0
- package/dist/cmd/cloud/ssh.d.ts.map +1 -1
- package/dist/cmd/dev/agents.d.ts +2 -0
- package/dist/cmd/dev/agents.d.ts.map +1 -0
- package/dist/cmd/dev/index.d.ts.map +1 -1
- package/dist/cmd/dev/sync.d.ts +12 -0
- package/dist/cmd/dev/sync.d.ts.map +1 -0
- package/dist/cmd/env/delete.d.ts.map +1 -1
- package/dist/cmd/env/get.d.ts.map +1 -1
- package/dist/cmd/env/import.d.ts.map +1 -1
- package/dist/cmd/env/index.d.ts.map +1 -1
- package/dist/cmd/env/list.d.ts.map +1 -1
- package/dist/cmd/env/pull.d.ts.map +1 -1
- package/dist/cmd/env/push.d.ts.map +1 -1
- package/dist/cmd/env/set.d.ts.map +1 -1
- package/dist/cmd/index.d.ts.map +1 -1
- package/dist/cmd/kv/create-namespace.d.ts +3 -0
- package/dist/cmd/kv/create-namespace.d.ts.map +1 -0
- package/dist/cmd/kv/delete-namespace.d.ts +3 -0
- package/dist/cmd/kv/delete-namespace.d.ts.map +1 -0
- package/dist/cmd/kv/delete.d.ts +3 -0
- package/dist/cmd/kv/delete.d.ts.map +1 -0
- package/dist/cmd/kv/get.d.ts +3 -0
- package/dist/cmd/kv/get.d.ts.map +1 -0
- package/dist/cmd/kv/index.d.ts +2 -0
- package/dist/cmd/kv/index.d.ts.map +1 -0
- package/dist/cmd/kv/keys.d.ts +3 -0
- package/dist/cmd/kv/keys.d.ts.map +1 -0
- package/dist/cmd/kv/list-namespaces.d.ts +3 -0
- package/dist/cmd/kv/list-namespaces.d.ts.map +1 -0
- package/dist/cmd/kv/repl.d.ts +3 -0
- package/dist/cmd/kv/repl.d.ts.map +1 -0
- package/dist/cmd/kv/search.d.ts +3 -0
- package/dist/cmd/kv/search.d.ts.map +1 -0
- package/dist/cmd/kv/set.d.ts +3 -0
- package/dist/cmd/kv/set.d.ts.map +1 -0
- package/dist/cmd/kv/stats.d.ts +3 -0
- package/dist/cmd/kv/stats.d.ts.map +1 -0
- package/dist/cmd/kv/util.d.ts +8 -0
- package/dist/cmd/kv/util.d.ts.map +1 -0
- package/dist/cmd/objectstore/delete-bucket.d.ts +3 -0
- package/dist/cmd/objectstore/delete-bucket.d.ts.map +1 -0
- package/dist/cmd/objectstore/delete.d.ts +3 -0
- package/dist/cmd/objectstore/delete.d.ts.map +1 -0
- package/dist/cmd/objectstore/get.d.ts +3 -0
- package/dist/cmd/objectstore/get.d.ts.map +1 -0
- package/dist/cmd/objectstore/index.d.ts +2 -0
- package/dist/cmd/objectstore/index.d.ts.map +1 -0
- package/dist/cmd/objectstore/list-buckets.d.ts +3 -0
- package/dist/cmd/objectstore/list-buckets.d.ts.map +1 -0
- package/dist/cmd/objectstore/list-keys.d.ts +3 -0
- package/dist/cmd/objectstore/list-keys.d.ts.map +1 -0
- package/dist/cmd/objectstore/put.d.ts +3 -0
- package/dist/cmd/objectstore/put.d.ts.map +1 -0
- package/dist/cmd/objectstore/repl.d.ts +3 -0
- package/dist/cmd/objectstore/repl.d.ts.map +1 -0
- package/dist/cmd/objectstore/url.d.ts +3 -0
- package/dist/cmd/objectstore/url.d.ts.map +1 -0
- package/dist/cmd/objectstore/util.d.ts +8 -0
- package/dist/cmd/objectstore/util.d.ts.map +1 -0
- package/dist/cmd/profile/create.d.ts.map +1 -1
- package/dist/cmd/profile/delete.d.ts.map +1 -1
- package/dist/cmd/profile/index.d.ts.map +1 -1
- package/dist/cmd/profile/list.d.ts +1 -2
- package/dist/cmd/profile/list.d.ts.map +1 -1
- package/dist/cmd/profile/show.d.ts.map +1 -1
- package/dist/cmd/profile/use.d.ts.map +1 -1
- package/dist/cmd/project/create.d.ts.map +1 -1
- package/dist/cmd/project/delete.d.ts.map +1 -1
- package/dist/cmd/project/index.d.ts.map +1 -1
- package/dist/cmd/project/list.d.ts.map +1 -1
- package/dist/cmd/project/show.d.ts.map +1 -1
- package/dist/cmd/project/template-flow.d.ts +1 -1
- package/dist/cmd/project/template-flow.d.ts.map +1 -1
- package/dist/cmd/prompt/index.d.ts +4 -0
- package/dist/cmd/prompt/index.d.ts.map +1 -0
- package/dist/cmd/prompt/llm.d.ts +3 -0
- package/dist/cmd/prompt/llm.d.ts.map +1 -0
- package/dist/cmd/repl/index.d.ts +3 -0
- package/dist/cmd/repl/index.d.ts.map +1 -0
- package/dist/cmd/schema/index.d.ts +4 -0
- package/dist/cmd/schema/index.d.ts.map +1 -0
- package/dist/cmd/schema/show.d.ts +3 -0
- package/dist/cmd/schema/show.d.ts.map +1 -0
- package/dist/cmd/secret/delete.d.ts.map +1 -1
- package/dist/cmd/secret/get.d.ts.map +1 -1
- package/dist/cmd/secret/import.d.ts.map +1 -1
- package/dist/cmd/secret/index.d.ts.map +1 -1
- package/dist/cmd/secret/list.d.ts.map +1 -1
- package/dist/cmd/secret/pull.d.ts.map +1 -1
- package/dist/cmd/secret/push.d.ts.map +1 -1
- package/dist/cmd/secret/set.d.ts.map +1 -1
- package/dist/cmd/version/index.d.ts.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/errors.d.ts +83 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/explain.d.ts +47 -0
- package/dist/explain.d.ts.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/json.d.ts +3 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/output.d.ts +136 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/repl.d.ts +120 -0
- package/dist/repl.d.ts.map +1 -0
- package/dist/schema-generator.d.ts +67 -0
- package/dist/schema-generator.d.ts.map +1 -0
- package/dist/tui.d.ts +35 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/types.d.ts +77 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/format.d.ts +9 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/package.json +12 -4
- package/src/banner.ts +7 -7
- package/src/cli-logger.ts +80 -0
- package/src/cli.ts +192 -58
- package/src/cmd/agents/index.ts +147 -0
- package/src/cmd/auth/index.ts +1 -0
- package/src/cmd/auth/login.ts +7 -2
- package/src/cmd/auth/logout.ts +4 -0
- package/src/cmd/auth/signup.ts +7 -2
- package/src/cmd/auth/ssh/add.ts +20 -3
- package/src/cmd/auth/ssh/delete.ts +57 -4
- package/src/cmd/auth/ssh/index.ts +4 -3
- package/src/cmd/auth/ssh/list.ts +38 -27
- package/src/cmd/auth/whoami.ts +32 -21
- package/src/cmd/bundle/ast.test.ts +2 -2
- package/src/cmd/bundle/ast.ts +112 -22
- package/src/cmd/bundle/index.ts +20 -0
- package/src/cmd/bundle/plugin.ts +60 -14
- package/src/cmd/capabilities/index.ts +12 -0
- package/src/cmd/capabilities/show.ts +256 -0
- package/src/cmd/cloud/deploy.ts +54 -0
- package/src/cmd/cloud/deployment/index.ts +1 -0
- package/src/cmd/cloud/deployment/list.ts +66 -25
- package/src/cmd/cloud/deployment/remove.ts +26 -2
- package/src/cmd/cloud/deployment/rollback.ts +35 -4
- package/src/cmd/cloud/deployment/show.ts +37 -2
- package/src/cmd/cloud/deployment/undeploy.ts +12 -1
- package/src/cmd/cloud/deployment/utils.ts +5 -2
- package/src/cmd/cloud/domain.ts +3 -2
- package/src/cmd/cloud/index.ts +10 -1
- package/src/cmd/cloud/resource/add.ts +19 -0
- package/src/cmd/cloud/resource/delete.ts +24 -3
- package/src/cmd/cloud/resource/index.ts +4 -3
- package/src/cmd/cloud/resource/list.ts +36 -10
- package/src/cmd/cloud/scp/download.ts +27 -1
- package/src/cmd/cloud/scp/index.ts +4 -3
- package/src/cmd/cloud/scp/upload.ts +27 -1
- package/src/cmd/cloud/session/get.ts +164 -0
- package/src/cmd/cloud/session/index.ts +11 -0
- package/src/cmd/cloud/session/list.ts +145 -0
- package/src/cmd/cloud/session/logs.ts +68 -0
- package/src/cmd/cloud/ssh.ts +12 -0
- package/src/cmd/dev/agents.ts +122 -0
- package/src/cmd/dev/index.ts +106 -8
- package/src/cmd/dev/sync.ts +414 -0
- package/src/cmd/dev/templates.ts +1 -1
- package/src/cmd/env/delete.ts +17 -0
- package/src/cmd/env/get.ts +17 -1
- package/src/cmd/env/import.ts +47 -3
- package/src/cmd/env/index.ts +1 -0
- package/src/cmd/env/list.ts +13 -1
- package/src/cmd/env/pull.ts +20 -0
- package/src/cmd/env/push.ts +33 -1
- package/src/cmd/env/set.ts +25 -1
- package/src/cmd/index.ts +9 -2
- package/src/cmd/kv/create-namespace.ts +45 -0
- package/src/cmd/kv/delete-namespace.ts +73 -0
- package/src/cmd/kv/delete.ts +51 -0
- package/src/cmd/kv/get.ts +65 -0
- package/src/cmd/kv/index.ts +31 -0
- package/src/cmd/kv/keys.ts +57 -0
- package/src/cmd/kv/list-namespaces.ts +43 -0
- package/src/cmd/kv/repl.ts +284 -0
- package/src/cmd/kv/search.ts +80 -0
- package/src/cmd/kv/set.ts +63 -0
- package/src/cmd/kv/stats.ts +96 -0
- package/src/cmd/kv/util.ts +32 -0
- package/src/cmd/objectstore/delete-bucket.ts +72 -0
- package/src/cmd/objectstore/delete.ts +59 -0
- package/src/cmd/objectstore/get.ts +64 -0
- package/src/cmd/objectstore/index.ts +27 -0
- package/src/cmd/objectstore/list-buckets.ts +45 -0
- package/src/cmd/objectstore/list-keys.ts +60 -0
- package/src/cmd/objectstore/put.ts +62 -0
- package/src/cmd/objectstore/repl.ts +235 -0
- package/src/cmd/objectstore/url.ts +59 -0
- package/src/cmd/objectstore/util.ts +28 -0
- package/src/cmd/profile/create.ts +28 -2
- package/src/cmd/profile/delete.ts +17 -2
- package/src/cmd/profile/index.ts +1 -0
- package/src/cmd/profile/list.ts +7 -3
- package/src/cmd/profile/show.ts +20 -5
- package/src/cmd/profile/use.ts +8 -0
- package/src/cmd/project/create.ts +31 -0
- package/src/cmd/project/delete.ts +24 -2
- package/src/cmd/project/index.ts +1 -0
- package/src/cmd/project/list.ts +24 -10
- package/src/cmd/project/show.ts +28 -9
- package/src/cmd/project/template-flow.ts +10 -6
- package/src/cmd/prompt/index.ts +12 -0
- package/src/cmd/prompt/llm.ts +368 -0
- package/src/cmd/repl/index.ts +477 -0
- package/src/cmd/schema/index.ts +12 -0
- package/src/cmd/schema/show.ts +27 -0
- package/src/cmd/secret/delete.ts +17 -0
- package/src/cmd/secret/get.ts +20 -1
- package/src/cmd/secret/import.ts +45 -2
- package/src/cmd/secret/index.ts +1 -0
- package/src/cmd/secret/list.ts +10 -1
- package/src/cmd/secret/pull.ts +20 -0
- package/src/cmd/secret/push.ts +33 -1
- package/src/cmd/secret/set.ts +20 -0
- package/src/cmd/version/index.ts +15 -2
- package/src/config.ts +17 -4
- package/src/errors.ts +222 -0
- package/src/explain.ts +126 -0
- package/src/index.ts +51 -0
- package/src/json.ts +28 -0
- package/src/output.ts +307 -0
- package/src/repl.ts +1507 -0
- package/src/schema-generator.ts +389 -0
- package/src/tui.ts +178 -13
- package/src/types.ts +75 -22
- 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
|
+
}
|