codeblog-app 2.3.3 → 2.3.4
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/package.json +7 -7
- package/src/auth/oauth.ts +5 -0
- package/src/cli/cmd/setup.ts +215 -2
- package/src/cli/ui.ts +68 -15
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "codeblog-app",
|
|
4
|
-
"version": "2.3.
|
|
4
|
+
"version": "2.3.4",
|
|
5
5
|
"description": "CLI client for CodeBlog — the forum where AI writes the posts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -58,11 +58,11 @@
|
|
|
58
58
|
"typescript": "5.8.2"
|
|
59
59
|
},
|
|
60
60
|
"optionalDependencies": {
|
|
61
|
-
"codeblog-app-darwin-arm64": "2.3.
|
|
62
|
-
"codeblog-app-darwin-x64": "2.3.
|
|
63
|
-
"codeblog-app-linux-arm64": "2.3.
|
|
64
|
-
"codeblog-app-linux-x64": "2.3.
|
|
65
|
-
"codeblog-app-windows-x64": "2.3.
|
|
61
|
+
"codeblog-app-darwin-arm64": "2.3.4",
|
|
62
|
+
"codeblog-app-darwin-x64": "2.3.4",
|
|
63
|
+
"codeblog-app-linux-arm64": "2.3.4",
|
|
64
|
+
"codeblog-app-linux-x64": "2.3.4",
|
|
65
|
+
"codeblog-app-windows-x64": "2.3.4"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"@ai-sdk/anthropic": "^3.0.44",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"@opentui/core": "^0.1.79",
|
|
74
74
|
"@opentui/solid": "^0.1.79",
|
|
75
75
|
"ai": "^6.0.86",
|
|
76
|
-
"codeblog-mcp": "2.2.
|
|
76
|
+
"codeblog-mcp": "2.2.2",
|
|
77
77
|
"drizzle-orm": "1.0.0-beta.12-a5629fb",
|
|
78
78
|
"fuzzysort": "^3.1.0",
|
|
79
79
|
"hono": "4.10.7",
|
package/src/auth/oauth.ts
CHANGED
|
@@ -6,6 +6,9 @@ import { Log } from "../util/log"
|
|
|
6
6
|
|
|
7
7
|
const log = Log.create({ service: "oauth" })
|
|
8
8
|
|
|
9
|
+
/** Set after a successful login — indicates whether the user already has agents. */
|
|
10
|
+
export let lastAuthHasAgents: boolean | undefined = undefined
|
|
11
|
+
|
|
9
12
|
export namespace OAuth {
|
|
10
13
|
export async function login(options?: { onUrl?: (url: string) => void }) {
|
|
11
14
|
const open = (await import("open")).default
|
|
@@ -15,6 +18,8 @@ export namespace OAuth {
|
|
|
15
18
|
const token = params.get("token")
|
|
16
19
|
const key = params.get("api_key")
|
|
17
20
|
const username = params.get("username") || undefined
|
|
21
|
+
const hasAgentsParam = params.get("has_agents")
|
|
22
|
+
lastAuthHasAgents = hasAgentsParam === "true" ? true : hasAgentsParam === "false" ? false : undefined
|
|
18
23
|
|
|
19
24
|
if (key) {
|
|
20
25
|
let ownerMismatch = ""
|
package/src/cli/cmd/setup.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CommandModule } from "yargs"
|
|
2
2
|
import { Auth } from "../../auth"
|
|
3
|
-
import { OAuth } from "../../auth/oauth"
|
|
3
|
+
import { OAuth, lastAuthHasAgents } from "../../auth/oauth"
|
|
4
4
|
import { McpBridge } from "../../mcp/client"
|
|
5
5
|
import { UI } from "../ui"
|
|
6
6
|
import { Config } from "../../config"
|
|
@@ -465,7 +465,7 @@ export async function runAISetupWizard(source: "setup" | "command" = "command"):
|
|
|
465
465
|
}
|
|
466
466
|
|
|
467
467
|
console.log("")
|
|
468
|
-
const modeIdx = await UI.select(" Onboarding mode", ["QuickStart (recommended)", "Manual", "Skip for now"])
|
|
468
|
+
const modeIdx = await UI.select(" Onboarding mode", ["QuickStart (recommended)", "Manual", "Skip for now"], { searchable: false })
|
|
469
469
|
if (modeIdx < 0 || modeIdx === 2) {
|
|
470
470
|
UI.info("Skipped AI setup.")
|
|
471
471
|
return
|
|
@@ -563,6 +563,212 @@ export async function runAISetupWizard(source: "setup" | "command" = "command"):
|
|
|
563
563
|
console.log(` ${UI.Style.TEXT_DIM}You can rerun this wizard with: codeblog ai setup${UI.Style.TEXT_NORMAL}`)
|
|
564
564
|
}
|
|
565
565
|
|
|
566
|
+
// ─── Agent Creation ─────────────────────────────────────────────────────────
|
|
567
|
+
|
|
568
|
+
const SOURCE_OPTIONS = ["Claude Code", "Cursor", "Windsurf", "Codex CLI", "Multiple / Other"]
|
|
569
|
+
const SOURCE_VALUES = ["claude-code", "cursor", "windsurf", "codex", "other"]
|
|
570
|
+
|
|
571
|
+
async function generateAgentNameAndEmoji(): Promise<{ name: string; emoji: string } | null> {
|
|
572
|
+
try {
|
|
573
|
+
const { AIProvider } = await import("../../ai/provider")
|
|
574
|
+
const { generateText } = await import("ai")
|
|
575
|
+
const model = await AIProvider.getModel()
|
|
576
|
+
const result = await generateText({
|
|
577
|
+
model,
|
|
578
|
+
prompt:
|
|
579
|
+
"You are naming an AI coding agent for a developer forum called CodeBlog. " +
|
|
580
|
+
"Generate a creative, short (1-3 words) agent name and pick a single emoji that fits as its avatar. " +
|
|
581
|
+
"The emoji should be fun, expressive, and have personality. " +
|
|
582
|
+
"Do NOT use 🤖 or plain colored circles (🟠🟣🟢🔵⚫) — those are reserved by the system. " +
|
|
583
|
+
"Respond ONLY with JSON: {\"name\": \"...\", \"emoji\": \"...\"}",
|
|
584
|
+
maxOutputTokens: 100,
|
|
585
|
+
})
|
|
586
|
+
const parsed = JSON.parse(result.text.trim())
|
|
587
|
+
if (parsed.name && parsed.emoji) {
|
|
588
|
+
return { name: String(parsed.name).slice(0, 50), emoji: String(parsed.emoji) }
|
|
589
|
+
}
|
|
590
|
+
} catch {}
|
|
591
|
+
return null
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async function generateEmojiForName(agentName: string): Promise<string> {
|
|
595
|
+
try {
|
|
596
|
+
const { AIProvider } = await import("../../ai/provider")
|
|
597
|
+
const { generateText } = await import("ai")
|
|
598
|
+
const model = await AIProvider.getModel()
|
|
599
|
+
const result = await generateText({
|
|
600
|
+
model,
|
|
601
|
+
prompt:
|
|
602
|
+
`Pick a single emoji that best represents an AI coding agent named "${agentName}". ` +
|
|
603
|
+
"The emoji should be fun, expressive, and have personality. " +
|
|
604
|
+
"Do NOT use 🤖 or plain colored circles (🟠🟣🟢🔵⚫) — those are reserved by the system. " +
|
|
605
|
+
"Respond with ONLY the emoji, nothing else.",
|
|
606
|
+
maxOutputTokens: 10,
|
|
607
|
+
})
|
|
608
|
+
const emoji = result.text.trim()
|
|
609
|
+
if (emoji && emoji.length <= 16) return emoji
|
|
610
|
+
} catch {}
|
|
611
|
+
return "🦊"
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async function detectSourceType(): Promise<string | null> {
|
|
615
|
+
try {
|
|
616
|
+
const result = await McpBridge.callTool("scan_sessions", { limit: 1 })
|
|
617
|
+
const sessions = JSON.parse(result)
|
|
618
|
+
if (sessions.length > 0 && sessions[0].source) {
|
|
619
|
+
const map: Record<string, string> = {
|
|
620
|
+
"claude-code": "claude-code",
|
|
621
|
+
cursor: "cursor",
|
|
622
|
+
windsurf: "windsurf",
|
|
623
|
+
codex: "codex",
|
|
624
|
+
}
|
|
625
|
+
return map[sessions[0].source] || null
|
|
626
|
+
}
|
|
627
|
+
} catch {}
|
|
628
|
+
return null
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
async function createAgentViaAPI(opts: {
|
|
632
|
+
name: string
|
|
633
|
+
avatar: string
|
|
634
|
+
sourceType: string
|
|
635
|
+
}): Promise<{ api_key: string; name: string; id: string } | null> {
|
|
636
|
+
const base = await Config.url()
|
|
637
|
+
const auth = await Auth.get()
|
|
638
|
+
if (!auth) return null
|
|
639
|
+
|
|
640
|
+
// This endpoint requires a JWT token, not an API key.
|
|
641
|
+
// During setup the user may only have a JWT (no agent/api_key yet).
|
|
642
|
+
const token = auth.type === "jwt" ? auth.value : null
|
|
643
|
+
if (!token) {
|
|
644
|
+
throw new Error("JWT token required to create an agent. Please re-login with: codeblog login")
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const res = await fetch(`${base}/api/auth/create-agent`, {
|
|
648
|
+
method: "POST",
|
|
649
|
+
headers: {
|
|
650
|
+
Authorization: `Bearer ${token}`,
|
|
651
|
+
"Content-Type": "application/json",
|
|
652
|
+
},
|
|
653
|
+
body: JSON.stringify({
|
|
654
|
+
name: opts.name,
|
|
655
|
+
avatar: opts.avatar,
|
|
656
|
+
source_type: opts.sourceType,
|
|
657
|
+
}),
|
|
658
|
+
})
|
|
659
|
+
|
|
660
|
+
if (!res.ok) {
|
|
661
|
+
const err = await res.json().catch(() => ({ error: "Unknown error" })) as { error?: string }
|
|
662
|
+
throw new Error(err.error || `Server returned ${res.status}`)
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const data = await res.json() as { agent: { api_key: string; name: string; id: string } }
|
|
666
|
+
return {
|
|
667
|
+
api_key: data.agent.api_key,
|
|
668
|
+
name: data.agent.name,
|
|
669
|
+
id: data.agent.id,
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
async function agentCreationWizard(): Promise<void> {
|
|
674
|
+
await UI.typeText("Now let's create your AI Agent!", { charDelay: 10 })
|
|
675
|
+
await UI.typeText("Your agent is your coding persona on CodeBlog — it represents you and your coding style.", { charDelay: 10 })
|
|
676
|
+
await UI.typeText("Give it a name, an emoji avatar, and it'll be ready to publish insights from your coding sessions.", { charDelay: 10 })
|
|
677
|
+
console.log("")
|
|
678
|
+
|
|
679
|
+
const { AIProvider } = await import("../../ai/provider")
|
|
680
|
+
const hasAI = await AIProvider.hasAnyKey()
|
|
681
|
+
|
|
682
|
+
let name = ""
|
|
683
|
+
let emoji = ""
|
|
684
|
+
|
|
685
|
+
// ── Name ──
|
|
686
|
+
if (hasAI) {
|
|
687
|
+
await UI.typeText("Want AI to come up with a creative name for your agent?", { charDelay: 10 })
|
|
688
|
+
const choice = await UI.waitEnter("Press Enter to generate, or Esc to type your own")
|
|
689
|
+
|
|
690
|
+
if (choice === "enter") {
|
|
691
|
+
await shimmerLine("Generating a creative name...", 1500)
|
|
692
|
+
const suggestion = await generateAgentNameAndEmoji()
|
|
693
|
+
if (suggestion) {
|
|
694
|
+
console.log("")
|
|
695
|
+
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}${suggestion.emoji} ${suggestion.name}${UI.Style.TEXT_NORMAL}`)
|
|
696
|
+
console.log("")
|
|
697
|
+
const accept = await UI.waitEnter("Like it? Press Enter to keep, or Esc to type your own")
|
|
698
|
+
if (accept === "enter") {
|
|
699
|
+
name = suggestion.name
|
|
700
|
+
emoji = suggestion.emoji
|
|
701
|
+
}
|
|
702
|
+
} else {
|
|
703
|
+
UI.warn("AI generation failed — let's type a name instead.")
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (!name) {
|
|
709
|
+
const entered = await UI.inputWithEscape(
|
|
710
|
+
` ${UI.Style.TEXT_NORMAL_BOLD}Agent name${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(e.g. "CodeNinja", "ByteWizard"):${UI.Style.TEXT_NORMAL} `,
|
|
711
|
+
)
|
|
712
|
+
if (entered === null || !entered.trim()) {
|
|
713
|
+
UI.info("No worries! You can create an agent later on the website or with: codeblog agent create")
|
|
714
|
+
return
|
|
715
|
+
}
|
|
716
|
+
name = entered.trim()
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// ── Emoji avatar ──
|
|
720
|
+
if (!emoji) {
|
|
721
|
+
if (hasAI) {
|
|
722
|
+
await shimmerLine("Picking the perfect emoji avatar...", 800)
|
|
723
|
+
emoji = await generateEmojiForName(name)
|
|
724
|
+
console.log(` ${UI.Style.TEXT_DIM}Avatar:${UI.Style.TEXT_NORMAL} ${emoji}`)
|
|
725
|
+
} else {
|
|
726
|
+
const EMOJI_POOL = ["🦊", "🐙", "🧠", "⚡", "🔥", "🌟", "🎯", "🛠️", "💡", "🚀", "🎨", "🐱", "🦉", "🐺", "🐲", "🦋", "🧙", "🛡️", "🌊", "🦈"]
|
|
727
|
+
const idx = await UI.select(" Pick an emoji avatar for your agent", EMOJI_POOL)
|
|
728
|
+
emoji = idx >= 0 ? EMOJI_POOL[idx]! : "🦊"
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// ── Source type ──
|
|
733
|
+
await shimmerLine("Detecting IDE...", 600)
|
|
734
|
+
let sourceType = await detectSourceType()
|
|
735
|
+
if (sourceType) {
|
|
736
|
+
const label = SOURCE_OPTIONS[SOURCE_VALUES.indexOf(sourceType)] || sourceType
|
|
737
|
+
console.log(` ${UI.Style.TEXT_DIM}Detected IDE:${UI.Style.TEXT_NORMAL} ${label}`)
|
|
738
|
+
} else {
|
|
739
|
+
const idx = await UI.select(" Which IDE do you primarily use?", SOURCE_OPTIONS)
|
|
740
|
+
sourceType = idx >= 0 ? SOURCE_VALUES[idx]! : "other"
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// ── Preview & create ──
|
|
744
|
+
console.log("")
|
|
745
|
+
console.log(` ${UI.Style.TEXT_DIM}Your agent:${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_NORMAL_BOLD}${emoji} ${name}${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(${SOURCE_OPTIONS[SOURCE_VALUES.indexOf(sourceType)] || sourceType})${UI.Style.TEXT_NORMAL}`)
|
|
746
|
+
console.log("")
|
|
747
|
+
|
|
748
|
+
await shimmerLine("Creating your agent...", 1200)
|
|
749
|
+
|
|
750
|
+
try {
|
|
751
|
+
const result = await createAgentViaAPI({ name, avatar: emoji, sourceType })
|
|
752
|
+
if (!result) throw new Error("No result returned")
|
|
753
|
+
|
|
754
|
+
// Save the new API key as primary auth
|
|
755
|
+
const auth = await Auth.get()
|
|
756
|
+
await Auth.set({ type: "apikey", value: result.api_key, username: auth?.username })
|
|
757
|
+
await Config.saveActiveAgent(result.name, auth?.username)
|
|
758
|
+
|
|
759
|
+
// Sync to MCP config
|
|
760
|
+
try {
|
|
761
|
+
await McpBridge.callTool("codeblog_setup", { api_key: result.api_key })
|
|
762
|
+
} catch {}
|
|
763
|
+
|
|
764
|
+
console.log("")
|
|
765
|
+
UI.success(`Your agent "${emoji} ${name}" is ready! It'll represent you on CodeBlog.`)
|
|
766
|
+
} catch (err) {
|
|
767
|
+
UI.error(`Failed to create agent: ${err instanceof Error ? err.message : String(err)}`)
|
|
768
|
+
await UI.typeText("No worries — you can create an agent later on the website or with: codeblog agent create")
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
566
772
|
// ─── Setup Command ───────────────────────────────────────────────────────────
|
|
567
773
|
|
|
568
774
|
export const SetupCommand: CommandModule = {
|
|
@@ -608,6 +814,13 @@ export const SetupCommand: CommandModule = {
|
|
|
608
814
|
console.log("")
|
|
609
815
|
await runAISetupWizard("setup")
|
|
610
816
|
|
|
817
|
+
// Phase 3.5: Agent creation (if the user has no agents yet)
|
|
818
|
+
const needsAgent = lastAuthHasAgents === false || (lastAuthHasAgents === undefined && !(await Auth.get())?.type?.startsWith("apikey"))
|
|
819
|
+
if (needsAgent) {
|
|
820
|
+
UI.divider()
|
|
821
|
+
await agentCreationWizard()
|
|
822
|
+
}
|
|
823
|
+
|
|
611
824
|
// Phase 4: Interactive scan & publish
|
|
612
825
|
UI.divider()
|
|
613
826
|
|
package/src/cli/ui.ts
CHANGED
|
@@ -200,7 +200,7 @@ export namespace UI {
|
|
|
200
200
|
})
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
export async function select(prompt: string, options: string[]): Promise<number> {
|
|
203
|
+
export async function select(prompt: string, options: string[], opts?: { searchable?: boolean }): Promise<number> {
|
|
204
204
|
if (options.length === 0) return 0
|
|
205
205
|
|
|
206
206
|
const stdin = process.stdin
|
|
@@ -209,8 +209,11 @@ export namespace UI {
|
|
|
209
209
|
process.stderr.write("\x1b[?25l")
|
|
210
210
|
|
|
211
211
|
let idx = 0
|
|
212
|
+
let filter = ""
|
|
213
|
+
let filtered = options.map((label, originalIndex) => ({ label, originalIndex }))
|
|
212
214
|
let drawnRows = 0
|
|
213
215
|
const maxRows = 12
|
|
216
|
+
const searchable = opts?.searchable !== false
|
|
214
217
|
let onData: ((ch: Buffer) => void) = () => {}
|
|
215
218
|
|
|
216
219
|
const stripAnsi = (text: string) => text.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b./g, "")
|
|
@@ -220,24 +223,42 @@ export namespace UI {
|
|
|
220
223
|
return Math.max(1, Math.ceil((len || 1) / cols))
|
|
221
224
|
}
|
|
222
225
|
|
|
226
|
+
const applyFilter = () => {
|
|
227
|
+
const q = filter.toLowerCase()
|
|
228
|
+
if (!q) {
|
|
229
|
+
filtered = options.map((label, originalIndex) => ({ label, originalIndex }))
|
|
230
|
+
} else {
|
|
231
|
+
filtered = options
|
|
232
|
+
.map((label, originalIndex) => ({ label, originalIndex }))
|
|
233
|
+
.filter(({ label }) => stripAnsi(label).toLowerCase().includes(q))
|
|
234
|
+
}
|
|
235
|
+
idx = 0
|
|
236
|
+
}
|
|
237
|
+
|
|
223
238
|
const draw = () => {
|
|
224
239
|
if (drawnRows > 1) process.stderr.write(`\x1b[${drawnRows - 1}F`)
|
|
225
240
|
process.stderr.write("\x1b[J")
|
|
226
241
|
|
|
227
|
-
const
|
|
228
|
-
const
|
|
242
|
+
const count = filtered.length
|
|
243
|
+
const start = count <= maxRows ? 0 : Math.max(0, Math.min(idx - 4, count - maxRows))
|
|
244
|
+
const items = filtered.slice(start, start + maxRows)
|
|
245
|
+
const searchLine = filter
|
|
246
|
+
? ` ${Style.TEXT_HIGHLIGHT}❯${Style.TEXT_NORMAL} ${filter}${Style.TEXT_DIM}█${Style.TEXT_NORMAL}`
|
|
247
|
+
: ` ${Style.TEXT_HIGHLIGHT}❯${Style.TEXT_NORMAL} ${Style.TEXT_DIM}type to search...${Style.TEXT_NORMAL}`
|
|
229
248
|
const lines = [
|
|
230
249
|
prompt,
|
|
250
|
+
...(searchable ? [searchLine] : []),
|
|
231
251
|
...items.map((item, i) => {
|
|
232
252
|
const active = start + i === idx
|
|
233
253
|
const marker = active ? `${Style.TEXT_HIGHLIGHT}●${Style.TEXT_NORMAL}` : "○"
|
|
234
|
-
const text = active ? `${Style.TEXT_NORMAL_BOLD}${item}${Style.TEXT_NORMAL}` : item
|
|
254
|
+
const text = active ? `${Style.TEXT_NORMAL_BOLD}${item.label}${Style.TEXT_NORMAL}` : item.label
|
|
235
255
|
return ` ${marker} ${text}`
|
|
236
256
|
}),
|
|
237
|
-
|
|
238
|
-
|
|
257
|
+
...(count === 0 ? [` ${Style.TEXT_DIM}No matches${Style.TEXT_NORMAL}`] : []),
|
|
258
|
+
count > maxRows
|
|
259
|
+
? ` ${Style.TEXT_DIM}${start > 0 ? "↑ more " : ""}${start + maxRows < count ? "↓ more" : ""}${Style.TEXT_NORMAL}`
|
|
239
260
|
: ` ${Style.TEXT_DIM}${Style.TEXT_NORMAL}`,
|
|
240
|
-
` ${Style.TEXT_DIM}
|
|
261
|
+
` ${Style.TEXT_DIM}↑/↓ select · Enter confirm · Esc ${filter ? "clear" : "cancel"}${Style.TEXT_NORMAL}`,
|
|
241
262
|
]
|
|
242
263
|
process.stderr.write(lines.map((line) => `\x1b[2K\r${line}`).join("\n"))
|
|
243
264
|
drawnRows = lines.reduce((sum, line) => sum + rowCount(line), 0)
|
|
@@ -260,25 +281,57 @@ export namespace UI {
|
|
|
260
281
|
process.exit(130)
|
|
261
282
|
}
|
|
262
283
|
if (c === "\r" || c === "\n") {
|
|
263
|
-
|
|
264
|
-
|
|
284
|
+
if (filtered.length > 0) {
|
|
285
|
+
restore()
|
|
286
|
+
resolve(filtered[idx]!.originalIndex)
|
|
287
|
+
}
|
|
265
288
|
return
|
|
266
289
|
}
|
|
267
290
|
if (c === "\x1b") {
|
|
268
|
-
|
|
269
|
-
|
|
291
|
+
if (searchable && filter) {
|
|
292
|
+
filter = ""
|
|
293
|
+
applyFilter()
|
|
294
|
+
draw()
|
|
295
|
+
} else {
|
|
296
|
+
restore()
|
|
297
|
+
resolve(-1)
|
|
298
|
+
}
|
|
270
299
|
return
|
|
271
300
|
}
|
|
272
|
-
if (c === "
|
|
273
|
-
|
|
301
|
+
if (c === "\x7f" || c === "\b") {
|
|
302
|
+
if (searchable && filter) {
|
|
303
|
+
filter = filter.slice(0, -1)
|
|
304
|
+
applyFilter()
|
|
305
|
+
draw()
|
|
306
|
+
}
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
if (c.includes("\x1b[A") || c.includes("\x1bOA")) {
|
|
310
|
+
if (filtered.length > 0) idx = (idx - 1 + filtered.length) % filtered.length
|
|
274
311
|
draw()
|
|
275
312
|
return
|
|
276
313
|
}
|
|
277
|
-
if (c
|
|
278
|
-
idx = (idx + 1) %
|
|
314
|
+
if (c.includes("\x1b[B") || c.includes("\x1bOB")) {
|
|
315
|
+
if (filtered.length > 0) idx = (idx + 1) % filtered.length
|
|
279
316
|
draw()
|
|
280
317
|
return
|
|
281
318
|
}
|
|
319
|
+
// j/k navigation only when filter is empty (otherwise they are search characters)
|
|
320
|
+
if (!filter && (c === "k" || c === "j")) {
|
|
321
|
+
if (c === "k") idx = (idx - 1 + filtered.length) % filtered.length
|
|
322
|
+
else idx = (idx + 1) % filtered.length
|
|
323
|
+
draw()
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
// Printable characters → append to search filter (only when searchable)
|
|
327
|
+
if (searchable) {
|
|
328
|
+
const printable = c.replace(/[\x00-\x1f\x7f]/g, "")
|
|
329
|
+
if (printable) {
|
|
330
|
+
filter += printable
|
|
331
|
+
applyFilter()
|
|
332
|
+
draw()
|
|
333
|
+
}
|
|
334
|
+
}
|
|
282
335
|
}
|
|
283
336
|
stdin.on("data", onData)
|
|
284
337
|
})
|