ygl-trees 0.66.0 → 0.68.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/CHANGELOG.md +15 -0
- package/dist/apps/cli/src/bin.js +165 -130
- package/dist/apps/cli/src/profile-boot.js +11 -3
- package/dist/bundle/bin.cjs +105 -36
- package/dist/packages/core/database/src/db.js +421 -379
- package/dist/packages/core/database/src/plugin.js +5 -3
- package/dist/packages/core/database/src/types.js +8 -2
- package/dist/packages/ops/tools/src/doctor.js +19 -7
- package/dist/packages/ops/tools/src/plugins.js +10 -2
- package/dist/packages/research/pipeline/src/entities.js +6 -0
- package/dist/packages/research/pipeline/src/writes.js +1 -1
- package/dist/packages/techtree/model/src/writes.js +14 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.68.0(2026-10-17)锁设计修复——serve/CLI 共存 + PID 活锁检测 + JSON 纯净 + v9 时间纪律
|
|
4
|
+
- **F1(P0):webui serve 不再持排他锁**——原实现进程级锁让 serve 生命周期内挡掉一切 CLI 命令;现 serve 跳过锁(`YGL_SERVE=1` 旁路),SQLite WAL 天然支持并发读 + 序列化写;CLI 短命令仍锁(单写者保护写窗口)
|
|
5
|
+
- **P1(用户反馈):PID 活锁检测替代 15 分钟 mtime**——Windows 上 `process.on("exit")` 的 rmSync 句柄未释放时静默失败→锁残留→下进程撞白锁;现用 `process.kill(pid, 0)` 探测持有者是否存活(EPERM=活/ESRCH=死),死 PID **立即清锁**(不看 mtime),活 PID **如实报告**(不误报 stale)
|
|
6
|
+
- **F2:--json 模式锁错误纯净**——原锁提示文本泄露到 stdout 破坏 JSON 通道契约;现 --json 模式抑制人类提示,且提示本身也改为 PID 活锁感知(活锁不叫 stale)
|
|
7
|
+
- **F3:taskSetStatus 更新 artifacts.updated**——任务状态变更现在同步写 updated 时间戳(v9 纪律补漏)
|
|
8
|
+
- **exit 清理加固**:`beforeExit` + `exit` 双钩子 + 3 次重试(Windows EBUSY 容忍)
|
|
9
|
+
- **vendor/cordis bin 字段清理**:bin.js 已删但声明残留——移除
|
|
10
|
+
|
|
11
|
+
## 0.67.0(2026-10-17)WebUI antd 迭代——Ant Design 组件库升级 React 客户端
|
|
12
|
+
- **antd v6 + @ant-design/icons**:专业组件库驱动的富客户端
|
|
13
|
+
- **组件替换**:Layout(Header/Content)、Card 统计卡片、Statistic 数字展示、Table 任务清单(可筛选)、Tag 状态标签、Alert 错误提示、Space/Row/Col 布局
|
|
14
|
+
- **DESIGN.md token 映射**:ConfigProvider theme(colorPrimary=#7c9885 sage、darkAlgorithm 暗色主题、zh_CN 中文语言包)
|
|
15
|
+
- **代码分割**:manualChunks 拆分 vendor/antd/xyflow/markdown 四块,antd 不阻塞首屏
|
|
16
|
+
- **保持不变**:xyflow 树视图、react-markdown 产出渲染、SSE 实时推送、内嵌 dashboard 回退
|
|
17
|
+
|
|
3
18
|
## 0.66.0(2026-10-17)新增 `llms` 命令——结构化能力清单(Ant Design for-agents 模式)
|
|
4
19
|
- **`ygl-trees llms`**:一条命令输出完整 markdown 能力清单——冻结契约表(6 项)+ 16 个命令组全部用法 + 决策模型说明 + Quick Start + 设计系统入口——**任何 LLM 一次性获取全部能力面**
|
|
5
20
|
- **静态 `llms.txt`**:仓库根目录,可被任何 LLM 爬取/注入上下文(同 Ant Design 的 llms.txt 模式)
|
package/dist/apps/cli/src/bin.js
CHANGED
|
@@ -116,132 +116,132 @@ function appVersion() {
|
|
|
116
116
|
}
|
|
117
117
|
// ── Usage text ──
|
|
118
118
|
// (kept in apps/cli because it's presentation, not domain logic)
|
|
119
|
-
const USAGE = `
|
|
120
|
-
${(0, fmt_1.paint)(fmt_1.C.bold, "ygl-trees", true)} 猿光 · 科技树
|
|
121
|
-
${(0, fmt_1.paint)(fmt_1.C.dim, "database-centric CLI: research → production → knowledge reuse", true)}
|
|
122
|
-
|
|
123
|
-
ygl-trees <group> <action> [args] [options] add --json for machine output
|
|
124
|
-
|
|
125
|
-
${(0, fmt_1.paint)(fmt_1.C.bold, "config", true)} user-tunable settings (registry defaults + overrides)
|
|
126
|
-
list all keys: value + default + note
|
|
127
|
-
get <key> resolved value (config table > default)
|
|
128
|
-
set <key> <value> validated against registry (type/range); events logged
|
|
129
|
-
reset <key> drop override → back to default
|
|
130
|
-
|
|
131
|
-
${(0, fmt_1.paint)(fmt_1.C.bold, "db", true)}
|
|
132
|
-
init create the workspace + schema
|
|
133
|
-
stats row counts per table
|
|
134
|
-
path print the database file path
|
|
135
|
-
export <file.sql> [--lean] dump ALL data tables (+ summary); --lean skips derived
|
|
136
|
-
sync pg <url> one-way full mirror to PostgreSQL (L1, see docs/pg-compat.md)
|
|
137
|
-
migrate pg <url> [--fresh] one-time full-fidelity graduation (constraints + data, atomic)
|
|
138
|
-
dups list duplicate-content rows
|
|
139
|
-
reset delete the database (asks nothing, be careful)
|
|
140
|
-
sql "<sql>" ad-hoc statement (can mutate — power tool, no guardrails)
|
|
141
|
-
|
|
142
|
-
${(0, fmt_1.paint)(fmt_1.C.bold, "skill", true)} L1 description / L2 body / L3 references
|
|
143
|
-
import <file.md> import a SKILL.md (frontmatter = L1, body = L2)
|
|
144
|
-
add --name --description --body [-f file.md]
|
|
145
|
-
list | show <name> | rm <name> | enable <name> | disable <name>
|
|
146
|
-
match "<query>" full-text match, prints L1 candidates for an agent
|
|
147
|
-
refs add <skill> <path> --body ...
|
|
148
|
-
|
|
149
|
-
${(0, fmt_1.paint)(fmt_1.C.bold, "project", true)}
|
|
150
|
-
new <name> -q "<question>" create + select
|
|
151
|
-
list | use <name> | show
|
|
152
|
-
|
|
153
|
-
${(0, fmt_1.paint)(fmt_1.C.bold, "research", true)} collect → screen → extract → claim
|
|
154
|
-
query add "<text>" register a search query
|
|
155
|
-
query attach <qid> <sid> link source to query (recalled count)
|
|
156
|
-
query list
|
|
157
|
-
source add --title --url [--abstract --content --relevance --confidence --query]
|
|
158
|
-
source fetch <url> fetch page, extract title/meta/text (zero side-effect on failure)
|
|
159
|
-
source list | search "<q>" | show <id> | rm <id>
|
|
160
|
-
evidence add <sourceId> --quote "..." [--note --locator]
|
|
161
|
-
evidence list
|
|
162
|
-
claim add "<text>" [--confidence A|B|C]
|
|
163
|
-
claim link <claimId> <evidenceId> [--stance supports|refutes]
|
|
164
|
-
claim resolve <claimId> --as supported|refuted|contested [--note]
|
|
165
|
-
claim list
|
|
166
|
-
|
|
167
|
-
${(0, fmt_1.paint)(fmt_1.C.bold, "produce", true)} outline → section → report · spec/design(工程产出)
|
|
168
|
-
outline auto-outline from claims
|
|
169
|
-
section <title> --body|-f add a section artifact
|
|
170
|
-
report assemble claims+evidence+sections into a report
|
|
171
|
-
spec <title> --body|-f requirements spec artifact (R12)
|
|
172
|
-
design <title> --body|-f design artifact (R12) --design-type ui-ux|arch|decision
|
|
173
|
-
list | show <id> | export <id> <file>
|
|
174
|
-
|
|
175
|
-
${(0, fmt_1.paint)(fmt_1.C.bold, "task", true)} 任务清单(todo|doing|done 状态机,R12)
|
|
176
|
-
add "<title>" [--note ...] create a task (status todo)
|
|
177
|
-
list [--status todo|doing|done]
|
|
178
|
-
doing|done <id> move along the state machine(勾销全程留痕)
|
|
179
|
-
|
|
180
|
-
${(0, fmt_1.paint)(fmt_1.C.bold, "kb", true)} long-term knowledge
|
|
181
|
-
add --title --body|-f [--tags --confidence]
|
|
182
|
-
import <dir|file.md> batch markdown (frontmatter, idempotent, single tx)
|
|
183
|
-
search "<q>" | list | show <id>
|
|
184
|
-
link <a> <b> [--rel]
|
|
185
|
-
ingest <artifactId> import a finished artifact
|
|
186
|
-
promote <noteId> [--name] turn a note into a reusable skill
|
|
187
|
-
|
|
188
|
-
${(0, fmt_1.paint)(fmt_1.C.bold, "job", true)} batch + schedule (drive with cron: job tick)
|
|
189
|
-
add <title> -- <command> [--every <min>] [--at "YYYY-MM-DD HH:MM"]
|
|
190
|
-
list | run <id> | tick | enable <id> | disable <id> | logs
|
|
191
|
-
|
|
192
|
-
${(0, fmt_1.paint)(fmt_1.C.bold, "tree / node", true)} tech-tree node model
|
|
193
|
-
tree new <name> [--project p] create a tree (nodes live in it)
|
|
194
|
-
tree list | tree use <name>
|
|
195
|
-
|
|
196
|
-
node add <key> [--name --kind --tier --cost --tags --payload]
|
|
197
|
-
node link <parent> <child> [--rel prereq|unlock|excludes|enables]
|
|
198
|
-
[--mode AND|OR|XOR|NOT] [--optional]
|
|
199
|
-
[--min-level n] [--weight n]
|
|
200
|
-
node unlink <parent> <child> [--rel prereq]
|
|
201
|
-
node cond <key> --name <c> [--kind flag|resource|level|quest|time|expr]
|
|
202
|
-
[--op >=|==|<=|!=|has] [--value v] [--optional]
|
|
203
|
-
[--op eval --value "<布尔问题>"]
|
|
204
|
-
[--op eval-choice --value "<问题>" --options "k=desc,..." --answer k]
|
|
205
|
-
[--op eval-score --value "<问题>" --levels "级0,..." --min-score n]
|
|
206
|
-
[--effect gate|cost_mul] [--weight n]
|
|
207
|
-
node index rebuild closure + topo order
|
|
208
|
-
node can <key> [--state k=v] evaluate required vs optional gates
|
|
209
|
-
(--eval: eval 题型经决策模型判定;
|
|
210
|
-
boolean P / choice P(期望答案) / score ±1 灰带)
|
|
211
|
-
node ready [--state k=v] [--eval] currently unlockable set
|
|
212
|
-
node path <key> dependency-order path to target (all locked ancestors; OR branches not optimized)
|
|
213
|
-
node parents|children <key> direct relations
|
|
214
|
-
node ancestors|descendants <key> transitive, via closure index
|
|
215
|
-
node list | show <key> | search "<q>" | unlock|lock <key>
|
|
216
|
-
node validate cycles, dangling edges, dirty index
|
|
217
|
-
node export [--out file.json] portable JSON
|
|
218
|
-
node import --file <file.json> [--tree name] [--merge] — strict, transactional
|
|
219
|
-
|
|
220
|
-
Writes are idempotent (claim/evidence/query/kb/job dedupe by content
|
|
221
|
-
hash); pass --force to keep a deliberate duplicate.
|
|
222
|
-
|
|
223
|
-
${(0, fmt_1.paint)(fmt_1.C.bold, "webui", true)} dashboard (zero-dependency)
|
|
224
|
-
export [--out <file>] single-file dashboard.html (open in browser)
|
|
225
|
-
serve [--port N] live dashboard at http://127.0.0.1:N/ (Ctrl+C to stop)
|
|
226
|
-
|
|
227
|
-
${(0, fmt_1.paint)(fmt_1.C.bold, "rel", true)} 关联织物(spec R9:任意两类实体互相挂接)
|
|
228
|
-
add <src> <dst> [--rel t] link two refs: node:key claim:id kb:id artifact:id
|
|
229
|
-
skill:name evidence:id source:id query:id
|
|
230
|
-
rm <src> <dst> [--rel t] unlink
|
|
231
|
-
list [--src r] [--dst r] browse (either side filter, resolves displays)
|
|
232
|
-
|
|
233
|
-
${(0, fmt_1.paint)(fmt_1.C.bold, "plugin", true)} 用户层插件分发(spec R14 L3,业务域扩展位)
|
|
234
|
-
init create $YGL_HOME/plugins profile
|
|
235
|
-
install <spec> npm package / git url / local dir (auto link:)
|
|
236
|
-
remove <name> | list manage the user layer ($YGL_HOME/ygl.user.bundle.json)
|
|
237
|
-
插件契约:export apply(ctx,config) + 可选 commands 贡献 CLI 命令组
|
|
238
|
-
|
|
239
|
-
${(0, fmt_1.paint)(fmt_1.C.bold, "agent", true)} agent-facing views
|
|
240
|
-
brief current project state + next actions (start here)
|
|
241
|
-
match "<task>" which skill should handle this task
|
|
242
|
-
log recent events
|
|
243
|
-
|
|
244
|
-
Global: --json --no-color --db <file> --home <dir>
|
|
119
|
+
const USAGE = `
|
|
120
|
+
${(0, fmt_1.paint)(fmt_1.C.bold, "ygl-trees", true)} 猿光 · 科技树
|
|
121
|
+
${(0, fmt_1.paint)(fmt_1.C.dim, "database-centric CLI: research → production → knowledge reuse", true)}
|
|
122
|
+
|
|
123
|
+
ygl-trees <group> <action> [args] [options] add --json for machine output
|
|
124
|
+
|
|
125
|
+
${(0, fmt_1.paint)(fmt_1.C.bold, "config", true)} user-tunable settings (registry defaults + overrides)
|
|
126
|
+
list all keys: value + default + note
|
|
127
|
+
get <key> resolved value (config table > default)
|
|
128
|
+
set <key> <value> validated against registry (type/range); events logged
|
|
129
|
+
reset <key> drop override → back to default
|
|
130
|
+
|
|
131
|
+
${(0, fmt_1.paint)(fmt_1.C.bold, "db", true)}
|
|
132
|
+
init create the workspace + schema
|
|
133
|
+
stats row counts per table
|
|
134
|
+
path print the database file path
|
|
135
|
+
export <file.sql> [--lean] dump ALL data tables (+ summary); --lean skips derived
|
|
136
|
+
sync pg <url> one-way full mirror to PostgreSQL (L1, see docs/pg-compat.md)
|
|
137
|
+
migrate pg <url> [--fresh] one-time full-fidelity graduation (constraints + data, atomic)
|
|
138
|
+
dups list duplicate-content rows
|
|
139
|
+
reset delete the database (asks nothing, be careful)
|
|
140
|
+
sql "<sql>" ad-hoc statement (can mutate — power tool, no guardrails)
|
|
141
|
+
|
|
142
|
+
${(0, fmt_1.paint)(fmt_1.C.bold, "skill", true)} L1 description / L2 body / L3 references
|
|
143
|
+
import <file.md> import a SKILL.md (frontmatter = L1, body = L2)
|
|
144
|
+
add --name --description --body [-f file.md]
|
|
145
|
+
list | show <name> | rm <name> | enable <name> | disable <name>
|
|
146
|
+
match "<query>" full-text match, prints L1 candidates for an agent
|
|
147
|
+
refs add <skill> <path> --body ...
|
|
148
|
+
|
|
149
|
+
${(0, fmt_1.paint)(fmt_1.C.bold, "project", true)}
|
|
150
|
+
new <name> -q "<question>" create + select
|
|
151
|
+
list | use <name> | show
|
|
152
|
+
|
|
153
|
+
${(0, fmt_1.paint)(fmt_1.C.bold, "research", true)} collect → screen → extract → claim
|
|
154
|
+
query add "<text>" register a search query
|
|
155
|
+
query attach <qid> <sid> link source to query (recalled count)
|
|
156
|
+
query list
|
|
157
|
+
source add --title --url [--abstract --content --relevance --confidence --query]
|
|
158
|
+
source fetch <url> fetch page, extract title/meta/text (zero side-effect on failure)
|
|
159
|
+
source list | search "<q>" | show <id> | rm <id>
|
|
160
|
+
evidence add <sourceId> --quote "..." [--note --locator]
|
|
161
|
+
evidence list
|
|
162
|
+
claim add "<text>" [--confidence A|B|C]
|
|
163
|
+
claim link <claimId> <evidenceId> [--stance supports|refutes]
|
|
164
|
+
claim resolve <claimId> --as supported|refuted|contested [--note]
|
|
165
|
+
claim list
|
|
166
|
+
|
|
167
|
+
${(0, fmt_1.paint)(fmt_1.C.bold, "produce", true)} outline → section → report · spec/design(工程产出)
|
|
168
|
+
outline auto-outline from claims
|
|
169
|
+
section <title> --body|-f add a section artifact
|
|
170
|
+
report assemble claims+evidence+sections into a report
|
|
171
|
+
spec <title> --body|-f requirements spec artifact (R12)
|
|
172
|
+
design <title> --body|-f design artifact (R12) --design-type ui-ux|arch|decision
|
|
173
|
+
list | show <id> | export <id> <file>
|
|
174
|
+
|
|
175
|
+
${(0, fmt_1.paint)(fmt_1.C.bold, "task", true)} 任务清单(todo|doing|done 状态机,R12)
|
|
176
|
+
add "<title>" [--note ...] create a task (status todo)
|
|
177
|
+
list [--status todo|doing|done]
|
|
178
|
+
doing|done <id> move along the state machine(勾销全程留痕)
|
|
179
|
+
|
|
180
|
+
${(0, fmt_1.paint)(fmt_1.C.bold, "kb", true)} long-term knowledge
|
|
181
|
+
add --title --body|-f [--tags --confidence]
|
|
182
|
+
import <dir|file.md> batch markdown (frontmatter, idempotent, single tx)
|
|
183
|
+
search "<q>" | list | show <id>
|
|
184
|
+
link <a> <b> [--rel]
|
|
185
|
+
ingest <artifactId> import a finished artifact
|
|
186
|
+
promote <noteId> [--name] turn a note into a reusable skill
|
|
187
|
+
|
|
188
|
+
${(0, fmt_1.paint)(fmt_1.C.bold, "job", true)} batch + schedule (drive with cron: job tick)
|
|
189
|
+
add <title> -- <command> [--every <min>] [--at "YYYY-MM-DD HH:MM"]
|
|
190
|
+
list | run <id> | tick | enable <id> | disable <id> | logs
|
|
191
|
+
|
|
192
|
+
${(0, fmt_1.paint)(fmt_1.C.bold, "tree / node", true)} tech-tree node model
|
|
193
|
+
tree new <name> [--project p] create a tree (nodes live in it)
|
|
194
|
+
tree list | tree use <name>
|
|
195
|
+
|
|
196
|
+
node add <key> [--name --kind --tier --cost --tags --payload]
|
|
197
|
+
node link <parent> <child> [--rel prereq|unlock|excludes|enables]
|
|
198
|
+
[--mode AND|OR|XOR|NOT] [--optional]
|
|
199
|
+
[--min-level n] [--weight n]
|
|
200
|
+
node unlink <parent> <child> [--rel prereq]
|
|
201
|
+
node cond <key> --name <c> [--kind flag|resource|level|quest|time|expr]
|
|
202
|
+
[--op >=|==|<=|!=|has] [--value v] [--optional]
|
|
203
|
+
[--op eval --value "<布尔问题>"]
|
|
204
|
+
[--op eval-choice --value "<问题>" --options "k=desc,..." --answer k]
|
|
205
|
+
[--op eval-score --value "<问题>" --levels "级0,..." --min-score n]
|
|
206
|
+
[--effect gate|cost_mul] [--weight n]
|
|
207
|
+
node index rebuild closure + topo order
|
|
208
|
+
node can <key> [--state k=v] evaluate required vs optional gates
|
|
209
|
+
(--eval: eval 题型经决策模型判定;
|
|
210
|
+
boolean P / choice P(期望答案) / score ±1 灰带)
|
|
211
|
+
node ready [--state k=v] [--eval] currently unlockable set
|
|
212
|
+
node path <key> dependency-order path to target (all locked ancestors; OR branches not optimized)
|
|
213
|
+
node parents|children <key> direct relations
|
|
214
|
+
node ancestors|descendants <key> transitive, via closure index
|
|
215
|
+
node list | show <key> | search "<q>" | unlock|lock <key>
|
|
216
|
+
node validate cycles, dangling edges, dirty index
|
|
217
|
+
node export [--out file.json] portable JSON
|
|
218
|
+
node import --file <file.json> [--tree name] [--merge] — strict, transactional
|
|
219
|
+
|
|
220
|
+
Writes are idempotent (claim/evidence/query/kb/job dedupe by content
|
|
221
|
+
hash); pass --force to keep a deliberate duplicate.
|
|
222
|
+
|
|
223
|
+
${(0, fmt_1.paint)(fmt_1.C.bold, "webui", true)} dashboard (zero-dependency)
|
|
224
|
+
export [--out <file>] single-file dashboard.html (open in browser)
|
|
225
|
+
serve [--port N] live dashboard at http://127.0.0.1:N/ (Ctrl+C to stop)
|
|
226
|
+
|
|
227
|
+
${(0, fmt_1.paint)(fmt_1.C.bold, "rel", true)} 关联织物(spec R9:任意两类实体互相挂接)
|
|
228
|
+
add <src> <dst> [--rel t] link two refs: node:key claim:id kb:id artifact:id
|
|
229
|
+
skill:name evidence:id source:id query:id
|
|
230
|
+
rm <src> <dst> [--rel t] unlink
|
|
231
|
+
list [--src r] [--dst r] browse (either side filter, resolves displays)
|
|
232
|
+
|
|
233
|
+
${(0, fmt_1.paint)(fmt_1.C.bold, "plugin", true)} 用户层插件分发(spec R14 L3,业务域扩展位)
|
|
234
|
+
init create $YGL_HOME/plugins profile
|
|
235
|
+
install <spec> npm package / git url / local dir (auto link:)
|
|
236
|
+
remove <name> | list manage the user layer ($YGL_HOME/ygl.user.bundle.json)
|
|
237
|
+
插件契约:export apply(ctx,config) + 可选 commands 贡献 CLI 命令组
|
|
238
|
+
|
|
239
|
+
${(0, fmt_1.paint)(fmt_1.C.bold, "agent", true)} agent-facing views
|
|
240
|
+
brief current project state + next actions (start here)
|
|
241
|
+
match "<task>" which skill should handle this task
|
|
242
|
+
log recent events
|
|
243
|
+
|
|
244
|
+
Global: --json --no-color --db <file> --home <dir>
|
|
245
245
|
`;
|
|
246
246
|
function usage() {
|
|
247
247
|
return USAGE;
|
|
@@ -256,6 +256,14 @@ async function main(argv) {
|
|
|
256
256
|
};
|
|
257
257
|
const group = args._[0];
|
|
258
258
|
const action = args._[1];
|
|
259
|
+
// F1 fix (a2a review): webui serve holds its DB connection for the process
|
|
260
|
+
// lifetime — a process-level exclusive lock there would block ALL CLI
|
|
261
|
+
// commands until serve exits. SQLite WAL already permits concurrent readers
|
|
262
|
+
// + serialized writers, so serve skips the app-level lock. Short CLI
|
|
263
|
+
// commands still lock (single-writer for the brief write window).
|
|
264
|
+
if (group === "webui" && (action === "serve" || action === "export")) {
|
|
265
|
+
process.env.YGL_SERVE = "1";
|
|
266
|
+
}
|
|
259
267
|
// P4 fix: --version prints the actual version number (cold-user report);
|
|
260
268
|
// the parser eats --version as a flag, so check flags not positional args.
|
|
261
269
|
if (args.flags.version === true || group === "version") {
|
|
@@ -307,10 +315,37 @@ async function main(argv) {
|
|
|
307
315
|
return 1;
|
|
308
316
|
}
|
|
309
317
|
(0, fmt_1.bad)(ctx, `cannot open database ${file}: ${e?.message || e}`);
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
318
|
+
// F2 fix (a2a review): --json mode must stay a single parseable object —
|
|
319
|
+
// suppress the human-only lock hints. Also check PID liveness, not just
|
|
320
|
+
// directory existence (a live serve holder is NOT stale).
|
|
321
|
+
const lock = `${file}.ygllock`;
|
|
322
|
+
if (!ctx.json && require("node:fs").existsSync(lock)) {
|
|
323
|
+
const pidFile = require("node:path").join(lock, "pid");
|
|
324
|
+
let holder = "?";
|
|
325
|
+
try {
|
|
326
|
+
holder = require("node:fs").readFileSync(pidFile, "utf8").trim();
|
|
327
|
+
}
|
|
328
|
+
catch { /* */ }
|
|
329
|
+
let alive = false;
|
|
330
|
+
try {
|
|
331
|
+
process.kill(Number(holder), 0);
|
|
332
|
+
alive = true;
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
alive = err.code === "EPERM";
|
|
336
|
+
}
|
|
337
|
+
if (alive) {
|
|
338
|
+
(0, fmt_1.out)(ctx, ` lock held by live process pid ${holder} (serve? webui dashboard)`);
|
|
339
|
+
(0, fmt_1.out)(ctx, ` to run CLI write commands, stop that process first — or use YGL_TEST=1 to bypass`);
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
(0, fmt_1.out)(ctx, ` orphaned lock (pid ${holder} is dead) — removing: ${lock}`);
|
|
343
|
+
try {
|
|
344
|
+
require("node:fs").rmSync(lock, { recursive: true, force: true });
|
|
345
|
+
}
|
|
346
|
+
catch { /* */ }
|
|
347
|
+
(0, fmt_1.out)(ctx, ` retry the command — the lock has been cleaned`);
|
|
348
|
+
}
|
|
314
349
|
}
|
|
315
350
|
return 1;
|
|
316
351
|
}
|
|
@@ -52,9 +52,17 @@ async function bootApp(opts) {
|
|
|
52
52
|
pluginCommands[g] = c;
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
// settle the service layer
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
// settle the service layer. Cordis runs plugin apply() inside fibers — one
|
|
56
|
+
// setImmediate tick does NOT guarantee the database service has constructed;
|
|
57
|
+
// the fallback below would then double-open the DB in one process (0.65.0
|
|
58
|
+
// P0: dir-lock self-deadlock / sqlite BUSY). Wait for it, bounded; the
|
|
59
|
+
// fallback stays as crash safety if the service never comes up.
|
|
60
|
+
let db = app.database?.db;
|
|
61
|
+
for (let i = 0; i < 50 && !db; i++) {
|
|
62
|
+
await new Promise((r) => setImmediate(r));
|
|
63
|
+
db = app.database?.db;
|
|
64
|
+
}
|
|
65
|
+
db = db || new db_1.DB(opts.dbFile, { lock: process.env.YGL_TEST !== '1' });
|
|
58
66
|
const store = app.research?.store || new reads_1.Store(db);
|
|
59
67
|
if (startupIssues.length) {
|
|
60
68
|
throw new startup_diagnostics_1.StartupError(`${startupIssues.length} plugin issue(s) during startup`, startupIssues);
|
package/dist/bundle/bin.cjs
CHANGED
|
@@ -550,35 +550,50 @@ CREATE UNIQUE INDEX IF NOT EXISTS uq_skill_refs ON skill_refs(skill_id, path);
|
|
|
550
550
|
this.path = file;
|
|
551
551
|
node_fs.mkdirSync(node_path.dirname(file), { recursive: true });
|
|
552
552
|
const useLock = opts?.lock === true;
|
|
553
|
-
const
|
|
553
|
+
const legacyLockDir = `${file}.lock`;
|
|
554
|
+
if (node_fs.existsSync(node_path.join(legacyLockDir, "pid"))) try {
|
|
555
|
+
node_fs.rmSync(legacyLockDir, {
|
|
556
|
+
recursive: true,
|
|
557
|
+
force: true
|
|
558
|
+
});
|
|
559
|
+
} catch {}
|
|
560
|
+
const lockDir = `${file}.ygllock`;
|
|
554
561
|
if (useLock && node_fs.existsSync(lockDir)) {
|
|
555
562
|
let holder = "unknown";
|
|
556
563
|
try {
|
|
557
564
|
holder = node_fs.readFileSync(node_path.join(lockDir, "pid"), "utf8").trim();
|
|
558
565
|
} catch {}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
566
|
+
const mine = holder === String(process.pid);
|
|
567
|
+
let holderAlive;
|
|
568
|
+
if (mine) holderAlive = false;
|
|
569
|
+
else try {
|
|
570
|
+
process.kill(Number(holder), 0);
|
|
571
|
+
holderAlive = true;
|
|
572
|
+
} catch (e) {
|
|
573
|
+
holderAlive = e.code === "EPERM";
|
|
574
|
+
}
|
|
575
|
+
if (!holderAlive) try {
|
|
576
|
+
node_fs.rmSync(lockDir, {
|
|
562
577
|
recursive: true,
|
|
563
578
|
force: true
|
|
564
579
|
});
|
|
565
|
-
|
|
566
|
-
}
|
|
567
|
-
if (String(e.message).includes("locked by another")) throw e;
|
|
568
|
-
throw new Error(`database is locked: ${lockDir} (holder unreadable)`);
|
|
569
|
-
}
|
|
580
|
+
} catch {}
|
|
581
|
+
else throw new Error(`database is locked by another process (pid: ${holder}).\n lock: ${lockDir}\n if this is a webui serve dashboard, it shares WAL safely — CLI reads work,\n write commands need the serve process stopped (or YGL_TEST=1 to bypass)\n if no other ygl-trees is running, remove it: rmdir "${lockDir}"`);
|
|
570
582
|
}
|
|
571
583
|
if (useLock) try {
|
|
572
584
|
node_fs.mkdirSync(lockDir, { recursive: true });
|
|
573
585
|
node_fs.writeFileSync(node_path.join(lockDir, "pid"), String(process.pid));
|
|
574
|
-
|
|
575
|
-
try {
|
|
586
|
+
const cleanup = () => {
|
|
587
|
+
for (let i = 0; i < 3; i++) try {
|
|
576
588
|
node_fs.rmSync(lockDir, {
|
|
577
589
|
recursive: true,
|
|
578
590
|
force: true
|
|
579
591
|
});
|
|
592
|
+
return;
|
|
580
593
|
} catch {}
|
|
581
|
-
}
|
|
594
|
+
};
|
|
595
|
+
process.on("beforeExit", cleanup);
|
|
596
|
+
process.on("exit", cleanup);
|
|
582
597
|
} catch {}
|
|
583
598
|
this.db = new node_sqlite3_wasm.Database(file);
|
|
584
599
|
this.db.exec("PRAGMA journal_mode=WAL");
|
|
@@ -589,7 +604,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS uq_skill_refs ON skill_refs(skill_id, path);
|
|
|
589
604
|
/** Release the write lock (called by close() and process exit). */
|
|
590
605
|
releaseLock() {
|
|
591
606
|
try {
|
|
592
|
-
const lockDir = `${this.path}.
|
|
607
|
+
const lockDir = `${this.path}.ygllock`;
|
|
593
608
|
if (node_fs.existsSync(lockDir)) node_fs.rmSync(lockDir, {
|
|
594
609
|
recursive: true,
|
|
595
610
|
force: true
|
|
@@ -599,7 +614,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS uq_skill_refs ON skill_refs(skill_id, path);
|
|
|
599
614
|
/** Static: force-release a lock (for cleanup hooks). */
|
|
600
615
|
static releaseLockFor(file) {
|
|
601
616
|
try {
|
|
602
|
-
const lockDir = `${file}.
|
|
617
|
+
const lockDir = `${file}.ygllock`;
|
|
603
618
|
if (node_fs.existsSync(lockDir)) node_fs.rmSync(lockDir, {
|
|
604
619
|
recursive: true,
|
|
605
620
|
force: true
|
|
@@ -919,8 +934,14 @@ function parse(argv) {
|
|
|
919
934
|
const flags = {};
|
|
920
935
|
let rest = false;
|
|
921
936
|
const takesValue = (name) => !BOOLEAN_FLAGS.has(name);
|
|
922
|
-
/**
|
|
923
|
-
|
|
937
|
+
/** A token is a flag boundary only if this parser could itself consume it as
|
|
938
|
+
* a flag: `--`, a long `--name`, or a single-letter `-x`. Any other token
|
|
939
|
+
* starting with `-` (e.g. `- 双平台…`, `-1`, `-abc`) can never be parsed as
|
|
940
|
+
* a flag, so a value-taking flag safely claims it — `--quote "- text"`
|
|
941
|
+
* keeps its value instead of collapsing to `true` and leaking the text
|
|
942
|
+
* into positionals. */
|
|
943
|
+
const looksLikeFlag = (s) => s === "--" || s.startsWith("--") || /^-[A-Za-z]$/.test(s);
|
|
944
|
+
const isValue = (s) => !looksLikeFlag(s);
|
|
924
945
|
for (let i = 0; i < argv.length; i++) {
|
|
925
946
|
const a = argv[i];
|
|
926
947
|
if (rest) {
|
|
@@ -973,7 +994,8 @@ var DatabaseService = class extends _deepseek_ai_cordis.Service {
|
|
|
973
994
|
db;
|
|
974
995
|
constructor(ctx, dbFile) {
|
|
975
996
|
super(ctx, "database");
|
|
976
|
-
|
|
997
|
+
const lock = process.env.YGL_TEST !== "1" && process.env.YGL_SERVE !== "1";
|
|
998
|
+
this.db = new DB(dbFile, { lock });
|
|
977
999
|
}
|
|
978
1000
|
};
|
|
979
1001
|
function apply$3(ctx, config) {
|
|
@@ -2655,7 +2677,8 @@ function pnpm(home, args) {
|
|
|
2655
2677
|
q(exe),
|
|
2656
2678
|
...args.map(q),
|
|
2657
2679
|
"--dir",
|
|
2658
|
-
q(profileDir(home))
|
|
2680
|
+
q(profileDir(home)),
|
|
2681
|
+
"--ignore-workspace"
|
|
2659
2682
|
].join(" "), {
|
|
2660
2683
|
stdio: "pipe",
|
|
2661
2684
|
shell: true
|
|
@@ -2665,7 +2688,8 @@ function pnpm(home, args) {
|
|
|
2665
2688
|
(0, node_child_process.execFileSync)(pm, [
|
|
2666
2689
|
...args,
|
|
2667
2690
|
"--dir",
|
|
2668
|
-
profileDir(home)
|
|
2691
|
+
profileDir(home),
|
|
2692
|
+
"--ignore-workspace"
|
|
2669
2693
|
], { stdio: "pipe" });
|
|
2670
2694
|
}
|
|
2671
2695
|
function resolvePm(pm) {
|
|
@@ -2911,8 +2935,12 @@ async function bootApp(opts) {
|
|
|
2911
2935
|
}
|
|
2912
2936
|
for (const [g, c] of Object.entries(up.commands || {})) if (typeof c.run === "function") pluginCommands[g] = c;
|
|
2913
2937
|
}
|
|
2914
|
-
|
|
2915
|
-
|
|
2938
|
+
let db = app.database?.db;
|
|
2939
|
+
for (let i = 0; i < 50 && !db; i++) {
|
|
2940
|
+
await new Promise((r) => setImmediate(r));
|
|
2941
|
+
db = app.database?.db;
|
|
2942
|
+
}
|
|
2943
|
+
db = db || new DB(opts.dbFile, { lock: process.env.YGL_TEST !== "1" });
|
|
2916
2944
|
const store = app.research?.store || new Store(db);
|
|
2917
2945
|
if (startupIssues.length) throw new StartupError(`${startupIssues.length} plugin issue(s) during startup`, startupIssues);
|
|
2918
2946
|
return {
|
|
@@ -3802,7 +3830,11 @@ var init_entities$1 = __esmMin((() => {
|
|
|
3802
3830
|
type: "text",
|
|
3803
3831
|
nullable: true
|
|
3804
3832
|
}), __decorateMetadata("design:type", String)], Claim.prototype, "updated", void 0);
|
|
3805
|
-
__decorate([
|
|
3833
|
+
__decorate([
|
|
3834
|
+
(0, typeorm.ManyToOne)(() => Project, (p) => p.claims, { createForeignKeyConstraints: false }),
|
|
3835
|
+
(0, typeorm.JoinColumn)({ name: "project_id" }),
|
|
3836
|
+
__decorateMetadata("design:type", typeof Project === "undefined" ? Object : Project)
|
|
3837
|
+
], Claim.prototype, "project", void 0);
|
|
3806
3838
|
__decorate([(0, typeorm.OneToMany)(() => ClaimEvidence, (ce) => ce.claim, { createForeignKeyConstraints: false }), __decorateMetadata("design:type", Array)], Claim.prototype, "claimEvidence", void 0);
|
|
3807
3839
|
Claim = __decorate([(0, typeorm.Entity)({ name: "claims" })], Claim);
|
|
3808
3840
|
ClaimEvidence = class ClaimEvidence {
|
|
@@ -4467,10 +4499,14 @@ async function taskSetStatus(ds, taskId, status) {
|
|
|
4467
4499
|
meta = rows[0].meta ? JSON.parse(rows[0].meta) : {};
|
|
4468
4500
|
} catch {}
|
|
4469
4501
|
const from = meta.status || "todo";
|
|
4470
|
-
await mgr.query(`UPDATE artifacts SET meta=? WHERE id=?`, [
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4502
|
+
await mgr.query(`UPDATE artifacts SET meta=?, updated=? WHERE id=?`, [
|
|
4503
|
+
JSON.stringify({
|
|
4504
|
+
...meta,
|
|
4505
|
+
status
|
|
4506
|
+
}),
|
|
4507
|
+
nowIso(),
|
|
4508
|
+
taskId
|
|
4509
|
+
]);
|
|
4474
4510
|
await logEvent$1(mgr, "task.status", "task", taskId, `${from} → ${status}`, status === "done" ? "info" : "info");
|
|
4475
4511
|
return {
|
|
4476
4512
|
id: taskId,
|
|
@@ -7196,7 +7232,7 @@ async function importTree(ds, spec, opts = {}) {
|
|
|
7196
7232
|
const pid = nodeId.get(String(e.parent));
|
|
7197
7233
|
const cid = nodeId.get(String(e.child));
|
|
7198
7234
|
await mgr.query(`INSERT INTO node_edges(id,tree_id,parent_id,child_id,rel,mode,required,min_level,weight,note,created)
|
|
7199
|
-
VALUES(
|
|
7235
|
+
VALUES(?,?,?,?,?,?,?,?,?,?,?)
|
|
7200
7236
|
ON CONFLICT(parent_id,child_id,rel) DO UPDATE SET
|
|
7201
7237
|
mode=excluded.mode, required=excluded.required, min_level=excluded.min_level,
|
|
7202
7238
|
weight=excluded.weight, note=excluded.note`, [
|
|
@@ -7215,7 +7251,8 @@ async function importTree(ds, spec, opts = {}) {
|
|
|
7215
7251
|
e.required === false ? 0 : 1,
|
|
7216
7252
|
Number(e.min_level ?? 1),
|
|
7217
7253
|
Number(e.weight ?? 1),
|
|
7218
|
-
String(e.note || "")
|
|
7254
|
+
String(e.note || ""),
|
|
7255
|
+
nowIso()
|
|
7219
7256
|
]);
|
|
7220
7257
|
}
|
|
7221
7258
|
for (const c of conds) {
|
|
@@ -10005,8 +10042,12 @@ function run(env, action, rest, args) {
|
|
|
10005
10042
|
status: "pass",
|
|
10006
10043
|
detail: configRows.length === 0 ? "no overrides (all defaults)" : `${configRows.length} override(s), all valid`
|
|
10007
10044
|
});
|
|
10008
|
-
const lockPath = `${db.path}.
|
|
10009
|
-
|
|
10045
|
+
const lockPath = `${db.path}.ygllock`;
|
|
10046
|
+
let lockHolder = "";
|
|
10047
|
+
try {
|
|
10048
|
+
lockHolder = require("node:fs").readFileSync(`${lockPath}/pid`, "utf8").trim();
|
|
10049
|
+
} catch {}
|
|
10050
|
+
const hasLock = require("node:fs").existsSync(lockPath) && lockHolder !== String(process.pid);
|
|
10010
10051
|
checks.push({
|
|
10011
10052
|
name: "stale_lock",
|
|
10012
10053
|
status: hasLock ? "warn" : "pass",
|
|
@@ -10080,8 +10121,12 @@ function run(env, action, rest, args) {
|
|
|
10080
10121
|
db.run("UPDATE meta SET value='0' WHERE key='index_dirty'");
|
|
10081
10122
|
fixes.push("index dirty flag cleared (run: ygl-trees node index)");
|
|
10082
10123
|
}
|
|
10083
|
-
const lockDir = `${env.db.path}.
|
|
10084
|
-
|
|
10124
|
+
const lockDir = `${env.db.path}.ygllock`;
|
|
10125
|
+
let holder = "";
|
|
10126
|
+
try {
|
|
10127
|
+
holder = node_fs.readFileSync(`${lockDir}/pid`, "utf8").trim();
|
|
10128
|
+
} catch {}
|
|
10129
|
+
if (node_fs.existsSync(lockDir) && holder !== String(process.pid)) try {
|
|
10085
10130
|
node_fs.rmSync(lockDir, {
|
|
10086
10131
|
recursive: true,
|
|
10087
10132
|
force: true
|
|
@@ -10309,6 +10354,7 @@ async function main(argv) {
|
|
|
10309
10354
|
};
|
|
10310
10355
|
const group = args._[0];
|
|
10311
10356
|
const action = args._[1];
|
|
10357
|
+
if (group === "webui" && (action === "serve" || action === "export")) process.env.YGL_SERVE = "1";
|
|
10312
10358
|
if (args.flags.version === true || group === "version") {
|
|
10313
10359
|
out(ctx, appVersion());
|
|
10314
10360
|
return 0;
|
|
@@ -10351,10 +10397,33 @@ async function main(argv) {
|
|
|
10351
10397
|
return 1;
|
|
10352
10398
|
}
|
|
10353
10399
|
bad(ctx, `cannot open database ${file}: ${e?.message || e}`);
|
|
10354
|
-
const lock = `${file}.
|
|
10355
|
-
if (require("node:fs").existsSync(lock)) {
|
|
10356
|
-
|
|
10357
|
-
|
|
10400
|
+
const lock = `${file}.ygllock`;
|
|
10401
|
+
if (!ctx.json && require("node:fs").existsSync(lock)) {
|
|
10402
|
+
const pidFile = require("node:path").join(lock, "pid");
|
|
10403
|
+
let holder = "?";
|
|
10404
|
+
try {
|
|
10405
|
+
holder = require("node:fs").readFileSync(pidFile, "utf8").trim();
|
|
10406
|
+
} catch {}
|
|
10407
|
+
let alive = false;
|
|
10408
|
+
try {
|
|
10409
|
+
process.kill(Number(holder), 0);
|
|
10410
|
+
alive = true;
|
|
10411
|
+
} catch (err) {
|
|
10412
|
+
alive = err.code === "EPERM";
|
|
10413
|
+
}
|
|
10414
|
+
if (alive) {
|
|
10415
|
+
out(ctx, ` lock held by live process pid ${holder} (serve? webui dashboard)`);
|
|
10416
|
+
out(ctx, ` to run CLI write commands, stop that process first — or use YGL_TEST=1 to bypass`);
|
|
10417
|
+
} else {
|
|
10418
|
+
out(ctx, ` orphaned lock (pid ${holder} is dead) — removing: ${lock}`);
|
|
10419
|
+
try {
|
|
10420
|
+
require("node:fs").rmSync(lock, {
|
|
10421
|
+
recursive: true,
|
|
10422
|
+
force: true
|
|
10423
|
+
});
|
|
10424
|
+
} catch {}
|
|
10425
|
+
out(ctx, ` retry the command — the lock has been cleaned`);
|
|
10426
|
+
}
|
|
10358
10427
|
}
|
|
10359
10428
|
return 1;
|
|
10360
10429
|
}
|