@openprd/cli 0.1.19 → 0.1.22
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/.openprd/changes/openprd-control-plane-v020/.openprd.yaml +2 -0
- package/.openprd/changes/openprd-control-plane-v020/design.md +78 -0
- package/.openprd/changes/openprd-control-plane-v020/proposal.md +54 -0
- package/.openprd/changes/openprd-control-plane-v020/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/openprd-control-plane-v020/task-events.jsonl +27 -0
- package/.openprd/changes/openprd-control-plane-v020/tasks-002.md +35 -0
- package/.openprd/changes/openprd-control-plane-v020/tasks.md +427 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/.openprd.yaml +2 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/design.md +52 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/proposal.md +35 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/task-events.jsonl +1 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/tasks.md +170 -0
- package/.openprd/design/active/asset-spec.md +19 -14
- package/.openprd/design/active/direction-plan.md +19 -3
- package/.openprd/design/active/facts-sheet.md +16 -7
- package/.openprd/design/active/image-preflight.md +6 -5
- package/.openprd/design/active/review-studio-v020-directions/compare-plan.json +34 -0
- package/.openprd/design/active/review-studio-v020-directions/contact-sheet.jpg +0 -0
- package/.openprd/design/active/review-studio-v020-directions/crops/01.png +0 -0
- package/.openprd/design/active/review-studio-v020-directions/crops/02.png +0 -0
- package/.openprd/design/active/review-studio-v020-directions/crops/03.png +0 -0
- package/.openprd/design/active/review-studio-v020-directions/focus-board.template.json +69 -0
- package/.openprd/design/active/review-studio-v020-directions/parallel-board.template.json +45 -0
- package/.openprd/design/active/review-studio-v020-directions/reference-set.json +143 -0
- package/.openprd/design/active/review-studio-v020-directions/source.png +0 -0
- package/.openprd/design/active/selected-direction.md +23 -9
- package/.openprd/engagements/active/control-plane-architecture.json +203 -0
- package/.openprd/engagements/active/control-plane-intake.json +418 -0
- package/.openprd/engagements/active/prd.md +183 -119
- package/.openprd/engagements/active/review-presentation-v0018.json +176 -0
- package/.openprd/i18n-config.json +12 -0
- package/.openprd/ledger/events.jsonl +6 -0
- package/.openprd/ledger/heads/ebea1a71a9daa566f1c91b53.json +10 -0
- package/.openprd/manifest.json +21 -0
- package/AGENTS.md +7 -6
- package/README.md +31 -29
- package/README_EN.md +36 -39
- package/package.json +1 -1
- package/skills/openprd-frontend-design/SKILL.md +16 -0
- package/skills/openprd-harness/SKILL.md +7 -7
- package/skills/openprd-quality/SKILL.md +1 -1
- package/skills/openprd-requirement-intake/SKILL.md +1 -1
- package/skills/openprd-shared/SKILL.md +6 -6
- package/src/adapters/adapter-spi.js +193 -0
- package/src/adapters/capability-envelope.js +98 -0
- package/src/adapters/event-normalizer.js +55 -0
- package/src/adapters/index.js +4 -0
- package/src/adapters/install-safety.js +249 -0
- package/src/agent-canonical-content.js +4 -2
- package/src/agent-integration.js +169 -46
- package/src/cli/args.js +63 -4
- package/src/cli/gate-print.js +17 -0
- package/src/cli/quality-commands.js +18 -0
- package/src/cli/quality-print.js +10 -0
- package/src/cli/runtime-print.js +24 -0
- package/src/codex-hook-runner-template.mjs +129 -199
- package/src/codex-runtime.js +48 -5
- package/src/context/cache.js +245 -0
- package/src/context/compiler.js +438 -0
- package/src/context/constants.js +30 -0
- package/src/context/index.js +39 -0
- package/src/context/redaction.js +84 -0
- package/src/context/stable.js +69 -0
- package/src/context/telemetry.js +42 -0
- package/src/dev-standards.js +57 -0
- package/src/fleet.js +112 -95
- package/src/gates/index.js +2 -0
- package/src/gates/scoped-gates.js +256 -0
- package/src/gates/store.js +126 -0
- package/src/gates/workspace.js +41 -0
- package/src/html-artifacts.js +725 -28
- package/src/kernel/atomic-store.js +299 -0
- package/src/kernel/event-envelope.js +166 -0
- package/src/kernel/index.js +4 -0
- package/src/kernel/project-ledger.js +467 -0
- package/src/kernel/project-manifest.js +205 -0
- package/src/knowledge-v3/index.js +1 -0
- package/src/knowledge-v3/lifecycle.js +290 -0
- package/src/knowledge.js +14 -7
- package/src/openprd.js +71 -2
- package/src/review-model.js +413 -0
- package/src/review-presentation.js +1 -1
- package/src/run-harness.js +432 -38
- package/src/runtime/cli_runtime_README.md +28 -0
- package/src/runtime/errors.js +66 -0
- package/src/runtime/index.js +44 -0
- package/src/runtime/lane-schema.js +141 -0
- package/src/runtime/lane-store.js +279 -0
- package/src/runtime/task-runtime.js +449 -0
- package/src/runtime/workspace.js +179 -0
- package/src/runtime/write-set.js +206 -0
- package/src/session-binding.js +16 -3
- package/src/session-registry.js +59 -1
- package/src/upgrade/fleet-mutation.js +166 -0
- package/src/upgrade/fleet-transaction.js +398 -0
- package/src/upgrade/transaction-store.js +416 -0
- package/src/visual-compare-core.js +66 -27
- package/src/visual-compare.js +18 -12
- package/src/workspace-core.js +109 -7
- package/src/workspace-registry.js +39 -1
- package/src/workspace-workflow.js +18 -15
|
@@ -33,7 +33,7 @@ description: OpenPrd 工作区与产物的共用守则。凡是需要查看、
|
|
|
33
33
|
- 只读命令:`status`、`validate`、`next`、`history`、`diff`、`interview`、`doctor`
|
|
34
34
|
- 写入命令:`init`、`setup`、`update`、`classify`、`synthesize`、`diagram`、`release`、`freeze`、`handoff`
|
|
35
35
|
- 执行命令:`loop --run`、`tasks --advance`、`discovery --advance`、`loop --finish --commit`、git commit、git push,必须有当前用户明确执行意图。
|
|
36
|
-
-
|
|
36
|
+
- 用户要求实现、继续、修复、部署或发布时,OpenPrd 不再追加授权门禁。风险词和动作类型只用于提醒 Agent 优先选择配置化、可回滚、幂等或标准状态机方案,不触发 OpenPrd 阻断或用户确认;是否需要确认由当前 Agent 依据宿主安全规则和真实上下文自行判断,禁止要求用户复述固定授权口令。
|
|
37
37
|
3. 不要虚构 OpenPrd 命令或产物类型。
|
|
38
38
|
- 不确定时先对照 `openprd --help`。
|
|
39
39
|
4. 共用规则放在这里,领域规则放到对应 skill。
|
|
@@ -99,8 +99,8 @@ description: OpenPrd 工作区与产物的共用守则。凡是需要查看、
|
|
|
99
99
|
13. 大界面改动实现前必须先完成视觉方案评审。
|
|
100
100
|
- 触发条件:会明显改变页面信息架构、主视觉、核心布局、关键路径、组件层级/密度,或用户需要先选择设计方向。
|
|
101
101
|
- 位置:需求分流之后、PRD 定稿或实现开工之前;它不是 `review.html`,也不是实现后的 `visual-compare`。
|
|
102
|
-
-
|
|
103
|
-
-
|
|
102
|
+
- 步骤:用户不需要额外提出生图。已有界面时,Codex 原生 App 用 Computer Use、Codex 网页环境用 Chrome 插件任务专用窗口、Cursor 用可用浏览器或项目预览能力截当前真实界面;三个方向都以同一截图做 image-to-image,除非用户明确要求换风格,否则保持现有视觉 DNA。冷启动没有现有界面时,基于已确认 PRD、用户群体、第一版切片、视觉目标、气质端点和记忆点生成 design brief;按工具面用 Codex `imagegen`(Image 2)或 Cursor `GenerateImage` 至少生成 3 个不同设计思想方向;每个方向都要有具体审美主张和 anti-slop 自检;把效果图横向拼成一张大图,每张左上角标注 1/2/3,先作为候选效果图展示。
|
|
103
|
+
- 交互:把候选方向直接给用户看,并说明 Agent 采用了哪个可逆默认方向及原因;OpenPrd 不强制等待用户确认,也不阻断大 UI 实现。用户明确选择时再覆盖默认方向并写入 `.openprd/harness/visual-reviews/`。
|
|
104
104
|
14. 界面任务进入实现前,先用 `.openprd/design/` 锁定设计框架。
|
|
105
105
|
- 页面涉及具体产品事实、版本、发布时间、规格、价格、引用数据或地点事实时,先补 `.openprd/design/active/facts-sheet.md`,不要凭记忆写页面。
|
|
106
106
|
- 页面依赖 logo、产品图、UI 图、摄影图、插图、图表或品牌色字体时,先补 `.openprd/design/active/asset-spec.md`。
|
|
@@ -146,7 +146,7 @@ description: OpenPrd 工作区与产物的共用守则。凡是需要查看、
|
|
|
146
146
|
21. 修改 skill、`SKILL.md`、`AGENTS.md` 或相关 workflow 前,先可视化确认。
|
|
147
147
|
- 先读取当前 skill / AGENTS 现状,再输出一张彩色 Mermaid 方案图。
|
|
148
148
|
- Mermaid 必须区分 `unchanged`、`added`、`changed`、`removed`,并在图后用短说明写清新增、修改、保持不变、删除或阻断。
|
|
149
|
-
-
|
|
149
|
+
- Mermaid 用于帮助用户理解变更;OpenPrd 不把它变成写入授权门禁。Agent 可在输出方案图后按用户已提出的修改目标继续。
|
|
150
150
|
22. 涉及微信小程序运行态时,只在明确需要运行态证据时再升级到本地验证。
|
|
151
151
|
- 只有当用户明确要求小程序实测、验证、复现、页面操作、截图、日志、网络请求、开发者工具自动化,或当前改动高风险到必须依赖运行态证据时,才升级到小程序本地验证。
|
|
152
152
|
- 一旦进入小程序运行态验证,默认沿用当前小程序运行态或开发者工具会话连续验证,不要为了验证自动重开应用;只有用户明确要求从 0 到 1、冷启动、重开或重新打开时,才从头启动。
|
|
@@ -170,8 +170,8 @@ description: OpenPrd 工作区与产物的共用守则。凡是需要查看、
|
|
|
170
170
|
- 单纯的“请帮我实现/继续实现”只表示有执行意图,不表示可以跳过 requirement 摘要确认、`capture/classify/synthesize` 写入路径或 review;只有用户明确表示“不需要进行任何确认”时,才允许静默走完整 requirement write path。
|
|
171
171
|
- 声称实现就绪前,要说明文档影响检查是新增、更新,还是有意保持 `docs/basic/`、文件说明书和文件夹 README 不变。
|
|
172
172
|
- 用户要求生成图片、封面图、配图、海报、插画、图标、贴纸、头像、banner、主视觉/KV、运营图、效果图、视觉稿、mockup、先看样子或先确认设计方向时,最终回复应给出 `imagegen` 生成的图片结果;只有实际发生 `imagegen` 调用后,才能汇报生图结果、失败或限流。Image 2 是工具路径,不是审美豁免;最终回复要能说明候选图是否满足用途、受众、气质和记忆点。生图结果先当候选效果图,并主动确认是否符合预期、是否纳入后续效果图/实现截图对比、以及是否按此继续实现。进入实现阶段后,已有确认参考图才给出 `openprd visual-compare --reference/--actual` 生成的 JPG 路径;如果参考图是一张整板、网格图或多对象组合,先运行 `openprd visual-prepare --reference <效果图> --grid <列>x<行>` 或 `--boxes <plan.json>` 并说明 contact sheet / compare-plan;没有参考图时先判断新建界面还是修改既有界面,新建界面先完成 3 方向方案评审,修改既有界面给出 `openprd visual-compare --before/--after` 生成的 JPG 路径;普通截图实测补 `openprd visual-compare --board <verification-board.json>`,同构列表、卡片、网格、表格或用户反馈没对齐时补 `openprd visual-compare --board <alignment-board.json>`,并说明相同槽位 spread 是否仍有偏差;最终同时说明是否仍有结构、气质、层级、颜色、字号、间距或记忆点差异。
|
|
173
|
-
-
|
|
174
|
-
-
|
|
173
|
+
- 大界面改动应给出 3 方向横向候选效果图大图;Agent 可选最符合目标且可逆的方向继续,OpenPrd 不强制等待确认。用户明确选择时优先采用用户方向。
|
|
174
|
+
- 在系统形态、产品流程或外部依赖仍有实质不确定性时,Agent 先选择可逆默认方案并显式说明假设;是否需要提问由 Agent 自行判断,不由 OpenPrd 强制。
|
|
175
175
|
- 当用户还没看过最新的 synthesize 产物或图示产物时,handoff 前先确认。
|
|
176
176
|
- 当用户要求可视化说明,或当前解释明显包含复杂关系、路径、对比、边界或风险传播时,路由到 `$openprd-diagram-review`;优先判断是否适合轻量解释型 SVG,而不是默认进入正式评审图。
|
|
177
177
|
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
ADAPTER_CONTRACT_VERSION,
|
|
4
|
+
capabilityAvailable,
|
|
5
|
+
describeCapabilities,
|
|
6
|
+
normalizeClient,
|
|
7
|
+
} from './capability-envelope.js';
|
|
8
|
+
import { normalizeLifecycleEvent } from './event-normalizer.js';
|
|
9
|
+
|
|
10
|
+
const DETECTION_PRIORITY = Object.freeze({
|
|
11
|
+
'hook-payload': 100,
|
|
12
|
+
'session-binding': 95,
|
|
13
|
+
'explicit-launcher': 90,
|
|
14
|
+
'sdk-session': 80,
|
|
15
|
+
'process-env': 50,
|
|
16
|
+
'config-probe': 20,
|
|
17
|
+
'agent-self-report': 10,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function normalizedEvidenceList(evidence) {
|
|
21
|
+
const list = Array.isArray(evidence) ? evidence : [evidence];
|
|
22
|
+
return list
|
|
23
|
+
.filter((item) => item && typeof item === 'object' && !Array.isArray(item))
|
|
24
|
+
.map((item, index) => ({
|
|
25
|
+
source: String(item.source ?? 'agent-self-report'),
|
|
26
|
+
client: item.client ? String(item.client).toLowerCase() : null,
|
|
27
|
+
surface: item.surface ?? 'unknown',
|
|
28
|
+
executionMode: item.executionMode ?? 'interactive',
|
|
29
|
+
sessionId: item.sessionId ?? null,
|
|
30
|
+
threadId: item.threadId ?? null,
|
|
31
|
+
observedCapabilities: Array.isArray(item.observedCapabilities) ? item.observedCapabilities : [],
|
|
32
|
+
confidence: Number.isFinite(item.confidence) ? Math.max(0, Math.min(1, item.confidence)) : null,
|
|
33
|
+
index,
|
|
34
|
+
}))
|
|
35
|
+
.filter((item) => ['codex', 'claude', 'cursor'].includes(item.client));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function detectRuntime(evidence = []) {
|
|
39
|
+
const candidates = normalizedEvidenceList(evidence)
|
|
40
|
+
.map((item) => {
|
|
41
|
+
const priority = DETECTION_PRIORITY[item.source] ?? 0;
|
|
42
|
+
return {
|
|
43
|
+
...item,
|
|
44
|
+
priority,
|
|
45
|
+
confidence: item.confidence ?? Math.min(1, Math.max(0.1, priority / 100)),
|
|
46
|
+
};
|
|
47
|
+
})
|
|
48
|
+
.sort((left, right) => right.priority - left.priority
|
|
49
|
+
|| right.confidence - left.confidence
|
|
50
|
+
|| left.index - right.index);
|
|
51
|
+
const selected = candidates[0] ?? null;
|
|
52
|
+
if (!selected) {
|
|
53
|
+
return {
|
|
54
|
+
schema: 'openprd.runtime-detection.v1',
|
|
55
|
+
activeClient: 'unknown',
|
|
56
|
+
surface: 'unknown',
|
|
57
|
+
executionMode: 'interactive',
|
|
58
|
+
sessionId: null,
|
|
59
|
+
threadId: null,
|
|
60
|
+
confidence: 0,
|
|
61
|
+
evidence: [],
|
|
62
|
+
capabilityEnvelope: null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
schema: 'openprd.runtime-detection.v1',
|
|
67
|
+
activeClient: selected.client,
|
|
68
|
+
surface: selected.surface,
|
|
69
|
+
executionMode: selected.executionMode,
|
|
70
|
+
sessionId: selected.sessionId,
|
|
71
|
+
threadId: selected.threadId,
|
|
72
|
+
confidence: selected.confidence,
|
|
73
|
+
evidence: candidates,
|
|
74
|
+
capabilityEnvelope: describeCapabilities(selected.client, {
|
|
75
|
+
surface: selected.surface,
|
|
76
|
+
observedCapabilities: selected.observedCapabilities,
|
|
77
|
+
}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function renderContextMarkdown(client, capsule) {
|
|
82
|
+
const normalized = normalizeClient(client);
|
|
83
|
+
if (capsule?.schemaVersion === 1 && Array.isArray(capsule.entries)) {
|
|
84
|
+
const lines = [
|
|
85
|
+
`OpenPrd ContextCapsule v1 (${normalized})`,
|
|
86
|
+
`digest: ${capsule.digest ?? 'unknown'}`,
|
|
87
|
+
`revision: ${capsule.revision ?? 'unknown'}`,
|
|
88
|
+
`delivery: ${capsule.delivery ?? 'full'}`,
|
|
89
|
+
`budget: ${capsule.budget?.estimatedTokens ?? 0}/${capsule.budget?.limitTokens ?? 'unknown'} tokens`,
|
|
90
|
+
];
|
|
91
|
+
for (const entry of capsule.entries) {
|
|
92
|
+
const value = entry?.value;
|
|
93
|
+
if (entry?.id === 'recommended-next-step') {
|
|
94
|
+
if (value?.title) lines.push(`next: ${value.title}`);
|
|
95
|
+
if (value?.reason) lines.push(`reason: ${value.reason}`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (entry?.id === 'task-progress') {
|
|
99
|
+
lines.push(`task: ${value?.summary ?? value?.lane ?? 'current'}`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (entry?.id === 'runtime-environment') {
|
|
103
|
+
lines.push(`runtime: ${value?.activeClient ?? 'unknown'} / ${value?.surface ?? 'unknown'}`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (entry?.id === 'requirement-gate') {
|
|
107
|
+
lines.push(`gate: ${value?.status ?? 'active'} (${value?.relevance ?? 'primary'})`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (entry?.id === 'project-health') {
|
|
111
|
+
lines.push(`health: ${value?.valid ? 'valid' : 'attention-required'}`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (entry?.id === 'current-focus') {
|
|
115
|
+
const focus = value?.focus?.changeId ?? value?.activeChange ?? value?.latestPrd?.title;
|
|
116
|
+
if (focus) lines.push(`focus: ${focus}`);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (entry?.id === 'project-knowledge') {
|
|
120
|
+
lines.push(`knowledge: ${Array.isArray(value) ? value.length : 0} scoped item(s)`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (capsule.delivery === 'delta' && capsule.entries.length === 0) {
|
|
124
|
+
lines.push(`unchanged: reuse ${capsule.delta?.previousCapsuleDigest ?? 'previous capsule'}`);
|
|
125
|
+
}
|
|
126
|
+
return `${lines.join('\n')}\n`;
|
|
127
|
+
}
|
|
128
|
+
const sections = capsule?.sections ?? {};
|
|
129
|
+
const lines = [
|
|
130
|
+
`OpenPrd context (${normalized})`,
|
|
131
|
+
`capsule: ${capsule?.capsuleId ?? 'unknown'}`,
|
|
132
|
+
`revision: ${capsule?.projectRevision ?? 'unknown'}`,
|
|
133
|
+
];
|
|
134
|
+
if (sections.taskContract) {
|
|
135
|
+
lines.push(`task: ${sections.taskContract.title ?? sections.taskContract.taskId ?? 'current'}`);
|
|
136
|
+
}
|
|
137
|
+
for (const fact of sections.pinnedFacts ?? []) {
|
|
138
|
+
lines.push(`fact: ${fact.text ?? fact.value ?? String(fact)}`);
|
|
139
|
+
}
|
|
140
|
+
for (const gate of sections.applicableGates ?? []) {
|
|
141
|
+
lines.push(`gate: ${gate.summary ?? gate.gateType ?? String(gate)}`);
|
|
142
|
+
}
|
|
143
|
+
return `${lines.join('\n')}\n`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function renderDecision(client, decision = {}) {
|
|
147
|
+
const normalized = normalizeClient(client);
|
|
148
|
+
const allowed = decision.decision === 'allow' || decision.allowed === true;
|
|
149
|
+
return {
|
|
150
|
+
schema: 'openprd.adapter-decision.v1',
|
|
151
|
+
adapterVersion: ADAPTER_CONTRACT_VERSION,
|
|
152
|
+
client: normalized,
|
|
153
|
+
allowed,
|
|
154
|
+
approvalId: decision.approvalId ?? null,
|
|
155
|
+
message: decision.message ?? (allowed ? '已允许继续当前操作。' : '当前操作尚未获准。'),
|
|
156
|
+
reason: decision.reason ?? null,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function createAdapter(client, overrides = {}) {
|
|
161
|
+
const normalized = normalizeClient(client);
|
|
162
|
+
return Object.freeze({
|
|
163
|
+
client: normalized,
|
|
164
|
+
contractVersion: ADAPTER_CONTRACT_VERSION,
|
|
165
|
+
detectRuntime: overrides.detectRuntime ?? detectRuntime,
|
|
166
|
+
describeCapabilities: (options = {}) => describeCapabilities(normalized, options),
|
|
167
|
+
normalizeLifecycleEvent: (raw) => normalizeLifecycleEvent(normalized, raw),
|
|
168
|
+
renderContext: overrides.renderContext ?? ((capsule) => ({
|
|
169
|
+
schema: 'openprd.adapter-context.v1',
|
|
170
|
+
client: normalized,
|
|
171
|
+
format: 'text/markdown',
|
|
172
|
+
content: renderContextMarkdown(normalized, capsule),
|
|
173
|
+
capsuleDigest: capsule?.digests?.content ?? capsule?.digest ?? null,
|
|
174
|
+
})),
|
|
175
|
+
renderDecision: overrides.renderDecision ?? ((decision) => renderDecision(normalized, decision)),
|
|
176
|
+
can: (capability, options = {}) => capabilityAvailable(describeCapabilities(normalized, options), capability),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function adapterContractDigest() {
|
|
181
|
+
const contract = ['codex', 'claude', 'cursor'].map((client) => describeCapabilities(client));
|
|
182
|
+
return crypto.createHash('sha256').update(JSON.stringify(contract)).digest('hex');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export {
|
|
186
|
+
ADAPTER_CONTRACT_VERSION,
|
|
187
|
+
DETECTION_PRIORITY,
|
|
188
|
+
adapterContractDigest,
|
|
189
|
+
createAdapter,
|
|
190
|
+
detectRuntime,
|
|
191
|
+
renderContextMarkdown,
|
|
192
|
+
renderDecision,
|
|
193
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const ADAPTER_CONTRACT_VERSION = 1;
|
|
2
|
+
|
|
3
|
+
const CAPABILITY_STATUS = Object.freeze({
|
|
4
|
+
NATIVE: 'native',
|
|
5
|
+
SURFACE_DEPENDENT: 'surface-dependent',
|
|
6
|
+
UNSUPPORTED: 'unsupported',
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const BASE_CAPABILITIES = Object.freeze({
|
|
10
|
+
codex: Object.freeze({
|
|
11
|
+
hooks: CAPABILITY_STATUS.NATIVE,
|
|
12
|
+
beforeTool: CAPABILITY_STATUS.NATIVE,
|
|
13
|
+
afterTool: CAPABILITY_STATUS.NATIVE,
|
|
14
|
+
approvalUI: CAPABILITY_STATUS.SURFACE_DEPENDENT,
|
|
15
|
+
browser: CAPABILITY_STATUS.SURFACE_DEPENDENT,
|
|
16
|
+
computerUse: CAPABILITY_STATUS.SURFACE_DEPENDENT,
|
|
17
|
+
imageGeneration: CAPABILITY_STATUS.SURFACE_DEPENDENT,
|
|
18
|
+
currentThreadMessaging: CAPABILITY_STATUS.SURFACE_DEPENDENT,
|
|
19
|
+
backgroundAgents: CAPABILITY_STATUS.SURFACE_DEPENDENT,
|
|
20
|
+
maxRecommendedContext: 800,
|
|
21
|
+
}),
|
|
22
|
+
claude: Object.freeze({
|
|
23
|
+
hooks: CAPABILITY_STATUS.UNSUPPORTED,
|
|
24
|
+
beforeTool: CAPABILITY_STATUS.UNSUPPORTED,
|
|
25
|
+
afterTool: CAPABILITY_STATUS.UNSUPPORTED,
|
|
26
|
+
approvalUI: CAPABILITY_STATUS.UNSUPPORTED,
|
|
27
|
+
browser: CAPABILITY_STATUS.UNSUPPORTED,
|
|
28
|
+
computerUse: CAPABILITY_STATUS.UNSUPPORTED,
|
|
29
|
+
imageGeneration: CAPABILITY_STATUS.UNSUPPORTED,
|
|
30
|
+
currentThreadMessaging: CAPABILITY_STATUS.UNSUPPORTED,
|
|
31
|
+
backgroundAgents: CAPABILITY_STATUS.SURFACE_DEPENDENT,
|
|
32
|
+
maxRecommendedContext: 800,
|
|
33
|
+
}),
|
|
34
|
+
cursor: Object.freeze({
|
|
35
|
+
hooks: CAPABILITY_STATUS.UNSUPPORTED,
|
|
36
|
+
beforeTool: CAPABILITY_STATUS.UNSUPPORTED,
|
|
37
|
+
afterTool: CAPABILITY_STATUS.UNSUPPORTED,
|
|
38
|
+
approvalUI: CAPABILITY_STATUS.UNSUPPORTED,
|
|
39
|
+
browser: CAPABILITY_STATUS.UNSUPPORTED,
|
|
40
|
+
computerUse: CAPABILITY_STATUS.UNSUPPORTED,
|
|
41
|
+
imageGeneration: CAPABILITY_STATUS.SURFACE_DEPENDENT,
|
|
42
|
+
currentThreadMessaging: CAPABILITY_STATUS.UNSUPPORTED,
|
|
43
|
+
backgroundAgents: CAPABILITY_STATUS.SURFACE_DEPENDENT,
|
|
44
|
+
maxRecommendedContext: 800,
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
function normalizeClient(value) {
|
|
49
|
+
const client = String(value ?? '').trim().toLowerCase();
|
|
50
|
+
if (!Object.prototype.hasOwnProperty.call(BASE_CAPABILITIES, client)) {
|
|
51
|
+
throw new Error(`Unsupported OpenPrd adapter client: ${value}`);
|
|
52
|
+
}
|
|
53
|
+
return client;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeObservedCapabilities(observed) {
|
|
57
|
+
if (!Array.isArray(observed)) return new Set();
|
|
58
|
+
return new Set(observed.map((item) => String(item ?? '').trim()).filter(Boolean));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function describeCapabilities(client, options = {}) {
|
|
62
|
+
const normalized = normalizeClient(client);
|
|
63
|
+
const observed = normalizeObservedCapabilities(options.observedCapabilities);
|
|
64
|
+
const baseline = BASE_CAPABILITIES[normalized];
|
|
65
|
+
const capabilities = {};
|
|
66
|
+
for (const [name, status] of Object.entries(baseline)) {
|
|
67
|
+
if (name === 'maxRecommendedContext') {
|
|
68
|
+
capabilities[name] = Number.isFinite(options.maxRecommendedContext)
|
|
69
|
+
? Math.max(1, Math.trunc(options.maxRecommendedContext))
|
|
70
|
+
: status;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
capabilities[name] = status === CAPABILITY_STATUS.SURFACE_DEPENDENT && observed.has(name)
|
|
74
|
+
? CAPABILITY_STATUS.NATIVE
|
|
75
|
+
: status;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
schema: 'openprd.adapter-capabilities.v1',
|
|
79
|
+
contractVersion: ADAPTER_CONTRACT_VERSION,
|
|
80
|
+
client: normalized,
|
|
81
|
+
surface: options.surface ?? 'unknown',
|
|
82
|
+
capabilities,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function capabilityAvailable(envelope, name) {
|
|
87
|
+
return envelope?.capabilities?.[name] === CAPABILITY_STATUS.NATIVE;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export {
|
|
91
|
+
ADAPTER_CONTRACT_VERSION,
|
|
92
|
+
BASE_CAPABILITIES,
|
|
93
|
+
CAPABILITY_STATUS,
|
|
94
|
+
capabilityAvailable,
|
|
95
|
+
describeCapabilities,
|
|
96
|
+
normalizeClient,
|
|
97
|
+
};
|
|
98
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { ADAPTER_CONTRACT_VERSION, normalizeClient } from './capability-envelope.js';
|
|
3
|
+
|
|
4
|
+
const EVENT_ALIASES = Object.freeze({
|
|
5
|
+
SessionStart: 'session.started',
|
|
6
|
+
session_start: 'session.started',
|
|
7
|
+
UserPromptSubmit: 'prompt.submitted',
|
|
8
|
+
user_prompt_submit: 'prompt.submitted',
|
|
9
|
+
PreToolUse: 'tool.before',
|
|
10
|
+
before_tool: 'tool.before',
|
|
11
|
+
PostToolUse: 'tool.after',
|
|
12
|
+
after_tool: 'tool.after',
|
|
13
|
+
Stop: 'turn.stopped',
|
|
14
|
+
stop: 'turn.stopped',
|
|
15
|
+
SubagentStart: 'agent.started',
|
|
16
|
+
SubagentStop: 'agent.stopped',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function stableEventId(client, raw) {
|
|
20
|
+
const seed = JSON.stringify({
|
|
21
|
+
client,
|
|
22
|
+
event: raw.event ?? raw.eventName ?? raw.type ?? null,
|
|
23
|
+
sessionId: raw.sessionId ?? raw.session_id ?? null,
|
|
24
|
+
threadId: raw.threadId ?? raw.thread_id ?? null,
|
|
25
|
+
turnId: raw.turnId ?? raw.turn_id ?? null,
|
|
26
|
+
toolUseId: raw.toolUseId ?? raw.tool_use_id ?? null,
|
|
27
|
+
occurredAt: raw.occurredAt ?? raw.timestamp ?? raw.at ?? null,
|
|
28
|
+
});
|
|
29
|
+
return `evt_${crypto.createHash('sha256').update(seed).digest('hex').slice(0, 24)}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeLifecycleEvent(client, rawEvent = {}) {
|
|
33
|
+
const normalizedClient = normalizeClient(client);
|
|
34
|
+
const raw = rawEvent && typeof rawEvent === 'object' && !Array.isArray(rawEvent) ? rawEvent : {};
|
|
35
|
+
const rawType = String(raw.event ?? raw.eventName ?? raw.type ?? 'unknown');
|
|
36
|
+
const occurredAt = raw.occurredAt ?? raw.timestamp ?? raw.at ?? new Date().toISOString();
|
|
37
|
+
return {
|
|
38
|
+
schema: 'openprd.adapter-event.v1',
|
|
39
|
+
adapterVersion: ADAPTER_CONTRACT_VERSION,
|
|
40
|
+
eventId: raw.eventId ?? raw.event_id ?? stableEventId(normalizedClient, raw),
|
|
41
|
+
client: normalizedClient,
|
|
42
|
+
eventType: EVENT_ALIASES[rawType] ?? rawType,
|
|
43
|
+
occurredAt,
|
|
44
|
+
sessionId: raw.sessionId ?? raw.session_id ?? null,
|
|
45
|
+
threadId: raw.threadId ?? raw.thread_id ?? raw.conversationId ?? null,
|
|
46
|
+
turnId: raw.turnId ?? raw.turn_id ?? null,
|
|
47
|
+
toolUseId: raw.toolUseId ?? raw.tool_use_id ?? null,
|
|
48
|
+
cwd: raw.cwd ?? raw.workspaceRoot ?? null,
|
|
49
|
+
payload: raw.payload && typeof raw.payload === 'object' ? raw.payload : {},
|
|
50
|
+
rawEventType: rawType,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { EVENT_ALIASES, normalizeLifecycleEvent };
|
|
55
|
+
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const INSTALL_SAFETY_PROTOCOL_VERSION = 1;
|
|
7
|
+
const SUPPORTED_SYMLINK_STRATEGIES = new Set(['block', 'materialize']);
|
|
8
|
+
|
|
9
|
+
class UnsafeManagedWriteError extends Error {
|
|
10
|
+
constructor(targetPath, symlink, strategy = 'block') {
|
|
11
|
+
const resolved = symlink.resolvedTarget ? ` -> ${symlink.resolvedTarget}` : '';
|
|
12
|
+
super(
|
|
13
|
+
`Refusing to write ${targetPath}: managed write path crosses symlink ${symlink.path}${resolved}. `
|
|
14
|
+
+ `Use symlinkStrategy=materialize with a trusted installSafetyBackupRoot to snapshot and replace the link before writing.`,
|
|
15
|
+
);
|
|
16
|
+
this.name = 'UnsafeManagedWriteError';
|
|
17
|
+
this.code = 'OPENPRD_UNSAFE_MANAGED_WRITE_SYMLINK';
|
|
18
|
+
this.targetPath = targetPath;
|
|
19
|
+
this.symlinkPath = symlink.path;
|
|
20
|
+
this.linkTarget = symlink.linkTarget;
|
|
21
|
+
this.resolvedTarget = symlink.resolvedTarget;
|
|
22
|
+
this.strategy = strategy;
|
|
23
|
+
this.repairHint = 'Choose a physical destination, or rerun with symlinkStrategy=materialize and a trusted backup root.';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeSymlinkStrategy(value) {
|
|
28
|
+
const strategy = String(value ?? 'block').trim().toLowerCase() || 'block';
|
|
29
|
+
if (!SUPPORTED_SYMLINK_STRATEGIES.has(strategy)) {
|
|
30
|
+
throw new Error(`Unsupported managed-write symlink strategy: ${value}. Use block or materialize.`);
|
|
31
|
+
}
|
|
32
|
+
return strategy;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function lstatOrNull(filePath) {
|
|
36
|
+
return fs.lstat(filePath).catch((error) => {
|
|
37
|
+
if (error?.code === 'ENOENT') return null;
|
|
38
|
+
throw error;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isWithinRoot(rootPath, targetPath) {
|
|
43
|
+
const relative = path.relative(rootPath, targetPath);
|
|
44
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function managedRootForTarget(targetPath, options = {}) {
|
|
48
|
+
const configured = Array.isArray(options.managedWriteRoots)
|
|
49
|
+
? options.managedWriteRoots
|
|
50
|
+
: [options.projectRoot].filter(Boolean);
|
|
51
|
+
return configured
|
|
52
|
+
.map((root) => path.resolve(root))
|
|
53
|
+
.filter((root) => isWithinRoot(root, targetPath))
|
|
54
|
+
.sort((left, right) => right.length - left.length)[0] ?? null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function findFirstSymlink(targetPath, options = {}) {
|
|
58
|
+
const absoluteTarget = path.resolve(targetPath);
|
|
59
|
+
const managedRoot = managedRootForTarget(absoluteTarget, options);
|
|
60
|
+
const parsed = path.parse(absoluteTarget);
|
|
61
|
+
const scanRoot = managedRoot ?? parsed.root;
|
|
62
|
+
const rootStat = managedRoot ? await lstatOrNull(scanRoot) : null;
|
|
63
|
+
if (rootStat?.isSymbolicLink()) {
|
|
64
|
+
const linkTarget = await fs.readlink(scanRoot);
|
|
65
|
+
const resolvedTarget = await fs.realpath(scanRoot).catch(() => path.resolve(path.dirname(scanRoot), linkTarget));
|
|
66
|
+
return {
|
|
67
|
+
path: scanRoot,
|
|
68
|
+
linkTarget,
|
|
69
|
+
resolvedTarget,
|
|
70
|
+
dangling: !(await lstatOrNull(resolvedTarget)),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (managedRoot && !rootStat) return null;
|
|
74
|
+
const parts = path.relative(scanRoot, absoluteTarget).split(path.sep).filter(Boolean);
|
|
75
|
+
let cursor = scanRoot;
|
|
76
|
+
for (const part of parts) {
|
|
77
|
+
cursor = path.join(cursor, part);
|
|
78
|
+
const stat = await lstatOrNull(cursor);
|
|
79
|
+
if (!stat) return null;
|
|
80
|
+
if (!stat.isSymbolicLink()) continue;
|
|
81
|
+
const linkTarget = await fs.readlink(cursor);
|
|
82
|
+
const resolvedTarget = await fs.realpath(cursor).catch(() => path.resolve(path.dirname(cursor), linkTarget));
|
|
83
|
+
return {
|
|
84
|
+
path: cursor,
|
|
85
|
+
linkTarget,
|
|
86
|
+
resolvedTarget,
|
|
87
|
+
dangling: !(await lstatOrNull(resolvedTarget)),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function resolveBackupRoot(options = {}) {
|
|
94
|
+
if (options.installSafetyBackupRoot) {
|
|
95
|
+
return path.resolve(options.installSafetyBackupRoot);
|
|
96
|
+
}
|
|
97
|
+
const physicalTemp = await fs.realpath(os.tmpdir()).catch(() => path.resolve(os.tmpdir()));
|
|
98
|
+
return path.join(physicalTemp, 'openprd-install-safety', String(process.pid));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function backupEntryName(symlinkPath) {
|
|
102
|
+
const digest = crypto.createHash('sha256').update(symlinkPath).digest('hex').slice(0, 12);
|
|
103
|
+
const suffix = `${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
104
|
+
return `${path.basename(symlinkPath) || 'root'}-${digest}-${suffix}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function copyPath(sourcePath, destinationPath, stat) {
|
|
108
|
+
if (stat.isDirectory()) {
|
|
109
|
+
await fs.cp(sourcePath, destinationPath, {
|
|
110
|
+
recursive: true,
|
|
111
|
+
dereference: false,
|
|
112
|
+
errorOnExist: true,
|
|
113
|
+
force: false,
|
|
114
|
+
});
|
|
115
|
+
return 'directory';
|
|
116
|
+
}
|
|
117
|
+
if (stat.isFile()) {
|
|
118
|
+
await fs.mkdir(path.dirname(destinationPath), { recursive: true });
|
|
119
|
+
await fs.copyFile(sourcePath, destinationPath);
|
|
120
|
+
return 'file';
|
|
121
|
+
}
|
|
122
|
+
throw new Error(`Cannot materialize unsupported symlink target type at ${sourcePath}.`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function materializeSymlink(targetPath, symlink, options = {}) {
|
|
126
|
+
const backupRoot = await resolveBackupRoot(options);
|
|
127
|
+
const backupSymlink = await findFirstSymlink(backupRoot);
|
|
128
|
+
if (backupSymlink) {
|
|
129
|
+
throw new UnsafeManagedWriteError(backupRoot, backupSymlink, 'block');
|
|
130
|
+
}
|
|
131
|
+
await fs.mkdir(backupRoot, { recursive: true });
|
|
132
|
+
|
|
133
|
+
const backupDir = path.join(backupRoot, backupEntryName(symlink.path));
|
|
134
|
+
await fs.mkdir(backupDir, { recursive: false });
|
|
135
|
+
const snapshotPath = path.join(backupDir, 'snapshot');
|
|
136
|
+
const targetStat = symlink.dangling ? null : await lstatOrNull(symlink.resolvedTarget);
|
|
137
|
+
const isDirectTarget = path.resolve(targetPath) === path.resolve(symlink.path);
|
|
138
|
+
if (targetStat?.isFile() && !isDirectTarget) {
|
|
139
|
+
throw new Error(`Cannot materialize ${symlink.path}: it resolves to a file but is an ancestor of ${targetPath}.`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let materializedType = isDirectTarget
|
|
143
|
+
? (options.managedWriteTargetType ?? 'file')
|
|
144
|
+
: 'directory';
|
|
145
|
+
if (targetStat) {
|
|
146
|
+
materializedType = await copyPath(symlink.resolvedTarget, snapshotPath, targetStat);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const metadata = {
|
|
150
|
+
protocolVersion: INSTALL_SAFETY_PROTOCOL_VERSION,
|
|
151
|
+
createdAt: new Date().toISOString(),
|
|
152
|
+
targetPath: path.resolve(targetPath),
|
|
153
|
+
symlinkPath: symlink.path,
|
|
154
|
+
linkTarget: symlink.linkTarget,
|
|
155
|
+
resolvedTarget: symlink.resolvedTarget,
|
|
156
|
+
dangling: symlink.dangling,
|
|
157
|
+
directTarget: isDirectTarget,
|
|
158
|
+
materializedType,
|
|
159
|
+
snapshotPath: targetStat ? snapshotPath : null,
|
|
160
|
+
};
|
|
161
|
+
await fs.writeFile(path.join(backupDir, 'metadata.json'), `${JSON.stringify(metadata, null, 2)}\n`, 'utf8');
|
|
162
|
+
|
|
163
|
+
const stagingPath = path.join(
|
|
164
|
+
path.dirname(symlink.path),
|
|
165
|
+
`.${path.basename(symlink.path)}.openprd-materialize-${crypto.randomBytes(6).toString('hex')}`,
|
|
166
|
+
);
|
|
167
|
+
if (targetStat) {
|
|
168
|
+
await copyPath(snapshotPath, stagingPath, targetStat);
|
|
169
|
+
} else if (!isDirectTarget) {
|
|
170
|
+
await fs.mkdir(stagingPath, { recursive: false });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const currentStat = await lstatOrNull(symlink.path);
|
|
174
|
+
if (!currentStat?.isSymbolicLink() || await fs.readlink(symlink.path) !== symlink.linkTarget) {
|
|
175
|
+
await fs.rm(stagingPath, { recursive: true, force: true }).catch(() => {});
|
|
176
|
+
throw new Error(`Symlink changed while preparing managed write: ${symlink.path}.`);
|
|
177
|
+
}
|
|
178
|
+
await fs.unlink(symlink.path);
|
|
179
|
+
if (targetStat || !isDirectTarget) {
|
|
180
|
+
await fs.rename(stagingPath, symlink.path);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const event = {
|
|
184
|
+
protocolVersion: INSTALL_SAFETY_PROTOCOL_VERSION,
|
|
185
|
+
strategy: 'materialize',
|
|
186
|
+
targetPath: path.resolve(targetPath),
|
|
187
|
+
symlinkPath: symlink.path,
|
|
188
|
+
linkTarget: symlink.linkTarget,
|
|
189
|
+
resolvedTarget: symlink.resolvedTarget,
|
|
190
|
+
backupPath: backupDir,
|
|
191
|
+
materializedType,
|
|
192
|
+
at: metadata.createdAt,
|
|
193
|
+
};
|
|
194
|
+
if (Array.isArray(options.installSafetyEvents)) {
|
|
195
|
+
options.installSafetyEvents.push(event);
|
|
196
|
+
}
|
|
197
|
+
return event;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function prepareManagedWriteTarget(targetPath, options = {}) {
|
|
201
|
+
const absoluteTarget = path.resolve(targetPath);
|
|
202
|
+
const strategy = normalizeSymlinkStrategy(options.symlinkStrategy);
|
|
203
|
+
const materializations = [];
|
|
204
|
+
for (let index = 0; index < 32; index += 1) {
|
|
205
|
+
const symlink = await findFirstSymlink(absoluteTarget, options);
|
|
206
|
+
if (!symlink) {
|
|
207
|
+
return {
|
|
208
|
+
ok: true,
|
|
209
|
+
protocolVersion: INSTALL_SAFETY_PROTOCOL_VERSION,
|
|
210
|
+
strategy,
|
|
211
|
+
targetPath: absoluteTarget,
|
|
212
|
+
materializations,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (strategy === 'block') {
|
|
216
|
+
throw new UnsafeManagedWriteError(absoluteTarget, symlink, strategy);
|
|
217
|
+
}
|
|
218
|
+
materializations.push(await materializeSymlink(absoluteTarget, symlink, options));
|
|
219
|
+
}
|
|
220
|
+
throw new Error(`Managed write path contains too many nested symlinks: ${absoluteTarget}.`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function safeEnsureDirectory(directoryPath, options = {}) {
|
|
224
|
+
await prepareManagedWriteTarget(directoryPath, { ...options, managedWriteTargetType: 'directory' });
|
|
225
|
+
await fs.mkdir(directoryPath, { recursive: true });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function safeWriteText(filePath, text, options = {}) {
|
|
229
|
+
await prepareManagedWriteTarget(filePath, { ...options, managedWriteTargetType: 'file' });
|
|
230
|
+
await safeEnsureDirectory(path.dirname(filePath), options);
|
|
231
|
+
await fs.writeFile(filePath, text, 'utf8');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function safeAppendText(filePath, text, options = {}) {
|
|
235
|
+
await prepareManagedWriteTarget(filePath, { ...options, managedWriteTargetType: 'file' });
|
|
236
|
+
await safeEnsureDirectory(path.dirname(filePath), options);
|
|
237
|
+
await fs.appendFile(filePath, text, 'utf8');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export {
|
|
241
|
+
INSTALL_SAFETY_PROTOCOL_VERSION,
|
|
242
|
+
UnsafeManagedWriteError,
|
|
243
|
+
findFirstSymlink,
|
|
244
|
+
normalizeSymlinkStrategy,
|
|
245
|
+
prepareManagedWriteTarget,
|
|
246
|
+
safeAppendText,
|
|
247
|
+
safeEnsureDirectory,
|
|
248
|
+
safeWriteText,
|
|
249
|
+
};
|