@adonis0123/weekly-report 1.0.6

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.
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const { getEnabledTargets,extractSkillName, detectInstallLocation } = require('./utils');
8
+
9
+ function installToTarget(target, config) {
10
+ console.log(`\n📦 Installing to ${target.name}...`);
11
+
12
+ // Check if this is a global installation
13
+ const isGlobal = process.env.npm_config_global === 'true';
14
+
15
+ // Determine installation location
16
+ const location = detectInstallLocation(target.paths, isGlobal);
17
+
18
+ // Extract skill name from package name (remove scope prefix)
19
+ const skillName = extractSkillName(config.name);
20
+
21
+ const targetDir = path.join(location.base, skillName);
22
+
23
+ // Alternative path format with full package name (including scope)
24
+ const altTargetDir = path.join(location.base, config.name);
25
+
26
+ console.log(` Type: ${location.type}${isGlobal ? ' (global)' : ' (project)'}`);
27
+ console.log(` Directory: ${targetDir}`);
28
+
29
+ // Clean up alternative path format
30
+ if (fs.existsSync(altTargetDir) && altTargetDir !== targetDir) {
31
+ console.log(` 🧹 Cleaning up alternative path format...`);
32
+ fs.rmSync(altTargetDir, { recursive: true, force: true });
33
+ console.log(` ✓ Removed directory: ${config.name}`);
34
+ }
35
+
36
+ // Create target directory
37
+ if (!fs.existsSync(targetDir)) {
38
+ fs.mkdirSync(targetDir, { recursive: true });
39
+ }
40
+
41
+ // Copy SKILL.md (required)
42
+ const skillMdSource = path.join(__dirname, 'SKILL.md');
43
+ if (!fs.existsSync(skillMdSource)) {
44
+ throw new Error('SKILL.md is required but not found');
45
+ }
46
+ fs.copyFileSync(skillMdSource, path.join(targetDir, 'SKILL.md'));
47
+ console.log(' ✓ Copied SKILL.md');
48
+
49
+ // Copy other files
50
+ if (config.files) {
51
+ Object.entries(config.files).forEach(([source, dest]) => {
52
+ const sourcePath = path.join(__dirname, source);
53
+ if (!fs.existsSync(sourcePath)) {
54
+ console.warn(` ⚠ Warning: ${source} not found, skipping`);
55
+ return;
56
+ }
57
+
58
+ const destPath = path.join(targetDir, dest);
59
+
60
+ if (fs.statSync(sourcePath).isDirectory()) {
61
+ copyDir(sourcePath, destPath);
62
+ console.log(` ✓ Copied directory: ${source}`);
63
+ } else {
64
+ // Ensure target directory exists
65
+ const destDir = path.dirname(destPath);
66
+ if (!fs.existsSync(destDir)) {
67
+ fs.mkdirSync(destDir, { recursive: true });
68
+ }
69
+ fs.copyFileSync(sourcePath, destPath);
70
+ console.log(` ✓ Copied file: ${source}`);
71
+ }
72
+ });
73
+ }
74
+
75
+ // Update manifest
76
+ updateManifest(location.base, config, target.name);
77
+
78
+ // Run postinstall hooks
79
+ if (config.hooks && config.hooks.postinstall) {
80
+ console.log(' 🔧 Running postinstall hook...');
81
+ const { execSync } = require('child_process');
82
+ try {
83
+ execSync(config.hooks.postinstall, {
84
+ cwd: targetDir,
85
+ stdio: 'pipe'
86
+ });
87
+ console.log(' ✓ Postinstall hook completed');
88
+ } catch (error) {
89
+ console.warn(` ⚠ Warning: postinstall hook failed`);
90
+ }
91
+ }
92
+
93
+ console.log(` ✅ Installed to ${target.name}`);
94
+ return targetDir;
95
+ }
96
+
97
+ function installSkill() {
98
+ console.log('🚀 Installing AI Coding Skill...\n');
99
+
100
+ // Read configuration
101
+ const configPath = path.join(__dirname, '.claude-skill.json');
102
+ if (!fs.existsSync(configPath)) {
103
+ throw new Error('.claude-skill.json not found');
104
+ }
105
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
106
+
107
+ // Get enabled targets
108
+ const enabledTargets = getEnabledTargets(config);
109
+
110
+ if (enabledTargets.length === 0) {
111
+ console.warn('⚠ No targets enabled in configuration');
112
+ console.warn('Please enable at least one target in .claude-skill.json');
113
+ return;
114
+ }
115
+
116
+ console.log(`Installing skill "${config.name}" to ${enabledTargets.length} target(s):`);
117
+ enabledTargets.forEach(target => {
118
+ console.log(` • ${target.name}`);
119
+ });
120
+
121
+ // Install to all enabled targets
122
+ const installedPaths = [];
123
+ for (const target of enabledTargets) {
124
+ try {
125
+ const installPath = installToTarget(target, config);
126
+ installedPaths.push({ target: target.name, path: installPath });
127
+ } catch (error) {
128
+ console.error(`\n❌ Failed to install to ${target.name}:`, error.message);
129
+ }
130
+ }
131
+
132
+ // Summary
133
+ console.log('\n' + '='.repeat(60));
134
+ console.log('✅ Installation Complete!');
135
+ console.log('='.repeat(60));
136
+
137
+ if (installedPaths.length > 0) {
138
+ console.log('\nInstalled to:');
139
+ installedPaths.forEach(({ target, path: installPath }) => {
140
+ console.log(` • ${target}: ${installPath}`);
141
+ });
142
+
143
+ console.log('\n📖 Next Steps:');
144
+ console.log(' 1. Restart your AI coding tool(s)');
145
+ console.log(' 2. Ask: "What skills are available?"');
146
+ console.log(' 3. Start using your skill!');
147
+ }
148
+ }
149
+
150
+ function copyDir(src, dest) {
151
+ fs.mkdirSync(dest, { recursive: true });
152
+ const entries = fs.readdirSync(src, { withFileTypes: true });
153
+
154
+ for (let entry of entries) {
155
+ const srcPath = path.join(src, entry.name);
156
+ const destPath = path.join(dest, entry.name);
157
+
158
+ if (entry.isDirectory()) {
159
+ copyDir(srcPath, destPath);
160
+ } else {
161
+ fs.copyFileSync(srcPath, destPath);
162
+ }
163
+ }
164
+ }
165
+
166
+ function updateManifest(skillsDir, config, targetName) {
167
+ const manifestPath = path.join(skillsDir, '.skills-manifest.json');
168
+ let manifest = { skills: {} };
169
+
170
+ if (fs.existsSync(manifestPath)) {
171
+ try {
172
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
173
+ } catch (error) {
174
+ console.warn(' Warning: Could not parse existing manifest, creating new one');
175
+ manifest = { skills: {} };
176
+ }
177
+ }
178
+
179
+ // Extract skill name from package name (remove scope prefix)
180
+ const skillName = config.name.startsWith('@') ?
181
+ config.name.split('/')[1] || config.name :
182
+ config.name;
183
+
184
+ manifest.skills[config.name] = {
185
+ version: config.version,
186
+ installedAt: new Date().toISOString(),
187
+ package: config.package || config.name,
188
+ path: path.join(skillsDir, skillName),
189
+ target: targetName
190
+ };
191
+
192
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
193
+ }
194
+
195
+ // Execute installation
196
+ try {
197
+ installSkill();
198
+ } catch (error) {
199
+ console.error('\n❌ Failed to install skill:', error.message);
200
+ console.error('\nTroubleshooting:');
201
+ console.error('- Ensure .claude-skill.json exists and is valid JSON');
202
+ console.error('- Ensure SKILL.md exists');
203
+ console.error('- Check file permissions for target directories');
204
+ console.error('- Verify at least one target is enabled in .claude-skill.json');
205
+ console.error('- Try running with sudo for global installation (if needed)');
206
+ process.exit(1);
207
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@adonis0123/weekly-report",
3
+ "version": "1.0.6",
4
+ "description": "Claude Code Skill - 自动读取 Git 提交记录生成周报,支持多仓库汇总和智能过滤",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "postinstall": "node install-skill.js",
8
+ "preuninstall": "node uninstall-skill.js",
9
+ "test": "node install-skill.js && echo 'Installation test completed. Check ~/.claude/skills/'",
10
+ "lint": "echo 'Add your linting commands here'"
11
+ },
12
+ "files": [
13
+ "SKILL.md",
14
+ "references/",
15
+ "src/",
16
+ "install-skill.js",
17
+ "uninstall-skill.js",
18
+ ".claude-skill.json",
19
+ "utils.js"
20
+ ],
21
+ "keywords": [
22
+ "claude-code",
23
+ "skill",
24
+ "ai-assistant",
25
+ "agent-skill",
26
+ "claude",
27
+ "weekly-report",
28
+ "git",
29
+ "commit-analyzer",
30
+ "automation"
31
+ ],
32
+ "author": "adonis",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/Adonis0123/weekly-flow.git"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/Adonis0123/weekly-flow/issues"
40
+ },
41
+ "homepage": "https://github.com/Adonis0123/weekly-flow#readme",
42
+ "engines": {
43
+ "node": ">=14.0.0"
44
+ }
45
+ }
@@ -0,0 +1,116 @@
1
+ # 周报格式规范
2
+
3
+ ## 角色定义
4
+
5
+ 你是一位专业的技术周报总结专家,擅长从 Git commit 记录或文字描述中提炼关键工作内容,生成简洁、结构化的周报摘要。
6
+
7
+ ## 任务目标
8
+
9
+ 将一周的工作内容(Git commit 记录截图或文字描述)提炼为结构化的周报总结。
10
+
11
+ ## 输入来源
12
+
13
+ - **文字输入**:直接提供的工作内容描述
14
+ - **Git 提交**:一周的 Git commit 提交记录
15
+
16
+ ## 输出格式
17
+
18
+ 使用 Markdown 格式,按项目分组,采用层级列表结构:
19
+
20
+ ```
21
+ 项目名称
22
+ - 主要工作点(必须,10 字以内,最长不超过 20 字)
23
+ - 补充说明(可选)
24
+ - 更详细的补充(可选)
25
+
26
+ 其他
27
+ - 不属于特定项目的工作内容
28
+ ```
29
+
30
+ ## 总结原则
31
+
32
+ ### 必须遵守
33
+
34
+ 1. **事实导向**:只总结实际完成的工作,不添加虚构或修饰性内容
35
+ 2. **简洁精炼**:主要工作点控制在 10 字以内,绝对不超过 20 字
36
+ 3. **重点突出**:只保留重要内容,过滤掉琐碎的修改
37
+ 4. **按项目分组**:相同项目的工作归类到一起
38
+ 5. **层级清晰**:用缩进表示内容的从属关系
39
+
40
+ ### 过滤规则
41
+
42
+ 以下类型的 commit 通常不需要单独列出:
43
+
44
+ - 纯格式化/代码风格调整(除非是大规模重构)
45
+ - 简单的 typo 修复
46
+ - 依赖版本小幅更新(除非涉及重大升级)
47
+ - 重复性的相似提交(合并为一条)
48
+ - Merge commit
49
+
50
+ ### 合并规则
51
+
52
+ - 相关联的多个小改动合并为一个主题
53
+ - 同一功能的多次迭代合并为一条记录
54
+ - 问题排查和解决归为一条
55
+
56
+ ## 输出示例
57
+
58
+ ### 示例 1
59
+
60
+ ```
61
+ project-frontend
62
+ - 构建工具升级改造及问题排查
63
+ - 核心功能开发流程跟进
64
+ - 异常处理优化
65
+ - 方案合理性优化
66
+ - 问题边界定义
67
+ - 对话头像渲染
68
+ - 脚本国际化检查优化
69
+
70
+ 其他
71
+ - 新版国际化方案讨论
72
+ ```
73
+
74
+ ### 示例 2
75
+
76
+ ```
77
+ project-backend
78
+ - 自定义类型化流式消息渲染
79
+ - 异常状态调试优化
80
+ - 支持 debug 模式
81
+ - 原始消息结构查看
82
+ - device id 修复及有效期处理
83
+ - 断线重连流程梳理
84
+
85
+ 其他
86
+ - 全局弹窗缺陷修复
87
+ ```
88
+
89
+ ### 示例 3
90
+
91
+ ```
92
+ project-frontend
93
+ - 前后端分离方案跟进
94
+ - 拆分服务(web/admin/server)
95
+ - 统一转发代理消除跨域
96
+ - 活动页需求设计
97
+ - 支持免发版管理
98
+ - SDK 方案调通
99
+ - SEO 预渲染方案
100
+
101
+ project-backend
102
+ - 断线重连方案实现
103
+ - 自动重试失败后提示刷新
104
+ - 依赖库升级尝试
105
+
106
+ 其他
107
+ - 多媒体资源运营管理方案讨论
108
+ ```
109
+
110
+ ## 注意事项
111
+
112
+ 1. 如果输入是图片,请仔细识别 commit 信息,按时间和项目归类
113
+ 2. commit message 中的技术术语可以保留,但要确保表述通顺
114
+ 3. 如有多个项目,按工作量或重要性排序
115
+ 4. "其他"分类用于放置不属于特定项目的工作内容
116
+ 5. 如果某周工作内容较少,如实反映即可,不需要刻意扩充
@@ -0,0 +1,3 @@
1
+ """Weekly Flow - Git 提交记录周报生成工具"""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,171 @@
1
+ """配置管理模块
2
+
3
+ 管理周报工具的配置,包括仓库列表等。
4
+ """
5
+
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List, Optional, Tuple
9
+
10
+
11
+ # 默认配置
12
+ DEFAULT_CONFIG: Dict[str, Any] = {
13
+ "repos": [],
14
+ "default_author": "auto",
15
+ "output_format": "markdown",
16
+ }
17
+
18
+
19
+ def get_config_path(base_dir: Optional[Path] = None) -> Path:
20
+ """获取配置文件路径
21
+
22
+ Args:
23
+ base_dir: 基础目录,默认为 ~/.weekly-reports
24
+
25
+ Returns:
26
+ 配置文件路径
27
+ """
28
+ if base_dir is None:
29
+ base_dir = Path.home() / ".weekly-reports"
30
+
31
+ return base_dir / "config.json"
32
+
33
+
34
+ def load_config(config_path: Optional[Path] = None) -> Dict[str, Any]:
35
+ """加载配置文件
36
+
37
+ Args:
38
+ config_path: 配置文件路径,默认为 ~/.weekly-reports/config.json
39
+
40
+ Returns:
41
+ 配置字典
42
+ """
43
+ if config_path is None:
44
+ config_path = get_config_path()
45
+
46
+ if not config_path.exists():
47
+ return DEFAULT_CONFIG.copy()
48
+
49
+ try:
50
+ with open(config_path, "r", encoding="utf-8") as f:
51
+ config = json.load(f)
52
+ # 合并默认配置,确保所有字段都存在
53
+ return {**DEFAULT_CONFIG, **config}
54
+ except (json.JSONDecodeError, OSError):
55
+ return DEFAULT_CONFIG.copy()
56
+
57
+
58
+ def save_config(
59
+ config: Dict[str, Any],
60
+ config_path: Optional[Path] = None,
61
+ ) -> None:
62
+ """保存配置文件
63
+
64
+ Args:
65
+ config: 配置字典
66
+ config_path: 配置文件路径
67
+ """
68
+ if config_path is None:
69
+ config_path = get_config_path()
70
+
71
+ # 确保父目录存在
72
+ config_path.parent.mkdir(parents=True, exist_ok=True)
73
+
74
+ with open(config_path, "w", encoding="utf-8") as f:
75
+ json.dump(config, f, indent=2, ensure_ascii=False)
76
+
77
+
78
+ def add_repo(
79
+ config: Dict[str, Any],
80
+ name: str,
81
+ path: str,
82
+ ) -> Dict[str, Any]:
83
+ """添加仓库到配置
84
+
85
+ Args:
86
+ config: 配置字典
87
+ name: 仓库名称
88
+ path: 仓库路径
89
+
90
+ Returns:
91
+ 更新后的配置
92
+ """
93
+ repos = config.get("repos", [])
94
+
95
+ # 检查是否已存在
96
+ for repo in repos:
97
+ if repo["name"] == name:
98
+ # 更新路径
99
+ repo["path"] = path
100
+ return config
101
+
102
+ # 添加新仓库
103
+ repos.append({"name": name, "path": path})
104
+ config["repos"] = repos
105
+
106
+ return config
107
+
108
+
109
+ def remove_repo(config: Dict[str, Any], name: str) -> Dict[str, Any]:
110
+ """从配置中移除仓库
111
+
112
+ Args:
113
+ config: 配置字典
114
+ name: 仓库名称
115
+
116
+ Returns:
117
+ 更新后的配置
118
+ """
119
+ repos = config.get("repos", [])
120
+ config["repos"] = [r for r in repos if r["name"] != name]
121
+ return config
122
+
123
+
124
+ def get_repos(config: Dict[str, Any]) -> List[Dict[str, str]]:
125
+ """获取仓库列表
126
+
127
+ Args:
128
+ config: 配置字典
129
+
130
+ Returns:
131
+ 仓库列表
132
+ """
133
+ return config.get("repos", [])
134
+
135
+
136
+ def validate_repo(path: Path) -> Tuple[bool, Optional[str]]:
137
+ """验证仓库路径是否有效
138
+
139
+ Args:
140
+ path: 仓库路径
141
+
142
+ Returns:
143
+ (is_valid, error_message)
144
+ """
145
+ if isinstance(path, str):
146
+ path = Path(path)
147
+
148
+ if not path.exists():
149
+ return False, f"路径不存在: {path}"
150
+
151
+ if not path.is_dir():
152
+ return False, f"路径不是目录: {path}"
153
+
154
+ git_dir = path / ".git"
155
+ if not git_dir.exists():
156
+ return False, f"不是有效的 Git 仓库: {path}"
157
+
158
+ return True, None
159
+
160
+
161
+ def get_repo_paths(config: Dict[str, Any]) -> List[Path]:
162
+ """获取所有仓库路径
163
+
164
+ Args:
165
+ config: 配置字典
166
+
167
+ Returns:
168
+ 仓库路径列表
169
+ """
170
+ repos = get_repos(config)
171
+ return [Path(r["path"]) for r in repos]