aura-basetools 0.0.0-stage → 0.7.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.
- package/LICENSE +21 -0
- package/README.md +26 -2
- package/THIRD-PARTY-NOTICES.md +9 -0
- package/bin/aura-mcp.mjs +424 -0
- package/bin/aura.mjs +396 -0
- package/lib/fs-provider.mjs +355 -0
- package/lib/observation-store.mjs +236 -0
- package/lib/ops.mjs +498 -0
- package/lib/transaction-store.mjs +350 -0
- package/lib/workspace-lock.mjs +130 -0
- package/locator-lab/anchor-core.mjs +586 -0
- package/locator-lab/args.mjs +491 -0
- package/locator-lab/handle-core.mjs +207 -0
- package/locator-lab/locator-api.mjs +921 -0
- package/locator-lab/read-core.mjs +3013 -0
- package/locator-lab/repo-map.mjs +768 -0
- package/locator-lab/struct-core.mjs +3026 -0
- package/locator-lab/write-path.mjs +3446 -0
- package/package.json +50 -4
- package/test/standalone-test.mjs +1575 -0
package/bin/aura.mjs
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aura CLI(薄壳):参数解析 → `lib/ops.mjs` → 一行 JSON。
|
|
4
|
+
*
|
|
5
|
+
* aura read --file F [--mode …] [--around X] [--lines a,b] [--search S] [--since V] [--max-chars N]
|
|
6
|
+
* aura edit --file F (--hunks '<json>' | --hunks-file P) [--dry-run] [--create <文本|@文件>]
|
|
7
|
+
* aura read --targets '[...]|@spec.json' 一次读多个目标(与 --file 互斥)
|
|
8
|
+
* aura edit --targets '[...]|@spec.json' 一次改多个文件(与 --file 互斥;任一规划失败 ⇒ 零写入)
|
|
9
|
+
* aura undo [--id <事务 id>] [--force] 撤销最近一次/指定写事务
|
|
10
|
+
* aura transactions 列出当前工作区写事务
|
|
11
|
+
* aura mcp
|
|
12
|
+
* aura --help | --version
|
|
13
|
+
*
|
|
14
|
+
* 退出码:0 成功;2 结构化拒绝(回执带 code/next 一类信息);1 用法错误。
|
|
15
|
+
*/
|
|
16
|
+
import fs from 'node:fs'
|
|
17
|
+
import process from 'node:process'
|
|
18
|
+
import {
|
|
19
|
+
openProvider,
|
|
20
|
+
readEnvelope,
|
|
21
|
+
editEnvelope,
|
|
22
|
+
flatHunk,
|
|
23
|
+
// 多目标(fs-contract-v2)的引擎绑定:与单目标同一处(`lib/ops.mjs`),bin 里不另找引擎。
|
|
24
|
+
readManyWith,
|
|
25
|
+
editManyEnvelope,
|
|
26
|
+
undoEnvelope,
|
|
27
|
+
transactionsEnvelope,
|
|
28
|
+
assertReadTargets,
|
|
29
|
+
assertEditTargets,
|
|
30
|
+
assertKnownArgs,
|
|
31
|
+
errorEvidence,
|
|
32
|
+
} from '../lib/ops.mjs'
|
|
33
|
+
// `flatHunk` 是 `locator-lab/args.mjs` 里那一份容错规则(DSH 壳与 CLI 共用),但**不在这里定位引擎**:
|
|
34
|
+
// 引擎由 `lib/ops.mjs` 一处解析(`engineBase`),两种布局各一个候选。CLI 自己也写一条
|
|
35
|
+
// `../locator-lab/…` 就等于又多一个只在摊平布局下成立的引用——src 布局会因此照旧 ERR_MODULE_NOT_FOUND。
|
|
36
|
+
|
|
37
|
+
// 版本从包自己的 package.json 读(写死过一次,对外报的版本就停在旧值);开发树里取不到报 dev
|
|
38
|
+
const VERSION = (() => {
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version
|
|
41
|
+
} catch {
|
|
42
|
+
return 'dev'
|
|
43
|
+
}
|
|
44
|
+
})()
|
|
45
|
+
const argv = process.argv.slice(2)
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
/**
|
|
49
|
+
* 本 CLI 的**布尔旗标**名单。
|
|
50
|
+
*
|
|
51
|
+
* 为什么必须有这份名单:`flag()` 原本把“下一个参数以 `--` 开头”一律当成“这个旗标没带值”,
|
|
52
|
+
* 于是 `--create "---\ntitle: x"`(YAML front matter、`--` 注释横幅这类内容)**值被静默丢掉**,
|
|
53
|
+
* 跟着报的错是 `PATCH_NOT_FOUND … to create it, pass `create``——传了 create 却被叫去传 create。
|
|
54
|
+
* 实测复现于 0.5.3(T6 第 3 条)。
|
|
55
|
+
*
|
|
56
|
+
* 有了这份名单,“下一个是已知布尔旗标”才当没带值,**其它以 `--` 开头的东西都当值**。
|
|
57
|
+
* 代价是名单要维护;收益是那个静默丢值的路径从根上消失,而不是靠叫用户改写法。
|
|
58
|
+
*/
|
|
59
|
+
const BOOLEAN_FLAGS = new Set(['pretty', 'dry-run', 'transaction', 'force'])
|
|
60
|
+
const isBooleanFlag = (token) => typeof token === 'string' && BOOLEAN_FLAGS.has(token.replace(/^--/, ''))
|
|
61
|
+
|
|
62
|
+
const flag = (name, fallback = undefined) => {
|
|
63
|
+
const i = argv.indexOf(`--${name}`)
|
|
64
|
+
if (i >= 0) {
|
|
65
|
+
const v = argv[i + 1]
|
|
66
|
+
return v === undefined || isBooleanFlag(v) ? true : v
|
|
67
|
+
}
|
|
68
|
+
const eq = argv.find((a) => a.startsWith(`--${name}=`))
|
|
69
|
+
return eq === undefined ? fallback : eq.slice(name.length + 3)
|
|
70
|
+
}
|
|
71
|
+
const has = (name) => argv.includes(`--${name}`)
|
|
72
|
+
const emit = (obj, code) => {
|
|
73
|
+
process.stdout.write(`${has('pretty') ? JSON.stringify(obj, null, 2) : JSON.stringify(obj)}\n`)
|
|
74
|
+
process.exit(code)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const USAGE = [
|
|
78
|
+
`aura ${VERSION} —— 按意图读写文件(独立版:不依赖 DeepSeek Harness,Windows / macOS / Linux 同形)`,
|
|
79
|
+
'',
|
|
80
|
+
' aura read --file <路径> [--mode map|digest|full|around|lines|search|changed]',
|
|
81
|
+
' [--around <符号>] [--lines <a,b>|<a-b>|<n>] [--start-line <n> --end-line <n>]',
|
|
82
|
+
' [--search <子串>] [--since <令牌>] [--max-chars <n>]',
|
|
83
|
+
// `--file` 指向**目录**时直接给一张仓库地图(模块/目录/依赖方向),不需要额外旗标。
|
|
84
|
+
' (--file 传目录 ⇒ 仓库地图:模块清单 + 目录骨架 + 被依赖最多的模块 + 入口点)',
|
|
85
|
+
' aura read --targets <json|@文件> # 一次读多个目标(≤32 项;与 --file 互斥)',
|
|
86
|
+
' aura edit --targets <json|@文件> [--dry-run] # 一次改多个文件(先全量规划,任一失败则零写入)',
|
|
87
|
+
" aura edit --file <路径> --create <内容|@文件> # 建文件(不必再给 --hunks)",
|
|
88
|
+
" aura edit --file <路径> (--hunks '<json>' | --hunks-file <路径>) [--dry-run]",
|
|
89
|
+
' aura edit --file <路径> --lines <a,b> --replace <文本> [--expect <文本>] [--near <n>]',
|
|
90
|
+
" (只改一处时不必包成 JSON:--target / --lines / --start-line+--end-line / --line-cue / --anchor 任选其一)",
|
|
91
|
+
// 句柄(fs-contract-v2 §5):读侧发的柄 + 三种定位写法。下面三行是三种**用法**,不是三种键。
|
|
92
|
+
' aura edit --file <路径> --handle <h…> --replace <文本> # 整段替换:区间由柄定,不靠转述',
|
|
93
|
+
' aura edit --file <路径> --handle <h…> --line <段内行号> --replace <文本> # 只换区间内第 k 行(1-based,从区间首行数起)',
|
|
94
|
+
' aura edit --file <路径> --handle <h…> --position before|after --replace <文本> # 在区间边界插入,区间本身不动',
|
|
95
|
+
" (--line / --position 是从属键:只有与 --handle 同时给才成立,单独给不构成一次编辑)",
|
|
96
|
+
' aura mcp 启动 MCP stdio 服务',
|
|
97
|
+
' aura --help | --version',
|
|
98
|
+
'',
|
|
99
|
+
'hunks 形状:[{"target":"符号名"|"lines":[a,b]|"line_cue":"唯一词"|"anchor":"记得的原文",',
|
|
100
|
+
' "replace":"替换文本","expect":"该处应是什么","near":行号}]',
|
|
101
|
+
'',
|
|
102
|
+
'例:',
|
|
103
|
+
" aura edit --file a.mjs --hunks '[{\"lines\":[5,5],\"replace\":\"x\"}]' # 单引号包住整段 JSON",
|
|
104
|
+
' aura edit --file a.mjs --hunks-file hunks.json # 内容走文件,绕开引号',
|
|
105
|
+
' aura edit --file new.md --create @body.md # @文件 ——推荐给长内容',
|
|
106
|
+
' aura edit --file new.md --create=--- # 值以 -- 开头时必须用 = 形式',
|
|
107
|
+
' aura read --file ./src # 目录 ⇒ 仓库地图',
|
|
108
|
+
'',
|
|
109
|
+
'PowerShell 两个已踩过的坑:',
|
|
110
|
+
' · 反引号 `n 写进 JSON 字符串会变成控制字符(JSON 不允许)⇒ 用 --hunks-file 或单引号 heredoc;',
|
|
111
|
+
' · 5.1 的 Set-Content -Encoding UTF8 默认写 BOM ⇒ 本工具会剥掉前导 BOM,但更早的工具链可能不认。',
|
|
112
|
+
'加 --pretty 输出多行 JSON。先 read 拿令牌,再 edit——编辑会核对“这块区域在你读之后有没有被改过”。',
|
|
113
|
+
].join('\n')
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* `--targets <spec>`:一次给多个目标(fs-contract-v2 §2.1/§2.2)。
|
|
117
|
+
*
|
|
118
|
+
* 取值形状与 `--hunks` **同形**:字面 JSON,或 `@文件`(`@` 前缀读文件、剥掉前导 BOM ——
|
|
119
|
+
* 理由与 `--hunks-file` 的注释同一条:PowerShell 5.1 的 `Set-Content -Encoding UTF8` 默认写 BOM,
|
|
120
|
+
* 而那个字符用户看不见)。spec 是 `[...]`(每个元素就是单目标那套参数),
|
|
121
|
+
* 或 `{"targets": [...], "dryRun": false}`。
|
|
122
|
+
*
|
|
123
|
+
* **形状判定一条都不在这里**:数组 / 上限 32 / 空批 / 逐项路径 / 与单目标形状互斥,全部由引擎
|
|
124
|
+
* (`locator-lab/args.mjs` 的 `assertReadTargets` / `assertEditTargets`)判 —— 两个调用面各写
|
|
125
|
+
* 一份必然漂开。本函数只做三件事:把 `targets` 取出来、把 `--file` 作为 `file_path` 一并递进去
|
|
126
|
+
* (于是“两个都给”由那条唯一判据说话)、把引擎抛的参数面错接到 exit 1 那一档。
|
|
127
|
+
*
|
|
128
|
+
* @param assertShape - 读侧 `assertReadTargets`、写侧 `assertEditTargets`(同一个判据,两套提示语)
|
|
129
|
+
* @param toolName - 回执里点名的工具(`aura_read` / `aura_edit`)
|
|
130
|
+
* @returns `{ targets, dryRun }`(逐项判决在引擎里,这里只递参数)
|
|
131
|
+
*/
|
|
132
|
+
const engineTargets = (assertShape, toolName) => {
|
|
133
|
+
const raw = flag('targets')
|
|
134
|
+
const where = typeof raw === 'string' && raw.startsWith('@') ? `--targets @${raw.slice(1)}` : '--targets'
|
|
135
|
+
if (typeof raw !== 'string') {
|
|
136
|
+
emit(
|
|
137
|
+
{
|
|
138
|
+
ok: false,
|
|
139
|
+
error: '--targets 需要跟一个 JSON 字符串,或 `@文件`(例:--targets \'[{"file_path":"a.mjs","mode":"digest"}]\')',
|
|
140
|
+
code: 'USAGE',
|
|
141
|
+
},
|
|
142
|
+
1,
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
let text = raw
|
|
146
|
+
if (raw.startsWith('@')) {
|
|
147
|
+
try {
|
|
148
|
+
text = fs.readFileSync(raw.slice(1), 'utf8')
|
|
149
|
+
} catch (error) {
|
|
150
|
+
emit({ ok: false, error: `${where} 读不到:${error.message}`, code: 'USAGE' }, 1)
|
|
151
|
+
}
|
|
152
|
+
text = text.replace(/^\uFEFF/, '')
|
|
153
|
+
}
|
|
154
|
+
let spec
|
|
155
|
+
try {
|
|
156
|
+
spec = JSON.parse(text)
|
|
157
|
+
} catch (error) {
|
|
158
|
+
emit(
|
|
159
|
+
{
|
|
160
|
+
ok: false,
|
|
161
|
+
error:
|
|
162
|
+
`${where} 不是合法 JSON:${error.message};接受 \`[...]\`(每个元素就是单目标那套参数),或 \`{"targets": [...]}\`` +
|
|
163
|
+
(raw.startsWith('@') ? '' : ';内容含引号/换行时用 `--targets @文件`'),
|
|
164
|
+
code: 'USAGE',
|
|
165
|
+
},
|
|
166
|
+
1,
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
if (spec === null || typeof spec !== 'object') {
|
|
170
|
+
emit({ ok: false, error: `${where} 需要数组或对象,收到 ${JSON.stringify(spec)};接受 \`[...]\` 或 \`{"targets": [...]}\``, code: 'USAGE' }, 1)
|
|
171
|
+
}
|
|
172
|
+
const array = Array.isArray(spec)
|
|
173
|
+
const source = array ? { targets: spec } : { ...spec }
|
|
174
|
+
if (!array) {
|
|
175
|
+
// 顶层不认识的键不许静默忽略:`{"targets":…,"dryrun":true}` 会变成“以为 dry-run 了、其实真写盘”。
|
|
176
|
+
// 合法键名单与报错方式都交给引擎那份守卫(判据只有一处)。
|
|
177
|
+
try {
|
|
178
|
+
assertKnownArgs(spec, ['targets', 'dryRun'], toolName)
|
|
179
|
+
} catch (error) {
|
|
180
|
+
refuseShape(where, error)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (typeof flag('file') === 'string') source.file_path = flag('file')
|
|
184
|
+
let targets
|
|
185
|
+
try {
|
|
186
|
+
targets = assertShape(source, { toolName })
|
|
187
|
+
} catch (error) {
|
|
188
|
+
refuseShape(where, error)
|
|
189
|
+
}
|
|
190
|
+
if (targets === undefined) {
|
|
191
|
+
emit({ ok: false, error: `${where} 里没有 targets 数组:接受 \`[...]\`(每个元素就是单目标那套参数),或 \`{"targets": [...]}\``, code: 'USAGE' }, 1)
|
|
192
|
+
}
|
|
193
|
+
return { targets, dryRun: !array && spec.dryRun === true }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 引擎的形状判据抛错 ⇒ **参数面** 拒绝(exit 1,与既有 `USAGE` 档一致)。
|
|
198
|
+
*
|
|
199
|
+
* 引擎自己那个码放在 `shape` 里原样带出来(`ARGS_TARGETS_LIMIT` / `ARGS_SHAPE_EXCLUSIVE` …):
|
|
200
|
+
* 退出码那一档是本 CLI 的既有档位,而“是哪条判据拒的”必须能机读 —— 两者各归各的字段。
|
|
201
|
+
*/
|
|
202
|
+
const refuseShape = (where, error) =>
|
|
203
|
+
emit(
|
|
204
|
+
{
|
|
205
|
+
ok: false,
|
|
206
|
+
error: `${where}:${String(error?.message ?? error)}`,
|
|
207
|
+
code: 'USAGE',
|
|
208
|
+
...(typeof error?.code === 'string' ? { shape: error.code } : {}),
|
|
209
|
+
...errorEvidence(error),
|
|
210
|
+
},
|
|
211
|
+
1,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
const command = argv[0]
|
|
215
|
+
if (command === undefined || command === '--help' || command === '-h') emit({ ok: true, usage: USAGE }, 0)
|
|
216
|
+
if (command === '--version' || command === '-v') emit({ ok: true, version: VERSION }, 0)
|
|
217
|
+
if (command === 'mcp') {
|
|
218
|
+
const { serve } = await import('./aura-mcp.mjs')
|
|
219
|
+
await serve()
|
|
220
|
+
process.exit(0)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const provider = openProvider(typeof flag('cwd') === 'string' ? flag('cwd') : process.cwd())
|
|
224
|
+
|
|
225
|
+
if (command === 'undo') {
|
|
226
|
+
const env = await undoEnvelope(provider, {
|
|
227
|
+
id: typeof flag('id') === 'string' ? flag('id') : undefined,
|
|
228
|
+
force: has('force'),
|
|
229
|
+
})
|
|
230
|
+
emit(env, env.ok ? 0 : 2)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (command === 'transactions') {
|
|
234
|
+
const env = transactionsEnvelope(provider)
|
|
235
|
+
emit(env, 0)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (command === 'read' || command === 'outline') {
|
|
239
|
+
/**
|
|
240
|
+
* 多目标(fs-contract-v2 §2.1):`--targets` 在场 ⇒ 引擎的批入口。
|
|
241
|
+
* `--file` 那条路一个字不动(就在下面,含它自己的 `USAGE` 与 `outline ⇒ map`)。
|
|
242
|
+
*/
|
|
243
|
+
if (has('targets')) {
|
|
244
|
+
const { targets } = engineTargets(assertReadTargets, 'aura_read')
|
|
245
|
+
const env = await readManyWith(provider, targets)
|
|
246
|
+
emit(env, env.ok ? 0 : 2)
|
|
247
|
+
}
|
|
248
|
+
const file = flag('file')
|
|
249
|
+
if (typeof file !== 'string') emit({ ok: false, error: 'read 需要 --file <路径>', code: 'USAGE' }, 1)
|
|
250
|
+
// 行区间**原样**交给收敛层(locator-lab/args.mjs):`--lines 5`、`--lines 5,8`、`--lines 5-8`
|
|
251
|
+
// 与 `--start-line/--end-line` 都认,解不出来时由它回一句"接受哪些写法"。
|
|
252
|
+
// 旧写法在这里自己用正则挑 `a,b`,其余情况**静默退回 auto** —— 用户以为读了第 5 行,
|
|
253
|
+
// 实际读了别的(与 0.4.1 修掉的那条 MCP/CLI 形状 bug 同一类:宁可报错,不可默默换语义)。
|
|
254
|
+
// `await` 不能省:`readEnvelope` 现在是 async(目录路径要走仓库地图)。
|
|
255
|
+
// 漏掉它的后果不是报错而是**谎言**——`JSON.stringify(Promise)` 是 `{}`,
|
|
256
|
+
// 而退出码由 `env.ok` 决定也变成 undefined⇒2:看起来像“结构化拒绝”,实际什么都没发生。
|
|
257
|
+
const env = await readEnvelope(provider, {
|
|
258
|
+
file,
|
|
259
|
+
mode: typeof flag('mode') === 'string' ? flag('mode') : command === 'outline' ? 'map' : undefined,
|
|
260
|
+
around: flag('around'),
|
|
261
|
+
lines: flag('lines'),
|
|
262
|
+
start_line: flag('start-line'),
|
|
263
|
+
end_line: flag('end-line'),
|
|
264
|
+
search: flag('search'),
|
|
265
|
+
since: flag('since'),
|
|
266
|
+
// 光写 `--max-chars` 不带值时 flag() 回 true,Number(true)=1 会变成"预算 1 字符"这种怪值
|
|
267
|
+
maxChars: typeof flag('max-chars') === 'string' ? Number(flag('max-chars')) : undefined,
|
|
268
|
+
})
|
|
269
|
+
emit(env, env.ok ? 0 : 2)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (command === 'edit') {
|
|
273
|
+
/**
|
|
274
|
+
* 多目标(fs-contract-v2 §2.2):`--targets` 在场 ⇒ 引擎的三阶段批入口
|
|
275
|
+
* (全量规划 → 全部可落才提交 → 逐文件回读校验)。`--file` 那条路一个字不动(就在下面)。
|
|
276
|
+
*
|
|
277
|
+
* `dryRun` 两个来源都认:spec 里的 `dryRun`(与 DSH 壳同名)与既有的 `--dry-run` 旗标。
|
|
278
|
+
*/
|
|
279
|
+
if (has('targets')) {
|
|
280
|
+
const spec = engineTargets(assertEditTargets, 'aura_edit')
|
|
281
|
+
const env = await editManyEnvelope(provider, {
|
|
282
|
+
targets: spec.targets,
|
|
283
|
+
dryRun: has('dry-run') || spec.dryRun,
|
|
284
|
+
includeTransaction: has('transaction'),
|
|
285
|
+
})
|
|
286
|
+
emit(env, env.ok ? 0 : 2)
|
|
287
|
+
}
|
|
288
|
+
const file = flag('file')
|
|
289
|
+
if (typeof file !== 'string') emit({ ok: false, error: 'edit 需要 --file <路径>', code: 'USAGE' }, 1)
|
|
290
|
+
/**
|
|
291
|
+
* 单个 hunk 也可以**平铺成旗标**:`--lines 5,8 --replace 'X'`。
|
|
292
|
+
*
|
|
293
|
+
* 与 DSH 壳那边容错同一个形状(那边实测 27 次失败是 `{file_path,start_line,end_line,replace}`)。
|
|
294
|
+
* CLI 这边本来就是人/脚本在用,但“一个 hunk 不必先包成 JSON 数组”对脚本更友好:
|
|
295
|
+
* 现在 `--lines 5 --replace X` 就能改一行,不必再手写转义过的 JSON。
|
|
296
|
+
* 两边**共用同一个 `flatHunk`**(引擎里),不是各写一份容错规则。
|
|
297
|
+
*/
|
|
298
|
+
const flat = flatHunk({
|
|
299
|
+
target: flag('target'),
|
|
300
|
+
lines: flag('lines'),
|
|
301
|
+
start_line: flag('start-line'),
|
|
302
|
+
end_line: flag('end-line'),
|
|
303
|
+
line_cue: flag('line-cue'),
|
|
304
|
+
anchor: flag('anchor'),
|
|
305
|
+
// 句柄(fs-contract-v2 / HD2):`handle` 是地址键,`line` / `position` 是它的**从属键**。
|
|
306
|
+
// 后两个照 `target`/`lines`/`line_cue`/`anchor` 同形递进去(旗标值就是字符串,`line` 由引擎的
|
|
307
|
+
// `toLineNumber` 收敛)。**这里不判“单独给算不算地址”**——那是 `FLAT_ADDRESS_KEYS` /
|
|
308
|
+
// `HANDLE_SHAPE_KEYS` 的语义(`flatHunk` 只看地址键),CLI 另判一份就是第二份判据。
|
|
309
|
+
handle: flag('handle'),
|
|
310
|
+
line: flag('line'),
|
|
311
|
+
position: flag('position'),
|
|
312
|
+
replace: flag('replace'),
|
|
313
|
+
expect: flag('expect'),
|
|
314
|
+
// 旗标值天然是字符串:`--near 42` ⇒ `'42'`。收敛层只认整数,所以在这里转一次
|
|
315
|
+
near: typeof flag('near') === 'string' && flag('near').trim() !== '' ? Number(flag('near')) : flag('near'),
|
|
316
|
+
})
|
|
317
|
+
const hunksFile = typeof flag('hunks-file') === 'string' ? flag('hunks-file') : undefined
|
|
318
|
+
/**
|
|
319
|
+
* 读 hunks 来源,并把错误报到**真正的来源**上。
|
|
320
|
+
*
|
|
321
|
+
* 两个实测过的坑(T6 第 2 条):
|
|
322
|
+
* 1. **BOM 会把干净的 JSON 打死**:PowerShell 5.1 的 `Set-Content -Encoding UTF8` 默认写 BOM,
|
|
323
|
+
* 于是 `JSON.parse` 报 `Unexpected token '\uFEFF'`。这是**工具可以自己处理**的事——剥掉前导 BOM 就好,
|
|
324
|
+
* 没有理由把这个转嫁给用户(何况他看不见那个字符)。
|
|
325
|
+
* 2. 旧代码无论来源是哪个都报 `--hunks 不是合法 JSON`:错报来源会把排查引到错的方向(他压根没用 --hunks)。
|
|
326
|
+
*/
|
|
327
|
+
const readHunksSource = () => {
|
|
328
|
+
if (hunksFile !== undefined) {
|
|
329
|
+
let text
|
|
330
|
+
try {
|
|
331
|
+
text = fs.readFileSync(hunksFile, 'utf8')
|
|
332
|
+
} catch (error) {
|
|
333
|
+
emit({ ok: false, error: `--hunks-file 读不到 ${hunksFile}:${error.message}`, code: 'USAGE' }, 1)
|
|
334
|
+
}
|
|
335
|
+
return { text: text.replace(/^\uFEFF/, ''), where: `--hunks-file ${hunksFile}` }
|
|
336
|
+
}
|
|
337
|
+
return { text: flag('hunks'), where: '--hunks' }
|
|
338
|
+
}
|
|
339
|
+
const from = hunksFile === undefined && flag('hunks') === undefined ? null : readHunksSource()
|
|
340
|
+
let hunks = flat === null ? null : [flat]
|
|
341
|
+
if (from !== null) {
|
|
342
|
+
if (typeof from.text !== 'string') emit({ ok: false, error: `${from.where} 需要跟一个 JSON 字符串`, code: 'USAGE' }, 1)
|
|
343
|
+
try {
|
|
344
|
+
hunks = JSON.parse(from.text)
|
|
345
|
+
} catch (error) {
|
|
346
|
+
emit(
|
|
347
|
+
{
|
|
348
|
+
ok: false,
|
|
349
|
+
error:
|
|
350
|
+
`${from.where} 不是合法 JSON:${error.message}` +
|
|
351
|
+
// 给出两条真实出路:内容走文件绕开命令行引号,或先 read 拿形状。
|
|
352
|
+
// 不说的话,用户只能反复调引号——而那正是 PowerShell 下最容易撞的坑。
|
|
353
|
+
';若内容里含引号/换行,优先用 `--hunks-file <路径>`(也接受平铺旗标 `--lines 5 --replace X`);' +
|
|
354
|
+
'写文件时注意别写 UTF-8 BOM(本工具会剥掉前导 BOM,但更早的工具链可能不认)',
|
|
355
|
+
code: 'USAGE',
|
|
356
|
+
},
|
|
357
|
+
1,
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
if (!Array.isArray(hunks)) emit({ ok: false, error: `${from.where} 必须是数组`, code: 'USAGE' }, 1)
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* `create` **可以单独用**(不要求 hunks),与 DSH 壳看齐。
|
|
364
|
+
*
|
|
365
|
+
* 旧行为:`edit --file F --create "hello"` 报 `edit 需要 --hunks/--hunks-file`——
|
|
366
|
+
* 一句话里有两个错:真正在做的事是**建文件**(不需要 hunks),而报错让人以为“还得多给一个参数”。
|
|
367
|
+
* DSH 壳早就修过同一个坑(`aura-edit/index.js`:`create` 在时 hunks 可为空,
|
|
368
|
+
* 且注释原文写着“第一版把‘hunks 非空’当无条件校验,于是创建被自己的校验挡住”)。
|
|
369
|
+
* 这里只是把同一件事在 CLI 面补上;`--hunks '[]' --create` 这种 workaround 仍照旧可用。
|
|
370
|
+
*/
|
|
371
|
+
const creating = typeof flag('create') === 'string'
|
|
372
|
+
if (hunks === null && !creating) {
|
|
373
|
+
emit(
|
|
374
|
+
{
|
|
375
|
+
ok: false,
|
|
376
|
+
error:
|
|
377
|
+
'edit 需要 --hunks/--hunks-file,或平铺一组地址旗标(--target / --lines / --start-line+--end-line / --line-cue / --anchor / --handle(配 --line 或 --position),外加 --replace),或 --create <整份内容>',
|
|
378
|
+
code: 'USAGE',
|
|
379
|
+
},
|
|
380
|
+
1,
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
const createRaw = flag('create')
|
|
384
|
+
// `@文件` 形式:内容完全不经过命令行引号。创建含引号/换行/front matter 的内容时这是最稳的路。
|
|
385
|
+
const create = typeof createRaw === 'string' && createRaw.startsWith('@') ? fs.readFileSync(createRaw.slice(1), 'utf8') : createRaw
|
|
386
|
+
const env = await editEnvelope(provider, {
|
|
387
|
+
file,
|
|
388
|
+
hunks: hunks ?? [],
|
|
389
|
+
dryRun: has('dry-run'),
|
|
390
|
+
create,
|
|
391
|
+
includeTransaction: has('transaction'),
|
|
392
|
+
})
|
|
393
|
+
emit(env, env.ok ? 0 : 2)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
emit({ ok: false, error: `unknown command ${JSON.stringify(command)}(试 aura --help)`, code: 'USAGE' }, 1)
|