kokoirc 0.2.0
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/README.md +227 -0
- package/docs/commands/alias.md +42 -0
- package/docs/commands/ban.md +26 -0
- package/docs/commands/close.md +25 -0
- package/docs/commands/connect.md +26 -0
- package/docs/commands/deop.md +24 -0
- package/docs/commands/devoice.md +24 -0
- package/docs/commands/disconnect.md +26 -0
- package/docs/commands/help.md +28 -0
- package/docs/commands/ignore.md +47 -0
- package/docs/commands/items.md +95 -0
- package/docs/commands/join.md +25 -0
- package/docs/commands/kb.md +26 -0
- package/docs/commands/kick.md +25 -0
- package/docs/commands/log.md +82 -0
- package/docs/commands/me.md +24 -0
- package/docs/commands/mode.md +29 -0
- package/docs/commands/msg.md +26 -0
- package/docs/commands/nick.md +24 -0
- package/docs/commands/notice.md +24 -0
- package/docs/commands/op.md +24 -0
- package/docs/commands/part.md +25 -0
- package/docs/commands/quit.md +24 -0
- package/docs/commands/reload.md +19 -0
- package/docs/commands/script.md +126 -0
- package/docs/commands/server.md +61 -0
- package/docs/commands/set.md +37 -0
- package/docs/commands/topic.md +24 -0
- package/docs/commands/unalias.md +22 -0
- package/docs/commands/unban.md +25 -0
- package/docs/commands/unignore.md +25 -0
- package/docs/commands/voice.md +25 -0
- package/docs/commands/whois.md +24 -0
- package/docs/commands/wii.md +23 -0
- package/package.json +38 -0
- package/src/app/App.tsx +205 -0
- package/src/core/commands/docs.ts +183 -0
- package/src/core/commands/execution.ts +114 -0
- package/src/core/commands/help-formatter.ts +185 -0
- package/src/core/commands/helpers.ts +168 -0
- package/src/core/commands/index.ts +7 -0
- package/src/core/commands/parser.ts +33 -0
- package/src/core/commands/registry.ts +1394 -0
- package/src/core/commands/types.ts +19 -0
- package/src/core/config/defaults.ts +66 -0
- package/src/core/config/loader.ts +209 -0
- package/src/core/constants.ts +20 -0
- package/src/core/init.ts +32 -0
- package/src/core/irc/antiflood.ts +244 -0
- package/src/core/irc/client.ts +145 -0
- package/src/core/irc/events.ts +1031 -0
- package/src/core/irc/formatting.ts +132 -0
- package/src/core/irc/ignore.ts +84 -0
- package/src/core/irc/index.ts +2 -0
- package/src/core/irc/netsplit.ts +292 -0
- package/src/core/scripts/api.ts +240 -0
- package/src/core/scripts/event-bus.ts +82 -0
- package/src/core/scripts/index.ts +26 -0
- package/src/core/scripts/manager.ts +154 -0
- package/src/core/scripts/types.ts +256 -0
- package/src/core/state/selectors.ts +39 -0
- package/src/core/state/sorting.ts +30 -0
- package/src/core/state/store.ts +242 -0
- package/src/core/storage/crypto.ts +78 -0
- package/src/core/storage/db.ts +107 -0
- package/src/core/storage/index.ts +80 -0
- package/src/core/storage/query.ts +204 -0
- package/src/core/storage/types.ts +37 -0
- package/src/core/storage/writer.ts +130 -0
- package/src/core/theme/index.ts +3 -0
- package/src/core/theme/loader.ts +45 -0
- package/src/core/theme/parser.ts +518 -0
- package/src/core/theme/renderer.tsx +25 -0
- package/src/index.tsx +17 -0
- package/src/types/config.ts +126 -0
- package/src/types/index.ts +107 -0
- package/src/types/irc-framework.d.ts +569 -0
- package/src/types/theme.ts +37 -0
- package/src/ui/ErrorBoundary.tsx +42 -0
- package/src/ui/chat/ChatView.tsx +39 -0
- package/src/ui/chat/MessageLine.tsx +92 -0
- package/src/ui/hooks/useStatusbarColors.ts +23 -0
- package/src/ui/input/CommandInput.tsx +273 -0
- package/src/ui/layout/AppLayout.tsx +126 -0
- package/src/ui/layout/TopicBar.tsx +46 -0
- package/src/ui/sidebar/BufferList.tsx +55 -0
- package/src/ui/sidebar/NickList.tsx +96 -0
- package/src/ui/splash/SplashScreen.tsx +100 -0
- package/src/ui/statusbar/StatusLine.tsx +205 -0
- package/themes/.gitkeep +0 -0
- package/themes/default.theme +57 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// ─── Help Display Formatter ─────────────────────────────────
|
|
2
|
+
// Three display modes using addLocalEvent() for in-buffer output.
|
|
3
|
+
|
|
4
|
+
import { addLocalEvent } from "./helpers"
|
|
5
|
+
import { getHelp, getCategories, type CommandHelp } from "./docs"
|
|
6
|
+
import { commands, findByAlias, getCanonicalName } from "./registry"
|
|
7
|
+
|
|
8
|
+
const CATEGORY_ORDER = [
|
|
9
|
+
"Connection", "Channel", "Messaging", "Moderation",
|
|
10
|
+
"Configuration", "Statusbar", "Info",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
// ─── /help (no args) — categorized command list ─────────────
|
|
14
|
+
|
|
15
|
+
export function showCommandList(): void {
|
|
16
|
+
const cats = getCategories()
|
|
17
|
+
|
|
18
|
+
// Collect commands that have doc files
|
|
19
|
+
const documented = new Set<string>()
|
|
20
|
+
for (const list of cats.values()) {
|
|
21
|
+
for (const name of list) documented.add(name)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Commands without doc files go under "Other"
|
|
25
|
+
const other: string[] = []
|
|
26
|
+
for (const name of Object.keys(commands)) {
|
|
27
|
+
if (!documented.has(name)) other.push(name)
|
|
28
|
+
}
|
|
29
|
+
if (other.length > 0) cats.set("Other", other.sort())
|
|
30
|
+
|
|
31
|
+
addLocalEvent(`%Z7aa2f7───── Commands ─────────────────────────────────%N`)
|
|
32
|
+
|
|
33
|
+
// Ordered categories first, then any extras
|
|
34
|
+
const seen = new Set<string>()
|
|
35
|
+
const orderedCats = [...CATEGORY_ORDER, ...cats.keys()].filter((c) => {
|
|
36
|
+
if (seen.has(c)) return false
|
|
37
|
+
seen.add(c)
|
|
38
|
+
return cats.has(c)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
for (const cat of orderedCats) {
|
|
42
|
+
const list = cats.get(cat)!
|
|
43
|
+
addLocalEvent(` %Z7aa2f7${cat}%N`)
|
|
44
|
+
for (const name of list) {
|
|
45
|
+
const help = getHelp(name)
|
|
46
|
+
const desc = help?.description ?? commands[name]?.description ?? ""
|
|
47
|
+
addLocalEvent(` %Zc0caf5${"/" + name}${" ".repeat(Math.max(1, 16 - name.length))}%Z565f89${desc}%N`)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
addLocalEvent(`%Z7aa2f7─────────────────────────────────────────────────%N`)
|
|
52
|
+
addLocalEvent(` %Z565f89Type %Z7aa2f7/help <command>%Z565f89 for details%N`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── /help <command> — full command help ────────────────────
|
|
56
|
+
|
|
57
|
+
export function showCommandHelp(name: string): void {
|
|
58
|
+
const canonical = getCanonicalName(name)
|
|
59
|
+
const help = getHelp(canonical)
|
|
60
|
+
const def = commands[canonical] ?? findByAlias(name)
|
|
61
|
+
|
|
62
|
+
if (!help && !def) {
|
|
63
|
+
addLocalEvent(`%Zf7768eUnknown command: ${name}%N`)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
addLocalEvent(`%Z7aa2f7───── /${canonical} ─────────────────────────────────%N`)
|
|
68
|
+
|
|
69
|
+
if (help) {
|
|
70
|
+
if (help.description) {
|
|
71
|
+
addLocalEvent(` %Za9b1d6${help.description}%N`)
|
|
72
|
+
}
|
|
73
|
+
if (help.syntax) {
|
|
74
|
+
addLocalEvent("")
|
|
75
|
+
for (const line of help.syntax.split("\n")) {
|
|
76
|
+
addLocalEvent(` %Zc0caf5${line}%N`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (def?.aliases?.length) {
|
|
80
|
+
addLocalEvent(` %Z565f89Aliases: ${def.aliases.map((a) => "/" + a).join(", ")}%N`)
|
|
81
|
+
}
|
|
82
|
+
if (help.subcommands.length > 0) {
|
|
83
|
+
addLocalEvent("")
|
|
84
|
+
addLocalEvent(` %Z7aa2f7Subcommands:%N`)
|
|
85
|
+
for (const sub of help.subcommands) {
|
|
86
|
+
const aliasNote = sub.aliases.length > 0 ? ` %Z565f89(${sub.aliases.join(", ")})%N` : ""
|
|
87
|
+
addLocalEvent(` %Zc0caf5${sub.name.padEnd(14)}%Z565f89${sub.description}%N${aliasNote}`)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (help.body) {
|
|
91
|
+
addLocalEvent("")
|
|
92
|
+
for (const line of help.body.split("\n")) {
|
|
93
|
+
addLocalEvent(` %Za9b1d6${line}%N`)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (help.examples.length > 0) {
|
|
97
|
+
addLocalEvent("")
|
|
98
|
+
addLocalEvent(` %Z7aa2f7Examples:%N`)
|
|
99
|
+
for (const ex of help.examples) {
|
|
100
|
+
addLocalEvent(` %Zc0caf5${ex}%N`)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (help.seeAlso.length > 0) {
|
|
104
|
+
addLocalEvent("")
|
|
105
|
+
addLocalEvent(` %Z565f89See also: ${help.seeAlso.map((s) => "/" + s).join(", ")}%N`)
|
|
106
|
+
}
|
|
107
|
+
} else if (def) {
|
|
108
|
+
// Fallback for commands without doc files
|
|
109
|
+
addLocalEvent(` %Zc0caf5${def.usage}%N`)
|
|
110
|
+
addLocalEvent(` %Za9b1d6${def.description}%N`)
|
|
111
|
+
if (def.aliases?.length) {
|
|
112
|
+
addLocalEvent(` %Z565f89Aliases: ${def.aliases.map((a) => "/" + a).join(", ")}%N`)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
addLocalEvent(`%Z7aa2f7─────────────────────────────────────────────────%N`)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ─── /help <command> <subcommand> — subcommand detail ───────
|
|
120
|
+
|
|
121
|
+
export function showSubcommandHelp(cmdName: string, subName: string): void {
|
|
122
|
+
const canonical = getCanonicalName(cmdName)
|
|
123
|
+
const help = getHelp(canonical)
|
|
124
|
+
|
|
125
|
+
if (!help) {
|
|
126
|
+
showCommandHelp(cmdName)
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const sub = help.subcommands.find(
|
|
131
|
+
(s) => s.name === subName || s.aliases.includes(subName)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if (!sub) {
|
|
135
|
+
addLocalEvent(`%Zf7768eUnknown subcommand: /${canonical} ${subName}%N`)
|
|
136
|
+
if (help.subcommands.length > 0) {
|
|
137
|
+
const names = help.subcommands.map((s) => s.name).join(", ")
|
|
138
|
+
addLocalEvent(` %Z565f89Available: ${names}%N`)
|
|
139
|
+
}
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
addLocalEvent(`%Z7aa2f7───── /${canonical} ${sub.name} ─────────────────────────%N`)
|
|
144
|
+
|
|
145
|
+
if (sub.description) {
|
|
146
|
+
addLocalEvent(` %Za9b1d6${sub.description}%N`)
|
|
147
|
+
}
|
|
148
|
+
if (sub.syntax) {
|
|
149
|
+
addLocalEvent("")
|
|
150
|
+
for (const line of sub.syntax.split("\n")) {
|
|
151
|
+
addLocalEvent(` %Zc0caf5${line}%N`)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (sub.aliases.length > 0) {
|
|
155
|
+
addLocalEvent(` %Z565f89Aliases: ${sub.aliases.join(", ")}%N`)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Show flags and extra body content (after first paragraph and syntax)
|
|
159
|
+
const bodyLines = sub.body.split("\n")
|
|
160
|
+
let inFlags = false
|
|
161
|
+
const extraLines: string[] = []
|
|
162
|
+
for (const line of bodyLines) {
|
|
163
|
+
if (line.startsWith("**Flags:**")) {
|
|
164
|
+
inFlags = true
|
|
165
|
+
extraLines.push("")
|
|
166
|
+
addLocalEvent(` %Z7aa2f7Flags:%N`)
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
if (inFlags) {
|
|
170
|
+
if (line.startsWith("- ")) {
|
|
171
|
+
const flagText = line.slice(2)
|
|
172
|
+
const dashIdx = flagText.indexOf(" — ")
|
|
173
|
+
if (dashIdx > 0) {
|
|
174
|
+
addLocalEvent(` %Zc0caf5${flagText.slice(0, dashIdx)}%Z565f89${flagText.slice(dashIdx)}%N`)
|
|
175
|
+
} else {
|
|
176
|
+
addLocalEvent(` %Zc0caf5${flagText}%N`)
|
|
177
|
+
}
|
|
178
|
+
} else if (line.trim() === "") {
|
|
179
|
+
inFlags = false
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
addLocalEvent(`%Z7aa2f7─────────────────────────────────────────────────%N`)
|
|
185
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { useStore } from "@/core/state/store"
|
|
2
|
+
import { makeBufferId, BufferType } from "@/types"
|
|
3
|
+
import type { AppConfig } from "@/types/config"
|
|
4
|
+
import { CREDENTIAL_FIELDS } from "./types"
|
|
5
|
+
import type { ResolvedConfig } from "./types"
|
|
6
|
+
|
|
7
|
+
/** Display a local event message in the active buffer. */
|
|
8
|
+
export function addLocalEvent(text: string) {
|
|
9
|
+
const s = useStore.getState()
|
|
10
|
+
const buf = s.activeBufferId
|
|
11
|
+
if (!buf) return
|
|
12
|
+
s.addMessage(buf, {
|
|
13
|
+
id: crypto.randomUUID(),
|
|
14
|
+
timestamp: new Date(),
|
|
15
|
+
type: "event",
|
|
16
|
+
text,
|
|
17
|
+
highlight: false,
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Get the channel name from the active buffer (if it's a channel buffer). */
|
|
22
|
+
export function getActiveChannel(): string | null {
|
|
23
|
+
const s = useStore.getState()
|
|
24
|
+
const buf = s.activeBufferId ? s.buffers.get(s.activeBufferId) : null
|
|
25
|
+
if (!buf || buf.type !== BufferType.Channel) return null
|
|
26
|
+
return buf.name
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Switch to a connection's Status buffer. */
|
|
30
|
+
export function switchToStatusBuffer(connId: string) {
|
|
31
|
+
const s = useStore.getState()
|
|
32
|
+
const statusId = makeBufferId(connId, "Status")
|
|
33
|
+
if (s.buffers.has(statusId)) {
|
|
34
|
+
s.setActiveBuffer(statusId)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── /set helpers ───────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/** Resolve a dot-path like "general.nick" or "servers.ircnet.port" to the config value. */
|
|
41
|
+
export function getConfigValue(config: AppConfig, path: string): ResolvedConfig | null {
|
|
42
|
+
const parts = path.split(".")
|
|
43
|
+
if (parts.length < 2) return null
|
|
44
|
+
|
|
45
|
+
const section = parts[0]
|
|
46
|
+
|
|
47
|
+
// servers.<id>.<field>
|
|
48
|
+
if (section === "servers") {
|
|
49
|
+
if (parts.length < 3) return null
|
|
50
|
+
const serverId = parts[1]
|
|
51
|
+
const field = parts.slice(2).join(".")
|
|
52
|
+
const server = config.servers[serverId]
|
|
53
|
+
if (!server || !(field in server)) return null
|
|
54
|
+
return {
|
|
55
|
+
value: (server as any)[field],
|
|
56
|
+
field,
|
|
57
|
+
isCredential: CREDENTIAL_FIELDS.has(field),
|
|
58
|
+
serverId,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// aliases.<name> — allow creating new aliases via /set
|
|
63
|
+
if (section === "aliases") {
|
|
64
|
+
if (parts.length < 2) return null
|
|
65
|
+
const name = parts[1]
|
|
66
|
+
const value = config.aliases[name] ?? ""
|
|
67
|
+
return { value, field: name, isCredential: false }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// sidepanel.left.<field> / sidepanel.right.<field>
|
|
71
|
+
if (section === "sidepanel") {
|
|
72
|
+
if (parts.length < 3) return null
|
|
73
|
+
const side = parts[1] as "left" | "right"
|
|
74
|
+
const field = parts[2]
|
|
75
|
+
const panel = config.sidepanel?.[side]
|
|
76
|
+
if (!panel || !(field in panel)) return null
|
|
77
|
+
return { value: (panel as any)[field], field, isCredential: false }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// general.<field>, display.<field>, statusbar.<field>
|
|
81
|
+
const field = parts.slice(1).join(".")
|
|
82
|
+
const obj = (config as any)[section]
|
|
83
|
+
if (!obj || typeof obj !== "object" || !(field in obj)) return null
|
|
84
|
+
return { value: obj[field], field, isCredential: false }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Set a value in the config object in-place. */
|
|
88
|
+
export function setConfigValue(config: AppConfig, path: string, value: any): void {
|
|
89
|
+
const parts = path.split(".")
|
|
90
|
+
const section = parts[0]
|
|
91
|
+
|
|
92
|
+
if (section === "aliases" && parts.length >= 2) {
|
|
93
|
+
config.aliases[parts[1]] = String(value)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (section === "servers" && parts.length >= 3) {
|
|
98
|
+
const server = config.servers[parts[1]]
|
|
99
|
+
if (server) (server as any)[parts.slice(2).join(".")] = value
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (section === "sidepanel" && parts.length >= 3) {
|
|
104
|
+
const panel = (config.sidepanel as any)?.[parts[1]]
|
|
105
|
+
if (panel) panel[parts[2]] = value
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const field = parts.slice(1).join(".")
|
|
110
|
+
const obj = (config as any)[section]
|
|
111
|
+
if (obj) obj[field] = value
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Coerce a raw string value to match the type of the existing value. */
|
|
115
|
+
export function coerceValue(raw: string, existing: any): any {
|
|
116
|
+
if (typeof existing === "boolean") {
|
|
117
|
+
if (raw === "true") return true
|
|
118
|
+
if (raw === "false") return false
|
|
119
|
+
return undefined
|
|
120
|
+
}
|
|
121
|
+
if (typeof existing === "number") {
|
|
122
|
+
const n = Number(raw)
|
|
123
|
+
return isNaN(n) ? undefined : n
|
|
124
|
+
}
|
|
125
|
+
if (Array.isArray(existing)) {
|
|
126
|
+
return raw.split(",").map((s) => s.trim())
|
|
127
|
+
}
|
|
128
|
+
return raw
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Format a value for display. Escapes % so the theme parser doesn't eat color codes. */
|
|
132
|
+
export function formatValue(v: any): string {
|
|
133
|
+
const raw = Array.isArray(v) ? v.join(", ") : String(v)
|
|
134
|
+
return raw.replace(/%/g, "%%")
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Display all settings grouped by section. */
|
|
138
|
+
export function listAllSettings(config: AppConfig): void {
|
|
139
|
+
addLocalEvent(`%Z7aa2f7───── Settings ─────────────────────────────────%N`)
|
|
140
|
+
|
|
141
|
+
const showSection = (label: string, prefix: string, obj: Record<string, any>) => {
|
|
142
|
+
addLocalEvent(`%Z565f89[${label}]%N`)
|
|
143
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
144
|
+
if (typeof val === "object" && val !== null && !Array.isArray(val)) continue
|
|
145
|
+
const fullPath = `${prefix}.${key}`
|
|
146
|
+
const isCredential = CREDENTIAL_FIELDS.has(key)
|
|
147
|
+
const display = isCredential ? "***" : formatValue(val)
|
|
148
|
+
const envTag = isCredential ? " %Z565f89[.env]%N" : ""
|
|
149
|
+
addLocalEvent(` %Z7aa2f7${fullPath.padEnd(32)}%N= %Zc0caf5${display}%N${envTag}`)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
showSection("general", "general", config.general)
|
|
154
|
+
showSection("display", "display", config.display)
|
|
155
|
+
showSection("sidepanel.left", "sidepanel.left", config.sidepanel.left)
|
|
156
|
+
showSection("sidepanel.right", "sidepanel.right", config.sidepanel.right)
|
|
157
|
+
showSection("statusbar", "statusbar", config.statusbar)
|
|
158
|
+
|
|
159
|
+
if (Object.keys(config.aliases).length > 0) {
|
|
160
|
+
showSection("aliases", "aliases", config.aliases)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
for (const [id, srv] of Object.entries(config.servers)) {
|
|
164
|
+
showSection(`servers.${id}`, `servers.${id}`, srv)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
addLocalEvent(`%Z7aa2f7─────────────────────────────────────────────────%N`)
|
|
168
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { parseCommand } from "./parser"
|
|
2
|
+
export type { ParsedCommand } from "./parser"
|
|
3
|
+
export { executeCommand, getCommandNames } from "./execution"
|
|
4
|
+
export { commands } from "./registry"
|
|
5
|
+
export type { CommandDef } from "./types"
|
|
6
|
+
export { loadAllDocs, getHelp, getSubcommands, getCategories } from "./docs"
|
|
7
|
+
export type { CommandHelp, SubcommandHelp } from "./docs"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface ParsedCommand {
|
|
2
|
+
command: string
|
|
3
|
+
args: string[]
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const GREEDY_COMMANDS = new Set(["msg", "notice", "me", "quit", "topic", "kick", "kb", "close", "disconnect", "set", "alias"])
|
|
7
|
+
|
|
8
|
+
export function parseCommand(input: string): ParsedCommand | null {
|
|
9
|
+
if (!input.startsWith("/")) return null
|
|
10
|
+
|
|
11
|
+
const trimmed = input.slice(1)
|
|
12
|
+
const spaceIndex = trimmed.indexOf(" ")
|
|
13
|
+
|
|
14
|
+
if (spaceIndex === -1) {
|
|
15
|
+
return { command: trimmed.toLowerCase(), args: [] }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const command = trimmed.slice(0, spaceIndex).toLowerCase()
|
|
19
|
+
const rest = trimmed.slice(spaceIndex + 1).trim()
|
|
20
|
+
|
|
21
|
+
if (GREEDY_COMMANDS.has(command)) {
|
|
22
|
+
if (command === "me" || command === "quit" || command === "close") {
|
|
23
|
+
return { command, args: [rest] }
|
|
24
|
+
}
|
|
25
|
+
const firstSpace = rest.indexOf(" ")
|
|
26
|
+
if (firstSpace === -1) {
|
|
27
|
+
return { command, args: [rest] }
|
|
28
|
+
}
|
|
29
|
+
return { command, args: [rest.slice(0, firstSpace), rest.slice(firstSpace + 1)] }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { command, args: rest.split(/\s+/) }
|
|
33
|
+
}
|