
Backup Project
- 3 installs
- 9 repo stars
- Updated August 4, 2026
- steelan9199/wechat-publisher
Helps with ai & agent building tasks.
About
backup-project is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- backup-project
- AI & Agent Building
- AI-coding skill
Backup Project by the numbers
- 3 all-time installs (skills.sh)
- Ranked #13,677 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/steelan9199/wechat-publisher --skill backup-projectAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 9 |
| Last updated | August 4, 2026 |
| Repository | steelan9199/wechat-publisher ↗ |
What it does
Helps with ai & agent building tasks.
Files
通用 JS 项目代码备份
功能概述
自动备份 JS 项目的入口文件及其所有 require() 依赖,创建完整的项目快照。
核心特性
- ✅ 自动发现入口文件:优先读取
package.json#main,其次检测main.js/index.js/app.js - ✅ 自动发现项目根:通过
-d参数指定,或向上查找package.json - ✅ 递归嵌套检测:自动检测多层嵌套依赖(A→B→C→D),使用 BFS 算法
- ✅ 自动编号管理:扫描已有备份文件夹,自动生成递增两位数编号
- ✅ 智能命名:用户提供描述性名称,自动拼接编号前缀(如 "09第一次重构")
- ✅ 循环依赖处理:正确处理 A→B→A 的循环依赖,不中断备份
- ✅ 过滤 npm 包:只匹配相对路径
require("./xxx")/require("../xxx"),自动过滤require("fs")等
环境要求
- Node.js: 14.x 或更高版本
- 脚本位置: 本 skill 目录下的
backup.mjs
用法
node backup.mjs "备份名称" # 使用 cwd 自动发现项目
node backup.mjs "备份名称" -d <项目根目录> # 指定项目根
node backup.mjs "备份名称" -e <入口文件> # 指定入口文件
node backup.mjs # 纯编号备份触发条件
当用户出现以下任一情况时立即调用此 skill:
1. "备份代码"、"备份项目"、"保存当前版本"、"备份代码 名字是xxx" 2. "在修改之前先备份"、"先存个档" 3. "创建快照"、"project-backup" 4. 准备进行重大代码重构前
AI 执行流程
步骤 1:确定项目根目录
获取当前会话的工作目录(项目根),供后续 -d 参数使用。
步骤 2:执行备份命令
使用 RunCommand 工具执行备份脚本,务必带 `-d` 参数指定项目根:
# cwd 可以是任意目录;必须通过 -d 指定项目根
node "<skill目录>/backup.mjs" "备份名称" -d "<项目根目录绝对路径>"关键参数:
blocking: truecommand_type: short_running_processrequires_approval: false
示例:
node "C:\Users\Administrator\.trae-cn\skills\backup-project\backup.mjs" "第一次重构" -d "D:\script\work-sop\...\"步骤 3:验证备份结果
用 LS 工具列出 project-backup/<备份名称>/ 目录,向用户展示备份成功。
脚本核心逻辑 (backup.mjs)
参数解析
-d / --dir 项目根目录(默认: 从 cwd 向上查找 package.json)
-e / --entry 入口文件名(默认: 自动检测)
第一个非选项参数 = 备份描述名称入口文件检测优先级
1. -e 命令行指定的文件 2. package.json 中 main 字段 3. 按序检测 main.js → index.js → app.js
依赖检测
正则:/require\(["'](\.\.?\/[^"']+|[^"']+\.js)["']\)/g
- 匹配
require("./xxx")、require("../xxx")以及require("xxx.js")裸文件名 - 自动跳过
require("fs")/require("lodash")等 npm 包(无后缀、无路径) - 使用 BFS 广度优先搜索,最大深度 10 层
- 依赖文件保留原始相对路径结构到备份目录
编号规则
- 扫描
project-backup/下所有子文件夹 - 提取文件夹名前缀数字,取最大值 +1
- 两位数补零(01, 02, ...)
- 拼接格式:
{编号}{用户名称},如09第一次重构
故障排除
| 问题 | 原因 | 解决 |
|---|---|---|
| 入口文件不存在 | 项目根不对 | 用 -d 明确指定项目根 |
| 遗漏依赖文件 | 使用了动态 require / ESM import | 脚本当前只支持静态 require() |
| 备份目录已存在 | 同名备份 | fs.mkdirSync recursive 模式不报错,文件会被覆盖 |
| 复制失败 | 文件被锁定 | 关闭打开该文件的编辑器 |
#!/usr/bin/env node
/**
* backup.mjs — 通用 JS 项目自动备份脚本
*
* 自动发现项目入口文件,递归检测所有 require 依赖并完整备份到 project-backup/
* 脚本可在任意位置运行,通过 -d 参数或 cwd 定位项目根目录。
*
* 用法:
* node backup.mjs "备份名称" # 使用 cwd 自动发现项目
* node backup.mjs "备份名称" -d <项目根目录> # 指定项目根目录
* node backup.mjs "备份名称" -e <入口文件> # 指定入口文件
* node backup.mjs # 仅用编号
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// ==================== 命令行参数解析 ====================
function parseArgs() {
const args = process.argv.slice(2);
const result = { userName: null, projectRoot: null, entryFile: null };
let i = 0;
while (i < args.length) {
if ((args[i] === "-d" || args[i] === "--dir") && i + 1 < args.length) {
result.projectRoot = path.resolve(args[i + 1]);
i += 2;
} else if ((args[i] === "-e" || args[i] === "--entry") && i + 1 < args.length) {
result.entryFile = args[i + 1];
i += 2;
} else if (!args[i].startsWith("-")) {
result.userName = args[i];
i += 1;
} else {
console.warn(`⚠ 未知参数: ${args[i]}`);
i += 1;
}
}
return result;
}
const cliArgs = parseArgs();
// ==================== 项目根目录自动发现 ====================
function findProjectRoot(startDir) {
let dir = path.resolve(startDir);
for (let i = 0; i < 20; i++) {
if (fs.existsSync(path.join(dir, "package.json"))) return dir;
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
return path.resolve(startDir);
}
const PROJECT_ROOT = cliArgs.projectRoot ? findProjectRoot(cliArgs.projectRoot) : findProjectRoot(process.cwd());
// ==================== 入口文件自动发现 ====================
function findEntryFile() {
if (cliArgs.entryFile) {
const p = path.resolve(PROJECT_ROOT, cliArgs.entryFile);
if (fs.existsSync(p)) return { path: cliArgs.entryFile, source: "命令行指定" };
console.warn(`⚠ 指定的入口文件不存在: ${cliArgs.entryFile}`);
}
const pkgPath = path.join(PROJECT_ROOT, "package.json");
if (fs.existsSync(pkgPath)) {
try {
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
if (pkg.main) {
const p = path.resolve(PROJECT_ROOT, pkg.main);
if (fs.existsSync(p)) return { path: pkg.main, source: "package.json#main" };
}
} catch {}
}
const candidates = [
"main.js",
"index.js",
"app.js"
];
for (const c of candidates) {
if (fs.existsSync(path.join(PROJECT_ROOT, c))) return { path: c, source: "自动检测" };
}
return null;
}
const entryInfo = findEntryFile();
const ENTRY_FILE = entryInfo ? entryInfo.path : null;
const BACKUP_ROOT = path.join(PROJECT_ROOT, "project-backup");
const MAX_DEPTH = 10;
// 匹配相对路径 require: "./xxx", "../xxx", 以及裸 .js 文件名如 "config.js"
// 自动过滤 npm 包(如 require("fs")、require("lodash") 等无后缀/无路径的)
const REQUIRE_REGEX = /require\(["'](\.\.?\/[^"']+|[^"']+\.js)["']\)/g;
// ==================== 核心功能 ====================
function getNextBackupNumber() {
if (!fs.existsSync(BACKUP_ROOT)) return "01";
let maxNum = 0;
const items = fs.readdirSync(BACKUP_ROOT, { withFileTypes: true });
for (const item of items) {
if (item.isDirectory()) {
const match = item.name.match(/^(\d+)/);
if (match) {
const num = parseInt(match[1], 10);
if (num > maxNum) maxNum = num;
}
}
}
return (maxNum + 1).toString().padStart(2, "0");
}
function extractDependencies(filePath) {
const content = fs.readFileSync(filePath, "utf-8");
const lines = content.split("\n").slice(0, 100);
const deps = new Set();
for (const line of lines) {
const matches = [...line.matchAll(REQUIRE_REGEX)];
for (const match of matches) {
deps.add(match[1]);
}
}
return [...deps];
}
function getRecursiveDependencies(entryFileAbs) {
const result = { files: [], tree: new Map(), circular: [] };
const visited = new Set();
const queue = [{ file: entryFileAbs, depth: 0 }];
while (queue.length > 0) {
const { file, depth } = queue.shift();
if (depth > MAX_DEPTH) {
console.log(` ⚠ 达到最大深度 ${MAX_DEPTH}: ${path.relative(PROJECT_ROOT, file)}`);
continue;
}
if (!fs.existsSync(file)) {
console.log(` ⚠ 文件不存在: ${path.relative(PROJECT_ROOT, file)}`);
continue;
}
if (visited.has(file)) continue;
visited.add(file);
result.files.push(file);
const dependencies = extractDependencies(file);
result.tree.set(file, dependencies);
const indent = " ".repeat(depth);
const relPath = path.relative(PROJECT_ROOT, file);
if (depth === 0) {
console.log(`📄 ${relPath} (入口)`);
} else {
console.log(`${indent}├─ ${relPath} (深度:${depth})`);
}
for (const dep of dependencies) {
const depPath = path.resolve(path.dirname(file), dep);
const relDepPath = path.relative(PROJECT_ROOT, depPath);
console.log(`${indent}│ └─ 依赖: ${relDepPath}`);
queue.push({ file: depPath, depth: depth + 1 });
}
}
return result;
}
function createBackupDirectory(backupName) {
const backupPath = path.join(BACKUP_ROOT, backupName);
if (!fs.existsSync(BACKUP_ROOT)) {
fs.mkdirSync(BACKUP_ROOT, { recursive: true });
console.log(`✓ 创建备份根目录: ${BACKUP_ROOT}`);
}
fs.mkdirSync(backupPath, { recursive: true });
console.log(`✓ 创建备份目录: ${backupPath}`);
return backupPath;
}
function copyFile(src, dest) {
fs.copyFileSync(src, dest);
const sizeKB = (fs.statSync(src).size / 1024).toFixed(2);
console.log(` ✓ ${path.relative(PROJECT_ROOT, src)} (${sizeKB} KB)`);
return parseFloat(sizeKB);
}
function printReport(backupName, backupPath, files, copied, skipped, totalSize) {
console.log("");
console.log("✅ 备份完成!");
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
console.log("");
console.log("📋 备份统计:");
console.log(` 📁 位置: ${backupPath}`);
console.log(` 📄 成功: ${copied} 个文件`);
if (skipped.length > 0) {
console.log(` ⚠️ 跳过: ${skipped.length} 个文件`);
for (const file of skipped) {
console.log(` - ${path.relative(PROJECT_ROOT, file)}`);
}
}
console.log(` 💾 总大小: ${totalSize.toFixed(2)} KB`);
console.log("");
console.log("📂 备份内容:");
if (fs.existsSync(backupPath)) {
const items = fs.readdirSync(backupPath, { withFileTypes: true });
for (const item of items) {
if (item.isFile()) {
const fullPath = path.join(backupPath, item.name);
const sizeKB = (fs.statSync(fullPath).size / 1024).toFixed(2);
console.log(` • ${item.name} (${sizeKB} KB)`);
}
}
}
}
// ==================== 主函数 ====================
function main() {
console.log(`🔍 项目根目录: ${PROJECT_ROOT}`);
if (!ENTRY_FILE) {
console.error("❌ 错误: 未找到入口文件!");
console.error(" 请在项目根目录运行,或使用 -e 参数指定入口文件:");
console.error(' node backup.mjs "备份名" -e main.js');
process.exit(1);
}
console.log(`📄 入口文件: ${ENTRY_FILE} (来源: ${entryInfo.source})`);
const nextNum = getNextBackupNumber();
const userName = cliArgs.userName;
const backupName = userName ? `${nextNum}${userName}` : nextNum;
if (userName) {
console.log(`📝 用户指定名称: "${userName}"`);
console.log(`🔢 自动分配编号: ${nextNum}`);
console.log(`📦 最终备份名: ${backupName}`);
} else {
console.log(`🔢 自动生成编号: ${backupName}`);
}
console.log("");
console.log("🔍 步骤1: 递归检测依赖关系...");
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
const entryFilePath = path.join(PROJECT_ROOT, ENTRY_FILE);
const deps = getRecursiveDependencies(entryFilePath);
console.log("");
console.log("📊 检测结果:");
console.log(` 总文件数: ${deps.files.length}`);
if (deps.circular.length > 0) {
console.log(` 循环依赖: ${deps.circular.length} 个`);
for (const c of deps.circular) {
console.log(` ↺ ${path.relative(PROJECT_ROOT, c)}`);
}
}
console.log("");
console.log("📁 步骤2: 创建备份目录...");
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
const backupPath = createBackupDirectory(backupName);
console.log("");
console.log("📦 步骤3: 复制文件...");
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
let copied = 0;
const skipped = [];
let totalSize = 0;
for (const file of deps.files) {
if (fs.existsSync(file)) {
const relPath = path.relative(PROJECT_ROOT, file);
const dest = path.join(backupPath, relPath);
fs.mkdirSync(path.dirname(dest), { recursive: true });
copyFile(file, dest);
totalSize += fs.statSync(file).size / 1024;
copied++;
} else {
skipped.push(file);
console.log(` ⚠ 不存在: ${path.relative(PROJECT_ROOT, file)} (跳过)`);
}
}
printReport(backupName, backupPath, deps.files, copied, skipped, totalSize);
}
main();