nx-sk 0.3.3 → 0.4.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 (41) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +18 -2
  3. package/assets/nx-sk/SKILL.md +0 -1
  4. package/assets/nx-sk/references/00-design.md +1 -1
  5. package/assets/nx-sk/references/10-sections-entries.md +1 -1
  6. package/assets/nx-sk/references/20-secrets.md +37 -0
  7. package/assets/nx-sk/references/30-export-backup.md +16 -0
  8. package/package.json +1 -1
  9. package/src/core/argsafe.js +53 -0
  10. package/src/modules/backup/index.js +41 -0
  11. package/src/modules/backup/service.js +188 -0
  12. package/src/modules/backup/view.jsx +145 -0
  13. package/src/modules/entries/index.js +3 -0
  14. package/src/runtime/cli.js +56 -1
  15. package/src/runtime/registry.js +2 -1
  16. package/src/web/frontend/App.jsx +1 -1
  17. package/src/web/frontend/index.html +4 -0
  18. package/src/web/frontend/public/favicon-16.png +0 -0
  19. package/src/web/frontend/public/favicon-32.png +0 -0
  20. package/src/web/frontend/public/favicon-48.png +0 -0
  21. package/src/web/frontend/public/favicon.ico +0 -0
  22. package/src/web/frontend/public/logo-rounded.png +0 -0
  23. package/src/web/frontend/public/logo.png +0 -0
  24. package/src/web/frontend/registry.js +1 -0
  25. package/src/web/frontend/styles.css +2 -1
  26. package/src/web/public/assets/{index-BnVuH1lC.js → index-Bbe3iqam.js} +7 -7
  27. package/src/web/public/assets/index-C1bxqdjl.css +1 -0
  28. package/src/web/public/assets/{view-DsyplQBh.js → view-Bnwx5y_O.js} +1 -1
  29. package/src/web/public/assets/{view-rvADq3vX.js → view-CekTHUPO.js} +1 -1
  30. package/src/web/public/assets/{view-BvyRxTgX.js → view-Djo-TKw9.js} +1 -1
  31. package/src/web/public/assets/{view-CDje4hqZ.js → view-DuKJm1oq.js} +1 -1
  32. package/src/web/public/assets/view-b8TAoyhU.js +3 -0
  33. package/src/web/public/favicon-16.png +0 -0
  34. package/src/web/public/favicon-32.png +0 -0
  35. package/src/web/public/favicon-48.png +0 -0
  36. package/src/web/public/favicon.ico +0 -0
  37. package/src/web/public/index.html +6 -2
  38. package/src/web/public/logo-rounded.png +0 -0
  39. package/src/web/public/logo.png +0 -0
  40. package/assets/nx-sk/references/40-extend.md +0 -75
  41. package/src/web/public/assets/index-CwF5hM2v.css +0 -1
@@ -1,8 +1,10 @@
1
1
  // 通用 CLI 运行器:解析 → 匹配 → 校验 → 渲染 → help。
2
2
  // 命令表由 action 声明派生,所以「Web 上能做的 CLI 都能做」是结构保证,不靠人记。
3
+ import { readFileSync } from 'node:fs';
3
4
  import { resolve as resolvePath } from 'node:path';
4
5
  import { APP_NAME, DEFAULT_PORT, ENV_STORE, VERSION } from '../core/paths.js';
5
6
  import { badInput, external, toErrorShape } from '../core/errors.js';
7
+ import { parseDataArg, parseStdinValue } from '../core/argsafe.js';
6
8
  import { ACTIONS, MODULES, MODULE_TITLES, commandEntry } from './registry.js';
7
9
  import { applySpec, argSpecsOf, cliPathsOf, flagSpecsOf, usageOf } from './spec.js';
8
10
 
@@ -198,7 +200,60 @@ function parseArgs(action, tokens) {
198
200
  for (let i = 0; i < positional.length; i++) raw[argSpecs[i].name] = positional[i];
199
201
  // 显式 flag 覆盖位置参数:`key set K --value=<值>` 与 `key set K <值>` 必须等价,
200
202
  // 而后者在值以 `-` 开头时会被当成 flag(那时只能走前者)。
201
- return { ...raw, ...flags };
203
+ // 位置参数与 flag 一起交给展开函数:`key set K -` 的 `-` 是位置参数,
204
+ // 不能因为它「看起来像 flag」就漏掉。
205
+ return expandSafeArgs({ ...raw, ...flags });
206
+ }
207
+
208
+ /**
209
+ * 安全通道展开 —— CLI 独有的「通道知识」全部集中在这一个函数里。
210
+ *
211
+ * Windows 上值经命令行参数进来时,启动器 shim(Volta / npm 的 .cmd)会把换行
212
+ * 当命令分隔符截断,而命令**报成功**——一次性凭据就这么静默丢掉大半(真实事故)。
213
+ * 文件与 stdin 不受影响,所以这里把 `--data @文件` / `-` 在进 applySpec 之前
214
+ * 换成真正的值。放这里而不是 spec.js:那边的 coerce() 是 CLI 与 HTTP 共用的
215
+ * 纯净解码器,而 HTTP 没有「命令行通道」这回事。
216
+ */
217
+ function expandSafeArgs(raw) {
218
+ const out = { ...raw };
219
+ let stdinUsed = false;
220
+ // stdin 只能被消费一次:两个参数都写 `-` 的话第二个读到的是空,
221
+ // 静默读空比报错危险,所以显式拒绝。
222
+ const takeStdin = () => {
223
+ if (stdinUsed) throw badInput('stdin 只能被一个参数使用(已经有一个参数用 - 读过了)');
224
+ stdinUsed = true;
225
+ return readFileSync(0, 'utf8');
226
+ };
227
+
228
+ // --data 统一在这里解析成对象:spec.coerce 的 json 分支对「已是对象」的值直接返回,
229
+ // 所以两处不会各解析一遍,`-`(stdin)这条新通道也自然被 coerce 认作合法输入。
230
+ //
231
+ // 注意顺序:**先**解析 --data,再处理 `-`。反过来的话 `--data -` 会在 data 还没读 stdin
232
+ // 时就被判定成「value 也是 stdin」,把两次消费搅在一起(真实踩到的顺序 bug)。
233
+ if (typeof out.data === 'string') out.data = parseDataArg(out.data, takeStdin);
234
+
235
+ // `--data` 是位置参数 value 的替代给法(KV 表格只有一个值字段,语义确定),
236
+ // 所以给了 data 就不该再因为缺 value 而报用法错。
237
+ // 取 value 键;没有 value 键时取对象里唯一那个键。
238
+ //
239
+ // 位置参数 `-` 要**先于**这里的提取被认出来,否则 `key set K -` 的 `-` 会被
240
+ // 当成字面值 `-`。所以 stdin 判定放在 data 提取之后、用「value 是否还等于 -」判断。
241
+ if (out.value === undefined && out.data && typeof out.data === 'object' && !Array.isArray(out.data)) {
242
+ const keys = Object.keys(out.data);
243
+ if (out.data.value !== undefined) out.value = out.data.value;
244
+ else if (keys.length === 1) out.value = out.data[keys[0]];
245
+ else throw badInput(`--data 需要含 value 键,或只含一个键(收到 ${keys.length} 个: ${keys.join(', ')})`);
246
+ }
247
+
248
+ if (typeof out.value === 'string' && parseStdinValue(out.value)) out.value = takeStdin();
249
+
250
+ // 注意:这里**没有**「值含换行就报错」的检测,因为做不到。
251
+ // 实测(Windows 11 / Volta):损坏后的值里 CR 是被**删掉**而不是保留,
252
+ // 所以「含 CR」既抓不到损坏(零真阳性),又会拦住用 --data 写入的合法
253
+ // CRLF 多行值(高假阳性)。而 LF 截断后与合法单行值完全不可区分。
254
+ // 能做的只有:① 给出可靠通道(--data / stdin,本函数);② 让通道本身不再损坏
255
+ // (scripts/link-local.mjs 的转发器直连 node.exe)。
256
+ return out;
202
257
  }
203
258
 
204
259
  const STATUS_NOTE = {
@@ -6,12 +6,13 @@ import { cliPathsOf, usageOf } from './spec.js';
6
6
  import sections from '../modules/sections/index.js';
7
7
  import entries from '../modules/entries/index.js';
8
8
  import exportModule from '../modules/export/index.js';
9
+ import backup from '../modules/backup/index.js';
9
10
  import skill from '../modules/skill/index.js';
10
11
  import settings from '../modules/settings/index.js';
11
12
  import system from '../modules/system/index.js';
12
13
 
13
14
  // 顺序 = 面板 tab 顺序与 help 分组顺序(system 放最后,它是聚合模块)
14
- export const MODULES = [sections, entries, exportModule, skill, settings, system]
15
+ export const MODULES = [sections, entries, exportModule, backup, skill, settings, system]
15
16
  .slice()
16
17
  .sort((a, b) => (a.order || 50) - (b.order || 50));
17
18
 
@@ -54,7 +54,7 @@ export default function App() {
54
54
  return (
55
55
  <div className="app">
56
56
  <header className="top">
57
- <span className="brand">nx-sk</span>
57
+ <span className="brand"><img src="/logo-rounded.png" alt="" />nx-sk</span>
58
58
  <span className="meta">本机个人资源管理器{boot ? ` · v${boot.app.version}` : ''}</span>
59
59
  {boot ? (
60
60
  <span className="meta">
@@ -4,6 +4,10 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>nx-sk · 个人资源管理器</title>
7
+ <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
8
+ <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16.png" />
9
+ <link rel="icon" type="image/x-icon" href="/favicon.ico" />
10
+ <link rel="apple-touch-icon" href="/logo-rounded.png" />
7
11
  </head>
8
12
  <body>
9
13
  <div id="root"></div>
Binary file
@@ -11,6 +11,7 @@ export const VIEWS = [
11
11
  { id: 'sections', title: '栏目', order: 500, component: lazy(() => import('../../modules/sections/view.jsx')) },
12
12
  { id: 'entries', title: '条目', order: 600, tab: false, component: lazy(() => import('../../modules/entries/view.jsx')) },
13
13
  { id: 'export', title: '导出', order: 700, component: lazy(() => import('../../modules/export/view.jsx')) },
14
+ { id: 'backup', title: '加密备份', order: 750, component: lazy(() => import('../../modules/backup/view.jsx')) },
14
15
  { id: 'settings', title: '设置', order: 900, component: lazy(() => import('../../modules/settings/view.jsx')) },
15
16
  ];
16
17
 
@@ -63,7 +63,8 @@ header.top {
63
63
  border-bottom: var(--border);
64
64
  flex-shrink: 0;
65
65
  }
66
- header.top .brand { font-weight: 700; letter-spacing: .3px; }
66
+ header.top .brand { font-weight: 700; letter-spacing: .3px; display: inline-flex; align-items: center; gap: 7px; }
67
+ header.top .brand img { width: 20px; height: 20px; border-radius: 5px; display: block; }
67
68
  header.top .meta { color: var(--mid); font-size: 11px; }
68
69
 
69
70
  nav.tabs {