workflow-cli 0.1.0__py3-none-any.whl

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.
wfcli/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """公司工作流 CLI 工具"""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1 @@
1
+ """commands 子命令包"""
@@ -0,0 +1,45 @@
1
+ """config 子命令 - 配置管理"""
2
+
3
+ import click
4
+
5
+ from wfcli import config as cfg
6
+
7
+
8
+ @click.group()
9
+ def config():
10
+ """配置管理"""
11
+ pass
12
+
13
+
14
+ @config.command()
15
+ def show():
16
+ """查看当前配置"""
17
+ data = cfg.load_config()
18
+ _print_dict(data)
19
+
20
+
21
+ @config.command()
22
+ @click.argument("key")
23
+ @click.argument("value")
24
+ def set(key: str, value: str):
25
+ """修改配置项,如: wfcli config set feishu.root_folder_token xxx"""
26
+ cfg.set_value(key, value)
27
+ click.echo(f"已更新: {key} = {value}")
28
+
29
+
30
+ @config.command()
31
+ def reset():
32
+ """重置为默认配置"""
33
+ cfg.save_config(cfg.DEFAULT_CONFIG)
34
+ click.echo("已重置为默认配置")
35
+
36
+
37
+ def _print_dict(data: dict, indent: int = 0) -> None:
38
+ """递归打印字典内容"""
39
+ prefix = " " * indent
40
+ for k, v in data.items():
41
+ if isinstance(v, dict):
42
+ click.echo(f"{prefix}[{k}]")
43
+ _print_dict(v, indent + 1)
44
+ else:
45
+ click.echo(f"{prefix}{k} = {v}")
wfcli/commands/doc.py ADDED
@@ -0,0 +1,149 @@
1
+ """doc 子命令 - 文档生成"""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+ from datetime import datetime
6
+
7
+ import click
8
+
9
+ from wfcli.core.docgen import (
10
+ select_template,
11
+ collect_inputs,
12
+ generate_document,
13
+ check_and_save,
14
+ run_checklist,
15
+ )
16
+ from wfcli.core.template import load_template
17
+ from wfcli.core.naming import build_filename
18
+ from wfcli.utils.fs import require_workspace_root, parse_workspace_md
19
+ from wfcli.utils.display import print_success, print_error, print_info
20
+
21
+
22
+ @click.group()
23
+ def doc():
24
+ """文档生成"""
25
+ pass
26
+
27
+
28
+ @doc.command()
29
+ @click.option("--type", "-t", "doc_type", default=None, help="文档类型(跳过交互式选择)")
30
+ @click.option("--topic", default=None, help="文档主题")
31
+ @click.option("--output", "-o", default=None, help="指定输出目录(相对于工作空间根目录)")
32
+ def generate(doc_type: str | None, topic: str | None, output: str | None):
33
+ """按模板生成交档
34
+
35
+ 交互式选择模板、填写内容,按命名规范保存到本地对应目录。
36
+ """
37
+ ws_root = require_workspace_root()
38
+ ws_info = parse_workspace_md(ws_root / "WORKSPACE.md")
39
+
40
+ # 1. 选择模板
41
+ if doc_type:
42
+ from wfcli.core.template import list_templates
43
+ templates = list_templates(ws_root)
44
+ template_info = next(
45
+ (t for t in templates if doc_type in t["type"] or doc_type in t["name"]),
46
+ None,
47
+ )
48
+ if not template_info:
49
+ print_error(f"未找到匹配的文档类型: {doc_type}")
50
+ print_info("可用类型:")
51
+ for t in templates:
52
+ click.echo(f" - {t['type']}")
53
+ return
54
+ else:
55
+ template_info = select_template(ws_root)
56
+
57
+ print_info(f"使用模板: {template_info['name']}")
58
+
59
+ # 2. 加载模板
60
+ template_data = load_template(template_info["path"])
61
+
62
+ # 3. 收集用户输入
63
+ if not topic:
64
+ topic = click.prompt("文档主题")
65
+
66
+ inputs = collect_inputs(template_data)
67
+
68
+ # 4. 生成文档
69
+ print_info("生成文档...")
70
+ content = generate_document(
71
+ template_data=template_data,
72
+ inputs=inputs,
73
+ title=topic,
74
+ )
75
+
76
+ # 5. 预览
77
+ click.echo("\n" + "─" * 50)
78
+ click.echo(content[:500])
79
+ if len(content) > 500:
80
+ click.echo(f"\n... (共 {len(content)} 字符)")
81
+ click.echo("─" * 50)
82
+
83
+ # 6. 自检
84
+ if not run_checklist(template_data):
85
+ if not click.confirm("\n自检未全部通过,是否仍要保存?", default=False):
86
+ return
87
+
88
+ # 7. 确定保存位置
89
+ if output:
90
+ target_dir = output
91
+ else:
92
+ # 根据模板类型推断存放目录
93
+ target_dir = _infer_target_dir(ws_root, template_info["type"])
94
+
95
+ # 8. 保存
96
+ saved_path = check_and_save(
97
+ content=content,
98
+ workspace_root=ws_root,
99
+ target_dir=target_dir,
100
+ topic=topic,
101
+ )
102
+
103
+ print_success(f"文档已保存: {saved_path.relative_to(ws_root)}")
104
+ print_info(f"完整路径: {saved_path}")
105
+ print_info("提交到云盘: wfcli sync push " + str(saved_path))
106
+
107
+
108
+ @doc.command()
109
+ def list():
110
+ """列出当前工作空间的可用文档类型"""
111
+ ws_root = require_workspace_root()
112
+
113
+ from wfcli.core.template import list_templates
114
+ templates = list_templates(ws_root)
115
+
116
+ if not templates:
117
+ print_info("当前工作空间没有可用模板")
118
+ return
119
+
120
+ print_info(f"工作空间: {ws_root.name}")
121
+ click.echo()
122
+ for t in templates:
123
+ click.echo(f" - {t['type']} ({t['name']}.md)")
124
+
125
+
126
+ def _infer_target_dir(ws_root: Path, doc_type: str) -> str:
127
+ """根据文档类型推断存放目录
128
+
129
+ 从 WORKSPACE.md 的模板索引表中查找,找不到则用类型名作为目录名。
130
+ """
131
+ ws_info = parse_workspace_md(ws_root / "WORKSPACE.md")
132
+ templates_map = ws_info.get("templates", {})
133
+
134
+ # 在模板索引中查找
135
+ for dtype, tpath in templates_map.items():
136
+ if doc_type in dtype:
137
+ # 从模板路径推断存放目录
138
+ # 模板路径如 "00_模板/01_01_会议纪要模板.md"
139
+ # 存放目录如 "01_会议纪要"
140
+ for d in (ws_root).iterdir():
141
+ if d.is_dir() and doc_type in d.name:
142
+ return d.name
143
+
144
+ # 兜底:查找包含类型名的目录
145
+ for d in ws_root.iterdir():
146
+ if d.is_dir() and doc_type in d.name:
147
+ return d.name
148
+
149
+ return doc_type
wfcli/commands/info.py ADDED
@@ -0,0 +1,46 @@
1
+ """info 子命令 - 版本和状态信息"""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import click
7
+
8
+ from wfcli import __version__
9
+ from wfcli import config as cfg
10
+ from wfcli.utils.fs import find_workspace_root
11
+ from wfcli.utils.display import print_info, print_success
12
+
13
+
14
+ @click.command()
15
+ def info():
16
+ """显示版本和当前工作状态"""
17
+ click.echo(f"wfcli v{__version__}")
18
+ click.echo(f"Python {sys.version.split()[0]}")
19
+ click.echo(f"配置文件: {cfg.CONFIG_FILE}")
20
+
21
+ if cfg.CONFIG_FILE.exists():
22
+ print_success("配置文件已就绪")
23
+ else:
24
+ print_info("配置文件不存在,将使用默认配置")
25
+
26
+ # 当前工作空间
27
+ ws_root = find_workspace_root()
28
+ if ws_root:
29
+ print_info(f"当前工作空间: {ws_root.name} ({ws_root})")
30
+ else:
31
+ print_info("当前不在任何工作空间中")
32
+
33
+ # lark-cli 检测
34
+ import subprocess
35
+ try:
36
+ result = subprocess.run(
37
+ ["lark-cli", "--version"],
38
+ capture_output=True, text=True, timeout=5,
39
+ )
40
+ if result.returncode == 0:
41
+ ver = result.stdout.strip().split("\n")[0]
42
+ print_info(f"lark-cli: {ver}")
43
+ else:
44
+ print_info("lark-cli: 未检测到")
45
+ except (FileNotFoundError, subprocess.TimeoutExpired):
46
+ print_info("lark-cli: 未安装或不可用")
wfcli/commands/sync.py ADDED
@@ -0,0 +1,213 @@
1
+ """sync 子命令 - 文件同步
2
+
3
+ push: 提交本地文件到云盘
4
+ pull: 从云盘同步更新到本地
5
+ """
6
+
7
+ from pathlib import Path
8
+
9
+ import click
10
+
11
+ from wfcli.lark import LarkClient, LarkError
12
+ from wfcli import config as cfg
13
+ from wfcli.core.workspace import (
14
+ list_workspaces,
15
+ download_workspace,
16
+ match_cloud_folder,
17
+ )
18
+ from wfcli.utils.fs import require_workspace_root, parse_workspace_md
19
+ from wfcli.utils.display import (
20
+ print_success,
21
+ print_error,
22
+ print_info,
23
+ print_step,
24
+ )
25
+
26
+
27
+ @click.group()
28
+ def sync():
29
+ """文件同步(云盘 <-> 本地)"""
30
+ pass
31
+
32
+
33
+ # ── push ──────────────────────────────────────────────────────────
34
+
35
+ @sync.command()
36
+ @click.argument("file", type=click.Path(exists=True))
37
+ def push(file: str):
38
+ """提交本地文件到云盘
39
+
40
+ FILE 为本地文件路径(可以是相对于工作空间根目录的路径)。
41
+ """
42
+ ws_root = require_workspace_root()
43
+ client = LarkClient()
44
+
45
+ file_path = Path(file).resolve()
46
+
47
+ # 1. 计算相对路径
48
+ try:
49
+ rel_path = file_path.relative_to(ws_root)
50
+ except ValueError:
51
+ print_error(f"文件不在当前工作空间内: {file_path}")
52
+ return
53
+
54
+ print_info(f"工作空间: {ws_root.name}")
55
+ print_info(f"相对路径: {rel_path}")
56
+
57
+ # 2. 查找工作空间 token
58
+ workspaces = list_workspaces(client)
59
+ ws = next((w for w in workspaces if w["name"] == ws_root.name), None)
60
+ if not ws:
61
+ print_error(f"云盘上未找到工作空间: {ws_root.name}")
62
+ return
63
+
64
+ ws_token = ws["token"]
65
+
66
+ # 3. 匹配云盘目标文件夹
67
+ print_info("匹配云盘目录...")
68
+ try:
69
+ target_folder_token = match_cloud_folder(str(rel_path), ws_token, client)
70
+ except LarkError:
71
+ # 目录不存在,尝试上传到工作空间根目录
72
+ target_folder_token = ws_token
73
+
74
+ # 4. 检查是否已有同名文件(覆盖上传)
75
+ existing_files = client.drive_list_files(target_folder_token)
76
+ same_name = next(
77
+ (f for f in existing_files if f.get("name") == file_path.name),
78
+ None,
79
+ )
80
+
81
+ # 5. 上传
82
+ if same_name:
83
+ print_info(f"覆盖上传: {file_path.name}")
84
+ client.drive_upload(file_path, file_token=same_name.get("token", ""))
85
+ else:
86
+ print_info(f"上传新文件: {file_path.name}")
87
+ client.drive_upload(file_path, folder_token=target_folder_token)
88
+
89
+ print_success(f"已提交到云盘: {rel_path}")
90
+
91
+
92
+ # ── pull ──────────────────────────────────────────────────────────
93
+
94
+ @sync.command("pull")
95
+ @click.argument("file", required=False, default=None)
96
+ def pull_cmd(file: str | None):
97
+ """从云盘同步更新到本地
98
+
99
+ 不指定文件则全量刷新整个工作空间。
100
+ 指定文件则增量更新单个文件。
101
+ """
102
+ ws_root = require_workspace_root()
103
+ client = LarkClient()
104
+
105
+ # 查找工作空间 token
106
+ workspaces = list_workspaces(client)
107
+ ws = next((w for w in workspaces if w["name"] == ws_root.name), None)
108
+ if not ws:
109
+ print_error(f"云盘上未找到工作空间: {ws_root.name}")
110
+ return
111
+
112
+ if file:
113
+ # 增量更新单个文件
114
+ _pull_single_file(ws_root, ws["token"], file, client)
115
+ else:
116
+ # 全量刷新
117
+ _pull_full(ws_root, ws["token"], client)
118
+
119
+
120
+ def _pull_full(ws_root: Path, ws_token: str, client: LarkClient) -> None:
121
+ """全量刷新本地工作空间"""
122
+ print_info(f"全量刷新工作空间: {ws_root.name}")
123
+
124
+ # 1. 重新下载工作空间
125
+ try:
126
+ download_workspace(ws_token, ws_root, client)
127
+ print_success("工作空间已刷新")
128
+ except LarkError as e:
129
+ print_error(f"刷新失败: {e}")
130
+
131
+
132
+ def _pull_single_file(
133
+ ws_root: Path,
134
+ ws_token: str,
135
+ file_path: str,
136
+ client: LarkClient,
137
+ ) -> None:
138
+ """增量更新单个文件"""
139
+ rel_path = file_path
140
+ print_info(f"增量更新: {rel_path}")
141
+
142
+ # 在云盘目录中查找目标文件
143
+ parts = Path(rel_path).parts
144
+ current_token = ws_token
145
+
146
+ # 逐级进入子目录
147
+ for part in parts[:-1]:
148
+ files = client.drive_list_files(current_token)
149
+ folder = next(
150
+ (f for f in files if f.get("name") == part and f.get("type") == "folder"),
151
+ None,
152
+ )
153
+ if not folder:
154
+ print_error(f"云盘上未找到目录: {part}")
155
+ return
156
+ current_token = folder.get("token", "")
157
+
158
+ # 查找目标文件
159
+ target_name = parts[-1]
160
+ files = client.drive_list_files(current_token)
161
+ target_file = next(
162
+ (f for f in files if f.get("name") == target_name),
163
+ None,
164
+ )
165
+ if not target_file:
166
+ print_error(f"云盘上未找到文件: {target_name}")
167
+ return
168
+
169
+ # 下载
170
+ file_token = target_file.get("token", "")
171
+ output_dir = ws_root / Path(rel_path).parent
172
+ output_dir.mkdir(parents=True, exist_ok=True)
173
+ client.drive_download(file_token, output_dir)
174
+ print_success(f"已更新: {rel_path}")
175
+
176
+
177
+ # ── status ────────────────────────────────────────────────────────
178
+
179
+ @sync.command()
180
+ def status():
181
+ """查看本地与云盘的同步状态"""
182
+ ws_root = require_workspace_root()
183
+ client = LarkClient()
184
+
185
+ # 列出云盘文件
186
+ workspaces = list_workspaces(client)
187
+ ws = next((w for w in workspaces if w["name"] == ws_root.name), None)
188
+ if not ws:
189
+ print_error(f"云盘上未找到工作空间: {ws_root.name}")
190
+ return
191
+
192
+ cloud_files = client.drive_list_files(ws["token"])
193
+ cloud_names = {f.get("name", "") for f in cloud_files}
194
+
195
+ # 列出本地目录
196
+ local_dirs = {d.name for d in ws_root.iterdir() if d.is_dir() and not d.name.startswith(".")}
197
+
198
+ # 对比
199
+ only_cloud = cloud_names - local_dirs - {"WORKSPACE.md"}
200
+ only_local = local_dirs - cloud_names
201
+
202
+ if not only_cloud and not only_local:
203
+ print_success("本地与云盘目录结构一致")
204
+ else:
205
+ if only_cloud:
206
+ print_info("仅云盘存在:")
207
+ for name in sorted(only_cloud):
208
+ click.echo(f" + {name}")
209
+ if only_local:
210
+ print_info("仅本地存在:")
211
+ for name in sorted(only_local):
212
+ click.echo(f" - {name}")
213
+ print_info("运行 'wfcli sync pull' 同步云盘更新")