project-node-memory 1.1.0 → 1.1.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.
- package/README.md +24 -4
- package/README_EN.md +28 -3
- package/lib/engine/migrate.js +171 -0
- package/lib/host/prompt.js +1 -1
- package/lib/host/routes.js +9 -0
- package/lib/host/tools.js +28 -0
- package/package.json +1 -1
- package/skills/project-node-memory-guide/SKILL.md +1 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> 简体中文 · [English](README_EN.md)
|
|
4
4
|
|
|
5
|
-
把 **Markdown 目录当成知识库** 的 DSH 插件:为 Agent 提供
|
|
5
|
+
把 **Markdown 目录当成知识库** 的 DSH 插件:为 Agent 提供 10 个 `kb_*` 工具,为人类提供知识节点面板(侧栏入口名「项目节点知识库」)。
|
|
6
6
|
|
|
7
7
|
没有索引数据库、没有向量、没有后台进程。完整知识库位于项目下的 `.nodekb/` 目录——一堆 md 文件,删掉插件之后它仍然是普通 md。
|
|
8
8
|
|
|
@@ -92,7 +92,7 @@ config:
|
|
|
92
92
|
面板只读,数据走宿主侧注册的 `/nodekb/api/*`。写库就是写 md——用 `write` / `edit` 工具,或者 `kb_write`。
|
|
93
93
|
右栏正文单次最多渲染 10000 字符(超出显示截断提示);宽表格横向滚动,长文本纵向滚动。
|
|
94
94
|
|
|
95
|
-
##
|
|
95
|
+
## 10 个工具
|
|
96
96
|
|
|
97
97
|
| 工具 | 干什么 |
|
|
98
98
|
| --------------- | ---------------------------------------------------------------------------------------------- |
|
|
@@ -103,7 +103,8 @@ config:
|
|
|
103
103
|
| `kb_backlinks` | 谁引用了它。改 TID / 删节点前先跑 |
|
|
104
104
|
| `kb_init` | 建 `.nodekb` 骨架(幂等),铺出 `doc/ lessons/ todo/ index/` 与落位约定 |
|
|
105
105
|
| `kb_write` | 写库:`append` / `replace` / `create` / `node`(就地改写一个 `<node>` 块);写前校验、写后自检 |
|
|
106
|
-
| `kb_rename` | 文件改名 +
|
|
106
|
+
| `kb_rename` | 文件改名 + 全库引用一起改写;写前重读,别人刚改的不覆盖 |
|
|
107
|
+
| `kb_migrate` | 旧语法裸引用 `[[x]]` → `[[node="x"]]` / `[[tscl="x"]]`;**默认只演练**,`dry_run=false` 才落盘 |
|
|
107
108
|
| `kb_vcs_status` | 库内嵌 git 的快照历史与未提交改动(git 缺失时只说明,不报错) |
|
|
108
109
|
|
|
109
110
|
## 库怎么定位
|
|
@@ -183,10 +184,29 @@ config:
|
|
|
183
184
|
同一 `tscl` id 全库只能定义一次,且不能与节点 TID 同名。
|
|
184
185
|
旧写法 `[[TID]]` 仍兼容(有定义时当宏、否则当链接),`kb_diag` 会 warning,2.0 移除。
|
|
185
186
|
|
|
187
|
+
## 从 1.0 升到 1.1
|
|
188
|
+
|
|
189
|
+
数据格式没变——md 还是 md,TID 还是那些 TID,插件卸了照样能读。变化只有两条:
|
|
190
|
+
|
|
191
|
+
1. **`[[x]]` 现在只出链接**,不再把被引用节点的正文复制进来(要复制原文请显式写 `<tscl>`)。
|
|
192
|
+
好处是引用不再撑大渲染结果,位置型小节 `## 3.1` 也能点了。
|
|
193
|
+
2. **`kb_init` 不再建 `facts/`**。已有的 `facts/` 不会被动,`kb_read`/`kb_search` 照常;只是新库默认只有
|
|
194
|
+
`doc/ lessons/ todo/ index/`,事实改写成 `<tscl>` 定义放进它所属的 `doc/` 里。
|
|
195
|
+
|
|
196
|
+
老库要不要迁?**不迁也能用**,只是 `kb_diag` 会给每条裸引用一条 warning。要迁就跑:
|
|
197
|
+
|
|
198
|
+
```text
|
|
199
|
+
kb_migrate # 演练:报每个文件会改几处、哪些引用本来就悬空
|
|
200
|
+
kb_migrate dry_run=false # 确认后落盘
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
迁移只改写法(`[[doc:x]]` → `[[node="doc:x"]]`,命中定义则 `[[tscl="…"]]`),不动任何 TID、不搬文件,
|
|
204
|
+
所以引用关系与渲染结果都不变;围栏代码块内一律跳过。
|
|
205
|
+
|
|
186
206
|
## 自检(不装 DSH 也能跑)
|
|
187
207
|
|
|
188
208
|
```bash
|
|
189
|
-
node scripts/check.mjs #
|
|
209
|
+
node scripts/check.mjs # 290 项:清单契约 / 工具 / 路由 / 客户端槽位 / 改名改写 / 建库写库 / tscl / 迁移 / 提示词段落 / 随包技能
|
|
190
210
|
node test/parity.mjs # 与 Python 原实现逐条对拍(需要 python 与 nodekb 源码)
|
|
191
211
|
```
|
|
192
212
|
|
package/README_EN.md
CHANGED
|
@@ -89,7 +89,7 @@ The Markdown renderer is a small home-grown one (headings, lists, code, tables,
|
|
|
89
89
|
The panel is read-only; data flows through host routes under `/nodekb/api/*`. Writing a library means writing Markdown — via `write` / `edit`, or `kb_write`.
|
|
90
90
|
The right column renders at most 10,000 characters per view (with a truncation notice); wide tables scroll horizontally and long text scrolls vertically.
|
|
91
91
|
|
|
92
|
-
## The
|
|
92
|
+
## The ten tools
|
|
93
93
|
|
|
94
94
|
| Tool | What it does |
|
|
95
95
|
|---|---|
|
|
@@ -100,7 +100,8 @@ The right column renders at most 10,000 characters per view (with a truncation n
|
|
|
100
100
|
| `kb_backlinks` | Who references this node. Run before renaming a TID or deleting a node |
|
|
101
101
|
| `kb_init` | Scaffold `.nodekb` (idempotent): `doc/ lessons/ todo/ index/` plus placement conventions |
|
|
102
102
|
| `kb_write` | Write into the library: `append` / `replace` / `create` / `node` (rewrite one `<node>` block in place); validates before writing, self-checks after |
|
|
103
|
-
| `kb_rename` | Rename a file and rewrite every
|
|
103
|
+
| `kb_rename` | Rename a file and rewrite every reference pointing at it; re-reads before writing, so concurrent edits are never clobbered |
|
|
104
|
+
| `kb_migrate` | Rewrites legacy bare refs `[[x]]` into `[[node="x"]]` / `[[tscl="x"]]`; **dry-run by default**, pass `dry_run=false` to write |
|
|
104
105
|
| `kb_vcs_status` | Snapshot history and uncommitted changes of the library's embedded git repo (explains itself when git is missing) |
|
|
105
106
|
|
|
106
107
|
## How a library is located
|
|
@@ -162,10 +163,34 @@ as structure (a `##` there is not a heading); a `tscl` id may be defined exactly
|
|
|
162
163
|
collide with a node TID. The old `[[TID]]` form still works (macro when a definition exists, link
|
|
163
164
|
otherwise) with a `kb_diag` warning, and goes away in 2.0.
|
|
164
165
|
|
|
166
|
+
## Upgrading from 1.0
|
|
167
|
+
|
|
168
|
+
The data format is unchanged — Markdown stays Markdown, TIDs stay the same, and the library remains
|
|
169
|
+
readable after the plugin is removed. Only two things changed:
|
|
170
|
+
|
|
171
|
+
1. **`[[x]]` now renders a link only**; it no longer copies the referenced node's body into the
|
|
172
|
+
output (use an explicit `<tscl>` for that). References stop inflating rendered size, and
|
|
173
|
+
positional sections (`## 3.1`) become clickable.
|
|
174
|
+
2. **`kb_init` no longer creates `facts/`.** Existing `facts/` folders are untouched and still
|
|
175
|
+
readable/searchable; new libraries get `doc/ lessons/ todo/ index/` only, and facts become
|
|
176
|
+
`<tscl>` definitions inside the `doc/` file they belong to.
|
|
177
|
+
|
|
178
|
+
Do old libraries need migrating? **No** — they keep working, `kb_diag` just emits one warning per
|
|
179
|
+
bare reference. To migrate:
|
|
180
|
+
|
|
181
|
+
```text
|
|
182
|
+
kb_migrate # dry run: how many refs per file, which ones are already dangling
|
|
183
|
+
kb_migrate dry_run=false # write
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Migration only changes syntax (`[[doc:x]]` → `[[node="doc:x"]]`, or `[[tscl="…"]]` when the id is a
|
|
187
|
+
definition). No TID changes, no file moves, so references and rendered output stay identical; fenced
|
|
188
|
+
code blocks are skipped.
|
|
189
|
+
|
|
165
190
|
## Self-check (no DSH required)
|
|
166
191
|
|
|
167
192
|
```bash
|
|
168
|
-
node scripts/check.mjs #
|
|
193
|
+
node scripts/check.mjs # 290 checks: manifest contract / tools / routes / client slots / rename rewriting / scaffold + write / tscl / migration / prompt section / bundled skill
|
|
169
194
|
node test/parity.mjs # case-by-case parity against the Python implementation (needs python and nodekb sources)
|
|
170
195
|
```
|
|
171
196
|
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* migrate —— 把旧写法裸引用 `[[x]]` 迁成 1.1 的双语法。
|
|
3
|
+
*
|
|
4
|
+
* 规则:
|
|
5
|
+
* - x 是 tscl 定义 id → `[[tscl="x"]]`(值嵌入)
|
|
6
|
+
* - x 是节点 TID → `[[node="x"]]`(链接)
|
|
7
|
+
* - 两者都不是 → 原样保留,记进 unresolved(本来就悬空,迁移不该猜)
|
|
8
|
+
*
|
|
9
|
+
* 三条纪律与 rename 一致:围栏内不动、写前重读复核 mtime/size、默认只演练(dry_run)。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'node:fs'
|
|
13
|
+
import path from 'node:path'
|
|
14
|
+
import { ROOT_TID } from './mdlib.js'
|
|
15
|
+
|
|
16
|
+
const FENCE = '```'
|
|
17
|
+
const BARE_REF = /\[\[([^[\]"=\s][^[\]]*)\]\]/g
|
|
18
|
+
|
|
19
|
+
function absOf(lib, node) {
|
|
20
|
+
const raw = String(node.source || '').split('#')[0]
|
|
21
|
+
if (!raw) return ''
|
|
22
|
+
return path.isAbsolute(raw) ? path.normalize(raw) : path.join(lib.nodekb, raw)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function relOf(lib, abs) {
|
|
26
|
+
return path.relative(lib.nodekb, abs).split(path.sep).join('/')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function allMarkdown(nodekb) {
|
|
30
|
+
const out = []
|
|
31
|
+
const walk = (d) => {
|
|
32
|
+
let entries
|
|
33
|
+
try {
|
|
34
|
+
entries = fs.readdirSync(d, { withFileTypes: true })
|
|
35
|
+
} catch {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
for (const e of entries) {
|
|
39
|
+
if (e.name.startsWith('.')) continue
|
|
40
|
+
const p = path.join(d, e.name)
|
|
41
|
+
if (e.isDirectory()) walk(p)
|
|
42
|
+
else if (e.name.endsWith('.md')) out.push(p)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
walk(nodekb)
|
|
46
|
+
return out
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 纯函数:把一段 md 文本里的裸引用改写(可单测) */
|
|
50
|
+
export function migrateText(text, { isTid, isDef }) {
|
|
51
|
+
const out = []
|
|
52
|
+
const hits = { node: 0, tscl: 0, unresolved: [] }
|
|
53
|
+
let fenced = false
|
|
54
|
+
for (const line of String(text).split('\n')) {
|
|
55
|
+
if (line.trim().startsWith(FENCE)) {
|
|
56
|
+
fenced = !fenced
|
|
57
|
+
out.push(line)
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
if (fenced) {
|
|
61
|
+
out.push(line)
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
out.push(
|
|
65
|
+
line.replace(BARE_REF, (whole, inner) => {
|
|
66
|
+
const id = inner.trim()
|
|
67
|
+
if (isDef(id)) {
|
|
68
|
+
hits.tscl += 1
|
|
69
|
+
return `[[tscl="${id}"]]`
|
|
70
|
+
}
|
|
71
|
+
if (isTid(id)) {
|
|
72
|
+
hits.node += 1
|
|
73
|
+
return `[[node="${id}"]]`
|
|
74
|
+
}
|
|
75
|
+
hits.unresolved.push(id)
|
|
76
|
+
return whole
|
|
77
|
+
}),
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
return { text: out.join('\n'), hits }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 迁移整个库。
|
|
85
|
+
* @param {{nodekb:string, nodes:Map, defs?:Map}} lib openLibrary 的结果
|
|
86
|
+
* @param {{dryRun?:boolean}} [opts] 默认演练
|
|
87
|
+
*/
|
|
88
|
+
export function migrateLibrary(lib, opts = {}) {
|
|
89
|
+
const dryRun = opts.dryRun !== false
|
|
90
|
+
const defs = lib.defs || new Map()
|
|
91
|
+
const nodes = lib.nodes
|
|
92
|
+
const isTid = (id) => nodes.has(id)
|
|
93
|
+
const isDef = (id) => defs.has(id)
|
|
94
|
+
|
|
95
|
+
const report = { dry_run: dryRun, files: [], node: 0, tscl: 0, unresolved: [], skipped: [] }
|
|
96
|
+
const edits = []
|
|
97
|
+
const visited = new Set()
|
|
98
|
+
|
|
99
|
+
for (const node of nodes.values()) {
|
|
100
|
+
if (node.kind !== 'file') continue
|
|
101
|
+
const abs = absOf(lib, node)
|
|
102
|
+
if (!abs) continue
|
|
103
|
+
visited.add(path.resolve(abs))
|
|
104
|
+
let text
|
|
105
|
+
let st1
|
|
106
|
+
try {
|
|
107
|
+
st1 = fs.statSync(abs)
|
|
108
|
+
text = fs.readFileSync(abs, 'utf8')
|
|
109
|
+
} catch (e) {
|
|
110
|
+
report.skipped.push({ path: relOf(lib, abs), why: `读失败: ${e.message}` })
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
if (!text.includes('[[')) continue
|
|
114
|
+
const { text: next, hits } = migrateText(text, { isTid, isDef })
|
|
115
|
+
const changed = hits.node + hits.tscl
|
|
116
|
+
for (const u of hits.unresolved) report.unresolved.push({ path: relOf(lib, abs), ref: u })
|
|
117
|
+
if (!changed) continue
|
|
118
|
+
report.node += hits.node
|
|
119
|
+
report.tscl += hits.tscl
|
|
120
|
+
report.files.push({ path: relOf(lib, abs), count: changed, node: hits.node, tscl: hits.tscl })
|
|
121
|
+
edits.push({ abs, st1, next, path: relOf(lib, abs) })
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!dryRun) {
|
|
125
|
+
for (const e of edits) {
|
|
126
|
+
try {
|
|
127
|
+
const st2 = fs.statSync(e.abs)
|
|
128
|
+
if (st2.mtimeMs !== e.st1.mtimeMs || st2.size !== e.st1.size) {
|
|
129
|
+
report.skipped.push({ path: e.path, why: '读后被改动,未覆盖' })
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
fs.writeFileSync(e.abs, e.next, 'utf8')
|
|
133
|
+
} catch (err) {
|
|
134
|
+
report.skipped.push({ path: e.path, why: err.message })
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 兜底:只报「存在但没进索引」的 md(解析失败的孤儿文件),不重复报已处理的
|
|
140
|
+
for (const abs of allMarkdown(lib.nodekb)) {
|
|
141
|
+
if (visited.has(path.resolve(abs))) continue
|
|
142
|
+
report.skipped.push({ path: relOf(lib, abs), why: '未被解析为目标节点,未处理' })
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return report
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function formatMigrate(report) {
|
|
149
|
+
const lines = []
|
|
150
|
+
lines.push(report.dry_run ? '(演练,未落盘;确认后传 dry_run=false 才写)' : '已写入')
|
|
151
|
+
const total = report.files.reduce((s, f) => s + f.count, 0)
|
|
152
|
+
if (!total) {
|
|
153
|
+
lines.push('没有需要迁移的裸引用')
|
|
154
|
+
} else {
|
|
155
|
+
lines.push(`共 ${total} 处裸引用 → ${report.tscl} 处 [[tscl="…"]]、${report.node} 处 [[node="…"]]`)
|
|
156
|
+
for (const f of report.files.slice(0, 20)) lines.push(` - ${f.path}: ${f.count}(tscl ${f.tscl} / node ${f.node})`)
|
|
157
|
+
if (report.files.length > 20) lines.push(` …另有 ${report.files.length - 20} 个文件`)
|
|
158
|
+
}
|
|
159
|
+
if (report.unresolved.length) {
|
|
160
|
+
lines.push(`保留未改 ${report.unresolved.length} 处(指向的 id 既不是节点也不是定义,本来悬空):`)
|
|
161
|
+
for (const u of report.unresolved.slice(0, 10)) lines.push(` - ${u.path}: [[${u.ref}]]`)
|
|
162
|
+
if (report.unresolved.length > 10) lines.push(` …另有 ${report.unresolved.length - 10} 处`)
|
|
163
|
+
}
|
|
164
|
+
if (report.skipped.length) {
|
|
165
|
+
lines.push(`跳过 ${report.skipped.length} 个文件:`)
|
|
166
|
+
for (const s of report.skipped.slice(0, 10)) lines.push(` - ${s.path}: ${s.why}`)
|
|
167
|
+
}
|
|
168
|
+
return lines.join('\n')
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export { ROOT_TID }
|
package/lib/host/prompt.js
CHANGED
|
@@ -21,7 +21,7 @@ export const DEFAULT_PROMPT = [
|
|
|
21
21
|
'- 读库:先 `kb_read mode=toc` 看结构(几百 token),再按 TID 读一支;找不到就 `kb_search`。别一次性把整库读进上下文。',
|
|
22
22
|
'- 改名前先 `kb_backlinks`,改名用 `kb_rename`(会重写全库引用并跳过被并发改过的文件)。',
|
|
23
23
|
'- 引用分两种,别混:**结构**用 `[[node="doc:03-build:3.1"]]`(只出一个链接,不复制正文);**值只写一处**——定义 `<tscl id="fact:abi">…</tscl>`,各处写 `[[tscl="fact:abi"]]`,同一 id 全库只能定义一次。',
|
|
24
|
-
'- 旧写法 `[[TID]]` 仍能用但 `kb_diag` 会 warning,2.0
|
|
24
|
+
'- 旧写法 `[[TID]]` 仍能用但 `kb_diag` 会 warning,2.0 移除(老库可先跑 `kb_migrate` 演练,确认后 `dry_run=false` 批量迁)。',
|
|
25
25
|
].join('\n')
|
|
26
26
|
|
|
27
27
|
function textOf(config) {
|
package/lib/host/routes.js
CHANGED
|
@@ -12,6 +12,7 @@ import { search, formatSearch } from '../engine/search.js'
|
|
|
12
12
|
import { diag } from '../engine/diag.js'
|
|
13
13
|
import { extractRefs } from '../engine/parse.js'
|
|
14
14
|
import { renameFileNode, formatRename } from '../engine/rename.js'
|
|
15
|
+
import { migrateLibrary, formatMigrate } from '../engine/migrate.js'
|
|
15
16
|
import { openLibrary, knownLibraries, whereInfo, workspaceOf } from './runtime.js'
|
|
16
17
|
import { promptState } from './prompt.js'
|
|
17
18
|
import { skillState } from './skill.js'
|
|
@@ -279,6 +280,14 @@ export function registerRoutes(ctx, config) {
|
|
|
279
280
|
return
|
|
280
281
|
}
|
|
281
282
|
|
|
283
|
+
if (url.pathname === `${PREFIX}/migrate`) {
|
|
284
|
+
const lib = open()
|
|
285
|
+
const dryRun = !(q.get('dry_run') === '0' || q.get('dry_run') === 'false')
|
|
286
|
+
const report = migrateLibrary(lib, { dryRun })
|
|
287
|
+
send(res, 200, { kb: lib.name, ...report, text: formatMigrate(report) })
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
|
|
282
291
|
send(res, 404, { error: `未知端点: ${url.pathname}` })
|
|
283
292
|
} catch (e) {
|
|
284
293
|
send(res, 400, { error: e && e.message ? e.message : String(e) })
|
package/lib/host/tools.js
CHANGED
|
@@ -14,6 +14,7 @@ import { search, formatSearch } from '../engine/search.js'
|
|
|
14
14
|
import { diag } from '../engine/diag.js'
|
|
15
15
|
import { extractRefs } from '../engine/parse.js'
|
|
16
16
|
import { renameFileNode, formatRename } from '../engine/rename.js'
|
|
17
|
+
import { migrateLibrary, formatMigrate } from '../engine/migrate.js'
|
|
17
18
|
import { scaffold } from '../engine/scaffold.js'
|
|
18
19
|
import { writeInto } from '../engine/write.js'
|
|
19
20
|
import { isInside } from '../engine/registry.js'
|
|
@@ -309,6 +310,33 @@ export function buildTools(ctx, config) {
|
|
|
309
310
|
},
|
|
310
311
|
})
|
|
311
312
|
|
|
313
|
+
// ── kb_migrate ───────────────────────────────────────────────────────────
|
|
314
|
+
tools.push({
|
|
315
|
+
name: 'kb_migrate',
|
|
316
|
+
description:
|
|
317
|
+
'把库里的旧写法裸引用 [[x]] 迁成 1.1 的双语法:[[tscl="x"]](x 是定义)或 [[node="x"]](x 是节点 TID)。' +
|
|
318
|
+
'默认只演练(dry_run 缺省为 true),返回每个文件会改几处、哪些引用指向不存在的 id 会原样保留。' +
|
|
319
|
+
'围栏代码块内不动;写前重读校验,别人刚改过的文件不覆盖。',
|
|
320
|
+
parameters: {
|
|
321
|
+
type: 'object',
|
|
322
|
+
properties: {
|
|
323
|
+
dry_run: { type: 'boolean', description: '默认 true(只报告)。确认无误后传 false 才真正写盘。' },
|
|
324
|
+
kb: KB_PARAM,
|
|
325
|
+
lib: LIB_PARAM,
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
output: {
|
|
329
|
+
schema: {},
|
|
330
|
+
render: (_args, value) => text(value.text),
|
|
331
|
+
},
|
|
332
|
+
async execute(args, exec) {
|
|
333
|
+
const lib = locate(config, exec, args)
|
|
334
|
+
const dryRun = args.dry_run === undefined ? true : Boolean(args.dry_run)
|
|
335
|
+
const report = migrateLibrary(lib, { dryRun })
|
|
336
|
+
return { ...report, kb: lib.name, text: warnPrefix(lib) + formatMigrate(report) }
|
|
337
|
+
},
|
|
338
|
+
})
|
|
339
|
+
|
|
312
340
|
// ── kb_vcs_status ────────────────────────────────────────────────────────
|
|
313
341
|
tools.push({
|
|
314
342
|
name: 'kb_vcs_status',
|
package/package.json
CHANGED