@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.
- package/README.md +2 -2
- package/dist/cli.js +158 -49
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
3673
|
-
*
|
|
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
|
-
|
|
3699
|
-
target: "official",
|
|
3700
|
-
server: OFFICIAL_SERVER
|
|
3701
|
-
};
|
|
3703
|
+
throw new CliError("jdu login 需要指定站点", "自部署用 --server <url>,本机用 --local");
|
|
3702
3704
|
}
|
|
3703
|
-
async function
|
|
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
|
|
3758
|
-
|
|
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(```
|
|
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: '
|
|
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\`\`\`
|
|
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
|
-
/** 原始代码块语言标识,如 \`
|
|
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
|
-
// \`\`\`
|
|
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
|
-
\`\`\`
|
|
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
|
|
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.
|
|
5267
|
-
program.command("
|
|
5268
|
-
const result = await
|
|
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("
|
|
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
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
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"), [{
|