@zhushanwen/pi-session-reader 0.3.1 → 0.5.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.
@@ -0,0 +1,238 @@
1
+ import { dirname } from 'node:path'
2
+
3
+ /**
4
+ * 环境判定(design 2026-09-10-session-root-discovery-and-env-transparency §6.2):
5
+ * 多信号合取 + evidence 附带。
6
+ *
7
+ * 零 pi 依赖:与 discovery/roots.ts 同一分层约定——全部输入经 EnvironmentSignals
8
+ * 显式注入(process.env / import.meta.url / getAgentDir() 的组装在 index.ts 接线层),
9
+ * 本模块是纯函数,不读全局状态,可完全单测。
10
+ *
11
+ * 输入接口在本文件内自定义(design §10 U2:与 U1 roots 无数据依赖,doctor 需两者
12
+ * 齐备),不 import roots.ts 的信号类型,避免并行单元耦合;接线层分别组装两种信号包。
13
+ *
14
+ * 信号可靠性依据(design §4.4 活体实测表,非推断):
15
+ * - XYZ_AGENT_EXT_LOG 单独使用可靠性「中」——runtime 无条件注入,但
16
+ * ENV_WHITELIST_PREFIXES 含裸 `XYZ_` 前缀,用户 shell 里的同名变量会透传进任何进程;
17
+ * - PI_CODING_AGENT_DIR 单独使用可靠性「中」——pi 原生变量,裸 pi 用户也可能自设,
18
+ * 但其值形态是 xyz-agent 专属约定:`<*>/.xyz-agent*` 目录后接 `/agent`;
19
+ * - XYZ_AGENT_PACKAGED **不可用**——在 SPAWN_ENV_OUTBOUND_DENY_LIST 被剥除
20
+ * (§4.4 在案结论),故不引入该信号,防后人重蹈。
21
+ * 因此托管判定必须是双信号合取:任一信号单独成立只判 standalone-pi,
22
+ * 透传/自设污染提示写入 evidence。
23
+ */
24
+
25
+ /** xyz-agent 托管信号 1:runtime 无条件注入的 extension 日志开关(恒 '1') */
26
+ export const XYZ_AGENT_EXT_LOG_ENV = 'XYZ_AGENT_EXT_LOG'
27
+ /** xyz-agent 托管信号 2:pi agentDir 注入变量;其**值形态**是 xyz-agent 专属约定 */
28
+ export const PI_CODING_AGENT_DIR_ENV = 'PI_CODING_AGENT_DIR'
29
+ /** 数据目录推导首选:runtime getDataDir() 的权威注入值 */
30
+ export const XYZ_AGENT_DATA_DIR_ENV = 'XYZ_AGENT_DATA_DIR'
31
+
32
+ /** 托管信号 1 的唯一合法值(注释「托管语义恒为 '1'」) */
33
+ const EXT_LOG_MANAGED_VALUE = '1'
34
+
35
+ // xyz-agent agentDir 值形态(§6.2 双形态兼容——迁移窗口期 B 前后两种布局并存):
36
+ // - B 后新布局:`<*>/.xyz-agent*` 目录后接 `/agent`(一层)
37
+ // - B 前旧布局:`<*>/.xyz-agent*` 目录后接 `/pi/agent`(两层)
38
+ // `.xyz-agent*` = 以 `.xyz-agent` 开头的目录段(prod `.xyz-agent` / dev `.xyz-agent-dev`)。
39
+ // 三条正则都要求匹配到路径末尾(`$`),调用前输入先经 normalizeXyzPath 剥尾斜杠。
40
+ // (用行注释:形态文本含星号斜杠序列,写进块注释会提前终止注释。)
41
+ const AGENT_DIR_SHAPE_ANY = /(^|\/)\.xyz-agent[^/]*\/(pi\/)?agent$/
42
+ const AGENT_DIR_SHAPE_OLD = /(^|\/)\.xyz-agent[^/]*\/pi\/agent$/
43
+ const AGENT_DIR_SHAPE_NEW = /(^|\/)\.xyz-agent[^/]*\/agent$/
44
+
45
+ /**
46
+ * 打包形态判据(§6.2:bundle 路径判发行形态;无法判定 → null 不猜)。
47
+ * electron-builder extraResources `to: extensions`:
48
+ * - macOS → `*.app/Contents/Resources/extensions/`(§4.4 TaiJi.app 实测形态)
49
+ * - Windows/Linux → 安装根 `resources/extensions/`(小写 resources;macOS 是大写
50
+ * `Resources`,故小写模式不会误吞 macOS 形态)
51
+ * dev 仓库 staged 根 `<projectRoot>/apps/electron/resources/extensions/`(§6.12 在案
52
+ * 「dev 仓库资源根」)与 Windows 打包目录在 `resources/extensions` 段同形——必须
53
+ * 先排除,否则 dev staged 的 bundle 会被误判 packaged。
54
+ */
55
+ const MACOS_PACKAGED_PATTERN = /\.app\/Contents\/Resources\/extensions\//
56
+ const REPO_STAGED_PATTERN = /(^|\/)apps\/electron\/resources\/extensions\//
57
+ const WIN_LINUX_PACKAGED_PATTERN = /(^|\/)resources\/extensions\//
58
+
59
+ export type EnvironmentKind = 'xyz-agent' | 'standalone-pi'
60
+ export type Distribution = 'packaged' | 'dev' | null
61
+
62
+ export interface EnvironmentSignals {
63
+ /** process.env 快照(接线层传入,本模块不读全局 process.env) */
64
+ env: Record<string, string | undefined>
65
+ /** extension 模块的 import.meta.url(file:// URL);缺失 = distribution 无法判定(null) */
66
+ bundleUrl?: string
67
+ /** pi 的 agentDir(getAgentDir() 值),透传进 evidence 供核对 */
68
+ agentDir: string
69
+ }
70
+
71
+ export interface DetectedEnvironment {
72
+ kind: EnvironmentKind
73
+ distribution: Distribution
74
+ /** 托管时的 xyz-agent 数据目录;standalone-pi 恒 undefined */
75
+ dataDir?: string
76
+ /** 每条命中/未命中信号的原文 + 判定依据,恒非空(§6.2:判定可被证据推翻,非黑盒断言) */
77
+ evidence: string[]
78
+ }
79
+
80
+ /** 形态匹配前的归一化:Windows 分隔符 → `/`,剥尾部斜杠 */
81
+ function normalizeXyzPath(p: string): string {
82
+ return p.replace(/\\/g, '/').replace(/\/+$/, '')
83
+ }
84
+
85
+ function matchesXyzAgentAgentDir(value: string): boolean {
86
+ return AGENT_DIR_SHAPE_ANY.test(normalizeXyzPath(value))
87
+ }
88
+
89
+ /** distribution 判定,判定行写入 evidence */
90
+ function detectDistribution(bundleUrl: string | undefined, evidence: string[]): Distribution {
91
+ if (!bundleUrl) {
92
+ evidence.push('bundle=<缺失> → distribution 无法判定(null,不猜)')
93
+ return null
94
+ }
95
+ const normalized = bundleUrl.replace(/\\/g, '/')
96
+ if (MACOS_PACKAGED_PATTERN.test(normalized)) {
97
+ evidence.push(`bundle='${bundleUrl}' → packaged(macOS app bundle)`)
98
+ return 'packaged'
99
+ }
100
+ if (REPO_STAGED_PATTERN.test(normalized)) {
101
+ evidence.push(`bundle='${bundleUrl}' → dev(仓库 staged 资源根,非安装产物)`)
102
+ return 'dev'
103
+ }
104
+ if (WIN_LINUX_PACKAGED_PATTERN.test(normalized)) {
105
+ evidence.push(`bundle='${bundleUrl}' → packaged(Windows/Linux 应用资源目录)`)
106
+ return 'packaged'
107
+ }
108
+ evidence.push(`bundle='${bundleUrl}' → dev(非打包资源目录形态)`)
109
+ return 'dev'
110
+ }
111
+
112
+ /**
113
+ * 托管时的数据目录推导:XYZ_AGENT_DATA_DIR 优先(runtime 注入的权威值,与
114
+ * getDataDir() 同源),缺失时按布局形态从 PI_CODING_AGENT_DIR 剥层。来源写入 evidence。
115
+ */
116
+ function deriveDataDir(env: Record<string, string | undefined>, evidence: string[]): string | undefined {
117
+ const dataDirEnv = env[XYZ_AGENT_DATA_DIR_ENV]
118
+ if (dataDirEnv) {
119
+ evidence.push(`数据目录:'${dataDirEnv}'(来源:${XYZ_AGENT_DATA_DIR_ENV})`)
120
+ return dataDirEnv
121
+ }
122
+ const agentDirEnv = env[PI_CODING_AGENT_DIR_ENV]
123
+ if (agentDirEnv) {
124
+ const normalized = normalizeXyzPath(agentDirEnv)
125
+ // 旧形态更长,先判:`.xyz-agent*/pi/agent` → 剥 pi/agent 两层
126
+ if (AGENT_DIR_SHAPE_OLD.test(normalized)) {
127
+ const dataDir = dirname(dirname(normalized))
128
+ evidence.push(`数据目录:'${dataDir}'(来源:${PI_CODING_AGENT_DIR_ENV} 剥 pi/agent 两层,B 前旧布局)`)
129
+ return dataDir
130
+ }
131
+ if (AGENT_DIR_SHAPE_NEW.test(normalized)) {
132
+ const dataDir = dirname(normalized)
133
+ evidence.push(`数据目录:'${dataDir}'(来源:${PI_CODING_AGENT_DIR_ENV} 剥 agent 一层,B 后新布局)`)
134
+ return dataDir
135
+ }
136
+ }
137
+ evidence.push(`数据目录:无法推导(${XYZ_AGENT_DATA_DIR_ENV} 未设置且 ${PI_CODING_AGENT_DIR_ENV} 形态不可剥层)`)
138
+ return undefined
139
+ }
140
+
141
+ /** 信号 1 evidence 行(命中/未命中原文 + 未设置形态 + 要求值)。 */
142
+ function pushExtLogEvidence(
143
+ evidence: string[],
144
+ extLog: string | undefined,
145
+ extLogHit: boolean,
146
+ ): void {
147
+ evidence.push(
148
+ extLogHit
149
+ ? `${XYZ_AGENT_EXT_LOG_ENV}='${extLog}' → 命中`
150
+ : `${XYZ_AGENT_EXT_LOG_ENV}=${extLog === undefined ? '<未设置>' : `'${extLog}'`} → 未命中(要求值为 '1')`,
151
+ )
152
+ }
153
+
154
+ /** 信号 2 的形态标注(§6.2 双形态兼容:B 前旧布局 / B 后新布局 / 非 xyz-agent 形态)。 */
155
+ function describeAgentDirShape(normalized: string): string {
156
+ if (AGENT_DIR_SHAPE_OLD.test(normalized)) return 'B 前旧布局 pi/agent'
157
+ if (AGENT_DIR_SHAPE_NEW.test(normalized)) return 'B 后新布局 agent'
158
+ return '非 xyz-agent 形态'
159
+ }
160
+
161
+ /** 信号 2 evidence 行(未设置 → 未命中;有值 → 形态匹配与否 + 形态标注)。 */
162
+ function pushAgentDirEvidence(
163
+ evidence: string[],
164
+ agentDirEnv: string | undefined,
165
+ agentDirHit: boolean,
166
+ ): void {
167
+ if (agentDirEnv === undefined) {
168
+ evidence.push(`${PI_CODING_AGENT_DIR_ENV}=<未设置> → 未命中`)
169
+ return
170
+ }
171
+ const shape = describeAgentDirShape(normalizeXyzPath(agentDirEnv))
172
+ evidence.push(
173
+ `${PI_CODING_AGENT_DIR_ENV}='${agentDirEnv}' → ${agentDirHit ? `形态匹配(${shape})` : `形态不匹配(${shape},要求 <*>/.xyz-agent*/agent)`}`,
174
+ )
175
+ }
176
+
177
+ /** 判定行:托管(双信号合取成立)/ standalone-pi(孤置信号点名,提示透传/自设污染)。 */
178
+ function judgedEvidenceLine(extLogHit: boolean, agentDirHit: boolean): string {
179
+ if (extLogHit && agentDirHit) {
180
+ return `判定:xyz-agent 托管(${XYZ_AGENT_EXT_LOG_ENV}='1' 且 ${PI_CODING_AGENT_DIR_ENV} 形态匹配,双信号合取成立)`
181
+ }
182
+ const orphaned: string[] = []
183
+ if (extLogHit) orphaned.push(XYZ_AGENT_EXT_LOG_ENV)
184
+ if (agentDirHit) orphaned.push(PI_CODING_AGENT_DIR_ENV)
185
+ const orphanNote =
186
+ orphaned.length > 0
187
+ ? `——${orphaned.join(' 与 ')} 单独成立不合取,可能为 shell 透传/自设污染,不判托管`
188
+ : ''
189
+ return `判定:standalone-pi(双信号合取不成立)${orphanNote}`
190
+ }
191
+
192
+ /**
193
+ * 判定当前宿主环境(design §6.2)。
194
+ *
195
+ * 托管(xyz-agent)= 合取:`XYZ_AGENT_EXT_LOG === '1'` **且** `PI_CODING_AGENT_DIR`
196
+ * 值匹配 xyz-agent 形态(`<*>/.xyz-agent*` 目录后接 `/agent`;B 前旧布局接
197
+ * `/pi/agent` 同样接受)。任一信号单独成立 → standalone-pi,evidence 点名孤置
198
+ * 信号(透传污染提示)。
199
+ * evidence 恒非空:每条命中/未命中信号的原文都在列,让 agent 能核对并推翻判定。
200
+ *
201
+ * 本函数只负责「读信号 → 按序委派 evidence 构造 → 组装返回值」;每条 evidence 的
202
+ * 文案与压入顺序由上方 per-信号 helper 承担(拆解自原单函数 if/三元链,零行为变更)。
203
+ */
204
+ export function detectEnvironment(signals: EnvironmentSignals): DetectedEnvironment {
205
+ const evidence: string[] = []
206
+ const { env } = signals
207
+
208
+ // 信号 1:XYZ_AGENT_EXT_LOG === '1'
209
+ const extLog = env[XYZ_AGENT_EXT_LOG_ENV]
210
+ const extLogHit = extLog === EXT_LOG_MANAGED_VALUE
211
+ pushExtLogEvidence(evidence, extLog, extLogHit)
212
+
213
+ // 信号 2:PI_CODING_AGENT_DIR 值形态
214
+ const agentDirEnv = env[PI_CODING_AGENT_DIR_ENV]
215
+ const agentDirHit = agentDirEnv !== undefined && matchesXyzAgentAgentDir(agentDirEnv)
216
+ pushAgentDirEvidence(evidence, agentDirEnv, agentDirHit)
217
+ evidence.push(`agentDir='${signals.agentDir}' → pi getAgentDir() 透传记录`)
218
+
219
+ const managed = extLogHit && agentDirHit
220
+ const kind: EnvironmentKind = managed ? 'xyz-agent' : 'standalone-pi'
221
+ evidence.push(judgedEvidenceLine(extLogHit, agentDirHit))
222
+
223
+ const distribution = detectDistribution(signals.bundleUrl, evidence)
224
+
225
+ let dataDir: string | undefined
226
+ if (managed) {
227
+ dataDir = deriveDataDir(env, evidence)
228
+ } else {
229
+ evidence.push('数据目录:不输出(standalone-pi 恒无托管数据目录)')
230
+ }
231
+
232
+ return {
233
+ kind,
234
+ distribution,
235
+ ...(dataDir !== undefined ? { dataDir } : {}),
236
+ evidence,
237
+ }
238
+ }