thincoder 0.8.2 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +2 -1
  2. package/bin/thincoder.mjs +25 -25
  3. package/package.json +1 -1
  4. package/src/agent/dispatch.mjs +17 -12
  5. package/src/agent/helpers.mjs +22 -11
  6. package/src/agent/setup.mjs +32 -10
  7. package/src/agent-tools/goal.mjs +7 -6
  8. package/src/agent-tools/plan.mjs +3 -3
  9. package/src/agent-tools/recent-changes.mjs +3 -3
  10. package/src/agent-tools/skill.mjs +6 -6
  11. package/src/agent-tools/subagent.mjs +19 -18
  12. package/src/agent-tools/task.mjs +4 -4
  13. package/src/agent-tools/verify.mjs +21 -19
  14. package/src/agent-tools.mjs +3 -3
  15. package/src/agent.mjs +58 -37
  16. package/src/cli/distill-command.mjs +2 -2
  17. package/src/cli/make-agent.mjs +9 -9
  18. package/src/cli/memory-command.mjs +1 -1
  19. package/src/cli/permission.mjs +3 -3
  20. package/src/cli/setup-wizard.mjs +15 -15
  21. package/src/config.mjs +55 -53
  22. package/src/context.mjs +59 -56
  23. package/src/distill.mjs +35 -35
  24. package/src/embedding.mjs +17 -17
  25. package/src/git/checkpoint.mjs +211 -38
  26. package/src/git/gitmem.mjs +21 -20
  27. package/src/markdown.mjs +13 -13
  28. package/src/mcp/helpers.mjs +6 -1
  29. package/src/mcp/transport-http.mjs +2 -1
  30. package/src/mcp/transport-stdio.mjs +3 -2
  31. package/src/mcp/transport-ws.mjs +3 -2
  32. package/src/mcp.mjs +5 -2
  33. package/src/memory/code-index.mjs +16 -14
  34. package/src/memory/code-sync.mjs +36 -26
  35. package/src/memory/core.mjs +42 -35
  36. package/src/memory/docs.mjs +24 -15
  37. package/src/memory/schema.mjs +28 -27
  38. package/src/memory.mjs +2 -2
  39. package/src/prompts/coder.md +1 -1
  40. package/src/prompts/discipline.md +3 -0
  41. package/src/prompts/main.md +1 -2
  42. package/src/prompts/system.md +21 -16
  43. package/src/provider/core.mjs +11 -6
  44. package/src/provider/index.mjs +2 -2
  45. package/src/provider/rate.mjs +11 -11
  46. package/src/session.mjs +73 -40
  47. package/src/skills.mjs +17 -17
  48. package/src/tools/checkpoint.md +6 -2
  49. package/src/tools/file.mjs +16 -12
  50. package/src/tools/git.mjs +83 -12
  51. package/src/tools/index.mjs +1 -1
  52. package/src/tools/patch.mjs +20 -18
  53. package/src/tools/repomap-parse.mjs +17 -17
  54. package/src/tools/repomap.mjs +29 -29
  55. package/src/tools/shared.mjs +49 -28
  56. package/src/tools/system.mjs +163 -118
  57. package/src/tools/web.mjs +6 -6
  58. package/src/tui/agent-turn.mjs +82 -26
  59. package/src/tui/ansi.mjs +4 -2
  60. package/src/tui/clipboard.mjs +2 -2
  61. package/src/tui/cmd-auto.mjs +1 -1
  62. package/src/tui/cmd-clear.mjs +1 -1
  63. package/src/tui/cmd-config.mjs +2 -2
  64. package/src/tui/cmd-exit.mjs +2 -2
  65. package/src/tui/cmd-extract.mjs +1 -1
  66. package/src/tui/cmd-goal.mjs +4 -4
  67. package/src/tui/cmd-help.mjs +1 -1
  68. package/src/tui/cmd-init.mjs +3 -3
  69. package/src/tui/cmd-mcp.mjs +5 -5
  70. package/src/tui/cmd-model.mjs +1 -1
  71. package/src/tui/cmd-new.mjs +1 -1
  72. package/src/tui/cmd-plan.mjs +1 -1
  73. package/src/tui/cmd-reindex.mjs +2 -2
  74. package/src/tui/cmd-restore.mjs +1 -1
  75. package/src/tui/cmd-session.mjs +1 -1
  76. package/src/tui/cmd-skills.mjs +1 -1
  77. package/src/tui/cmd-think.mjs +2 -2
  78. package/src/tui/config-helpers.mjs +6 -6
  79. package/src/tui/distill-cmd.mjs +3 -3
  80. package/src/tui/index.mjs +131 -72
  81. package/src/tui/interaction.mjs +13 -12
  82. package/src/tui/key-handler.mjs +31 -23
  83. package/src/tui/layout.mjs +22 -17
  84. package/src/tui/pickers.mjs +19 -19
  85. package/src/tui/render-frame.mjs +37 -11
  86. package/src/tui/render.mjs +25 -24
  87. package/src/tui/slash-commands.mjs +14 -14
  88. package/src/tui/startup.mjs +15 -14
  89. package/src/tui/wizard.mjs +11 -11
  90. package/src/tui.mjs +2 -2
  91. package/bin/thincoder.js +0 -4
  92. package/src/tools/bash.mjs +0 -144
  93. package/src/tools/glob.mjs +0 -51
  94. package/src/tools/grep.mjs +0 -100
  95. package/src/tools/ls.mjs +0 -36
  96. package/src/tools.mjs +0 -2
  97. package/src/tui-render.mjs +0 -4
@@ -1,7 +1,7 @@
1
1
  /**
2
- * wizard.mjs — 首次启动 Config 向导
3
- * index.mjs 抽出:provider 选择逐项输入 (name/baseURL/model/key/embedkey) → 持久化接模型选择器。
4
- * 通过 ctx 对象访问 startTUI 闭包中的共享状态与 UI 函数。
2
+ * wizard.mjs — first-launch config wizard
3
+ * Extracted from index.mjs: provider selectstep-by-step input (name/baseURL/model/key/embedkey) → persistthen model picker.
4
+ * Accesses shared state and UI functions from the startTUI closure via ctx object.
5
5
  * ctx: { agent, state, pushLine, pushLabel, render, persistRaw, openModelPicker }
6
6
  */
7
7
 
@@ -9,13 +9,13 @@ import { PROVIDER_PRESETS as PRESETS } from "../config.mjs"
9
9
  import { ansi, C } from "./ansi.mjs"
10
10
 
11
11
  /**
12
- * 创建向导控制器。
13
- * 返回 { startWizard, renderWizard, wizardChooseProvider, wizardSubmitText, cancelWizard, finishWizard }
12
+ * Creates the wizard controller.
13
+ * Returns { startWizard, renderWizard, wizardChooseProvider, wizardSubmitText, cancelWizard, finishWizard }
14
14
  */
15
15
  export function createWizard(ctx) {
16
16
  const { agent, state, pushLine, pushLabel, render, persistRaw } = ctx
17
17
 
18
- /** 菜单步的候选项:已有 provider (no key 的标注)+ 未添加的预设 + 自定义 */
18
+ /** Candidates for the menu step: existing providers (marked "no key" if missing), unadded presets, custom */
19
19
  function wizardProviderItems() {
20
20
  const items = []
21
21
  for (const p of agent.providers) {
@@ -30,7 +30,7 @@ export function createWizard(ctx) {
30
30
  return items
31
31
  }
32
32
 
33
- /** 文本步骤定义:提示语 + 校验 (通过返回 true,否则返回错误文案) */
33
+ /** Text step definitions: prompt + validation (returns true if valid, otherwise error message) */
34
34
  const WIZARD_STEPS = {
35
35
  name: {
36
36
  prompt: "Name this provider (alphanumeric/-/_ e.g. my-openai)",
@@ -51,7 +51,7 @@ export function createWizard(ctx) {
51
51
  },
52
52
  embedkey: {
53
53
  prompt: "Optional: embedding API key (SiliconFlow, for memory vector search; press Enter to skip)",
54
- validate: () => true, // 可跳过
54
+ validate: () => true, // skippable
55
55
  },
56
56
  }
57
57
  const WIZARD_NEXT = { name: "baseURL", baseURL: "model", model: "key", key: "embedkey", embedkey: null }
@@ -123,11 +123,11 @@ export function createWizard(ctx) {
123
123
 
124
124
  function cancelWizard() {
125
125
  state.wizard = null
126
- pushLine("Skipped initial setup. Add a provider anytime via /provider add, set its key via /provider key.", C.dim)
126
+ pushLine("Skipped initial setup. Use /model to add providers and configure API keys anytime.", C.dim)
127
127
  render()
128
128
  }
129
129
 
130
- /** 向导完成:写入 provider (有则更新)、设为激活、持久化,然后接模型选择器 */
130
+ /** Wizard complete: write provider (update if exists), set active, persist, then open model picker */
131
131
  async function finishWizard() {
132
132
  const f = state.wizard.fields
133
133
  state.wizard = null
@@ -147,7 +147,7 @@ export function createWizard(ctx) {
147
147
  agent.config.activeProvider = f.name
148
148
  pushLabel(`❯ Setup`, ansi.bold + C.tool)
149
149
  pushLine(`Setup complete: ${f.name} / ${f.model} (saved to config)`, C.tool)
150
- // embedding key:配了就启用向量检索,没配提示事后通道
150
+ // embedding key: if provided, enable vector search; if not, show how to enable later
151
151
  if (f.embedkey) {
152
152
  agent.config.embedding ??= {}
153
153
  agent.config.embedding.apiKey = f.embedkey
package/src/tui.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * tui.mjs — 终端 UI(重新导出中心)
3
- * 子模块在 src/tui/ 目录下。
2
+ * tui.mjs — Terminal UI (re-export hub).
3
+ * Submodules under src/tui/ directory.
4
4
  */
5
5
  export { startTUI } from "./tui/index.mjs"
package/bin/thincoder.js DELETED
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env node
2
- // Thin shim — npm's "bin" field doesn't accept .mjs entries.
3
- // Delegates to the real entry point.
4
- import "./thincoder.mjs"
@@ -1,144 +0,0 @@
1
- import {
2
- DESC,
3
- sanitizeOutput,
4
- truncate,
5
- makeDecoder,
6
- BASH_TIMEOUT_MS,
7
- hasFileRedirection,
8
- shellSegments,
9
- isDestructiveGitSegment,
10
- isDestructiveCommand,
11
- insideGitRepo,
12
- } from "./shared.mjs"
13
- import { spawn, execFileSync } from "node:child_process"
14
-
15
- /** 子进程环境变量白名单:只透传安全变量,隔离 API key 等敏感信息 */
16
- const SAFE_ENV_KEYS = new Set([
17
- "PATH", "HOME", "USERPROFILE", "APPDATA", "LOCALAPPDATA",
18
- "TEMP", "TMP", "TMPDIR", "LANG", "LC_ALL", "LC_CTYPE", "SHELL",
19
- "ComSpec", "PATHEXT", "SystemRoot", "windir",
20
- "NUMBER_OF_PROCESSORS", "PROCESSOR_ARCHITECTURE", "OS",
21
- "PYTHONIOENCODING", "GIT_EDITOR", "EDITOR", "VISUAL",
22
- "GIT_PAGER", "PAGER", "TERM",
23
- ])
24
-
25
- export const bashTool = {
26
- name: "bash",
27
- description: DESC("bash"),
28
- parameters: {
29
- type: "object",
30
- properties: {
31
- command: { type: "string", description: "Shell command to execute" },
32
- timeout: { type: "number", description: `Timeout in ms (default ${BASH_TIMEOUT_MS})` },
33
- },
34
- required: ["command"],
35
- },
36
- readonly: false,
37
- async execute(args, ctx) {
38
- // 安全预检:禁止 shell 重定向(> >> <)——应改用 write/edit/insert_after 工具
39
- if (hasFileRedirection(args.command)) {
40
- throw new Error("File redirection via bash is not allowed — use the write/edit/insert_after tools instead")
41
- }
42
- // 安全预检:破坏性非 git 命令(rm -rf / DROP TABLE 等)直接拒绝
43
- if (shellSegments(args.command).some(isDestructiveCommand)) {
44
- throw new Error("Destructive command blocked — use specific tools or confirm with the user first")
45
- }
46
- // 安全预检:销毁性 git 操作先检查未提交改动,有则拒绝——防一键清掉几小时工作
47
- if (shellSegments(args.command).some(isDestructiveGitSegment)) {
48
- if (!insideGitRepo(ctx.cwd)) {
49
- throw new Error(`Refusing destructive git command: not a git repository: ${ctx.cwd}`)
50
- }
51
- const status = execFileSync("git", ["status", "--porcelain"], {
52
- cwd: ctx.cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"],
53
- }).trim()
54
- if (status) {
55
- throw new Error(
56
- `Refusing destructive git command: uncommitted changes exist. Commit or stash first.\n` +
57
- `(If uncommitted work was already lost, the checkpoint tool can restore the auto-snapshot: action=list, then action=rewind.)\n\n${status}`
58
- )
59
- }
60
- }
61
-
62
- return new Promise((resolve) => {
63
- // detached: 让子进程成为进程组组长,超时/中断时才能整树杀掉(POSIX 用负 pid 组杀,
64
- // Windows 用 taskkill /T)——只 kill 壳进程会把孙进程(如 npm test)留在后台继续跑
65
- const killTree = () => {
66
- if (process.platform === "win32") {
67
- try { execFileSync("taskkill", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore" }) } catch {}
68
- } else {
69
- try { process.kill(-child.pid, "SIGKILL") } catch {}
70
- try { child.kill("SIGKILL") } catch {} // 组杀失败时兜底杀本体
71
- }
72
- }
73
- // Windows 中文系统默认代码页是 GBK (CP936),cmd.exe 重定向写文件时用 ANSI 代码页,
74
- // chcp 65001 也改不了重定向的编码。bash 工具写含 CJK 的文件会产生 GBK——
75
- // 提示词层已禁止用 bash 写文件(用 write/edit 工具替代),这里设 PYTHONIOENCODING
76
- // 覆盖 Python 脚本的 stdout 编码(Python 是唯一可能正确响应环境变量的子进程)
77
- const winCmd = process.platform === "win32"
78
- const child = spawn(args.command, {
79
- cwd: ctx.cwd,
80
- shell: true,
81
- windowsHide: true,
82
- detached: process.platform !== "win32",
83
- stdio: ["ignore", "pipe", "pipe"],
84
- env: {
85
- ...Object.fromEntries(
86
- Object.entries(process.env).filter(([k]) => SAFE_ENV_KEYS.has(k))
87
- ),
88
- GIT_EDITOR: "true",
89
- EDITOR: "true",
90
- VISUAL: "true",
91
- GIT_PAGER: "cat",
92
- PAGER: "cat",
93
- TERM: "dumb",
94
- ...(winCmd ? { PYTHONIOENCODING: "utf-8" } : {}),
95
- },
96
- })
97
- // stdout / stderr 各自独立解码(同进程通常同编码,但分开收集更干净,
98
- // 且允许模型按 stderr 快速定位错误)
99
- const outDecoder = makeDecoder()
100
- const errDecoder = makeDecoder()
101
- let outBuf = ""
102
- let errBuf = ""
103
- let truncatedNote = ""
104
-
105
- const onStdout = (d) => {
106
- const s = sanitizeOutput(outDecoder(d))
107
- if (s) {
108
- ctx.onOutput?.(s)
109
- if (outBuf.length < 2_000_000) outBuf += s
110
- else if (!truncatedNote) truncatedNote = "\n[... output exceeded 2MB, remainder discarded]"
111
- }
112
- }
113
- const onStderr = (d) => {
114
- const s = sanitizeOutput(errDecoder(d)) // 始终解码,防 pending 无限累积
115
- if (errBuf.length < 2_000_000) errBuf += s
116
- }
117
-
118
- child.stdout.on("data", onStdout)
119
- child.stderr.on("data", onStderr)
120
-
121
- const timer = setTimeout(killTree, args.timeout ?? BASH_TIMEOUT_MS)
122
- if (ctx.signal) {
123
- ctx.signal.addEventListener("abort", killTree, { once: true })
124
- }
125
- child.on("error", (error) => {
126
- clearTimeout(timer)
127
- resolve(truncate(`Command failed: ${error.message}\n[stdout]:\n${outBuf || "(empty)"}`))
128
- })
129
- child.on("close", (code, signal) => {
130
- clearTimeout(timer)
131
- // 冲刷解码器尾部
132
- outBuf += sanitizeOutput(outDecoder(Buffer.alloc(0), true))
133
- errBuf += sanitizeOutput(errDecoder(Buffer.alloc(0), true))
134
- const status = signal
135
- ? `killed: ${ctx.signal?.aborted ? "user interrupted" : "timeout"}`
136
- : `exit code ${code}`
137
- const parts = [`[stdout]:\n${outBuf.trim() || "(empty)"}`]
138
- if (errBuf.trim()) parts.push(`[stderr]:\n${errBuf.trim()}`)
139
- parts.push(`(${status})`)
140
- resolve(truncate(parts.join("\n\n") + truncatedNote))
141
- })
142
- })
143
- },
144
- }
@@ -1,51 +0,0 @@
1
- import { DESC, truncate, resolveInCwd, globToRegex, IGNORED_DIRS } from "./shared.mjs"
2
- import { readdir } from "node:fs/promises"
3
- import { join } from "node:path"
4
-
5
- export const globTool = {
6
- name: "glob",
7
- description: DESC("glob"),
8
- parameters: {
9
- type: "object",
10
- properties: {
11
- pattern: { type: "string", description: "Glob pattern" },
12
- path: { type: "string", description: "Directory to search in (default cwd)" },
13
- },
14
- required: ["pattern"],
15
- },
16
- readonly: true,
17
- async execute(args, ctx) {
18
- const base = resolveInCwd(ctx, args.path ?? ".")
19
- const regex = globToRegex(args.pattern)
20
- const results = []
21
- for await (const relPath of walkFiles(base)) {
22
- if (regex.test(relPath)) {
23
- results.push(relPath)
24
- if (results.length >= 1000) break
25
- }
26
- }
27
- if (results.length === 0) return "(no matches)"
28
- return truncate(results.sort().join("\n"))
29
- },
30
- }
31
-
32
- // ---------------------------------------------------------- 内部实现
33
-
34
- /** 递归遍历文件,产出相对路径(跳过 IGNORED_DIRS) */
35
- async function* walkFiles(dir, rel = "") {
36
- let entries
37
- try {
38
- entries = await readdir(dir, { withFileTypes: true })
39
- } catch {
40
- return
41
- }
42
- for (const e of entries) {
43
- if (e.isDirectory() && IGNORED_DIRS.has(e.name)) continue
44
- const relPath = rel ? `${rel}/${e.name}` : e.name
45
- if (e.isDirectory()) {
46
- yield* walkFiles(join(dir, e.name), relPath)
47
- } else {
48
- yield relPath
49
- }
50
- }
51
- }
@@ -1,100 +0,0 @@
1
- import { DESC, truncate, resolveInCwd, globToRegex, IGNORED_DIRS } from "./shared.mjs"
2
- import { readFile, readdir } from "node:fs/promises"
3
- import { stat, lstat } from "node:fs/promises"
4
- import { join } from "node:path"
5
-
6
- export const grepTool = {
7
- name: "grep",
8
- description: DESC("grep"),
9
- parameters: {
10
- type: "object",
11
- properties: {
12
- pattern: { type: "string", description: "Regular expression" },
13
- path: { type: "string", description: "Directory or file to search (default cwd)" },
14
- glob: { type: "string", description: "Only search files matching this glob (e.g. '*.mjs')" },
15
- before: { type: "integer", description: "Lines of context to show before each match (grep -B). Default 0" },
16
- after: { type: "integer", description: "Lines of context to show after each match (grep -A). Default 0" },
17
- },
18
- required: ["pattern"],
19
- },
20
- readonly: true,
21
- async execute(args, ctx) {
22
- const base = resolveInCwd(ctx, args.path ?? ".")
23
- const regex = new RegExp(args.pattern)
24
- const fileFilter = args.glob ? globToRegex(args.glob) : null
25
- const before = Math.max(0, Math.floor(args.before ?? 0))
26
- const after = Math.max(0, Math.floor(args.after ?? 0))
27
- const wantCtx = before > 0 || after > 0
28
- const hits = [] // { file, line(1-based), text }
29
- const fileLines = new Map() // file -> string[](仅 wantCtx 时缓存)
30
-
31
- async function search(file) {
32
- let content
33
- try {
34
- // 大文件保护:超 10MB 跳过,防 OOM
35
- const fst = await stat(file)
36
- if (fst.size > 10_000_000) return
37
- content = await readFile(file, "utf8")
38
- } catch {
39
- return // 不可读文件跳过;二进制会被按 utf8 读入并照常搜索(可能产生乱码匹配)
40
- }
41
- const lines = content.split("\n")
42
- if (wantCtx) fileLines.set(file, lines)
43
- for (let i = 0; i < lines.length; i++) {
44
- if (regex.test(lines[i])) {
45
- hits.push({ file, line: i + 1, text: lines[i] })
46
- if (hits.length >= 200) return
47
- }
48
- }
49
- }
50
-
51
- async function walk(target) {
52
- if (hits.length >= 200) return
53
- // 用 lstat 不跟随符号链接——防 ./evil → /etc 时 grep 读遍 /etc
54
- let s
55
- try { s = await lstat(target) } catch { return }
56
- if (!s.isDirectory()) {
57
- if (!fileFilter || fileFilter.test(target.split(/[\\/]/).pop())) await search(target)
58
- return
59
- }
60
- let entries
61
- try {
62
- entries = await readdir(target, { withFileTypes: true })
63
- } catch {
64
- return
65
- }
66
- for (const e of entries) {
67
- if (e.isDirectory() && IGNORED_DIRS.has(e.name)) continue
68
- await walk(join(target, e.name))
69
- }
70
- }
71
-
72
- await walk(base)
73
- if (hits.length === 0) return "(no matches)"
74
-
75
- // 无上下文:保持原 path:line: content 格式
76
- if (!wantCtx) {
77
- return truncate(hits.map((h) => `${h.file}:${h.line}: ${h.text}`).join("\n"))
78
- }
79
-
80
- // 带上下文:匹配行用 ':',上下文行用 '-'(同 ripgrep);同文件相邻区间去重合并
81
- const fileMatched = new Map() // file -> Set<line>
82
- for (const h of hits) {
83
- if (!fileMatched.has(h.file)) fileMatched.set(h.file, new Set())
84
- fileMatched.get(h.file).add(h.line)
85
- }
86
- const out = []
87
- for (const [file, matchedLines] of fileMatched) {
88
- const lines = fileLines.get(file) ?? []
89
- const lineSet = new Set()
90
- for (const ml of matchedLines) {
91
- for (let l = Math.max(1, ml - before); l <= Math.min(lines.length, ml + after); l++) lineSet.add(l)
92
- }
93
- for (const l of [...lineSet].sort((a, b) => a - b)) {
94
- const sep = matchedLines.has(l) ? ":" : "-"
95
- out.push(`${file}${sep}${l}${sep} ${lines[l - 1]}`)
96
- }
97
- }
98
- return truncate(out.join("\n"))
99
- },
100
- }
package/src/tools/ls.mjs DELETED
@@ -1,36 +0,0 @@
1
- import { DESC, truncate, resolveInCwd } from "./shared.mjs"
2
- import { readdir } from "node:fs/promises"
3
- import { stat } from "node:fs/promises"
4
- import { join } from "node:path"
5
-
6
- export const lsTool = {
7
- name: "ls",
8
- description: DESC("ls"),
9
- parameters: {
10
- type: "object",
11
- properties: {
12
- path: { type: "string", description: "Directory path (default cwd)" },
13
- },
14
- },
15
- readonly: true,
16
- async execute(args, ctx) {
17
- const abs = resolveInCwd(ctx, args.path ?? ".")
18
- const entries = await readdir(abs, { withFileTypes: true })
19
- const rows = await Promise.all(
20
- entries.slice(0, 500).map(async (e) => {
21
- const s = await stat(join(abs, e.name)).catch(() => null)
22
- const isDir = e.isDirectory()
23
- return {
24
- dir: isDir,
25
- name: e.name + (isDir ? "/" : ""),
26
- size: s?.size ?? 0,
27
- mtime: s ? s.mtime.toISOString().slice(0, 16).replace("T", " ") : "?",
28
- }
29
- }),
30
- )
31
- rows.sort((a, b) => (a.dir === b.dir ? a.name.localeCompare(b.name) : a.dir ? -1 : 1))
32
- if (rows.length === 0) return "(empty directory)"
33
- const out = rows.map((r) => `${r.dir ? "d" : "-"} ${r.name.padEnd(40)} ${String(r.size).padStart(10)} ${r.mtime}`)
34
- return truncate(out.join("\n"))
35
- },
36
- }
package/src/tools.mjs DELETED
@@ -1,2 +0,0 @@
1
- // Stub — see src/tools/index.mjs for actual implementation
2
- export * from "./tools/index.mjs"
@@ -1,4 +0,0 @@
1
- /**
2
- * tui-render.mjs — 终端显示工具(重新导出)
3
- */
4
- export { charWidth, stringWidth, sliceByWidth, formatTables, layoutInput, sanitizeDisplay, wrapText } from "./tui/render.mjs"