nx-sk 0.3.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/LICENSE +21 -0
  3. package/README.md +222 -0
  4. package/assets/nx-sk/SKILL.md +102 -0
  5. package/assets/nx-sk/references/00-design.md +116 -0
  6. package/assets/nx-sk/references/10-sections-entries.md +165 -0
  7. package/assets/nx-sk/references/20-secrets.md +170 -0
  8. package/assets/nx-sk/references/30-export-backup.md +80 -0
  9. package/assets/nx-sk/references/40-extend.md +75 -0
  10. package/bin/nx-sk.mjs +11 -0
  11. package/package.json +56 -0
  12. package/src/core/crypto.js +73 -0
  13. package/src/core/errors.js +52 -0
  14. package/src/core/fields.js +345 -0
  15. package/src/core/fsx.js +103 -0
  16. package/src/core/ids.js +19 -0
  17. package/src/core/open.js +32 -0
  18. package/src/core/paths.js +141 -0
  19. package/src/core/render.js +144 -0
  20. package/src/core/store.js +317 -0
  21. package/src/core/vault.js +109 -0
  22. package/src/index.js +14 -0
  23. package/src/modules/entries/index.js +152 -0
  24. package/src/modules/entries/kvTable.jsx +158 -0
  25. package/src/modules/entries/service.js +539 -0
  26. package/src/modules/entries/view.jsx +240 -0
  27. package/src/modules/export/index.js +39 -0
  28. package/src/modules/export/service.js +192 -0
  29. package/src/modules/export/view.jsx +155 -0
  30. package/src/modules/sections/index.js +104 -0
  31. package/src/modules/sections/service.js +337 -0
  32. package/src/modules/sections/view.jsx +311 -0
  33. package/src/modules/settings/index.js +34 -0
  34. package/src/modules/settings/service.js +78 -0
  35. package/src/modules/settings/view.jsx +193 -0
  36. package/src/modules/skill/index.js +51 -0
  37. package/src/modules/skill/service.js +200 -0
  38. package/src/modules/system/index.js +42 -0
  39. package/src/modules/system/service.js +155 -0
  40. package/src/runtime/api.js +101 -0
  41. package/src/runtime/cli.js +266 -0
  42. package/src/runtime/registry.js +65 -0
  43. package/src/runtime/server.js +80 -0
  44. package/src/runtime/spec.js +158 -0
  45. package/src/web/frontend/App.jsx +108 -0
  46. package/src/web/frontend/api/client.js +26 -0
  47. package/src/web/frontend/components/fieldEditor.jsx +85 -0
  48. package/src/web/frontend/components/ui.jsx +143 -0
  49. package/src/web/frontend/index.html +12 -0
  50. package/src/web/frontend/main.jsx +13 -0
  51. package/src/web/frontend/registry.js +20 -0
  52. package/src/web/frontend/store.jsx +87 -0
  53. package/src/web/frontend/styles.css +301 -0
  54. package/src/web/public/assets/index-BWGLG7Qa.js +40 -0
  55. package/src/web/public/assets/index-CwF5hM2v.css +1 -0
  56. package/src/web/public/assets/view-B6gPh1NQ.js +1 -0
  57. package/src/web/public/assets/view-CfXXYwW2.js +1 -0
  58. package/src/web/public/assets/view-CxzKrdA3.js +7 -0
  59. package/src/web/public/assets/view-DjO1C54o.js +7 -0
  60. package/src/web/public/index.html +13 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,133 @@
1
+ # Changelog
2
+
3
+ 本文件格式参考 [Keep a Changelog](https://keepachangelog.com/),中文正文。
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.3.0] - 2026-09-25
8
+
9
+ ### Added
10
+
11
+ - **字段填写策略 `fill`:`normal` / `optional` / `avoid`** —— 有些格子填了反而减分
12
+ (应届生的「上一家公司」、政治面貌为「群众」时的「入党时间」),这次把这种判断做成**数据**。
13
+ - `optional`(不必填)与 `avoid`(不填)**既不计入完整度的分子、也不计入分母**。
14
+ 完整度这个数字是用来导航「还该补什么」的,被一批主动跳过的字段长期压着就失效了。
15
+ - 它们不会被藏起来:`serializeEntry` 新增顶层 `excluded[]`,与 `missing[]` **并列**
16
+ (「主动跳过」和「还没填」是两件事);`completeness` 新增 `excluded`(个数)与
17
+ `excludedFilled`(标了不填却仍然填了的个数,非 0 就说明策略该更新了)。
18
+ CLI 的 `entry get` / `entry list` 把两者分开报,面板在字段标签上打中性标签。
19
+ - 新增 `section update --fill <字段>=normal|optional|avoid`(可重复,label 或 key 都认;
20
+ 改回照常填传 `normal`)。`entry fields` / `section dump` 的字段字典输出带上
21
+ `fill` 与 `fillLabel`。
22
+ - `validateFieldDef` 放行 `fill` —— 该函数是**白名单**,漏了会静默丢弃新属性,
23
+ 后果是「用户设了不填、完整度照旧算它」,很难查。非法策略报错而非忽略;`normal` 是缺省、不落盘。
24
+ - **非破坏性**:没有任何字段带 `fill` 时,行为与之前完全一致,旧 store 无需迁移。
25
+
26
+ ### Fixed
27
+
28
+ - **「面板无 emoji / 不使用原生弹窗」这条守卫覆盖不全**:它只 `readdir` 了
29
+ `src/web/frontend/` 的**直接子文件**,于是 `frontend/components/`(`fieldEditor.jsx`、`ui.jsx`)、
30
+ `App.jsx`、`main.jsx`、`api/` 全都没被扫到 —— 等于**面板规则不覆盖面板**;
31
+ `src/core/`、`bin/`、`scripts/` 同样漏了。改为递归收集并补齐这几处,
32
+ 顺手清掉 `core/render.js`、`core/store.js` 里两处漏网的 emoji。
33
+ 已按项目「改完规则必须反向测试」的要求验证:往原先扫不到的 `components/fieldEditor.jsx`
34
+ 注入 emoji,测试如期变红,还原后恢复全绿。
35
+
36
+ ## [0.2.0] - 2026-09-24
37
+
38
+ ### Changed
39
+
40
+ - **【破坏性】密文字段默认原文显示**,打码改成显式选项:
41
+ `--reveal` 全部换成 `--mask`(`key get/list`、`entry get`、`section dump`、`GET /api/keys?mask=1`);
42
+ 导出默认也含明文(`settings.includeSecretsInExport` 默认 `true`,要打码加 `--no-secrets`)。
43
+ 理由:本机单人工具,数据是给自己看的,默认打码等于每次先拦自己一道。
44
+ **落盘仍然是 AES-256-GCM 密文**——「store.json 泄露」不等于「凭据泄露」这一点没变。
45
+ 面板里的勾选框从「显示密文字段明文」翻成「打码显示(投屏/截图时用)」,默认不勾。
46
+ - **【破坏性】skill 不再出现在 Web 面板**:删掉 Skill tab 与它的视图;`skill install` / `skill get`
47
+ 仍完整保留在 CLI(用户明确要求,偏离了骨架 ref「三条最高优先级命令要有 Web 入口」的建议)。
48
+ - **【破坏性】密钥栏目从 8 字段缩成极简 KV**:只保留「名称(条目名)→ 值」。
49
+ 删掉了服务商 / 接口地址 / 模型 / 用途 / 有效期 / 额度 / 备注 ——
50
+ 存一个 key 不该先填一张表;「这个 key 干什么用」写进键名里就够了。
51
+ - **自动迁移**:`store.json` 版本升到 `2`。首次读盘时把旧 `keyValue` 的值搬到 `value`,
52
+ 并**先留一份迁移前的原始文件快照**到 `~/nx-sk/backup/…-migrate-v1-to-v2.json`;
53
+ 其余旧值**不删除**(不再显示 ≠ 删掉),`bootstrap --json` 的 `migration` 字段会报告迁移结果。
54
+ - 迁移幂等:`version` 落盘后不再触发。
55
+
56
+ ### Added
57
+
58
+ - **README 补「安装」节**:`npm link` 做成全局垫片(全局 node_modules 是指向项目的软链,
59
+ 改代码即刻生效)、skill 的**软链垫片**做法、以及 `npm unlink -g nx-sk` 的回退方式。
60
+ - **skill 在本机改成软链**:`~/.claude/skills/nx-sk` 与 `~/.workbuddy/skills/nx-sk`
61
+ 现在都软链到项目 `assets/nx-sk`(与 `server-cli-web-scaffold` 同一种做法)。
62
+ `skill install` 装的是**副本**,只在分发/跨机器时用;日常用软链才不会让 agent 拿到旧版手册。
63
+ - **【破坏性】KV 栏目在面板里就是一张表**:`secret` 标了 `kv: true`,面板渲染成
64
+ 「名称 / 值」两列表格(改值即改、加行即加键、行内重命名、一键复制),
65
+ **不再有**条目列表、字段表单、完整度百分比 —— 那些只对「一族同构条目」的栏目(求职)有意义。
66
+ CLI 侧 `key *` 缺省作用在标了 `kv` 的栏目上,并新增 `--section`;
67
+ `settings.kvSection` 这个间接层**删掉**(哪个栏目是 KV,由栏目自己说了算)。
68
+ - **KV 栏目拒绝加字段**:往里塞字段会让「值在哪一列」变得不确定,所以直接报错并指出
69
+ 该用 `key set`。这条是真实事故换来的:早先面板那行「KV 直填」把 `3123312=xxx`
70
+ 登记成了字段,用户看到的是一堆无意义的「模板建议」。
71
+ 配套 v2 → v3 迁移:把误当字段用的值搬进值列,字段收成一个,旧键一律不删,迁移前留原始快照。
72
+ - **`entry update` 改名时挡住重名**(同栏目已有同名条目报 `CONFLICT`)——
73
+ 重名会让「按名字取」变成二义。
74
+ - **字段台账不再拦写入**:`--set 任意键=值` 一律能存,没见过的键自动登记
75
+ (键名就是字段名,**中文也可以**;类型按值推断:`是/否` → bool、数组 → tags、其余 → text)。
76
+ `--allow-new-field` 随之取消(它现在是默认行为)。
77
+ **刻意不推 number**:18 位身份证号这类长数字超过 2^53,推成数字会静默丢精度。
78
+ - **面板加「KV 直填」行**:敲 `字段=值` 回车即写入,不再必须先在分组表单里找到那一格;
79
+ 「未填 N 项」也从告警口吻改成中性的「模板建议里还有 N 项没填(不影响使用)」。
80
+ - `assertFieldKey` 放宽到允许中文(仍挡空白 / 路径分隔符 / `..`),
81
+ 这样 `--set 微信号=xxx` 存下去就是 `微信号`,不必再记一个英文别名。
82
+ - **`key` 系列命令(KV 直通)**:`key set <名称> <值>` / `key get <名称> [--reveal]` /
83
+ `key list [--reveal]` / `key remove <名称>`,对应接口
84
+ `PUT /api/keys/:name` / `GET /api/keys/:name` / `GET /api/keys` / `DELETE /api/keys/:name`。
85
+ - `key *` 是 `entry *` 的**语法糖**,与 `entry` 同模块、共用同一批 service 函数
86
+ (加解密 / 打码 / 快照 / `--dry-run` 只有一份实现),有一致性测试钉住「不许另起一套」。
87
+ - `key set` 是 **SET 语义**(同名覆盖,返回值带 `created`)。这是它与 `entry add`
88
+ (同名报 `CONFLICT`)唯一刻意的差别。
89
+ - 重复写同一个值 → `{ status: 'skipped', unchanged: true }`,不重写密文、不留快照。
90
+ - 作用栏目由新设置项 `settings.kvSection` 决定(默认 `secret`)。
91
+ - CLI 位置参数与 flag 的优先级明确为**显式 flag 覆盖位置参数**,
92
+ 于是 `key set K <值>` 与 `key set K --value=<值>` 等价 ——
93
+ 后者是值以 `-` 开头时唯一可用的写法。
94
+
95
+ ### Fixed
96
+
97
+ - `cli.js` 的 `parseArgs` 原本让位置参数覆盖同名的显式 flag,导致 `--value=` 被静默忽略。
98
+ - `key set` 的「值没变」判定原本只比明文,面板原样回传打码值时会被误判成覆盖
99
+ (多写一次密文 + 多留一份快照)。现在明文相同与掩码相同都算「未改动」。
100
+
101
+ ## [0.1.0] - 2026-09-24
102
+
103
+ 首个版本:本机个人资源管理器(server-cli-web 骨架的完整实现)。
104
+
105
+ ### Added
106
+
107
+ - **栏目化数据模型**:栏目 = 一组同构条目 + 一份字段字典(字典是数据,不是代码分支)。
108
+ 内置两个模板:`job`(求职 / 个人信息,9 分组 75 字段)、`secret`(密钥,8 字段)。
109
+ - **CLI**(26 条命令):`serve` / `help` / `version` 三个平台命令,
110
+ 以及 `bootstrap` / `health` / `routes` / `section.*` / `entry.*` / `export.*` / `setting.*` / `skill.*`。
111
+ - **HTTP API**:由 action 声明同源编译,路由按「字面量段优先」排序(`/api/sections/templates`
112
+ 不会被 `/api/sections/:ref` 遮蔽)。写操作校验 `Origin`。
113
+ - **Web 面板**:栏目动态 tab、按分组渲染的字段表单、PATCH 语义保存(只发改动过的字段)、
114
+ 密文字段一键揭示、完整度与未填清单、派生式 CLI 提示、页内 toast / dialog / 错误边界。
115
+ - **密文存储**:敏感字段 AES-256-GCM 落盘,密钥来源支持 `NX_SK_PASSPHRASE`(scrypt + 盐)
116
+ 或本机 `~/.vaultkey`;密文带密钥指纹 `kid`,换来源时给明确的 `BLOCKED` 而不是乱码。
117
+ 打码值原样回传会被识别为「不修改」,直接写入打码占位会被拒绝。
118
+ - **整体导出**:JSON + Markdown,支持按栏目、指定目录、`--dry-run`;凭据默认打码,
119
+ `--with-secrets` 才出明文并在导出文件里标记 `withSecrets`。
120
+ - **快照与回滚**:每次破坏性写入前整库快照到 `~/nx-sk/backup/`(最多 20 份)。
121
+ - **损坏保护**:`store.json` 解析失败先改名为 `.corrupt-<时间戳>.json`,不静默覆盖。
122
+ - **内置 skill**:`skill install` 装到 `~/.claude/skills`(三态,不静默覆盖),
123
+ `skill get` 输出「prefix → 文档 → install 状态」三段,`--json` 为四元契约。
124
+ - **测试**:40 项单测(含一致性断言:模块目录 ↔ 注册表 ↔ 前端视图表 ↔ 视图里的 `/api` 字面量、
125
+ CRUD 五操作与路由形状、CLI 路径可解析回自身、eslint 互依禁列覆盖、文档漂移防护、
126
+ 装载期自检的负向反证)+ 56 项端到端冒烟断言。
127
+
128
+ ### Notes
129
+
130
+ - 数据默认落在 `~/nx-sk/`(骨架 ref 的默认是 `~/.<主题名>/`,本实现按项目要求改成可见目录)。
131
+ - 存储路径支持 `NX_SK_HOME` / `NX_SK_STORE` 覆盖 —— **不是可选项**:没有它测试会写脏真实数据。
132
+ - eslint 的模块互依禁列是**逐模块枚举**的:新增模块必须补一行,漏补是静默的
133
+ (已由 `tests/unit/consistency.test.mjs` 兜住)。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nx-sk contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,222 @@
1
+ # nx-sk · 本机个人资源管理器
2
+
3
+ 把你的信息按**栏目**存起来。CLI 与 Web 面板是同一条业务链,敏感字段密文落盘,
4
+ 数据全在你自己机器上 —— 默认 `~/nx-sk/`。
5
+
6
+ ```
7
+ nx-sk serve # 打开面板(http://127.0.0.1:7866)
8
+ ```
9
+
10
+ ![结构](https://img.shields.io/badge/node-%3E%3D18.17-informational) ![license](https://img.shields.io/badge/license-MIT-informational)
11
+
12
+ ## 安装
13
+
14
+ 命令名就是包名。**推荐做成全局垫片**(全局 node_modules 里是指向本项目的软链,
15
+ 改代码立刻生效,不用重新安装):
16
+
17
+ ```bash
18
+ cd <本项目>
19
+ pnpm install # 只为构建面板;CLI 侧零依赖
20
+ pnpm run build # 产出 src/web/public,serve 才有面板可服务
21
+ npm link # → 全局多了 nx-sk / nx-sk.cmd / nx-sk.ps1
22
+ nx-sk version # 从任何目录都能跑
23
+ nx-sk serve # 打开面板
24
+ ```
25
+
26
+ 取消:`npm unlink -g nx-sk`(只删全局垫片,不动项目)。
27
+
28
+ > **npm 的全局 prefix 不在你的 PATH 里怎么办**(用 Volta / nvm 管理 node 时很常见):
29
+ > `npm link` 会把垫片放进 npm 的 prefix 目录,而那个目录可能不在终端的 PATH 上,
30
+ > 于是你敲 `nx-sk` 会 command not found —— 但某些 shell(比如被别的工具注入过 PATH 的)却能跑。
31
+ > 这种情况自己做一个垫片,放进**你自己的 bin 目录**(确认它在 PATH 里):
32
+ >
33
+ > ```sh
34
+ > # ~/.local/bin/nx-sk(Git Bash / sh)
35
+ > #!/bin/sh
36
+ > MSYS_NO_PATHCONV=1; export MSYS_NO_PATHCONV # 不开这个,MSYS 会把 /d/... 按当前盘符改写成 d:\d\...
37
+ > exec node "<项目绝对路径>/bin/nx-sk.mjs" "$@"
38
+ > ```
39
+ >
40
+ > ```bat
41
+ > :: ~/.local/bin/nx-sk.cmd(cmd.exe / PowerShell)
42
+ > @echo off
43
+ > node "D:\a_js\js_proj\nx-sk\bin\nx-sk.mjs" %*
44
+ > ```
45
+ >
46
+ > 别忘了 `chmod +x ~/.local/bin/nx-sk`。验证:`cd / && nx-sk version`。
47
+
48
+ > 不想污染全局也行:`node bin/nx-sk.mjs <子命令>` 等价,或 `npx --no-install . <子命令>`。
49
+
50
+ ### skill:用**软链垫片**(推荐)
51
+
52
+ `skill install` 装出去的是**副本**,不会跟着源码走 —— 改了 `assets/nx-sk/` 就变旧版手册。
53
+ 日常开发应该用**软链**指向项目里的源,改完立刻生效、永远不会漂移:
54
+
55
+ ```bash
56
+ # Windows(cmd / 终端里,目录软链)
57
+ mklink /D "%USERPROFILE%\.claude\skills\nx-sk" "D:\a_js\js_proj\nx-sk\assets\nx-sk"
58
+ mklink /D "%USERPROFILE%\.workbuddy\skills\nx-sk" "D:\a_js\js_proj\nx-sk\assets\nx-sk"
59
+ # Windows(Git Bash,需要原生软链)
60
+ MSYS=winsymlinks:nativestrict ln -s "D:/a_js/js_proj/nx-sk/assets/nx-sk" "$HOME/.claude/skills/nx-sk"
61
+
62
+ # macOS / Linux
63
+ ln -s "$(pwd)/assets/nx-sk" "$HOME/.claude/skills/nx-sk"
64
+ ln -s "$(pwd)/assets/nx-sk" "$HOME/.workbuddy/skills/nx-sk"
65
+ ```
66
+
67
+ 建好之后 `nx-sk skill install` 会变成 no-op(内容一致 → 跳过),因为目标就是源本身。
68
+
69
+ > 软链建不出来?退回到复制式安装(适合分发给别人 / 跨机器,但**改完记得重装**):
70
+ >
71
+ > ```bash
72
+ > nx-sk skill install # → ~/.claude/skills/nx-sk(三态:安装/跳过/冲突)
73
+ > nx-sk skill install --to ~/.workbuddy/skills # WorkBuddy 侧的 agent 也读得到
74
+ > nx-sk skill install --force # 改了 assets/nx-sk/ 之后必须重装,否则是旧版
75
+ > nx-sk skill get nx-sk --json # 外部 agent 一键拿全上下文(四元:skillName/ref/content/contentBytes/install)
76
+
77
+ ## 它是什么
78
+
79
+ 一个「本机工具类」项目的标准骨架实现(server-cli-web):**serve 驱动 CLI 与 Web,
80
+ CLI 与 API 同源,skill 驱动 agent**。落到具体功能上:
81
+
82
+ | 栏目 | id | 内容 |
83
+ | --- | --- | --- |
84
+ | 求职 | `job` | 求职用的个人信息档案:9 个分组 / 75 个字段(姓名、学历、联系方式、求职意向、紧急联系人…)。模板只是**建议清单**,`--set 任意键=值` 直接写 |
85
+ | 密钥 | `secret` | **一张 KV 表**(`kv: true`):面板上就是两列表格(名称 / 值),一行一个键。落盘是 **AES-256-GCM** 密文,读出来默认是原文(`--mask` 才打码) |
86
+
87
+ 栏目不是写死的:字段字典是**数据**,加字段、加栏目都不用改代码。
88
+
89
+ ## 三条核心不变量
90
+
91
+ 1. **一条 action 同时声明 CLI 与 HTTP。** 面板上能做的,CLI 都能做;参数名与错误码两端一致。
92
+ 2. **依赖只能向下**:`core ← modules ← runtime ← bin`,模块之间不许互相 import(lint 强制)。
93
+ 3. **失败抛异常(退出码 1),业务结果返回 `{ status }`(退出码 0)。**
94
+ `conflict` / `blocked` / `skipped` 都是正常返回 —— agent 看 `status`,不看退出码。
95
+
96
+ ## 命令总表
97
+
98
+ 平台命令(`runtime/cli.js` 的 BUILTINS):
99
+
100
+ | 命令 | 作用 |
101
+ | --- | --- |
102
+ | `nx-sk serve [--port N] [--no-open]` | 起 Web 面板(唯一常驻命令;默认只绑 `127.0.0.1`) |
103
+ | `nx-sk help [主题]` | 命令表(由 action 声明生成,不是手写) |
104
+ | `nx-sk version` | 裸输出版本号 |
105
+
106
+ 模块命令(`src/modules/*/index.js` 声明,`nx-sk routes` 可双向查):
107
+
108
+ | 命令 | 作用 |
109
+ | --- | --- |
110
+ | `nx-sk bootstrap [--json]` | 一次拿齐上下文:版本 / 存储路径 / 栏目 / 设置 / 命令表 |
111
+ | `nx-sk health` | 存活与存储可达性检查 |
112
+ | `nx-sk routes [--module M] [--http "POST /api/entries"]` | 命令 ↔ 路由双向对照 |
113
+ | `nx-sk section list \| get \| add \| update \| remove` | 栏目 CRUD |
114
+ | `nx-sk section dump <id> [--mask]` | **一个栏目的全部信息**:字段台账 + 全部条目 + 每个条目的未填清单 |
115
+ | `nx-sk section templates` | 内置模板(job / secret) |
116
+ | `nx-sk entry list \| get \| add \| update \| remove` | 条目 CRUD(PATCH 语义) |
117
+ | `nx-sk entry fields [--section job]` | 字段字典 —— 填之前先看这个,别猜字段名 |
118
+ | `nx-sk export run [--format json\|md\|both] [--section id] [--with-secrets] [--out DIR] [--dry-run]` | 整体导出 |
119
+ | `nx-sk export list` | 历次导出与目录内文件 |
120
+ | `nx-sk setting get \| set` | 全局设置 |
121
+ | `nx-sk skill list \| install \| get` | 把内置 skill 装给 agent / 给外部 agent 取上下文 |
122
+
123
+ 所有写命令都有 `--dry-run`;所有命令都支持 `--json`(标准输出**单个** JSON 值)。
124
+
125
+ ## 快速上手
126
+
127
+ ```bash
128
+ pnpm install
129
+ pnpm start # 构建前端 + 起面板
130
+ # 或开发模式(vite :5180 + serve :7866,一个 Ctrl-C 关两个)
131
+ pnpm run dev
132
+ ```
133
+
134
+ 用 CLI 填一份档案:
135
+
136
+ ```bash
137
+ nx-sk entry fields --section job --json # ① 看字段字典
138
+ nx-sk entry add --section job --set 姓名=张三 --set 电话=13800000000 --json
139
+ nx-sk entry update 张三 --data @补全.json --dry-run # ② 批量补,先试运行
140
+ nx-sk entry update 张三 --data @补全.json
141
+ nx-sk section dump job --json # ③ 复核:完整度 + 未填清单
142
+ ```
143
+
144
+ 存一个大模型 key —— 密钥栏目就是**极简 KV:名称 → 值**(**值不会以明文进 store.json**):
145
+
146
+ ```bash
147
+ nx-sk key set OPENAI_KEY sk-xxxxxxxx # 写入:不存在则新建,存在则覆盖
148
+ nx-sk key get OPENAI_KEY # 取值:默认就是原文
149
+ nx-sk key get OPENAI_KEY --mask # 投屏/截图时才打码:sk-x******xxxx
150
+ nx-sk key list # 所有键(默认原文)
151
+ nx-sk key remove OPENAI_KEY # 删除,写前自动留快照
152
+ ```
153
+
154
+ 状态与栏目一样:`key *` 只是 `entry *` 的语法糖(同模块、同一批 service 函数),
155
+ 作用在 `settings.kvSection` 指向的单字段栏目上(默认 `secret`)。
156
+
157
+ 整体导出(凭据默认**含明文**——本机自己用;要分享给别人时加 `--no-secrets` 打码):
158
+
159
+ ```bash
160
+ nx-sk export run --format both --out D:/备份/nx-sk
161
+ nx-sk export list
162
+ ```
163
+
164
+ ## 数据与安全
165
+
166
+ | 项 | 位置 / 做法 |
167
+ | --- | --- |
168
+ | 数据目录 | `~/nx-sk/`,可用 `NX_SK_HOME` 覆盖 |
169
+ | 状态文件 | `~/nx-sk/store.json`,可用 `NX_SK_STORE` 覆盖;**原子写**(临时文件 + rename) |
170
+ | 密文密钥 | 环境变量 `NX_SK_PASSPHRASE`(scrypt 派生 + 盐)优先,否则 `~/nx-sk/.vaultkey`(32 字节随机,0600) |
171
+ | 快照 | 每次破坏性写入前整库备份到 `~/nx-sk/backup/`(最多 20 份),拷回即回滚 |
172
+ | 导出 | `~/nx-sk/export/`,默认不含凭据明文 |
173
+ | 服务绑定 | 默认 `127.0.0.1`,不暴露到局域网;写操作校验 `Origin` |
174
+ | 数据损坏 | `store.json` 解析失败会先被改名为 `.corrupt-<时间戳>.json`,不会静默覆盖 |
175
+
176
+ 换成口令模式更安全(口令不落盘):
177
+
178
+ ```bash
179
+ export NX_SK_PASSPHRASE='你的长口令' # 之后所有密文都用它派生
180
+ ```
181
+
182
+ 注意:**换口令或删掉密钥文件后,旧密文解不开**(这是设计,密文指纹 `kid` 会明确报错)。
183
+
184
+ ## 给 agent 用
185
+
186
+ ```bash
187
+ nx-sk skill install # 装到 ~/.claude/skills/nx-sk(三态,不静默覆盖)
188
+ nx-sk skill get nx-sk --json # {skillName, ref, content, contentBytes, install}
189
+ nx-sk skill get nx-sk 10-sections-entries # 裸名 → references/10-sections-entries.md
190
+ ```
191
+
192
+ 内置 skill 的 ref 路由表:
193
+
194
+ | ref | 何时读 |
195
+ | --- | --- |
196
+ | `00-design` | 为什么这么分层、错误码与 `{status}` 的分工 |
197
+ | `10-sections-entries` | 填/改信息的完整命令与 AI 补全流程 |
198
+ | `20-secrets` | 密文机制、密钥来源、排查「解不开」 |
199
+ | `30-export-backup` | 导出、快照与回滚 |
200
+ | `40-extend` | 改这个项目本身:闭环落点表与静默失效点 |
201
+
202
+ ## 开发
203
+
204
+ ```bash
205
+ pnpm test # = lint + build + 冒烟(56 项断言) + 单测(40 项)
206
+ ```
207
+
208
+ | 目录 | 职责 |
209
+ | --- | --- |
210
+ | `bin/nx-sk.mjs` | 唯一入口,只转发 argv + 兜异常 |
211
+ | `src/core/` | 零业务语义:paths / errors / store / fsx / crypto / vault / fields / render / ids / open |
212
+ | `src/modules/<域>/` | `index.js`(action 声明) + `service.js`(业务) + `view.jsx`(面板) |
213
+ | `src/runtime/` | registry / spec / cli / api / server —— 装配层,不写业务 |
214
+ | `src/web/frontend/` | React 壳 + 组件 + 视图注册表 |
215
+ | `assets/nx-sk/` | 随包分发的 skill(SKILL.md + references/) |
216
+ | `tests/unit/` · `tests/smoke.mjs` | 一致性断言 + 端到端冒烟 |
217
+
218
+ **加一个功能域要碰的 12 处**(其中 3 处没有任何断言会替你发现)见 `assets/nx-sk/references/40-extend.md`。
219
+
220
+ ## License
221
+
222
+ MIT
@@ -0,0 +1,102 @@
1
+ ---
2
+ name: nx-sk
3
+ description: 本机个人资源管理器——栏目化存储个人信息(求职档案等)与大模型 API Key,CLI 与 Web 面板同源,敏感字段密文落盘。当用户要"记录/更新我的个人信息"、"填一份求职档案"、"存一个大模型 key"、"管理我的密钥"、"导出我的资料"、"起一个个人资料面板",或需要在填表单/网申时批量取用个人字段、用 CLI 快速设置某个栏目内容时使用。触发词:nx-sk、个人资源管理器、个人信息、求职档案、栏目、条目、密钥、API key、密文、导出、面板、serve。不适用:纯问答、不需要落盘的临时讨论;也不适用于把密钥同步/上传到远程服务的场景——nx-sk 只在本机落盘,不做任何上传。
4
+ ---
5
+
6
+ # nx-sk · 本机个人资源管理器
7
+
8
+ 一句话:**把「你的信息」按栏目存起来,CLI 与 Web 面板是同一套业务,敏感字段密文落盘。**
9
+
10
+ 数据落在 `~/nx-sk/`(可用环境变量 `NX_SK_HOME` / `NX_SK_STORE` 覆盖)。默认两个栏目:
11
+
12
+ | 栏目 | id | 内容 |
13
+ | --- | --- | --- |
14
+ | 求职 | `job` | 求职用的个人信息档案(姓名/学历/求职意向…约 70 个字段) |
15
+ | 密钥 | `secret` | **一张 KV 表**(`kv: true`):一行一个键值对。面板渲染成表格,值以 AES-256-GCM 密文落盘 |
16
+
17
+ ## 核心不变量
18
+
19
+ 1. **栏目 = 一组同构条目 + 一份字段台账。台账是数据,不是代码。**
20
+ 字典随栏目存在 store 里,所以 agent 与面板看到的字段永远一致。
21
+ **它不拦写入**:`--set 任意键=值` 一律能存,没见过的键会自动登记(键名就是它本身,
22
+ 中文也可以)。台账的用处是「模板建议 + 你实际写过什么」,不是门禁。
23
+ *违反会怎样*:如果把它做成门禁,用户每换一个表单就要先改一次 schema ——
24
+ 而那正是「只想存个 kv」时最不想付的成本。
25
+
26
+ 2. **一条 action 同时声明 CLI 与 HTTP。** 面板上能做的,CLI 都能做;两边参数名、错误码一致。
27
+ *违反会怎样*:只能靠字符串匹配猜错误,无法可靠分支。
28
+
29
+ 3. **失败抛异常(退出码 1),业务结果返回 `{ status }`(退出码 0)。**
30
+ `conflict` / `blocked` / `skipped` 都是**正常返回**,agent 要看 `status` 字段而不是退出码。
31
+ *违反会怎样*:把一个「等用户决策」的冲突当成失败去重试。
32
+
33
+ ## 命令面(先跑这一条)
34
+
35
+ ```bash
36
+ nx-sk bootstrap --json # 版本 / 存储路径 / 栏目 / 设置 / 命令表,一次拿齐
37
+ ```
38
+
39
+ 零知识时的主力命令:
40
+
41
+ ```bash
42
+ nx-sk entry fields --section job --json # ① 看台账:模板建议字段 + 你写过什么
43
+ nx-sk entry update <条目id或条目名> --set 字段=值 # ② 直接写:字典里没有的键会自动登记
44
+ nx-sk section dump job --json # ③ 一个栏目的全部信息 + 每个条目的未填清单
45
+ ```
46
+
47
+ **键就是键**:`--set 微信=xxx` 存下去就是 `微信`,不需要先翻译成英文 key。
48
+
49
+ `--set` 的键**可以用 key 也可以用中文标签**(`--set 电话=138…` 与 `--set phone=138…` 等价)。
50
+ 一次写很多字段用 `--data @file.json`(`--data` 也接受内联 JSON)。
51
+
52
+ 存/取密钥(KV)走这四条,别用 `entry --set value=` 绕一圈:
53
+
54
+ ```bash
55
+ nx-sk key set OPENAI_KEY sk-xxxxxxxx # 写入(存在即覆盖,返回 created 告诉你是哪种)
56
+ nx-sk key get OPENAI_KEY # 取值:默认就是原文
57
+ nx-sk key get OPENAI_KEY --mask # 投屏/截图时才打码
58
+ nx-sk key list # 所有键(默认原文)
59
+ nx-sk key remove OPENAI_KEY # 删除,写前自动留快照
60
+ ```
61
+
62
+ `key *` 是 `entry *` 的**语法糖**(同模块、同一批 service 函数),缺省作用在标了 `kv` 的栏目上
63
+ (`--section` 可指定)。区别只有一个:`key set` 遇到同名是**覆盖**,`entry add` 是报 `CONFLICT`。
64
+
65
+ **KV 栏目不许加字段**:它的形状就是一行一个键值对。想加东西就用 `key set <名称> <值>`
66
+ (在 KV 栏目里 `entry update --set 新键=值` 会被拒绝并告诉你该用哪条命令)。
67
+
68
+ ## ref 路由表(按需加载)
69
+
70
+ | ref | 何时读取 |
71
+ | --- | --- |
72
+ | `00-design` | 想搞清楚「为什么这么设计」、分层与依赖方向、错误码与 `{status}` 的分工时 |
73
+ | `10-sections-entries` | **动手填/改信息时必读**——栏目与条目的完整命令、AI 补全个人信息的推荐流程、字段字典怎么扩、读命令与写命令的参数陷阱 |
74
+ | `20-secrets` | 存/读大模型 key、密文机制、密钥来源(环境变量 vs 本机密钥文件)、什么时候会解不开 |
75
+ | `30-export-backup` | 整体导出、只导出某个栏目、快照与回滚、明文密钥的取舍 |
76
+ | `40-extend` | 要**改这个项目本身**(加一个栏目模板、加一个功能域)时——闭环落点表与静默失效点 |
77
+
78
+ 取 ref 全文(外部 agent 用这条,不必进终端翻文件):
79
+
80
+ ```bash
81
+ nx-sk skill get nx-sk # SKILL.md 全文
82
+ nx-sk skill get nx-sk 10-sections-entries # 裸名 → references/10-sections-entries.md
83
+ nx-sk skill get nx-sk references/20-secrets.md # 也可写完整路径
84
+ nx-sk skill get nx-sk --json # 四元 JSON:{skillName, ref, content, contentBytes, install}
85
+ ```
86
+
87
+ ## 什么时候不用
88
+
89
+ - 用户只是随口问一句、不需要落盘 —— 别动数据。
90
+ - 想找「面板上的 skill 页」—— 没有:**skill 只走 CLI**(`nx-sk skill install` / `nx-sk skill get`),
91
+ 面板里不呈现它。
92
+ - 用户要把密钥**同步到云端/团队** —— nx-sk 只做本机落盘与本地导出,不做上传。
93
+ - 只是要看一眼面板 —— 提示 `nx-sk serve`,别用 CLI 重放一遍。
94
+
95
+ ## 硬约束(动手前先记住)
96
+
97
+ - 所有写命令都有 `--dry-run`,先试运行再落盘;删除类命令**自动留快照**在 `~/nx-sk/backup/`。
98
+ - 密文字段**落盘是密文,读出来默认是原文** —— 本机单人工具,自己看的东西不先拦一道。
99
+ 要打码形态(投屏 / 截图)显式加 `--mask`。
100
+ - 把打码后的值(含 `******`)当新值写回去会被拒绝 —— 那通常意味着「不修改」,什么都不传即可。
101
+ - 导出默认**含明文**;要分享出去时加 `--no-secrets`。
102
+ - **绝不把密钥明文打进日志、提交进 git、或写进给第三方的输出里。**
@@ -0,0 +1,116 @@
1
+ # 00 · 设计思想
2
+
3
+ > 骨架级文档:回答「为什么这么设计」。**不放场景级 SOP** —— 怎么调命令看 `10-sections-entries` / `20-secrets` / `30-export-backup`。
4
+
5
+ ## 一、驱动关系
6
+
7
+ ```
8
+ ┌──────────────────────────────────────────┐
9
+ │ serve(唯一常驻入口,默认 127.0.0.1:7866) │
10
+ └───────────────┬──────────────────────────┘
11
+ │ 静态面板 + /api
12
+ ┌───────────────┴──────────────┬────────────┐
13
+ ▼ ▼ │
14
+ Web 面板 HTTP API │
15
+ └──────────────┬───────────────┘ │
16
+ ▼ │
17
+ action 声明(唯一真相源) │
18
+ ▼ ▼
19
+ service 层(业务) CLI(同一条链)
20
+ ▼ │
21
+ core(存储 / 密文 / 字段字典 / 渲染) │
22
+ ▼
23
+ skill install ──► ~/.claude/skills ──► agent
24
+ ```
25
+
26
+ 三条驱动链,一条比一条向上一级:serve 驱动 CLI 与 Web;CLI/Web 驱动 skill;skill 驱动 agent。
27
+
28
+ **第 3 条是这个骨架跟普通 CLI 工具的分水岭**:nx-sk 不只是给人用的,也是给 agent 用的。
29
+ 一旦承认这点,`--json` 的稳定性、错误码、幂等性就从「锦上添花」变成硬契约。
30
+
31
+ ## 二、分层与依赖方向
32
+
33
+ ```
34
+ core/ 零业务语义:paths / errors / store / fsx / crypto / vault / fields / render / ids / open
35
+ modules/ 功能域,每个自包含:index.js(声明) + service.js(业务) + view.jsx(面板)
36
+ runtime/ 装配层:registry / spec / cli / api / server —— 不写业务
37
+ web/frontend/ React 壳,只 import 各模块的 view.jsx
38
+ ```
39
+
40
+ | 允许 | 禁止 |
41
+ | --- | --- |
42
+ | `core` ← `modules` ← `runtime` ← `bin` | 反向依赖(core 不许 import 上层) |
43
+ | `web/frontend/*` 与 `view.jsx` import 组件、api 客户端、store | 前端 import `core/`、`runtime/`、`node:*`、任何 `service.js` |
44
+ | 任意模块 → `../settings/service.js`(单向只读,基础模块) | 其它模块之间互相 import |
45
+ | `view.jsx` → `../../web/frontend/components/*` | `core` / `runtime` / `service.js` → 任何 `.jsx` |
46
+
47
+ **例外只有一处**:`settings` 是基础模块,允许被单向只读依赖。它的落地方式是——
48
+ eslint 的模块互依禁列**逐模块枚举**,"例外"就等于**不把它列进去**。
49
+
50
+ **模块要 runtime 的数据怎么办**(如 `system` 需要命令表):用**函数体内的动态 import** 破环。
51
+ 静态 import 会成环,动态 import 不会——这是全项目唯一需要用到它的地方。
52
+
53
+ ## 三、关键不变量
54
+
55
+ ### 1. 字段字典是数据,不是代码分支
56
+
57
+ 栏目自己带一份 `fields[]`(key / label / type / group / options / sensitive),
58
+ 条目按它存取。所以:
59
+
60
+ - CLI 与面板的字段集合不可能分叉——它们读的是同一份;
61
+ - 写入**不受字典限制**:没见过的键自动登记(键名即字段名),agent 与用户都不必先改 schema;
62
+ - `--set` 的键同时接受 `key` 与中文 `label`,人机都顺手。
63
+
64
+ *违反会怎样*:如果字段写死在代码里,面板加一个字段就要改前端 + 后端 + 文档三处,
65
+ 而漏改任何一处都不会报错——只是「面板上有、CLI 里没有」。
66
+ 反过来,如果把它做成写入门禁,用户每见一个新表单就得先扩一次字典——那是把成本推给了用户。
67
+
68
+ ### 2. 一条 action 同时声明 cli 与 http
69
+
70
+ ```js
71
+ { id: 'entry.update', cli: ['entry','update'], http: ['PATCH','/api/entries/:ref'], run, render }
72
+ ```
73
+
74
+ CLI 命令表、HTTP 路由表、`help` 文本全部由此派生。
75
+ **方向刻意不对称**:`cli` 必填,`http` 可为 `null`(纯 CLI 命令)。
76
+ 这样「Web 上能做的 CLI 都能做」是结构保证,而不要求反向。
77
+
78
+ *违反会怎样*:两份清单必然分叉 —— 面板上有按钮、CLI 里没命令,且没人发现。
79
+
80
+ ### 3. 失败抛异常,业务结果返回 `{ status }`
81
+
82
+ 判据是**「调用方要不要处理它」**:
83
+
84
+ | 情形 | 表达 | 例子 |
85
+ | --- | --- | --- |
86
+ | 调用方无从处理,只能中断上报 | `throw` | 字段名打错、条目不存在、密钥解不开 |
87
+ | 调用方要拿它做决策 | `return { status }` | 栏目非空需 `--force`(`blocked`)、技能安装冲突(`conflict`)、已是期望状态(`skipped`) |
88
+
89
+ **冲突不是错误。** 面板要拿它弹窗让用户决策,所以不能抛。
90
+ CLI 侧这四个取值分别渲染成不同的可读提示,退出码只有「抛异常」才是 1。
91
+
92
+ ## 四、错误契约
93
+
94
+ | code | 判据 | HTTP |
95
+ | --- | --- | --- |
96
+ | `INVALID_INPUT` | 输入本身不合法 | 400 |
97
+ | `NOT_FOUND` | 指定的目标不存在 | 404 |
98
+ | `CONFLICT` | 目标存在且与期望状态冲突,需要用户选一侧 | 409 |
99
+ | `BLOCKED` | 操作合法但被前置条件挡住(或密钥不一致) | 409 |
100
+ | `EXTERNAL` | 外部命令/端口失败 | 502 |
101
+ | `INTERNAL` | 兜底 | 500 |
102
+
103
+ 映射**只写一处**(`core/errors.js`)。`--json` 的错误对象向后兼容:`error` 保持字符串,`code` 是新增字段。
104
+
105
+ ## 五、错误案例表(都实测踩过)
106
+
107
+ | 错误操作 | 实际后果 | 正确做法 |
108
+ | --- | --- | --- |
109
+ | 把「读命令的过滤 flag」照抄写命令的 `default` | 参数层无条件注入默认值 → 「不传 = 全部」的分支**永远走不到**,不报错、不崩,只是永远返回半个结果 | 写命令的 flag 可带 `default`;读命令的过滤 flag 一律不带 |
110
+ | 路由只按声明顺序匹配 | `GET /api/sections/templates` 被 `GET /api/sections/:ref` 遮蔽,命中哪条取决于 actions 数组顺序 | 路由按「字面量段优先」排序,让声明顺序无关(`runtime/spec.js` 的 `compareRoutes`) |
111
+ | 把打码值当成新值写回去 | 密钥字段被写成 `sk-1****cdef` 这串掩码,**原凭据永久丢失** | 写入前比对掩码:相等就当成「未改动」保留原密文;含 `******` 的值直接拒绝 |
112
+ | 删条目/栏目不留快照 | 误删即不可逆;这是用户的**唯一一份**个人信息 | 所有破坏性写操作先 `snapshotStore()` 到 `~/nx-sk/backup/` |
113
+ | store.json 解析失败就当空库继续 | 下一次写入把用户数据永久覆盖 | 先把损坏文件改名为 `store.json.corrupt-<时间>.json`,再降级返回空结构 |
114
+ | 前端 import 了 `node:*` 或 `core/` | Vite 把 Node 侧代码打进浏览器包,dev 模式白屏 | lint 的 `files` 同时覆盖 `view.jsx`,禁掉 `node:*` / `core` / `runtime` / `service.js` |
115
+ | 新增模块忘了往 eslint 互依禁列里补一行 | **静默**:新模块变成「谁都可以依赖」,且规则随模块数增加持续衰减 | 把「加模块必补禁列」写进 `40-extend` 的闭环表,并在禁列旁留注释说明漏补是静默的 |
116
+ | 代理前缀 `/api` 与前端源码目录 `api/` 撞车 | 只在 dev 模式整页白屏,报错却是 404 / ECONNREFUSED,离根因很远 | vite 代理加 `bypass`,按「是不是前端资源」分流;并用测试钉住 `shouldServeLocally` |