claude-init 1.0.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/.claude/agents/library-usage-researcher.md +89 -0
- package/.claude/commands/debug.md +3 -0
- package/.claude/commands/gogogo.md +3 -0
- package/.claude/commands/plan.md +28 -0
- package/.claude/commands/prompt-enhancement.md +3 -0
- package/.claude/settings.json +18 -0
- package/.claude/settings.local.json +17 -0
- package/.devcontainer/Dockerfile +91 -0
- package/.devcontainer/devcontainer.json +56 -0
- package/.devcontainer/init-firewall.sh +131 -0
- package/CLAUDE.md +4 -0
- package/README.md +104 -0
- package/bin/index.js +9 -0
- package/package.json +52 -0
- package/src/cli.js +116 -0
- package/src/fileManager.js +124 -0
- package/src/utils.js +65 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: library-usage-researcher
|
|
3
|
+
description: Use this agent when you need to research how to use a specific library, framework, or technology. This agent will systematically gather information about best practices, API details, advanced techniques, and real-world usage examples. The agent follows a strict sequence: first identifying the library, then getting official documentation, and finally searching for real-world implementations. Examples:\n\n<example>\nContext: User wants to understand how to use React Query for data fetching\nuser: "我想了解如何使用 React Query 进行数据获取"\nassistant: "我将使用 library-usage-researcher 代理来系统地研究 React Query 的使用方法"\n<commentary>\nSince the user wants to understand library usage, use the library-usage-researcher agent to gather comprehensive information about React Query.\n</commentary>\n</example>\n\n<example>\nContext: User needs to know advanced Redux Toolkit patterns\nuser: "Redux Toolkit 有哪些高级用法和技巧?"\nassistant: "让我启动 library-usage-researcher 代理来深入研究 Redux Toolkit 的高级模式和最佳实践"\n<commentary>\nThe user is asking about advanced usage patterns, which is exactly what the library-usage-researcher agent is designed to investigate.\n</commentary>\n</example>
|
|
4
|
+
tools: Task, mcp__grep__searchGitHub, mcp__context7__resolve-library-id, mcp__context7__get-library-docs, TodoWrite, WebFetch, Bash, LS, Read, Edit, Write
|
|
5
|
+
color: blue
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
你是一位专业的技术研究专家,专门负责深入调研库、框架和技术的使用方法。你的任务是系统性地收集和整理关于特定技术的全面信息。
|
|
9
|
+
|
|
10
|
+
## 工作流程
|
|
11
|
+
|
|
12
|
+
你必须严格按照以下顺序执行研究任务:
|
|
13
|
+
|
|
14
|
+
1. **识别目标库**
|
|
15
|
+
- 使用 `resolve-library-id` 工具准确找到用户询问的库或框架
|
|
16
|
+
- 确保获得正确的库标识符,避免混淆相似名称的库
|
|
17
|
+
|
|
18
|
+
2. **获取官方文档**
|
|
19
|
+
- 使用 `get-library-docs` 工具深入了解:
|
|
20
|
+
- API 规范和接口定义
|
|
21
|
+
- 官方推荐的最佳实践
|
|
22
|
+
- 核心概念和设计理念
|
|
23
|
+
- 使用示例和代码片段
|
|
24
|
+
|
|
25
|
+
3. **搜索真实案例**
|
|
26
|
+
- 使用 `searchGitHub` 工具查找真实项目中的使用案例
|
|
27
|
+
- 重点关注:
|
|
28
|
+
- 生产环境的实际用法
|
|
29
|
+
- 社区认可的模式和技巧
|
|
30
|
+
- 常见问题的解决方案
|
|
31
|
+
- 性能优化和高级技巧
|
|
32
|
+
|
|
33
|
+
## 研究重点
|
|
34
|
+
|
|
35
|
+
你需要特别关注以下方面:
|
|
36
|
+
- **功能用法**:基础功能如何使用,参数配置方式
|
|
37
|
+
- **巧妙用法**:社区发现的创新使用方式
|
|
38
|
+
- **高级技巧**:性能优化、复杂场景处理
|
|
39
|
+
- **真实细节**:实际项目中的具体实现
|
|
40
|
+
- **常见陷阱**:容易出错的地方和反模式
|
|
41
|
+
- **重要警告**:安全问题、性能问题、兼容性问题
|
|
42
|
+
|
|
43
|
+
## 输出格式
|
|
44
|
+
|
|
45
|
+
你必须按照以下结构组织你的研究结果,并编写文档保存在当前项目的根目录下:
|
|
46
|
+
|
|
47
|
+
1. **接口规范**
|
|
48
|
+
- 核心 API 和方法签名
|
|
49
|
+
- 参数说明和返回值
|
|
50
|
+
- 类型定义(如果适用)
|
|
51
|
+
|
|
52
|
+
2. **基础使用**
|
|
53
|
+
- 安装和初始化步骤
|
|
54
|
+
- 最简单的使用示例
|
|
55
|
+
- 基本配置选项
|
|
56
|
+
|
|
57
|
+
3. **进阶技巧**
|
|
58
|
+
- 高级配置和优化
|
|
59
|
+
- 复杂场景的处理方法
|
|
60
|
+
- 性能调优建议
|
|
61
|
+
|
|
62
|
+
4. **巧妙用法**
|
|
63
|
+
- 社区创新的使用模式
|
|
64
|
+
- 与其他工具的集成技巧
|
|
65
|
+
- 非常规但有效的解决方案
|
|
66
|
+
|
|
67
|
+
5. **注意事项**
|
|
68
|
+
- 常见错误和如何避免
|
|
69
|
+
- 性能陷阱和最佳实践
|
|
70
|
+
- 版本兼容性问题
|
|
71
|
+
|
|
72
|
+
6. **真实代码片段**
|
|
73
|
+
- 从 GitHub 找到的优秀示例
|
|
74
|
+
- 包含上下文的完整代码
|
|
75
|
+
- 说明为什么这是好的实践
|
|
76
|
+
|
|
77
|
+
7. **引用来源**
|
|
78
|
+
- 提供所有关键信息的来源 URL
|
|
79
|
+
- 标注哪些是官方文档,哪些是社区资源
|
|
80
|
+
|
|
81
|
+
## 重要原则
|
|
82
|
+
|
|
83
|
+
- **不要本地化**:你专注于获取外部信息,不关心用户的本地代码情况
|
|
84
|
+
- **诚实报告**:如果某个步骤没有获得有效信息,明确说明"未找到相关信息",绝不杜撰
|
|
85
|
+
- **保持客观**:基于事实报告,不加入个人偏好或推测
|
|
86
|
+
- **注重实用**:优先展示能立即应用的实践知识
|
|
87
|
+
- **中文表达**:所有内容用清晰的中文表达,包括对英文资料的翻译和解释
|
|
88
|
+
|
|
89
|
+
记住:你的目标是为用户提供关于特定技术最全面、最实用的研究报告,让他们能够快速掌握并正确使用该技术。
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
You are an experienced project manager. For every question posed by the user, you do not rush to write code. Instead, you focus on thoughtful, structured reasoning to provide high-quality answers, explore multiple possible solutions, and identify the best one. Before starting, ensure you fully understand the following project requirements and process descriptions.
|
|
2
|
+
|
|
3
|
+
You possess the following capabilities:
|
|
4
|
+
|
|
5
|
+
1. Requirements Clarification
|
|
6
|
+
|
|
7
|
+
- Clearly restate the user's question or problem in your own words.
|
|
8
|
+
- Establish high-level communication to clarify the user's needs.
|
|
9
|
+
- Offer analogous case studies to inspire the user's thinking.
|
|
10
|
+
- Explain the primary challenges and constraints.
|
|
11
|
+
- During your reasoning process, ask questions to fill in any missing information or details you need.
|
|
12
|
+
|
|
13
|
+
2. Solution Exploration
|
|
14
|
+
|
|
15
|
+
- Explore multiple feasible implementation methods based on existing technologies.
|
|
16
|
+
- List the pros, cons, applicable scenarios, and costs of each solution.
|
|
17
|
+
- Prioritize leveraging existing technical solutions in the ecosystem to avoid reinventing the wheel.
|
|
18
|
+
- Provide an optimal recommendation based on the requirements, explaining the rationale and potential improvements.
|
|
19
|
+
- Ensure the recommended solution is scalable and maintainable, offering corresponding optimization suggestions.
|
|
20
|
+
|
|
21
|
+
Additionally, maintain the following principles throughout your work process:
|
|
22
|
+
|
|
23
|
+
1. Optimal Choices: When selecting technologies or implementation methods, prioritize mature, stable, and efficient solutions.
|
|
24
|
+
2. Environment Considerations: Thoughtfully address potential environment issues, such as dependency version conflicts or system compatibility, and proactively provide solutions.
|
|
25
|
+
|
|
26
|
+
Now, no need to code, read through and understand the codebase, ultrathink and give me a plan on:
|
|
27
|
+
|
|
28
|
+
$ARGUMENTS
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"WebSearch"
|
|
5
|
+
],
|
|
6
|
+
"deny": [
|
|
7
|
+
"Read(**/.env*)",
|
|
8
|
+
"Edit(**/.env*)",
|
|
9
|
+
"Write(**/.env*)",
|
|
10
|
+
"Bash(**:.env*)"
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
"statusLine": {
|
|
14
|
+
"type": "command",
|
|
15
|
+
"command": "npx ccusage statusline"
|
|
16
|
+
},
|
|
17
|
+
"outputStyle": "Explanatory"
|
|
18
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
FROM node:20
|
|
2
|
+
|
|
3
|
+
ARG TZ
|
|
4
|
+
ENV TZ="$TZ"
|
|
5
|
+
|
|
6
|
+
ARG CLAUDE_CODE_VERSION=latest
|
|
7
|
+
|
|
8
|
+
# Install basic development tools and iptables/ipset
|
|
9
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
10
|
+
less \
|
|
11
|
+
git \
|
|
12
|
+
procps \
|
|
13
|
+
sudo \
|
|
14
|
+
fzf \
|
|
15
|
+
zsh \
|
|
16
|
+
man-db \
|
|
17
|
+
unzip \
|
|
18
|
+
gnupg2 \
|
|
19
|
+
gh \
|
|
20
|
+
iptables \
|
|
21
|
+
ipset \
|
|
22
|
+
iproute2 \
|
|
23
|
+
dnsutils \
|
|
24
|
+
aggregate \
|
|
25
|
+
jq \
|
|
26
|
+
nano \
|
|
27
|
+
vim \
|
|
28
|
+
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
|
29
|
+
|
|
30
|
+
# Ensure default node user has access to /usr/local/share
|
|
31
|
+
RUN mkdir -p /usr/local/share/npm-global && \
|
|
32
|
+
chown -R node:node /usr/local/share
|
|
33
|
+
|
|
34
|
+
ARG USERNAME=node
|
|
35
|
+
|
|
36
|
+
# Persist bash history.
|
|
37
|
+
RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
|
|
38
|
+
&& mkdir /commandhistory \
|
|
39
|
+
&& touch /commandhistory/.bash_history \
|
|
40
|
+
&& chown -R $USERNAME /commandhistory
|
|
41
|
+
|
|
42
|
+
# Set `DEVCONTAINER` environment variable to help with orientation
|
|
43
|
+
ENV DEVCONTAINER=true
|
|
44
|
+
|
|
45
|
+
# Create workspace and config directories and set permissions
|
|
46
|
+
RUN mkdir -p /workspace /home/node/.claude && \
|
|
47
|
+
chown -R node:node /workspace /home/node/.claude
|
|
48
|
+
|
|
49
|
+
WORKDIR /workspace
|
|
50
|
+
|
|
51
|
+
ARG GIT_DELTA_VERSION=0.18.2
|
|
52
|
+
RUN ARCH=$(dpkg --print-architecture) && \
|
|
53
|
+
wget "https://github.com/dandavison/delta/releases/download/${GIT_DELTA_VERSION}/git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \
|
|
54
|
+
sudo dpkg -i "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \
|
|
55
|
+
rm "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb"
|
|
56
|
+
|
|
57
|
+
# Set up non-root user
|
|
58
|
+
USER node
|
|
59
|
+
|
|
60
|
+
# Install global packages
|
|
61
|
+
ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
|
|
62
|
+
ENV PATH=$PATH:/usr/local/share/npm-global/bin
|
|
63
|
+
|
|
64
|
+
# Set the default shell to zsh rather than sh
|
|
65
|
+
ENV SHELL=/bin/zsh
|
|
66
|
+
|
|
67
|
+
# Set the default editor and visual
|
|
68
|
+
ENV EDITOR nano
|
|
69
|
+
ENV VISUAL nano
|
|
70
|
+
|
|
71
|
+
# Default powerline10k theme
|
|
72
|
+
ARG ZSH_IN_DOCKER_VERSION=1.2.0
|
|
73
|
+
RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh)" -- \
|
|
74
|
+
-p git \
|
|
75
|
+
-p fzf \
|
|
76
|
+
-a "source /usr/share/doc/fzf/examples/key-bindings.zsh" \
|
|
77
|
+
-a "source /usr/share/doc/fzf/examples/completion.zsh" \
|
|
78
|
+
-a "export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
|
|
79
|
+
-x
|
|
80
|
+
|
|
81
|
+
# Install Claude
|
|
82
|
+
RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# Copy and set up firewall script
|
|
86
|
+
COPY init-firewall.sh /usr/local/bin/
|
|
87
|
+
USER root
|
|
88
|
+
RUN chmod +x /usr/local/bin/init-firewall.sh && \
|
|
89
|
+
echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/node-firewall && \
|
|
90
|
+
chmod 0440 /etc/sudoers.d/node-firewall
|
|
91
|
+
USER node
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Claude Code Sandbox",
|
|
3
|
+
"build": {
|
|
4
|
+
"dockerfile": "Dockerfile",
|
|
5
|
+
"args": {
|
|
6
|
+
"TZ": "${localEnv:TZ:America/Los_Angeles}",
|
|
7
|
+
"CLAUDE_CODE_VERSION": "latest",
|
|
8
|
+
"GIT_DELTA_VERSION": "0.18.2",
|
|
9
|
+
"ZSH_IN_DOCKER_VERSION": "1.2.0"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"runArgs": [
|
|
13
|
+
"--cap-add=NET_ADMIN",
|
|
14
|
+
"--cap-add=NET_RAW"
|
|
15
|
+
],
|
|
16
|
+
"customizations": {
|
|
17
|
+
"vscode": {
|
|
18
|
+
"extensions": [
|
|
19
|
+
"dbaeumer.vscode-eslint",
|
|
20
|
+
"esbenp.prettier-vscode",
|
|
21
|
+
"eamodio.gitlens",
|
|
22
|
+
"anthropic.claude-code"
|
|
23
|
+
],
|
|
24
|
+
"settings": {
|
|
25
|
+
"editor.formatOnSave": true,
|
|
26
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
27
|
+
"editor.codeActionsOnSave": {
|
|
28
|
+
"source.fixAll.eslint": "explicit"
|
|
29
|
+
},
|
|
30
|
+
"terminal.integrated.defaultProfile.linux": "zsh",
|
|
31
|
+
"terminal.integrated.profiles.linux": {
|
|
32
|
+
"bash": {
|
|
33
|
+
"path": "bash",
|
|
34
|
+
"icon": "terminal-bash"
|
|
35
|
+
},
|
|
36
|
+
"zsh": {
|
|
37
|
+
"path": "zsh"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"remoteUser": "node",
|
|
44
|
+
"mounts": [
|
|
45
|
+
"source=claude-code-bashhistory-${devcontainerId},target=/commandhistory,type=volume",
|
|
46
|
+
"source=claude-code-config-${devcontainerId},target=/home/node/.claude,type=volume"
|
|
47
|
+
],
|
|
48
|
+
"containerEnv": {
|
|
49
|
+
"NODE_OPTIONS": "--max-old-space-size=4096",
|
|
50
|
+
"CLAUDE_CONFIG_DIR": "/home/node/.claude",
|
|
51
|
+
"POWERLEVEL9K_DISABLE_GITSTATUS": "true"
|
|
52
|
+
},
|
|
53
|
+
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated",
|
|
54
|
+
"workspaceFolder": "/workspace",
|
|
55
|
+
"postCreateCommand": "sudo /usr/local/bin/init-firewall.sh"
|
|
56
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail # Exit on error, undefined vars, and pipeline failures
|
|
3
|
+
IFS=$'\n\t' # Stricter word splitting
|
|
4
|
+
|
|
5
|
+
# 1. Extract Docker DNS info BEFORE any flushing
|
|
6
|
+
DOCKER_DNS_RULES=$(iptables-save -t nat | grep "127\.0\.0\.11" || true)
|
|
7
|
+
|
|
8
|
+
# Flush existing rules and delete existing ipsets
|
|
9
|
+
iptables -F
|
|
10
|
+
iptables -X
|
|
11
|
+
iptables -t nat -F
|
|
12
|
+
iptables -t nat -X
|
|
13
|
+
iptables -t mangle -F
|
|
14
|
+
iptables -t mangle -X
|
|
15
|
+
ipset destroy allowed-domains 2>/dev/null || true
|
|
16
|
+
|
|
17
|
+
# 2. Selectively restore ONLY internal Docker DNS resolution
|
|
18
|
+
if [ -n "$DOCKER_DNS_RULES" ]; then
|
|
19
|
+
echo "Restoring Docker DNS rules..."
|
|
20
|
+
iptables -t nat -N DOCKER_OUTPUT 2>/dev/null || true
|
|
21
|
+
iptables -t nat -N DOCKER_POSTROUTING 2>/dev/null || true
|
|
22
|
+
echo "$DOCKER_DNS_RULES" | xargs -L 1 iptables -t nat
|
|
23
|
+
else
|
|
24
|
+
echo "No Docker DNS rules to restore"
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# First allow DNS and localhost before any restrictions
|
|
28
|
+
# Allow outbound DNS
|
|
29
|
+
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
|
|
30
|
+
# Allow inbound DNS responses
|
|
31
|
+
iptables -A INPUT -p udp --sport 53 -j ACCEPT
|
|
32
|
+
# Allow outbound SSH
|
|
33
|
+
iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT
|
|
34
|
+
# Allow inbound SSH responses
|
|
35
|
+
iptables -A INPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
|
|
36
|
+
# Allow localhost
|
|
37
|
+
iptables -A INPUT -i lo -j ACCEPT
|
|
38
|
+
iptables -A OUTPUT -o lo -j ACCEPT
|
|
39
|
+
|
|
40
|
+
# Create ipset with CIDR support
|
|
41
|
+
ipset create allowed-domains hash:net
|
|
42
|
+
|
|
43
|
+
# Fetch GitHub meta information and aggregate + add their IP ranges
|
|
44
|
+
echo "Fetching GitHub IP ranges..."
|
|
45
|
+
gh_ranges=$(curl -s https://api.github.com/meta)
|
|
46
|
+
if [ -z "$gh_ranges" ]; then
|
|
47
|
+
echo "ERROR: Failed to fetch GitHub IP ranges"
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
if ! echo "$gh_ranges" | jq -e '.web and .api and .git' >/dev/null; then
|
|
52
|
+
echo "ERROR: GitHub API response missing required fields"
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
echo "Processing GitHub IPs..."
|
|
57
|
+
while read -r cidr; do
|
|
58
|
+
if [[ ! "$cidr" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$ ]]; then
|
|
59
|
+
echo "ERROR: Invalid CIDR range from GitHub meta: $cidr"
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
echo "Adding GitHub range $cidr"
|
|
63
|
+
ipset add allowed-domains "$cidr"
|
|
64
|
+
done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]' | aggregate -q)
|
|
65
|
+
|
|
66
|
+
# Resolve and add other allowed domains
|
|
67
|
+
for domain in \
|
|
68
|
+
"registry.npmjs.org" \
|
|
69
|
+
"api.anthropic.com" \
|
|
70
|
+
"sentry.io" \
|
|
71
|
+
"statsig.anthropic.com" \
|
|
72
|
+
"statsig.com"; do
|
|
73
|
+
echo "Resolving $domain..."
|
|
74
|
+
ips=$(dig +noall +answer A "$domain" | awk '$4 == "A" {print $5}')
|
|
75
|
+
if [ -z "$ips" ]; then
|
|
76
|
+
echo "ERROR: Failed to resolve $domain"
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
while read -r ip; do
|
|
81
|
+
if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
|
|
82
|
+
echo "ERROR: Invalid IP from DNS for $domain: $ip"
|
|
83
|
+
exit 1
|
|
84
|
+
fi
|
|
85
|
+
echo "Adding $ip for $domain"
|
|
86
|
+
ipset add allowed-domains "$ip"
|
|
87
|
+
done < <(echo "$ips")
|
|
88
|
+
done
|
|
89
|
+
|
|
90
|
+
# Get host IP from default route
|
|
91
|
+
HOST_IP=$(ip route | grep default | cut -d" " -f3)
|
|
92
|
+
if [ -z "$HOST_IP" ]; then
|
|
93
|
+
echo "ERROR: Failed to detect host IP"
|
|
94
|
+
exit 1
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
HOST_NETWORK=$(echo "$HOST_IP" | sed "s/\.[0-9]*$/.0\/24/")
|
|
98
|
+
echo "Host network detected as: $HOST_NETWORK"
|
|
99
|
+
|
|
100
|
+
# Set up remaining iptables rules
|
|
101
|
+
iptables -A INPUT -s "$HOST_NETWORK" -j ACCEPT
|
|
102
|
+
iptables -A OUTPUT -d "$HOST_NETWORK" -j ACCEPT
|
|
103
|
+
|
|
104
|
+
# Set default policies to DROP first
|
|
105
|
+
iptables -P INPUT DROP
|
|
106
|
+
iptables -P FORWARD DROP
|
|
107
|
+
iptables -P OUTPUT DROP
|
|
108
|
+
|
|
109
|
+
# First allow established connections for already approved traffic
|
|
110
|
+
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
|
111
|
+
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
|
112
|
+
|
|
113
|
+
# Then allow only specific outbound traffic to allowed domains
|
|
114
|
+
iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT
|
|
115
|
+
|
|
116
|
+
echo "Firewall configuration complete"
|
|
117
|
+
echo "Verifying firewall rules..."
|
|
118
|
+
if curl --connect-timeout 5 https://example.com >/dev/null 2>&1; then
|
|
119
|
+
echo "ERROR: Firewall verification failed - was able to reach https://example.com"
|
|
120
|
+
exit 1
|
|
121
|
+
else
|
|
122
|
+
echo "Firewall verification passed - unable to reach https://example.com as expected"
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# Verify GitHub API access
|
|
126
|
+
if ! curl --connect-timeout 5 https://api.github.com/zen >/dev/null 2>&1; then
|
|
127
|
+
echo "ERROR: Firewall verification failed - unable to reach https://api.github.com"
|
|
128
|
+
exit 1
|
|
129
|
+
else
|
|
130
|
+
echo "Firewall verification passed - able to reach https://api.github.com as expected"
|
|
131
|
+
fi
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
+ During you interaction with the user, if you find anything reusable in this project (e.g. version of a library, model name), especially about a fix to a mistake you made or a correction you received, you should take note in the `Lessons` section in the `CLAUDE.md` file so you will not make the same mistake again.
|
|
2
|
+
+ You should also use the `CLAUDE.md` file as a scratchpad to organize your thoughts. Especially when you receive a new task, you should first review the content of the scratchpad, clear old different task but keep lessons learned, then explain the task, and plan the steps you need to take to complete the task. You can use todo markers to indicate the progress, e.g.
|
|
3
|
+
[X] Task 1
|
|
4
|
+
[ ] Task 2
|
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# claude-init
|
|
2
|
+
|
|
3
|
+
A CLI tool to initialize Claude development environment with standardized configurations and templates.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Smart Setup**: Automatically detects existing files and only creates/updates what's needed
|
|
8
|
+
- **Non-destructive**: Never overwrites existing files, only adds missing content
|
|
9
|
+
- **Progress Feedback**: Clear visual indicators of what's being created, updated, or skipped
|
|
10
|
+
- **Cross-platform**: Works on Windows, macOS, and Linux
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
### Quick Start
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx claude-init
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This will set up your current directory with:
|
|
21
|
+
|
|
22
|
+
- `CLAUDE.md` - Project instructions and scratchpad for Claude
|
|
23
|
+
- `.devcontainer/` - Development container configuration
|
|
24
|
+
- `.claude/settings.json` - Claude-specific settings
|
|
25
|
+
- `.claude/commands/` - Custom Claude commands
|
|
26
|
+
- `.claude/agents/` - Specialized agent configurations
|
|
27
|
+
|
|
28
|
+
### What It Does
|
|
29
|
+
|
|
30
|
+
#### 📄 CLAUDE.md
|
|
31
|
+
- **If missing**: Creates new file with template content
|
|
32
|
+
- **If exists**: Appends template content under "# Claude Scratchpad Rules" heading
|
|
33
|
+
- **If already contains template**: Skips to preserve your content
|
|
34
|
+
|
|
35
|
+
#### 📁 .devcontainer
|
|
36
|
+
- **If missing**: Creates complete directory with Docker configuration
|
|
37
|
+
- **If exists**: Skips entirely to preserve your setup
|
|
38
|
+
|
|
39
|
+
#### ⚙️ .claude/settings.json
|
|
40
|
+
- **If missing**: Creates with default Claude settings
|
|
41
|
+
- **If exists**: Skips to preserve your custom settings
|
|
42
|
+
|
|
43
|
+
#### 📋 .claude/commands & .claude/agents
|
|
44
|
+
- **If missing**: Creates with all template files
|
|
45
|
+
- **If exists**: Only adds missing files, preserves existing ones
|
|
46
|
+
|
|
47
|
+
## Example Output
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
🚀 Claude Environment Initializer
|
|
51
|
+
Initializing in: /path/to/your/project
|
|
52
|
+
|
|
53
|
+
✓ Created new CLAUDE.md
|
|
54
|
+
✓ Created .devcontainer directory
|
|
55
|
+
✓ Created .claude/settings.json
|
|
56
|
+
↻ Added 2 missing files to .claude/commands (2 files)
|
|
57
|
+
- All files in .claude/agents are up to date
|
|
58
|
+
|
|
59
|
+
📊 Summary:
|
|
60
|
+
Created: 3 items
|
|
61
|
+
Updated: 1 items
|
|
62
|
+
Skipped: 1 items (already exist)
|
|
63
|
+
Files added: 2
|
|
64
|
+
|
|
65
|
+
✨ Claude environment is ready!
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## File Structure Created
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
your-project/
|
|
72
|
+
├── CLAUDE.md # Project instructions
|
|
73
|
+
├── .devcontainer/
|
|
74
|
+
│ ├── devcontainer.json # VS Code dev container config
|
|
75
|
+
│ ├── Dockerfile # Container setup
|
|
76
|
+
│ └── init-firewall.sh # Initialization script
|
|
77
|
+
└── .claude/
|
|
78
|
+
├── settings.json # Claude settings
|
|
79
|
+
├── commands/ # Custom commands
|
|
80
|
+
│ ├── debug.md
|
|
81
|
+
│ ├── gogogo.md
|
|
82
|
+
│ ├── plan.md
|
|
83
|
+
│ └── prompt-enhancement.md
|
|
84
|
+
└── agents/ # Specialized agents
|
|
85
|
+
└── library-usage-researcher.md
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Development
|
|
89
|
+
|
|
90
|
+
### Running Tests
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm test
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Local Development
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm run dev
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT
|
package/bin/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-init",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Initialize Claude development environment with configurations and templates",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claude-init": "./bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./src/cli.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"src/",
|
|
13
|
+
"CLAUDE.md",
|
|
14
|
+
".claude/",
|
|
15
|
+
".devcontainer/"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test test/*.test.js",
|
|
19
|
+
"dev": "node bin/index.js",
|
|
20
|
+
"prepublishOnly": "npm test"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"claude",
|
|
24
|
+
"ai",
|
|
25
|
+
"development",
|
|
26
|
+
"environment",
|
|
27
|
+
"cli",
|
|
28
|
+
"initialization",
|
|
29
|
+
"scaffolding"
|
|
30
|
+
],
|
|
31
|
+
"author": "ChrisLinn <nanyishanshaoxiu@gmail.com>",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=16.0.0"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"chalk": "^5.3.0",
|
|
38
|
+
"fs-extra": "^11.2.0",
|
|
39
|
+
"ora": "^7.0.1"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"tmp": "^0.2.1"
|
|
43
|
+
},
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": ""
|
|
47
|
+
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": ""
|
|
50
|
+
},
|
|
51
|
+
"homepage": ""
|
|
52
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import {
|
|
4
|
+
handleClaudeMarkdown,
|
|
5
|
+
handleDirectoryMirror,
|
|
6
|
+
handleSelectiveFileCopy,
|
|
7
|
+
handleSingleFile
|
|
8
|
+
} from './fileManager.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Format action result with appropriate colors and icons
|
|
12
|
+
*/
|
|
13
|
+
function formatResult(result) {
|
|
14
|
+
const { action, details, filesAdded } = result;
|
|
15
|
+
|
|
16
|
+
switch (action) {
|
|
17
|
+
case 'created':
|
|
18
|
+
return chalk.green(`✓ ${details}`);
|
|
19
|
+
case 'updated':
|
|
20
|
+
const fileInfo = filesAdded !== undefined ? ` (${filesAdded} files)` : '';
|
|
21
|
+
return chalk.yellow(`↻ ${details}${fileInfo}`);
|
|
22
|
+
case 'skipped':
|
|
23
|
+
return chalk.gray(`- ${details}`);
|
|
24
|
+
default:
|
|
25
|
+
return details;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Main CLI function
|
|
31
|
+
*/
|
|
32
|
+
export async function cli() {
|
|
33
|
+
const targetDir = process.cwd();
|
|
34
|
+
|
|
35
|
+
console.log(chalk.blue.bold('🚀 Claude Environment Initializer'));
|
|
36
|
+
console.log(chalk.gray(`Initializing in: ${targetDir}`));
|
|
37
|
+
console.log();
|
|
38
|
+
|
|
39
|
+
const tasks = [
|
|
40
|
+
{
|
|
41
|
+
name: 'CLAUDE.md',
|
|
42
|
+
handler: () => handleClaudeMarkdown(targetDir)
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: '.devcontainer',
|
|
46
|
+
handler: () => handleDirectoryMirror(targetDir, '.devcontainer')
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: '.claude/settings.json',
|
|
50
|
+
handler: () => handleSingleFile(targetDir, '.claude/settings.json')
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: '.claude/commands',
|
|
54
|
+
handler: () => handleSelectiveFileCopy(targetDir, '.claude/commands')
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: '.claude/agents',
|
|
58
|
+
handler: () => handleSelectiveFileCopy(targetDir, '.claude/agents')
|
|
59
|
+
}
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
let totalActions = { created: 0, updated: 0, skipped: 0, filesAdded: 0 };
|
|
63
|
+
|
|
64
|
+
for (const task of tasks) {
|
|
65
|
+
const spinner = ora(`Processing ${task.name}...`).start();
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const result = await task.handler();
|
|
69
|
+
spinner.succeed(formatResult(result));
|
|
70
|
+
|
|
71
|
+
// Track statistics
|
|
72
|
+
totalActions[result.action]++;
|
|
73
|
+
if (result.filesAdded) {
|
|
74
|
+
totalActions.filesAdded += result.filesAdded;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
} catch (error) {
|
|
78
|
+
spinner.fail(`Failed to process ${task.name}: ${error.message}`);
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Summary
|
|
84
|
+
console.log();
|
|
85
|
+
console.log(chalk.blue.bold('📊 Summary:'));
|
|
86
|
+
|
|
87
|
+
if (totalActions.created > 0) {
|
|
88
|
+
console.log(chalk.green(` Created: ${totalActions.created} items`));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (totalActions.updated > 0) {
|
|
92
|
+
console.log(chalk.yellow(` Updated: ${totalActions.updated} items`));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (totalActions.skipped > 0) {
|
|
96
|
+
console.log(chalk.gray(` Skipped: ${totalActions.skipped} items (already exist)`));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (totalActions.filesAdded > 0) {
|
|
100
|
+
console.log(chalk.cyan(` Files added: ${totalActions.filesAdded}`));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log();
|
|
104
|
+
|
|
105
|
+
if (totalActions.created > 0 || totalActions.updated > 0) {
|
|
106
|
+
console.log(chalk.green.bold('✨ Claude environment is ready!'));
|
|
107
|
+
} else {
|
|
108
|
+
console.log(chalk.blue.bold('✅ Claude environment is already up to date!'));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log();
|
|
112
|
+
console.log(chalk.gray('Next steps:'));
|
|
113
|
+
console.log(chalk.gray('• Review CLAUDE.md for project-specific instructions'));
|
|
114
|
+
console.log(chalk.gray('• Customize .claude/settings.json if needed'));
|
|
115
|
+
console.log(chalk.gray('• Add custom commands to .claude/commands/'));
|
|
116
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import {
|
|
4
|
+
getTemplatesPath,
|
|
5
|
+
createContentHash,
|
|
6
|
+
contentContains,
|
|
7
|
+
readTemplate,
|
|
8
|
+
getFilesInDirectory
|
|
9
|
+
} from './utils.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Handle CLAUDE.md file creation or updating
|
|
13
|
+
*/
|
|
14
|
+
export async function handleClaudeMarkdown(targetDir) {
|
|
15
|
+
const claudeFile = path.join(targetDir, 'CLAUDE.md');
|
|
16
|
+
const templateContent = await readTemplate('CLAUDE.md');
|
|
17
|
+
const templateHash = createContentHash(templateContent);
|
|
18
|
+
|
|
19
|
+
if (!(await fs.pathExists(claudeFile))) {
|
|
20
|
+
// File doesn't exist, create it
|
|
21
|
+
await fs.writeFile(claudeFile, templateContent);
|
|
22
|
+
return { action: 'created', details: 'Created new CLAUDE.md' };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// File exists, check if it contains our template content
|
|
26
|
+
const existingContent = await fs.readFile(claudeFile, 'utf8');
|
|
27
|
+
const existingHash = createContentHash(existingContent);
|
|
28
|
+
|
|
29
|
+
// Check if template content is already present
|
|
30
|
+
if (existingHash === templateHash || contentContains(existingContent, templateContent.trim())) {
|
|
31
|
+
return { action: 'skipped', details: 'CLAUDE.md already contains template content' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Append template content under a heading
|
|
35
|
+
const heading = '\\n\\n# Claude Scratchpad Rules\\n\\n';
|
|
36
|
+
const updatedContent = existingContent + heading + templateContent;
|
|
37
|
+
await fs.writeFile(claudeFile, updatedContent);
|
|
38
|
+
|
|
39
|
+
return { action: 'updated', details: 'Appended template content to existing CLAUDE.md' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Handle directory mirroring (create if not exists, skip if exists)
|
|
44
|
+
*/
|
|
45
|
+
export async function handleDirectoryMirror(targetDir, relativePath) {
|
|
46
|
+
const targetPath = path.join(targetDir, relativePath);
|
|
47
|
+
const templatePath = path.join(getTemplatesPath(), relativePath);
|
|
48
|
+
|
|
49
|
+
if (await fs.pathExists(targetPath)) {
|
|
50
|
+
return { action: 'skipped', details: `Directory ${relativePath} already exists` };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Copy entire directory
|
|
54
|
+
await fs.copy(templatePath, targetPath);
|
|
55
|
+
return { action: 'created', details: `Created ${relativePath} directory` };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Handle selective file copying (add missing files only)
|
|
60
|
+
*/
|
|
61
|
+
export async function handleSelectiveFileCopy(targetDir, relativePath) {
|
|
62
|
+
const targetPath = path.join(targetDir, relativePath);
|
|
63
|
+
const templatePath = path.join(getTemplatesPath(), relativePath);
|
|
64
|
+
|
|
65
|
+
if (!(await fs.pathExists(targetPath))) {
|
|
66
|
+
// Directory doesn't exist, create it with all template files
|
|
67
|
+
await fs.copy(templatePath, targetPath);
|
|
68
|
+
const files = await getFilesInDirectory(templatePath);
|
|
69
|
+
return {
|
|
70
|
+
action: 'created',
|
|
71
|
+
details: `Created ${relativePath} with ${files.length} files`,
|
|
72
|
+
filesAdded: files.length
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Directory exists, check for missing files
|
|
77
|
+
const templateFiles = await getFilesInDirectory(templatePath);
|
|
78
|
+
const targetFiles = await getFilesInDirectory(targetPath);
|
|
79
|
+
const missingFiles = templateFiles.filter(file => !targetFiles.includes(file));
|
|
80
|
+
|
|
81
|
+
if (missingFiles.length === 0) {
|
|
82
|
+
return {
|
|
83
|
+
action: 'skipped',
|
|
84
|
+
details: `All files in ${relativePath} are up to date`,
|
|
85
|
+
filesAdded: 0
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Copy missing files
|
|
90
|
+
let copiedCount = 0;
|
|
91
|
+
for (const missingFile of missingFiles) {
|
|
92
|
+
const sourcePath = path.join(templatePath, missingFile);
|
|
93
|
+
const destPath = path.join(targetPath, missingFile);
|
|
94
|
+
|
|
95
|
+
// Ensure destination directory exists
|
|
96
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
97
|
+
await fs.copy(sourcePath, destPath);
|
|
98
|
+
copiedCount++;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
action: 'updated',
|
|
103
|
+
details: `Added ${copiedCount} missing files to ${relativePath}`,
|
|
104
|
+
filesAdded: copiedCount
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Handle single file copying (create if not exists, skip if exists)
|
|
110
|
+
*/
|
|
111
|
+
export async function handleSingleFile(targetDir, relativePath) {
|
|
112
|
+
const targetPath = path.join(targetDir, relativePath);
|
|
113
|
+
const templatePath = path.join(getTemplatesPath(), relativePath);
|
|
114
|
+
|
|
115
|
+
if (await fs.pathExists(targetPath)) {
|
|
116
|
+
return { action: 'skipped', details: `File ${relativePath} already exists` };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Ensure target directory exists
|
|
120
|
+
await fs.ensureDir(path.dirname(targetPath));
|
|
121
|
+
await fs.copy(templatePath, targetPath);
|
|
122
|
+
|
|
123
|
+
return { action: 'created', details: `Created ${relativePath}` };
|
|
124
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get the absolute path to the templates directory
|
|
11
|
+
*/
|
|
12
|
+
export function getTemplatesPath() {
|
|
13
|
+
// return path.join(__dirname, '..', 'templates');
|
|
14
|
+
return path.join(__dirname, '..');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a hash of file content for comparison
|
|
19
|
+
*/
|
|
20
|
+
export function createContentHash(content) {
|
|
21
|
+
return createHash('md5').update(content.trim()).digest('hex');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if content contains a specific text (case-insensitive)
|
|
26
|
+
*/
|
|
27
|
+
export function contentContains(content, searchText) {
|
|
28
|
+
return content.toLowerCase().includes(searchText.toLowerCase());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Read a template file
|
|
33
|
+
*/
|
|
34
|
+
export async function readTemplate(relativePath) {
|
|
35
|
+
const templatePath = path.join(getTemplatesPath(), relativePath);
|
|
36
|
+
return await fs.readFile(templatePath, 'utf8');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get all files in a directory recursively
|
|
41
|
+
*/
|
|
42
|
+
export async function getFilesInDirectory(dirPath) {
|
|
43
|
+
const files = [];
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const entries = await fs.readdir(dirPath);
|
|
47
|
+
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
const fullPath = path.join(dirPath, entry);
|
|
50
|
+
const stats = await fs.stat(fullPath);
|
|
51
|
+
|
|
52
|
+
if (stats.isFile()) {
|
|
53
|
+
files.push(entry);
|
|
54
|
+
} else if (stats.isDirectory()) {
|
|
55
|
+
const subFiles = await getFilesInDirectory(fullPath);
|
|
56
|
+
files.push(...subFiles.map(f => path.join(entry, f)));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
// Directory doesn't exist
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return files;
|
|
65
|
+
}
|