@monoedge/jdu-cli 0.3.0 → 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 (3) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.js +158 -49
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  ```bash
8
8
  npm i -g @monoedge/jdu-cli
9
- jdu init # 默认官方 https://docs.mszhou.com,浏览器里 passkey 点一次授权;自部署加 --server <url>
9
+ jdu login --server <url> # 指定简牍站点(本机自部署用 --local);站点支持时打开浏览器点一次授权
10
10
  jdu push notes/hello.md --visibility public
11
11
  jdu official add <docId> # 管理员:上架到官方知识库
12
12
  ```
@@ -16,7 +16,7 @@ jdu official add <docId> # 管理员:上架到官方知识库
16
16
  登录方式听 server 的 `/healthz`:password 站点注册过 passkey 就打开浏览器授权签 token;没有浏览器的机器(CI、SSH 远端)带 `--token`:
17
17
 
18
18
  ```bash
19
- jdu init --server https://docs.example.com --token <token>
19
+ jdu login --server https://docs.example.com --token <token>
20
20
  ```
21
21
 
22
22
  签出去的 token 用 `jdu token list` / `jdu token rm <id>` 管理。
package/dist/cli.js CHANGED
@@ -3502,7 +3502,7 @@ async function httpError(method, url, res) {
3502
3502
  //#endregion
3503
3503
  //#region src/browser-auth.ts
3504
3504
  /**
3505
- * jdu init 的浏览器授权(#66):本机起一个 loopback 回调,打开 server 下发的 authorizeUrl,
3505
+ * jdu login 的浏览器授权(#66):本机起一个 loopback 回调,打开 server 下发的 authorizeUrl,
3506
3506
  * 人在浏览器里用 passkey 会话点一次「授权」,回跳带回一次性 code,再用 PKCE verifier 向 server 换 token。
3507
3507
  *
3508
3508
  * authorizeUrl 与 server 可能不同源(CLI 连 127.0.0.1:18082,浏览器开 https://md.mason.local)——
@@ -3517,7 +3517,7 @@ async function browserAuthorize(input) {
3517
3517
  server.close();
3518
3518
  fn(value);
3519
3519
  };
3520
- const fail = finish((msg) => reject(new CliError(msg, "重新执行 jdu init;不想开浏览器就带 --token")));
3520
+ const fail = finish((msg) => reject(new CliError(msg, "重新执行 jdu login;不想开浏览器就带 --token")));
3521
3521
  const handler = callbackHandler(state, finish(resolve));
3522
3522
  const server = createServer((req, res) => {
3523
3523
  if (new URL(req.url ?? "/", "http://localhost").pathname !== "/callback") {
@@ -3550,7 +3550,7 @@ async function browserAuthorize(input) {
3550
3550
  })
3551
3551
  });
3552
3552
  const text = await res.text();
3553
- if (!res.ok) throw new CliError(`code 换 token 失败(HTTP ${res.status}):${text.slice(0, 200)}`, "重新执行 jdu init 再试一次");
3553
+ if (!res.ok) throw new CliError(`code 换 token 失败(HTTP ${res.status}):${text.slice(0, 200)}`, "重新执行 jdu login 再试一次");
3554
3554
  let token;
3555
3555
  try {
3556
3556
  token = JSON.parse(text).token;
@@ -3596,7 +3596,11 @@ async function probeApiWithBearer(server, token) {
3596
3596
  redirect: "manual"
3597
3597
  })).ok;
3598
3598
  }
3599
- async function runLogin(opts) {
3599
+ /**
3600
+ * forward-auth / OIDC 分支,外加 --user 直连与 --token 直存。
3601
+ * 不是命令入口——入口是 init.ts 的 runLogin,它探完 healthz 才分派到这里。
3602
+ */
3603
+ async function runOidcLogin(opts) {
3600
3604
  const server = opts.server.trim().replace(/\/+$/, "");
3601
3605
  if (server === "") throw new CliError("--server 不能为空");
3602
3606
  const healthz = await (opts.fetchHealthz ?? fetchHealthz)(server);
@@ -3621,7 +3625,7 @@ async function runLogin(opts) {
3621
3625
  configPath: saveConfig({ server }),
3622
3626
  source: "anonymous"
3623
3627
  };
3624
- if (provider === "token" || provider === "password") throw new CliError(`这个 server 用 ${provider} 鉴权,CLI 需要 --token`, "jdu init --server <url>(站点支持时打开浏览器授权),或 jdu login --server <url> --token <token>(token 在 server 首启 stdout / data/initial-token.txt)");
3628
+ if (provider === "token" || provider === "password") throw new CliError(`这个 server 用 ${provider} 鉴权,CLI 需要 --token`, "jdu login --server <url> --token <token>(token 在 server 首启 stdout / data/initial-token.txt)");
3625
3629
  const oidc = resolveOidc({
3626
3630
  issuer: opts.issuer,
3627
3631
  clientId: opts.clientId,
@@ -3659,21 +3663,22 @@ async function runLogin(opts) {
3659
3663
  //#endregion
3660
3664
  //#region src/init.ts
3661
3665
  /**
3662
- * jdu init:选定部署目标并写好本地配置,一条命令把「指向哪台简牍」定下来。
3666
+ * jdu login:选定站点、拿到凭据、写好本地配置,一条命令把「登录到哪台简牍」定下来。
3663
3667
  *
3664
- * 目标三选一:
3665
- * (默认) 官方线上服务 https://docs.mszhou.com(OFFICIAL_SERVER)
3668
+ * 目标必须显式给出(没有默认站点,官方商用服务尚未上线):
3666
3669
  * --local 本机自部署(http://127.0.0.1:8080)
3667
3670
  * --server <u> 任意自部署地址
3668
3671
  *
3669
3672
  * 为 agent 联动而设计:全 flag 驱动、无交互提示、--json 输出机器可读结果,
3670
3673
  * 拿到 next 字段就知道下一步该干什么(要不要 token、去哪拿)。
3671
3674
  *
3672
- * 登录方式听 server 的(healthz.cliAuth,#66):声明了 browser 就开浏览器授权换 token;
3673
- * 只有 token 就把 server 给的 tokenHint 拼进 next。CLI 不按「官方 / 自部署」猜。
3675
+ * 登录方式全听 server 的(healthz.cliAuth,#66),用户不必先知道自己的站点用什么鉴权:
3676
+ * browser → 开浏览器授权换 token
3677
+ * oidc → 委托 runOidcLogin 走 SSO(forward-auth)
3678
+ * token → 把 server 给的 tokenHint 拼进 next,告诉去哪拿
3679
+ * anonymous→ 直接就绪
3680
+ * CLI 不按「官方 / 自部署」猜,也不再要求用户在 init / login 之间二选一。
3674
3681
  */
3675
- /** 官方线上地址。password 鉴权:浏览器 passkey,CLI 走 token,token 向站点管理员申领。 */
3676
- var OFFICIAL_SERVER = "https://docs.mszhou.com";
3677
3682
  var LOCAL_SERVER = "http://127.0.0.1:8080";
3678
3683
  function resolveInitTarget(opts) {
3679
3684
  if (opts.local && opts.server) throw new CliError("--local 与 --server 只能二选一");
@@ -3695,12 +3700,9 @@ function resolveInitTarget(opts) {
3695
3700
  target: "local",
3696
3701
  server: LOCAL_SERVER
3697
3702
  };
3698
- return {
3699
- target: "official",
3700
- server: OFFICIAL_SERVER
3701
- };
3703
+ throw new CliError("jdu login 需要指定站点", "自部署用 --server <url>,本机用 --local");
3702
3704
  }
3703
- async function runInit(opts) {
3705
+ async function runLogin(opts) {
3704
3706
  const { target, server } = resolveInitTarget(opts);
3705
3707
  const healthz = await (opts.fetchHealthz ?? fetchHealthz)(server);
3706
3708
  const provider = healthz.authProvider ?? "token";
@@ -3711,6 +3713,21 @@ async function runInit(opts) {
3711
3713
  authProvider: provider,
3712
3714
  authMethods: methods
3713
3715
  };
3716
+ if (opts.user !== void 0 && opts.user.trim() !== "") {
3717
+ const r = await runOidcLogin({
3718
+ server,
3719
+ user: opts.user,
3720
+ proxySecret: opts.proxySecret,
3721
+ fetchHealthz: async () => healthz
3722
+ });
3723
+ return {
3724
+ ...base,
3725
+ loggedIn: true,
3726
+ configPath: r.configPath,
3727
+ next: null,
3728
+ warning: r.warning
3729
+ };
3730
+ }
3714
3731
  if (opts.token !== void 0 && opts.token !== "") {
3715
3732
  if (!await (opts.probe ?? probeApiWithBearer)(server, opts.token)) throw new CliError("token 校验失败(/api/docs 未放行)", "确认 token 正确、且网关放行 Authorization: Bearer");
3716
3733
  const path = saveConfig({
@@ -3754,8 +3771,25 @@ async function runInit(opts) {
3754
3771
  next: null
3755
3772
  };
3756
3773
  }
3757
- const viaToken = `jdu init --server '${server}' --token <token>(${healthz.cliAuth?.tokenHint ?? "token 在 server 首启 stdout / data/initial-token.txt"})`;
3758
- const next = provider === "forward-auth" ? `jdu login --server '${server}'(浏览器 SSO),或 ${viaToken}` : methods.includes("browser") ? `jdu init --server '${server}'(不带 --json / --no-browser,打开浏览器授权),或 ${viaToken}` : viaToken;
3774
+ const viaToken = `jdu login --server '${server}' --token <token>(${healthz.cliAuth?.tokenHint ?? "token 在 server 首启 stdout / data/initial-token.txt"})`;
3775
+ if (provider === "forward-auth" && opts.browser) {
3776
+ const r = await runOidcLogin({
3777
+ server,
3778
+ issuer: opts.issuer,
3779
+ clientId: opts.clientId,
3780
+ fetchHealthz: async () => healthz,
3781
+ oidcLogin: opts.oidcLogin,
3782
+ probe: opts.probe
3783
+ });
3784
+ return {
3785
+ ...base,
3786
+ loggedIn: true,
3787
+ configPath: r.configPath,
3788
+ next: null,
3789
+ warning: r.warning
3790
+ };
3791
+ }
3792
+ const next = methods.includes("browser") ? `jdu login --server '${server}'(不带 --json / --no-browser,打开浏览器授权),或 ${viaToken}` : viaToken;
3759
3793
  return {
3760
3794
  ...base,
3761
3795
  loggedIn: false,
@@ -3937,6 +3971,7 @@ async function pushDoc(api, entryArg, opts) {
3937
3971
  if (opts.id !== void 0 && opts.id !== "") body.id = opts.id;
3938
3972
  if (opts.tag !== void 0) body.tags = opts.tag.flatMap((t) => t.split(",")).map((t) => t.trim()).filter((t) => t.length > 0);
3939
3973
  if (opts.official === true) body.official = true;
3974
+ if (opts.team !== void 0) body.team = opts.team;
3940
3975
  const res = await api.postJson("/api/docs", body);
3941
3976
  const id = typeof res?.id === "string" ? res.id : opts.id;
3942
3977
  const url = typeof res?.url === "string" && res.url !== "" ? res.url : id !== void 0 ? api.url(`/d/${id}`) : void 0;
@@ -4066,7 +4101,7 @@ var SERVER_INFO = {
4066
4101
  name: "jiandu",
4067
4102
  version: "0.2.0"
4068
4103
  };
4069
- var INSTRUCTIONS = "jiandu(简牍)是一个 Markdown 知识库。先用 search_docs / list_docs 找到文档 id,read_doc 拿 markdown 原文;改稿后用 push_doc 发布新版本(带 id 才是更新,不带是新建)。文档里的 widget fence(```widget_component:Name)保留原样。写新文档前先 list_templates 看有没有对应骨架(周报 / 技术方案…),有就 read_doc 取模板按节填,占位注释自己替换掉。";
4104
+ var INSTRUCTIONS = "jiandu(简牍)是一个 Markdown 知识库。先用 search_docs / list_docs 找到文档 id,read_doc 拿 markdown 原文;改稿后用 push_doc 发布新版本(带 id 才是更新,不带是新建)。文档里的 widget fence(```widget:Name)保留原样。写新文档前先 list_templates 看有没有对应骨架(周报 / 技术方案…),有就 read_doc 取模板按节填,占位注释自己替换掉。";
4070
4105
  function str(v, name) {
4071
4106
  if (typeof v !== "string" || v.trim() === "") throw new CliError(`${name} 必填且为非空字符串`);
4072
4107
  return v.trim();
@@ -4590,11 +4625,11 @@ $('dark').addEventListener('click', () => { document.documentElement.classList.t
4590
4625
 
4591
4626
  // 与 viewer loader 同一条链路:import → new → mount(el, ctx);抛错就按线上的降级形态显示原始 fence
4592
4627
  let meta = {}, inst = null, W = null, data = {};
4593
- const ctx = (d) => ({ data: d, loading: false, blockId: 'b0-dev00000', source: JSON.stringify(d, null, 2), language: 'widget_component:' + (meta.name ?? '') });
4628
+ const ctx = (d) => ({ data: d, loading: false, blockId: 'b0-dev00000', source: JSON.stringify(d, null, 2), language: 'widget:' + (meta.name ?? '') });
4594
4629
  function degrade(reason) {
4595
4630
  const pre = document.createElement('pre');
4596
4631
  pre.className = 'dev-degraded';
4597
- pre.textContent = '[降级为代码块] ' + reason + '\\n\\n\`\`\`widget_component:' + (meta.name ?? '') + '\\n' + JSON.stringify(data, null, 2) + '\\n\`\`\`';
4632
+ pre.textContent = '[降级为代码块] ' + reason + '\\n\\n\`\`\`widget:' + (meta.name ?? '') + '\\n' + JSON.stringify(data, null, 2) + '\\n\`\`\`';
4598
4633
  island.replaceChildren(pre);
4599
4634
  status.textContent = '挂载失败';
4600
4635
  }
@@ -4732,7 +4767,7 @@ export interface WidgetContext<D = unknown> {
4732
4767
  blockId: string;
4733
4768
  /** 原始 fence 正文 */
4734
4769
  source: string;
4735
- /** 原始代码块语言标识,如 \`widget_component:StarRating\` */
4770
+ /** 原始代码块语言标识,如 \`widget:StarRating\` */
4736
4771
  language: string;
4737
4772
  /**
4738
4773
  * 嵌套 markdown:dataSchema 里标了 \`contentMediaType: "text/markdown"\` 的字符串字段,宿主渲成已 sanitize 的 HTML
@@ -4761,7 +4796,7 @@ export interface WidgetClass<D = unknown> {
4761
4796
  var TYPES_TS = (c) => `// fence 里的 data 长什么样,这里是唯一事实:\`npm run build\` 会把 Data 生成为 widget.json 的 dataSchema
4762
4797
  // (字段上的 JSDoc → description,@default → default)。文档里的 data 不合法时平台会把 fence 降级成代码块。
4763
4798
  //
4764
- // \`\`\`widget_component:${c.name}
4799
+ // \`\`\`widget:${c.name}
4765
4800
  // { "title": "你好,简牍", "count": 3 }
4766
4801
  // \`\`\`
4767
4802
  //
@@ -5143,7 +5178,7 @@ npm run push # = build + jdu widget push dist
5143
5178
  然后在任何一篇文档里:
5144
5179
 
5145
5180
  \`\`\`\`markdown
5146
- \`\`\`widget_component:${c.name}
5181
+ \`\`\`widget:${c.name}
5147
5182
  { "title": "你好,简牍", "count": 3 }
5148
5183
  \`\`\`
5149
5184
  \`\`\`\`
@@ -5198,7 +5233,7 @@ async function fetchRuntime(api) {
5198
5233
  try {
5199
5234
  client = await api();
5200
5235
  } catch (err) {
5201
- throw new CliError(`shared 档要从 server 取 runtime 地址,但 CLI 未配置:${err instanceof Error ? err.message : String(err)}`, "先 jdu init / login,或改用 --runtime custom(自带 react 全内联,不需要 server)");
5236
+ throw new CliError(`shared 档要从 server 取 runtime 地址,但 CLI 未配置:${err instanceof Error ? err.message : String(err)}`, "先 jdu login,或改用 --runtime custom(自带 react 全内联,不需要 server)");
5202
5237
  }
5203
5238
  const res = await client.getJson("/api/runtimes");
5204
5239
  const name = typeof res?.default === "string" ? res.default : null;
@@ -5263,9 +5298,9 @@ function collectTag(value, previous) {
5263
5298
  return [...previous ?? [], value];
5264
5299
  }
5265
5300
  var program = new Command();
5266
- program.name("jdu").description("jiandu 命令行").version("0.3.0").showHelpAfterError();
5267
- program.command("init").description("选定部署目标并初始化本地配置(默认官方线上;自部署 / 本机需显式指定)").option("--local", "目标是本机自部署(http://127.0.0.1:8080)").option("--server <url>", "目标是指定的自部署地址").option("--token <token>", "一并完成 token 登录(server 首启 stdout / data/initial-token.txt)").option("--no-browser", "站点支持浏览器授权时也不打开浏览器,只给 token 指引").option("--json", "机器可读输出(agent 联动用;不会打开浏览器)", false).action(async (opts) => {
5268
- const result = await runInit({
5301
+ program.name("jdu").description("jiandu 命令行").version("0.4.0").showHelpAfterError();
5302
+ program.command("login").description("登录到一台简牍:定站点、拿凭据、写本地配置(必须指定 --server 或 --local)").option("--local", "目标是本机自部署(http://127.0.0.1:8080)").option("--server <url>", "目标简牍站点地址").option("--token <token>", "直接用 token 登录(无浏览器的机器;token 在 server 首启 stdout / data/initial-token.txt)").option("--user <owner>", "forward-auth:以指定 owner 直连,跳过 SSO(请求注入 X-Forwarded-User 头)").option("--proxy-secret <secret>", "forward-auth:网关共享密钥(X-Jiandu-Proxy-Secret 头)").option("--issuer <url>", "OIDC issuer(缺省读 healthz.oidc 或 JIANDU_OIDC_ISSUER)").option("--client-id <id>", "OIDC client id(缺省读 healthz.oidc 或 JIANDU_OIDC_CLIENT_ID)").option("--no-browser", "站点支持浏览器授权时也不打开浏览器,只给 token 指引").option("--json", "机器可读输出(agent 联动用;不会打开浏览器)", false).action(async (opts) => {
5303
+ const result = await runLogin({
5269
5304
  ...opts,
5270
5305
  browser: opts.browser && !opts.json
5271
5306
  });
@@ -5276,24 +5311,9 @@ program.command("init").description("选定部署目标并初始化本地配置
5276
5311
  process.stdout.write(`目标 ${result.target} · ${result.server}(auth: ${result.authProvider})\n`);
5277
5312
  process.stdout.write(`配置已写入 ${result.configPath}\n`);
5278
5313
  process.stdout.write(result.loggedIn ? "凭据就绪,可以直接 jdu push。\n" : `下一步:${result.next ?? ""}\n`);
5279
- });
5280
- program.command("login").description("登录到 jiandu server(forward-auth 默认打开浏览器走 SSO;--user 则直连注入身份头)").requiredOption("--server <url>", "jiandu server 地址").option("--token <token>", "直接保存访问 token(token provider / 跳过浏览器)").option("--user <owner>", "forward-auth:以指定 owner 直连(请求注入 X-Forwarded-User 头,跳过 SSO)").option("--proxy-secret <secret>", "forward-auth:网关共享密钥(请求注入 X-Jiandu-Proxy-Secret 头)").option("--issuer <url>", "OIDC issuer(缺省读 healthz.oidc 或 JIANDU_OIDC_ISSUER)").option("--client-id <id>", "OIDC client id(缺省读 healthz.oidc 或 JIANDU_OIDC_CLIENT_ID)").action(async (opts) => {
5281
- const result = await runLogin({
5282
- server: opts.server,
5283
- token: opts.token,
5284
- user: opts.user,
5285
- proxySecret: opts.proxySecret,
5286
- issuer: opts.issuer,
5287
- clientId: opts.clientId
5288
- });
5289
- process.stdout.write(`${result.configPath}\n`);
5290
- if (result.source === "anonymous") process.stderr.write("这个 server 是匿名模式,无需登录。\n");
5291
- if (result.source === "forward-auth") process.stderr.write("已保存 forward-auth 直连身份(X-Forwarded-User)。\n");
5292
- if (result.source === "oidc-cache") process.stderr.write("已复用本机 SSO credential。\n");
5293
- if (result.source === "oidc-browser") process.stderr.write("SSO 登录成功。\n");
5294
5314
  if (result.warning) process.stderr.write(`jdu: ${result.warning}\n`);
5295
5315
  });
5296
- program.command("push").argument("<entry.md>", "入口 markdown 文件").description("递归收集本地引用、增量上传并发布一个新版本").option("--title <title>", "文档标题,默认取首个一级标题").option("--visibility <v>", `${VISIBILITIES.join(" | ")}(新文档默认 private;更新已有文档时不传则保持原值)`).option("--id <docId>", "复用已有文档 id(发布新版本)").option("--tag <name>", "打标签,可重复;不传则保持原有标签", collectTag, void 0).option("--official", "上架到官方知识库(需管理员;同时公开)").action(async (entry, opts) => {
5316
+ program.command("push").argument("<entry.md>", "入口 markdown 文件").description("递归收集本地引用、增量上传并发布一个新版本").option("--title <title>", "文档标题,默认取首个一级标题").option("--visibility <v>", `${VISIBILITIES.join(" | ")}(新文档默认 private;更新已有文档时不传则保持原值)`).option("--id <docId>", "复用已有文档 id(发布新版本)").option("--tag <name>", "打标签,可重复;不传则保持原有标签", collectTag, void 0).option("--official", "上架到官方知识库(需管理员;同时公开)").option("--team <teamId>", "进团队(需是该团队成员;传空串移出团队;不传则保持原值)").action(async (entry, opts) => {
5297
5317
  const res = await pushDoc(await client(), entry, opts);
5298
5318
  process.stdout.write(`${res.url}\n`);
5299
5319
  });
@@ -5423,7 +5443,7 @@ template.command("push").argument("<file.md>", "模板文件").description("发
5423
5443
  program.command("mcp").description("以 MCP server(stdio)暴露 list_docs / search_docs / read_doc / list_versions / push_doc,给本机 agent 用").action(async () => {
5424
5444
  await runMcp(await client());
5425
5445
  });
5426
- program.command("share").argument("<docId>").description("改可见性,或管理读者池(定向分享,可撤销)").option("--visibility <v>", VISIBILITIES.join(" | ")).option("--to <reader>", "加入读者池(身份串,如邮箱 / forward-auth 的 owner),可重复", collectTag, void 0).option("--revoke <reader>", "从读者池移除,立刻失效,可重复", collectTag, void 0).option("--readers", "列出当前读者池", false).action(async (docId, opts) => {
5446
+ program.command("share").argument("<docId>").description("改可见性 / 进出团队,或管理读者池(定向分享,可撤销)").option("--visibility <v>", VISIBILITIES.join(" | ")).option("--team <teamId>", "进团队(需是该团队成员)").option("--no-team", "移出团队").option("--to <reader>", "加入读者池(身份串,如邮箱 / forward-auth 的 owner),可重复", collectTag, void 0).option("--revoke <reader>", "从读者池移除,立刻失效,可重复", collectTag, void 0).option("--readers", "列出当前读者池", false).action(async (docId, opts) => {
5427
5447
  const api = await client();
5428
5448
  const url = `/api/docs/${encodeURIComponent(docId)}`;
5429
5449
  if (opts.readers) {
@@ -5441,9 +5461,12 @@ program.command("share").argument("<docId>").description("改可见性,或管
5441
5461
  process.stdout.write(`${readers.length > 0 ? readers.join("\n") : "(读者池为空)"}\n`);
5442
5462
  return;
5443
5463
  }
5444
- if (opts.visibility === void 0) throw new CliError("share 需要 --visibility / --to / --revoke / --readers 之一");
5445
- const res = await api.putJson(`${url}/visibility`, { visibility: opts.visibility });
5446
- process.stdout.write(`可见性 ${String(res.visibility)}\n`);
5464
+ const payload = {};
5465
+ if (opts.visibility !== void 0) payload.visibility = opts.visibility;
5466
+ if (opts.team !== void 0) payload.team = opts.team === false ? "" : opts.team;
5467
+ if (Object.keys(payload).length === 0) throw new CliError("share 需要 --visibility / --team / --no-team / --to / --revoke / --readers 之一");
5468
+ const res = await api.putJson(`${url}/visibility`, payload);
5469
+ process.stdout.write(`可见性 ${String(res.visibility)}${res.team ? `,团队 ${String(res.team)}` : ""}\n`);
5447
5470
  });
5448
5471
  var tokenCmd = program.command("token").description("CLI token(首启那枚与浏览器授权签发的)");
5449
5472
  tokenCmd.command("list").description("列出我的 token;CURRENT 是本次请求用的那枚").action(async () => {
@@ -5474,6 +5497,92 @@ tokenCmd.command("rm").argument("<id>", "jdu token list 里的 ID").description(
5474
5497
  await (await client()).deleteJson(`/api/tokens/${encodeURIComponent(id)}`);
5475
5498
  process.stdout.write(`revoked ${id}\n`);
5476
5499
  });
5500
+ var teamCmd = program.command("team").description("团队:共享阅读与评论的空间(实例管理员建团队,团队管理员按用户名加人;文档进团队用 push/share --team)");
5501
+ teamCmd.command("list").description("我所属的团队(实例管理员:全部团队)").action(async () => {
5502
+ printTable(asRows(await (await client()).getJson("/api/teams"), "teams"), [
5503
+ {
5504
+ key: "id",
5505
+ header: "ID"
5506
+ },
5507
+ {
5508
+ key: "name",
5509
+ header: "NAME"
5510
+ },
5511
+ {
5512
+ key: "role",
5513
+ header: "ROLE"
5514
+ },
5515
+ {
5516
+ key: "members",
5517
+ header: "MEMBERS"
5518
+ },
5519
+ {
5520
+ key: "docs",
5521
+ header: "DOCS"
5522
+ }
5523
+ ]);
5524
+ });
5525
+ teamCmd.command("create").argument("<name>", "团队名").description("新建团队(实例管理员);你自动成为团队管理员,stdout 是团队 id").action(async (name) => {
5526
+ const res = await (await client()).postJson("/api/teams", { name });
5527
+ process.stdout.write(`${res.id}\n`);
5528
+ process.stderr.write(`团队「${res.name}」已创建:jdu team add ${res.id} <用户名> 加人\n`);
5529
+ });
5530
+ teamCmd.command("add").argument("<team>", "团队 id").argument("<user>", "成员用户名").option("--admin", "设为团队管理员(可加人 / 踢人 / 改名)").description("把成员加进团队;对已在团队的人 = 改角色").action(async (team, user, opts) => {
5531
+ await (await client()).postJson(`/api/teams/${encodeURIComponent(team)}/members`, {
5532
+ owner: user,
5533
+ role: opts.admin ? "admin" : "member"
5534
+ });
5535
+ process.stdout.write(`added ${user} → ${team}${opts.admin ? " (admin)" : ""}\n`);
5536
+ });
5537
+ teamCmd.command("rm").argument("<team>", "团队 id").argument("<user>", "成员用户名").description("把成员移出团队(团队管理员)").action(async (team, user) => {
5538
+ await (await client()).postJson(`/api/teams/${encodeURIComponent(team)}/members/remove`, { owner: user });
5539
+ process.stdout.write(`removed ${user} ← ${team}\n`);
5540
+ });
5541
+ teamCmd.command("leave").argument("<team>", "团队 id").description("自己退出团队").action(async (team) => {
5542
+ await (await client()).postJson(`/api/teams/${encodeURIComponent(team)}/members/remove`, {});
5543
+ process.stdout.write(`left ${team}\n`);
5544
+ });
5545
+ var memberCmd = program.command("member").description("成员(password 鉴权,管理员专用):准入只有邀请,被邀请者用链接自己绑 passkey");
5546
+ memberCmd.command("list").description("成员名单:pending = 已邀请未绑定,bound = 已绑定 passkey").action(async () => {
5547
+ printTable(asRows(await (await client()).getJson("/api/members"), "members"), [
5548
+ {
5549
+ key: "owner",
5550
+ header: "OWNER"
5551
+ },
5552
+ {
5553
+ key: "status",
5554
+ header: "STATUS"
5555
+ },
5556
+ {
5557
+ key: "invitedAt",
5558
+ header: "INVITED"
5559
+ },
5560
+ {
5561
+ key: "boundAt",
5562
+ header: "BOUND"
5563
+ },
5564
+ {
5565
+ key: "tokens",
5566
+ header: "TOKENS"
5567
+ },
5568
+ {
5569
+ key: "docs",
5570
+ header: "DOCS"
5571
+ }
5572
+ ]);
5573
+ });
5574
+ memberCmd.command("invite").argument("<name>", "用户名:小写字母 / 数字开头,可含 . _ -,≤30 位").option("--reset", "已绑定的成员重置 passkey(清掉凭据,所有设备与会话即刻失效)").description("签一张 7 天有效的一次性邀请链接,自己交给对方;对方打开后绑定 passkey 即成为成员").action(async (name, opts) => {
5575
+ const res = await (await client()).postJson("/api/members/invite", {
5576
+ owner: name,
5577
+ reset: opts.reset === true
5578
+ });
5579
+ process.stdout.write(`${res.url}\n`);
5580
+ process.stderr.write(`邀请 ${res.owner},${new Date(res.expiresAt).toLocaleString()} 前有效;链接只显示这一次\n`);
5581
+ });
5582
+ memberCmd.command("rm").argument("<name>", "用户名").description("移除成员:删 passkey 与其全部 token;文档 / 评论 / 标签不动,同名重邀即接回").action(async (name) => {
5583
+ await (await client()).postJson("/api/members/remove", { owner: name });
5584
+ process.stdout.write(`removed ${name}\n`);
5585
+ });
5477
5586
  var officialCmd = program.command("official").description("官方知识库上架");
5478
5587
  officialCmd.command("list").description("列出已上架的官方文档").action(async () => {
5479
5588
  printTable(asRows(await (await client()).getJson("/api/official"), "docs"), [{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monoedge/jdu-cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "jdu — 把 Markdown 发布到简牍服务",
5
5
  "type": "module",
6
6
  "license": "MIT",