lantern-harness 0.0.0-stage → 0.1.1

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 (136) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/LICENSE +21 -0
  3. package/README.md +107 -2
  4. package/builtin/session/README.md +21 -0
  5. package/builtin/session/bash-session.mjs +379 -0
  6. package/builtin/session/fs-observation-policy.mjs +38 -0
  7. package/builtin/session/lh-bash.mjs +778 -0
  8. package/builtin/session/lh-fs.mjs +1262 -0
  9. package/builtin/session/lh-nav-groups.mjs +717 -0
  10. package/builtin/session/lh-schedule.mjs +299 -0
  11. package/builtin/session/lh-spaces.mjs +423 -0
  12. package/builtin/session/lh-spill.mjs +138 -0
  13. package/builtin/session/lh-web.mjs +205 -0
  14. package/builtin/session/office-to-pdf.mjs +210 -0
  15. package/builtin/session/pwsh-session.mjs +376 -0
  16. package/builtin/session/sandbox-policy.mjs +116 -0
  17. package/builtin/session/session-runs.mjs +1352 -0
  18. package/builtin/session/skill-fs.mjs +148 -0
  19. package/builtin/session/space-context.mjs +70 -0
  20. package/builtin/session/space-layout.mjs +222 -0
  21. package/builtin/session/workspace-deps.mjs +63 -0
  22. package/cli/acp-server.mjs +185 -0
  23. package/cli/ai-testbench.mjs +5 -0
  24. package/cli/data-root.mjs +623 -0
  25. package/cli/ensure-dist.mjs +45 -0
  26. package/cli/headless.mjs +318 -0
  27. package/cli/jsonrpc-server.mjs +60 -0
  28. package/cli/lantern.mjs +981 -0
  29. package/cli/server.mjs +117 -0
  30. package/cli/spaces.mjs +995 -0
  31. package/cli/user-config.mjs +183 -0
  32. package/config/app.defaults.json +29 -0
  33. package/config/seeds//346/217/220/347/244/272/350/257/215/347/256/241/347/220/206//345/212/237/350/203/275/346/265/213/350/257/225/347/224/250/344/276/213/346/217/220/347/244/272/350/257/215v1.0.md +155 -0
  34. package/config/seeds//346/217/220/347/244/272/350/257/215/347/256/241/347/220/206//351/273/230/350/256/244/346/217/220/347/244/272/350/257/215.md +1 -0
  35. package/config/seeds//346/265/213/350/257/225/346/250/241/346/235/277/345/272/223//345/212/237/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212//345/212/237/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212/346/250/241/346/235/277.md +65 -0
  36. package/config/seeds//346/265/213/350/257/225/346/250/241/346/235/277/345/272/223//345/212/237/350/203/275/346/265/213/350/257/225/347/224/250/344/276/213//345/212/237/350/203/275/346/265/213/350/257/225/347/224/250/344/276/213/346/250/241/346/235/277.xlsx +0 -0
  37. package/config/seeds//346/265/213/350/257/225/346/250/241/346/235/277/345/272/223//346/200/247/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212//346/200/247/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212/346/250/241/346/235/277.md +61 -0
  38. package/config/seeds//346/265/213/350/257/225/346/250/241/346/235/277/345/272/223//346/200/247/350/203/275/346/265/213/350/257/225/347/224/250/344/276/213//346/200/247/350/203/275/346/265/213/350/257/225/347/224/250/344/276/213/346/250/241/346/235/277.md +42 -0
  39. package/config/seeds//346/265/213/350/257/225/346/250/241/346/235/277/345/272/223//346/216/245/345/217/243/346/265/213/350/257/225/346/212/245/345/221/212//346/216/245/345/217/243/346/265/213/350/257/225/346/212/245/345/221/212/346/250/241/346/235/277.md +55 -0
  40. package/config/seeds//346/265/213/350/257/225/346/250/241/346/235/277/345/272/223//346/216/245/345/217/243/346/265/213/350/257/225/347/224/250/344/276/213//346/216/245/345/217/243/346/265/213/350/257/225/347/224/250/344/276/213/346/250/241/347/211/210.xlsx +0 -0
  41. package/config/seeds//346/265/213/350/257/225/346/250/241/346/235/277/345/272/223//350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/347/224/250/344/276/213//350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/347/224/250/344/276/213/346/250/241/346/235/277.md +37 -0
  42. package/config/seeds//347/237/245/350/257/206/345/272/223/347/256/241/347/220/206//345/212/237/350/203/275/346/265/213/350/257/225/347/224/250/344/276/213/347/274/226/345/206/231/350/247/204/350/214/203.md +174 -0
  43. package/dist/500.html +250 -0
  44. package/dist/_astro/Base.B9f_iFAZ.css +1 -0
  45. package/dist/_astro/Base.astro_astro_type_script_index_0_lang.lxEwQ1R4.js +1 -0
  46. package/dist/_astro/CaseGenerateApp.Br4uKjun.js +9 -0
  47. package/dist/_astro/CasesLibrary.CpPoVQ4P.js +1 -0
  48. package/dist/_astro/ChatApp.CYSPlSNP.js +258 -0
  49. package/dist/_astro/ClientRouter.astro_astro_type_script_index_0_lang.CO1cjG9V.js +1 -0
  50. package/dist/_astro/ConfirmDialog.BYz4HREM.js +1 -0
  51. package/dist/_astro/DocsFileBrowser.DkE26UTR.js +1 -0
  52. package/dist/_astro/ExcelFieldTags.B_XMwNNx.js +152 -0
  53. package/dist/_astro/KnowledgeLibrary.DZinlAWn.js +1 -0
  54. package/dist/_astro/NamePromptDialog.DGBlDH_k.js +1 -0
  55. package/dist/_astro/NavLibraries.EayBsg5g.js +1 -0
  56. package/dist/_astro/OverflowHint.lzmplP5j.js +1 -0
  57. package/dist/_astro/PluginsPage.BNzpPg2H.js +39 -0
  58. package/dist/_astro/PluginsPage.CY61glLU.js +1 -0
  59. package/dist/_astro/PromptsLibrary.DfrrCjs7.js +1 -0
  60. package/dist/_astro/RequirementsLibrary.Dt0Gj6Nr.js +1 -0
  61. package/dist/_astro/SearchableSelect.DMS2XmXc.js +1 -0
  62. package/dist/_astro/SelectDropdown.Dc7QaPbH.js +1 -0
  63. package/dist/_astro/SettingsDialog.DD-XOFEX.js +1 -0
  64. package/dist/_astro/SpaceSwitcher.Yy2w8MDF.js +7 -0
  65. package/dist/_astro/TemplatesLibrary.uK-SjK1z.js +1 -0
  66. package/dist/_astro/app-config.CpjKWnzM.js +1 -0
  67. package/dist/_astro/client.Be_SakrB.js +9 -0
  68. package/dist/_astro/i18n.BVvhz-vw.js +7 -0
  69. package/dist/_astro/jsx-runtime.BUXbF_vF.js +1 -0
  70. package/dist/_astro/lib.DJNDi_Ht.js +43 -0
  71. package/dist/_astro/listbox.DdDDDJ5Z.js +1 -0
  72. package/dist/_astro/llm.BZEzE0x2.js +7 -0
  73. package/dist/_astro/menu.CqkL87bV.js +1 -0
  74. package/dist/_astro/open-docs-in-session.D3JCwMWB.js +1 -0
  75. package/dist/_astro/page.Bx2KSkeL.js +1 -0
  76. package/dist/_astro/plugins.CrpY8UI9.js +1 -0
  77. package/dist/_astro/portal.C3KrUREL.js +5 -0
  78. package/dist/_astro/prefetch.GJhwodf8.js +1 -0
  79. package/dist/_astro/prompts-meta.F4wwkjUA.js +2 -0
  80. package/dist/_astro/react-dom.CqAxpHMt.js +1 -0
  81. package/dist/_astro/settings.BoQ0e0m1.js +2 -0
  82. package/dist/_astro/use-text-value.C0IDspmh.js +1 -0
  83. package/dist/_astro/use-tree-walker.oRTnhHS-.js +1 -0
  84. package/dist/_astro/useListAnim.CnbC_9MC.js +1 -0
  85. package/dist/apple-touch-icon.png +0 -0
  86. package/dist/brand-mark.png +0 -0
  87. package/dist/cases/generate/index.html +249 -0
  88. package/dist/cases/index.html +1 -0
  89. package/dist/cases/library/index.html +249 -0
  90. package/dist/changelog/index.html +1 -0
  91. package/dist/docs/changelog/index.html +364 -0
  92. package/dist/docs/index.html +1 -0
  93. package/dist/docs/install/index.html +312 -0
  94. package/dist/docs/manual/index.html +631 -0
  95. package/dist/favicon.png +0 -0
  96. package/dist/icon-192.png +0 -0
  97. package/dist/index.html +249 -0
  98. package/dist/knowledge/index.html +249 -0
  99. package/dist/knowledge//345/212/237/350/203/275/346/265/213/350/257/225/347/224/250/344/276/213/347/274/226/345/206/231/350/247/204/350/214/203/index.html +535 -0
  100. package/dist/plugins/index.html +249 -0
  101. package/dist/prompts/index.html +249 -0
  102. package/dist/raw/knowledge//345/212/237/350/203/275/346/265/213/350/257/225/347/224/250/344/276/213/347/274/226/345/206/231/350/247/204/350/214/203.md +168 -0
  103. package/dist/raw/templates//345/212/237/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212//345/212/237/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212/346/250/241/346/235/277.md +58 -0
  104. package/dist/raw/templates//346/200/247/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212//346/200/247/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212/346/250/241/346/235/277.md +54 -0
  105. package/dist/raw/templates//346/200/247/350/203/275/346/265/213/350/257/225/347/224/250/344/276/213//346/200/247/350/203/275/346/265/213/350/257/225/347/224/250/344/276/213/346/250/241/346/235/277.md +35 -0
  106. package/dist/raw/templates//346/216/245/345/217/243/346/265/213/350/257/225/346/212/245/345/221/212//346/216/245/345/217/243/346/265/213/350/257/225/346/212/245/345/221/212/346/250/241/346/235/277.md +48 -0
  107. package/dist/raw/templates//350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/347/224/250/344/276/213//350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/347/224/250/344/276/213/346/250/241/346/235/277.md +30 -0
  108. package/dist/requirements/index.html +249 -0
  109. package/dist/templates/c//345/212/237/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212/index.html +1 -0
  110. package/dist/templates/c//345/212/237/350/203/275/346/265/213/350/257/225/347/224/250/344/276/213/index.html +1 -0
  111. package/dist/templates/c//346/200/247/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212/index.html +1 -0
  112. package/dist/templates/c//346/200/247/350/203/275/346/265/213/350/257/225/347/224/250/344/276/213/index.html +1 -0
  113. package/dist/templates/c//346/216/245/345/217/243/346/265/213/350/257/225/346/212/245/345/221/212/index.html +1 -0
  114. package/dist/templates/c//346/216/245/345/217/243/346/265/213/350/257/225/347/224/250/344/276/213/index.html +1 -0
  115. package/dist/templates/c//350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/347/224/250/344/276/213/index.html +1 -0
  116. package/dist/templates/index.html +249 -0
  117. package/dist/templates//345/212/237/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212//345/212/237/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212/346/250/241/346/235/277/index.html +394 -0
  118. package/dist/templates//346/200/247/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212//346/200/247/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212/346/250/241/346/235/277/index.html +374 -0
  119. package/dist/templates//346/200/247/350/203/275/346/265/213/350/257/225/347/224/250/344/276/213//346/200/247/350/203/275/346/265/213/350/257/225/347/224/250/344/276/213/346/250/241/346/235/277/index.html +336 -0
  120. package/dist/templates//346/216/245/345/217/243/346/265/213/350/257/225/346/212/245/345/221/212//346/216/245/345/217/243/346/265/213/350/257/225/346/212/245/345/221/212/346/250/241/346/235/277/index.html +363 -0
  121. package/dist/templates//350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/347/224/250/344/276/213//350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/347/224/250/344/276/213/346/250/241/346/235/277/index.html +338 -0
  122. package/docs//344/274/232/350/257/235/346/217/222/344/273/266.md +500 -0
  123. package/docs//345/205/250/345/261/200/346/217/222/344/273/266.md +1337 -0
  124. package/docs//345/256/211/350/243/205/346/211/213/345/206/214.md +110 -0
  125. package/docs//346/223/215/344/275/234/346/211/213/345/206/214.md +246 -0
  126. package/lh-plugins/lh-config.mjs +483 -0
  127. package/lh-plugins/lh-kb.mjs +206 -0
  128. package/lh-plugins/lh-memory.mjs +613 -0
  129. package/lh-plugins/lh-project-docs.mjs +875 -0
  130. package/lh-plugins/lh-resources.mjs +236 -0
  131. package/lh-plugins/session-log-deepseek.mjs +53 -0
  132. package/lh-plugins/session-persist.mjs +112 -0
  133. package/lh-plugins/session-query-sqlite.mjs +164 -0
  134. package/lh-plugins/storage-domain.mjs +82 -0
  135. package/package.json +124 -4
  136. package//347/251/272/351/227/264/346/261/240/.gitkeep +1 -0
@@ -0,0 +1,379 @@
1
+ /**
2
+ * 持久 bash 会话:优先 node-pty,失败则管道 + marker 回退。
3
+ */
4
+ import { spawn } from 'node:child_process';
5
+ import { assertCommandAllowed, buildChildEnv } from './sandbox-policy.mjs';
6
+
7
+ const MAX_SESSIONS = 4;
8
+ const SCROLL_MAX = 256 * 1024;
9
+ const DEFAULT_TIMEOUT_MS = 30_000;
10
+ const MARKER_PREFIX = '__TB_PS1_';
11
+
12
+ /** @type {Map<string, object>} */
13
+ const sessions = new Map();
14
+
15
+ /** @type {boolean | null} */
16
+ let ptyAvailableCache = null;
17
+ /** @type {any} */
18
+ let ptyModule = null;
19
+
20
+ export async function detectPtyBackend() {
21
+ if (ptyAvailableCache != null) {
22
+ return { available: ptyAvailableCache, pty: ptyModule };
23
+ }
24
+ try {
25
+ ptyModule = await import('node-pty');
26
+ ptyAvailableCache = true;
27
+ } catch {
28
+ ptyModule = null;
29
+ ptyAvailableCache = false;
30
+ }
31
+ return { available: ptyAvailableCache, pty: ptyModule };
32
+ }
33
+
34
+ export function ptyAvailableSync() {
35
+ return ptyAvailableCache === true;
36
+ }
37
+
38
+ /**
39
+ * 标记 pty 后端不可用:`import('node-pty')` 可能成功,但真正 spawn 时失败
40
+ * (例如 macOS 上 prebuilds 的 spawn-helper 解包后没有可执行位)。
41
+ * 一旦失败就永久降级为管道会话,避免每个会话都重复踩坑。
42
+ */
43
+ export function markPtyUnavailable() {
44
+ ptyAvailableCache = false;
45
+ ptyModule = null;
46
+ }
47
+
48
+ function uid() {
49
+ return `sh_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
50
+ }
51
+
52
+ function bashBinary() {
53
+ return process.platform === 'win32' ? 'bash' : '/bin/bash';
54
+ }
55
+
56
+ function appendScroll(session, chunk) {
57
+ const s = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk ?? '');
58
+ session.scroll += s;
59
+ if (session.scroll.length > SCROLL_MAX) {
60
+ session.scroll = `…[truncated]\n${session.scroll.slice(-SCROLL_MAX)}`;
61
+ session.truncated = true;
62
+ }
63
+ session.aliveBuf += s;
64
+ if (session.aliveBuf.length > SCROLL_MAX) {
65
+ session.aliveBuf = session.aliveBuf.slice(-SCROLL_MAX);
66
+ }
67
+ }
68
+
69
+ function publicSession(session) {
70
+ return {
71
+ id: session.id,
72
+ backend: session.backend,
73
+ cwd: session.cwdDisplay,
74
+ status: session.status,
75
+ createdAt: session.createdAt,
76
+ lastActiveAt: session.lastActiveAt,
77
+ truncated: session.truncated,
78
+ scrollBytes: Buffer.byteLength(session.scroll, 'utf8'),
79
+ };
80
+ }
81
+
82
+ function pruneSessions() {
83
+ if (sessions.size <= MAX_SESSIONS) return;
84
+ const idle = [...sessions.values()]
85
+ .filter((s) => s.status !== 'running')
86
+ .sort((a, b) => a.lastActiveAt - b.lastActiveAt);
87
+ while (sessions.size > MAX_SESSIONS && idle.length) {
88
+ const old = idle.shift();
89
+ if (old) closeSessionInternal(old.id);
90
+ }
91
+ }
92
+
93
+ function closeSessionInternal(id) {
94
+ const session = sessions.get(id);
95
+ if (!session) return { ok: false, error: 'session not found' };
96
+ try {
97
+ if (session.backend === 'pty' && session.pty) {
98
+ session.pty.kill();
99
+ } else if (session.child) {
100
+ session.child.kill('SIGTERM');
101
+ }
102
+ } catch {
103
+ // ignore
104
+ }
105
+ session.status = 'closed';
106
+ session.pty = null;
107
+ session.child = null;
108
+ sessions.delete(id);
109
+ return { ok: true, session: publicSession(session) };
110
+ }
111
+
112
+ /**
113
+ * Wait until marker appears in aliveBuf (or timeout / exit).
114
+ * @returns {Promise<{ output: string, timedOut: boolean, exited: boolean }>}
115
+ */
116
+ function waitForMarker(session, marker, timeoutMs) {
117
+ const startedLen = session.aliveBuf.length;
118
+ return new Promise((resolve) => {
119
+ let settled = false;
120
+ const finish = (timedOut, exited) => {
121
+ if (settled) return;
122
+ settled = true;
123
+ clearTimeout(timer);
124
+ clearInterval(poll);
125
+ const slice = session.aliveBuf.slice(startedLen);
126
+ const idx = slice.lastIndexOf(marker);
127
+ const output = idx >= 0 ? slice.slice(0, idx) : slice;
128
+ resolve({ output, timedOut, exited });
129
+ };
130
+ const poll = setInterval(() => {
131
+ if (session.status !== 'running') {
132
+ finish(false, true);
133
+ return;
134
+ }
135
+ if (session.aliveBuf.includes(marker, startedLen)) {
136
+ finish(false, false);
137
+ }
138
+ }, 40);
139
+ const timer = setTimeout(() => finish(true, false), timeoutMs);
140
+ });
141
+ }
142
+
143
+ async function openPtySession(cwdAbs, cwdDisplay, env, marker) {
144
+ const { available, pty } = await detectPtyBackend();
145
+ if (!available || !pty) return null;
146
+ const bin = bashBinary();
147
+ let term;
148
+ try {
149
+ term = pty.spawn(bin, ['--noprofile', '--norc'], {
150
+ name: 'xterm-color',
151
+ cols: 120,
152
+ rows: 40,
153
+ cwd: cwdAbs,
154
+ env: {
155
+ ...env,
156
+ TERM: 'xterm-256color',
157
+ PS1: `${marker}`,
158
+ PROMPT_COMMAND: '',
159
+ },
160
+ });
161
+ } catch {
162
+ // 能 import 但无法 spawn(spawn-helper 不可执行等)→ 降级管道会话
163
+ markPtyUnavailable();
164
+ return null;
165
+ }
166
+ const id = uid();
167
+ const session = {
168
+ id,
169
+ backend: 'pty',
170
+ cwdAbs,
171
+ cwdDisplay,
172
+ status: 'running',
173
+ createdAt: Date.now(),
174
+ lastActiveAt: Date.now(),
175
+ scroll: '',
176
+ aliveBuf: '',
177
+ truncated: false,
178
+ marker,
179
+ pty: term,
180
+ child: null,
181
+ };
182
+ term.onData((data) => appendScroll(session, data));
183
+ term.onExit(() => {
184
+ session.status = 'exited';
185
+ session.pty = null;
186
+ });
187
+ sessions.set(id, session);
188
+ pruneSessions();
189
+ // drain initial prompt
190
+ await waitForMarker(session, marker, 5_000);
191
+ session.aliveBuf = '';
192
+ return session;
193
+ }
194
+
195
+ async function openPipeSession(cwdAbs, cwdDisplay, env, marker) {
196
+ const bin = bashBinary();
197
+ // Interactive-ish: read commands from stdin line by line via a tiny driver loop
198
+ const driver = [
199
+ `export PS1='${marker}'`,
200
+ `export PROMPT_COMMAND=''`,
201
+ `printf '%s' "$PS1"`,
202
+ `while IFS= read -r __tb_line || [ -n "$__tb_line" ]; do`,
203
+ ` eval "$__tb_line"`,
204
+ ` printf '%s' "$PS1"`,
205
+ `done`,
206
+ ].join('\n');
207
+
208
+ const child = spawn(bin, ['--noprofile', '--norc', '-c', driver], {
209
+ cwd: cwdAbs,
210
+ env: { ...env, TERM: 'dumb' },
211
+ windowsHide: true,
212
+ stdio: ['pipe', 'pipe', 'pipe'],
213
+ });
214
+
215
+ const id = uid();
216
+ const session = {
217
+ id,
218
+ backend: 'pipe',
219
+ cwdAbs,
220
+ cwdDisplay,
221
+ status: 'running',
222
+ createdAt: Date.now(),
223
+ lastActiveAt: Date.now(),
224
+ scroll: '',
225
+ aliveBuf: '',
226
+ truncated: false,
227
+ marker,
228
+ pty: null,
229
+ child,
230
+ };
231
+ child.stdout?.on('data', (c) => appendScroll(session, c));
232
+ child.stderr?.on('data', (c) => appendScroll(session, c));
233
+ child.on('close', () => {
234
+ session.status = 'exited';
235
+ session.child = null;
236
+ });
237
+ child.on('error', () => {
238
+ session.status = 'exited';
239
+ session.child = null;
240
+ });
241
+ sessions.set(id, session);
242
+ pruneSessions();
243
+ await waitForMarker(session, marker, 5_000);
244
+ session.aliveBuf = '';
245
+ return session;
246
+ }
247
+
248
+ /**
249
+ * @param {{ cwdAbs: string, cwdDisplay: string, env: Record<string, string> }} opts
250
+ */
251
+ export async function openBashSession(opts) {
252
+ if (sessions.size >= MAX_SESSIONS) {
253
+ return { ok: false, error: `too many persistent sessions (max ${MAX_SESSIONS})` };
254
+ }
255
+ const marker = `${MARKER_PREFIX}${Math.random().toString(36).slice(2, 10)}__ `;
256
+ let session = await openPtySession(opts.cwdAbs, opts.cwdDisplay, opts.env, marker);
257
+ if (!session) {
258
+ session = await openPipeSession(opts.cwdAbs, opts.cwdDisplay, opts.env, marker);
259
+ }
260
+ return {
261
+ ok: true,
262
+ sessionId: session.id,
263
+ backend: session.backend,
264
+ session: publicSession(session),
265
+ };
266
+ }
267
+
268
+ /**
269
+ * @param {{ sessionId: string, command: string, timeoutMs?: number }} opts
270
+ */
271
+ export async function execBashSession(opts) {
272
+ const id = String(opts.sessionId || '').trim();
273
+ const command = String(opts.command || '');
274
+ const allowed = assertCommandAllowed(command);
275
+ if (!allowed.ok) return { ok: false, error: allowed.error };
276
+
277
+ const session = sessions.get(id);
278
+ if (!session) return { ok: false, error: 'session not found' };
279
+ if (session.status !== 'running') {
280
+ return {
281
+ ok: false,
282
+ error: `session not running (${session.status})`,
283
+ session: publicSession(session),
284
+ };
285
+ }
286
+
287
+ const timeoutMs = Math.min(
288
+ Math.max(Number(opts.timeoutMs) || DEFAULT_TIMEOUT_MS, 1_000),
289
+ 120_000,
290
+ );
291
+
292
+ // Normalize to single-line for pipe driver; keep newlines as ; for pty
293
+ const payload = session.backend === 'pipe' ? command.replace(/\r?\n/g, ' ; ') : command;
294
+
295
+ session.lastActiveAt = Date.now();
296
+ session.aliveBuf = '';
297
+
298
+ try {
299
+ if (session.backend === 'pty' && session.pty) {
300
+ session.pty.write(`${payload}\n`);
301
+ } else if (session.child?.stdin) {
302
+ session.child.stdin.write(`${payload}\n`);
303
+ } else {
304
+ return { ok: false, error: 'session has no writable stdin' };
305
+ }
306
+ } catch (e) {
307
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
308
+ }
309
+
310
+ const { output, timedOut, exited } = await waitForMarker(session, session.marker, timeoutMs);
311
+ session.lastActiveAt = Date.now();
312
+
313
+ // Strip echoed command line if present at start
314
+ let text = output;
315
+ const echoIdx = text.indexOf(payload);
316
+ if (echoIdx >= 0 && echoIdx < 8) {
317
+ const after = text.slice(echoIdx + payload.length).replace(/^\r?\n/, '');
318
+ text = after;
319
+ }
320
+
321
+ return {
322
+ ok: !timedOut && !exited,
323
+ sessionId: session.id,
324
+ backend: session.backend,
325
+ output: text,
326
+ timedOut,
327
+ exited,
328
+ session: publicSession(session),
329
+ };
330
+ }
331
+
332
+ export function readBashSession(sessionId, offset = 0) {
333
+ const session = sessions.get(String(sessionId || '').trim());
334
+ if (!session) return { ok: false, error: 'session not found' };
335
+ const off = Math.max(0, Number(offset) || 0);
336
+ const slice = session.scroll.slice(off);
337
+ return {
338
+ ok: true,
339
+ session: publicSession(session),
340
+ output: slice,
341
+ offset: off,
342
+ nextOffset: off + slice.length,
343
+ truncated: session.truncated,
344
+ };
345
+ }
346
+
347
+ export function closeBashSession(sessionId) {
348
+ return closeSessionInternal(String(sessionId || '').trim());
349
+ }
350
+
351
+ export function listBashSessions() {
352
+ return [...sessions.values()].sort((a, b) => b.lastActiveAt - a.lastActiveAt).map(publicSession);
353
+ }
354
+
355
+ export function sessionStats() {
356
+ return {
357
+ count: sessions.size,
358
+ max: MAX_SESSIONS,
359
+ pty: ptyAvailableCache === true,
360
+ ptyProbed: ptyAvailableCache != null,
361
+ };
362
+ }
363
+
364
+ /**
365
+ * 关闭所有常驻会话。数据空间切换时必须调用:会话的 cwd / env 绑在旧空间上,
366
+ * 继续复用会读到上一个空间的文件。
367
+ */
368
+ export function closeAllBashSessions() {
369
+ for (const id of [...sessions.keys()]) closeSessionInternal(id);
370
+ }
371
+
372
+ /** 测试用 */
373
+ export function resetBashSessionsForTests() {
374
+ closeAllBashSessions();
375
+ ptyAvailableCache = false;
376
+ ptyModule = null;
377
+ }
378
+
379
+ export { buildChildEnv };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * fs-observation-policy:文件系统观察/上报策略(跳过目录、读上限、搜索上限)。
3
+ */
4
+ import { META_DIR, META_DIR_LEGACY } from './space-layout.mjs';
5
+
6
+ export const FS_SKIP_DIRS = [
7
+ 'node_modules',
8
+ '.git',
9
+ 'dist',
10
+ META_DIR,
11
+ META_DIR_LEGACY,
12
+ // 旧布局的元数据目录:老空间可能还没迁移
13
+ '.local',
14
+ '.astro',
15
+ '.DS_Store',
16
+ 'coverage',
17
+ '.next',
18
+ '.turbo',
19
+ ];
20
+
21
+ export const FS_MAX_READ_BYTES = 2 * 1024 * 1024;
22
+ export const FS_MAX_WRITE_BYTES = 10 * 1024 * 1024;
23
+ export const FS_GLOB_MAX = 100;
24
+ export const FS_GREP_MAX = 250;
25
+
26
+ export function getFsObservationPolicy() {
27
+ return {
28
+ skipDirs: [...FS_SKIP_DIRS],
29
+ maxReadBytes: FS_MAX_READ_BYTES,
30
+ maxWriteBytes: FS_MAX_WRITE_BYTES,
31
+ globMax: FS_GLOB_MAX,
32
+ grepMax: FS_GREP_MAX,
33
+ };
34
+ }
35
+
36
+ export function shouldSkipDirName(name) {
37
+ return FS_SKIP_DIRS.includes(String(name ?? ''));
38
+ }