From 8cb1fbacab51b400dd9ba9d252c2e0d8d991f896 Mon Sep 17 00:00:00 2001 From: jeffyxu Date: Tue, 3 Mar 2026 14:38:43 +0800 Subject: [PATCH] Initial release: tad v0.1.0 Team AI DevKit CLI for sharing skills, rules, docs, and hooks across team members via TGit. --- .gitignore | 4 + README.md | 80 ++ package-lock.json | 1979 +++++++++++++++++++++++++++++++++ package.json | 38 + src/config.ts | 85 ++ src/doctor.ts | 76 ++ src/hooks.ts | 95 ++ src/index.ts | 94 ++ src/init.ts | 144 +++ src/members.ts | 40 + src/pull.ts | 72 ++ src/push.ts | 103 ++ src/resources/base.ts | 64 ++ src/resources/docs.ts | 44 + src/resources/hooks-config.ts | 111 ++ src/resources/index.ts | 25 + src/resources/instincts.ts | 124 +++ src/resources/rules.ts | 96 ++ src/resources/skills.ts | 88 ++ src/status.ts | 128 +++ src/sync.ts | 21 + src/types.ts | 110 ++ src/utils/fs.ts | 111 ++ src/utils/git.ts | 43 + src/utils/logger.ts | 47 + src/utils/tgit-api.ts | 50 + tsconfig.json | 16 + tsup.config.ts | 15 + 28 files changed, 3903 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/config.ts create mode 100644 src/doctor.ts create mode 100644 src/hooks.ts create mode 100644 src/index.ts create mode 100644 src/init.ts create mode 100644 src/members.ts create mode 100644 src/pull.ts create mode 100644 src/push.ts create mode 100644 src/resources/base.ts create mode 100644 src/resources/docs.ts create mode 100644 src/resources/hooks-config.ts create mode 100644 src/resources/index.ts create mode 100644 src/resources/instincts.ts create mode 100644 src/resources/rules.ts create mode 100644 src/resources/skills.ts create mode 100644 src/status.ts create mode 100644 src/sync.ts create mode 100644 src/types.ts create mode 100644 src/utils/fs.ts create mode 100644 src/utils/git.ts create mode 100644 src/utils/logger.ts create mode 100644 src/utils/tgit-api.ts create mode 100644 tsconfig.json create mode 100644 tsup.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f5d9e557 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.tgz +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 00000000..2a568191 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# tad — Team AI DevKit + +团队 AI 经验共享框架。自动在团队成员之间同步 skills、rules、docs、hooks 等 AI 工具配置。 + +## 安装 + +```bash +# 一行安装 +git clone git@git.woa.com:jeffyxu/tad-cli.git ~/.tad/tad-cli \ + && cd ~/.tad/tad-cli && npm install && npm run build && npm link +``` + +## 前置条件 + +设置 TGit Personal Access Token(需要 `api` 权限): + +```bash +# 获取 token: https://git.woa.com/profile/personal_access_tokens +echo 'export TGIT_TOKEN=your_token_here' >> ~/.bashrc +source ~/.bashrc +``` + +## 快速开始 + +```bash +# 1. 初始化(关联团队仓库、注册成员、注入 hooks) +tad init --repo git@git.woa.com:jeffyxu/tad-team.git + +# 2. 拉取团队资源 +tad pull + +# 3. 推送本地新 skills 到团队 +tad push + +# 4. 查看状态 +tad status +``` + +## 命令 + +| 命令 | 说明 | +|------|------| +| `tad init` | 初始化(TGit 认证、关联仓库、注册成员、注入 hooks) | +| `tad push [--all]` | 推送本地新资源到团队仓库 | +| `tad pull [--silent]` | 拉取团队资源并注入到本地 AI 工具 | +| `tad sync` | 双向同步(push + pull) | +| `tad status` | 查看本地 vs 团队仓库差异 | +| `tad list [type]` | 列出资源(skills\|rules\|hooks\|docs\|instincts) | +| `tad members` | 列出团队成员 | +| `tad doctor` | 诊断配置问题 | + +全局选项: +- `--dry-run` — 预览模式,不做实际变更 +- `--verbose, -v` — 详细输出 + +## 工作原理 + +``` +成员 A 成员 B + 创建 skill / 写规则 同上 + │ │ + ▼ ▼ + tad push tad push + │ │ + └──────► TGit 团队仓库 ◄──────────────┘ + │ + ▼ SessionStart hook → tad pull --silent + 自动拉取到所有成员本地 +``` + +- `tad init` 会自动注入 SessionStart hook,每次启动 AI 工具会话时自动拉取团队最新内容 +- Skills 同步到 `~/.claude/skills/`、`~/.codex/skills/`、`~/.claude-internal/skills/`、`~/.cursor/skills-cursor/` +- Rules 合并到 `~/.claude/CLAUDE.md`(使用标记注释管理) +- Docs 同步到 `~/.tad/docs/` + +## 更新 + +```bash +cd ~/.tad/tad-cli && git pull && npm install && npm run build +``` diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..d003d62e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1979 @@ +{ + "name": "tad-cli", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tad-cli", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "commander": "^12.1.0", + "fs-extra": "^11.2.0", + "gray-matter": "^4.0.3", + "ora": "^8.1.0", + "simple-git": "^3.27.0", + "yaml": "^2.6.0", + "zod": "^3.24.0" + }, + "bin": { + "tad": "dist/index.js" + }, + "devDependencies": { + "@types/fs-extra": "^11.0.4", + "@types/node": "^20.17.0", + "tsup": "^8.3.0", + "typescript": "^5.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.19.35", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", + "integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-git": { + "version": "3.32.3", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.32.3.tgz", + "integrity": "sha512-56a5oxFdWlsGygOXHWrG+xjj5w9ZIt2uQbzqiIGdR/6i5iococ7WQ/bNPzWxCJdEUGUCmyMH0t9zMpRJTaKxmw==", + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..a7490e02 --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "tad-cli", + "version": "0.1.0", + "description": "Team AI DevKit — 团队 AI 经验共享框架", + "type": "module", + "bin": { + "tad": "./dist/index.js" + }, + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "typecheck": "tsc --noEmit", + "prepublishOnly": "npm run build" + }, + "repository": { + "type": "git", + "url": "git@git.woa.com:jeffyxu/tad-cli.git" + }, + "keywords": ["ai", "team", "devkit", "skills", "sharing"], + "author": "jeffyxu", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "commander": "^12.1.0", + "fs-extra": "^11.2.0", + "gray-matter": "^4.0.3", + "ora": "^8.1.0", + "simple-git": "^3.27.0", + "yaml": "^2.6.0", + "zod": "^3.24.0" + }, + "devDependencies": { + "@types/fs-extra": "^11.0.4", + "@types/node": "^20.17.0", + "tsup": "^8.3.0", + "typescript": "^5.7.0" + } +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 00000000..23dda845 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,85 @@ +import YAML from 'yaml'; +import path from 'node:path'; +import { + TadConfigSchema, + LocalConfigSchema, + StateSchema, + TAD_CONFIG_PATH, + TAD_STATE_PATH, + type TadConfig, + type LocalConfig, + type State, +} from './types.js'; +import { readFileSafe, readJson, writeFile, writeJson, expandHome } from './utils/fs.js'; +import { log } from './utils/logger.js'; + +/** + * Load the team config (tad.yaml) from the team repo + */ +export async function loadTeamConfig(repoPath: string): Promise { + const content = await readFileSafe(path.join(repoPath, 'tad.yaml')); + if (!content) { + log.debug('tad.yaml not found in repo'); + return null; + } + try { + const raw = YAML.parse(content); + return TadConfigSchema.parse(raw); + } catch (e) { + log.error(`Invalid tad.yaml: ${(e as Error).message}`); + return null; + } +} + +/** + * Load the local config (~/.tad/config.yaml) + */ +export async function loadLocalConfig(): Promise { + const content = await readFileSafe(expandHome(TAD_CONFIG_PATH)); + if (!content) return null; + try { + const raw = YAML.parse(content); + return LocalConfigSchema.parse(raw); + } catch (e) { + log.debug(`Invalid local config: ${(e as Error).message}`); + return null; + } +} + +/** + * Save the local config + */ +export async function saveLocalConfig(config: LocalConfig): Promise { + await writeFile(expandHome(TAD_CONFIG_PATH), YAML.stringify(config)); +} + +/** + * Load the local state (~/.tad/state.json) + */ +export async function loadState(): Promise { + const raw = await readJson>(expandHome(TAD_STATE_PATH)); + if (!raw) return StateSchema.parse({}); + return StateSchema.parse(raw); +} + +/** + * Save the local state + */ +export async function saveState(state: State): Promise { + await writeJson(expandHome(TAD_STATE_PATH), state); +} + +/** + * Require that tad is initialized (local config exists) + */ +export async function requireInit(): Promise<{ localConfig: LocalConfig; teamConfig: TadConfig }> { + const localConfig = await loadLocalConfig(); + if (!localConfig) { + throw new Error('tad is not initialized. Run `tad init` first.'); + } + const teamConfig = await loadTeamConfig(localConfig.repo.localPath); + if (!teamConfig) { + throw new Error('Team config (tad.yaml) not found. Check your repo path.'); + } + return { localConfig, teamConfig }; +} diff --git a/src/doctor.ts b/src/doctor.ts new file mode 100644 index 00000000..8793aa6a --- /dev/null +++ b/src/doctor.ts @@ -0,0 +1,76 @@ +import path from 'node:path'; +import { loadLocalConfig, loadTeamConfig } from './config.js'; +import { pathExists } from './utils/fs.js'; +import { log } from './utils/logger.js'; +import type { GlobalOptions } from './types.js'; + +interface Check { + name: string; + check: () => Promise; + fix?: string; +} + +export async function doctor(options: GlobalOptions): Promise { + log.info('Running diagnostics...\n'); + const localConfig = await loadLocalConfig(); + + const checks: Check[] = [ + { + name: 'TGIT_TOKEN is set', + check: async () => !!process.env.TGIT_TOKEN, + fix: 'Set TGIT_TOKEN env var. Get a token from https://git.woa.com/profile/personal_access_tokens', + }, + { + name: 'Local config exists (~/.tad/config.yaml)', + check: async () => localConfig !== null, + fix: 'Run `tad init` to initialize', + }, + { + name: 'Team repo exists locally', + check: async () => { + if (!localConfig) return false; + return pathExists(localConfig.repo.localPath); + }, + fix: 'Run `tad init` to clone the team repo', + }, + { + name: 'Team config (tad.yaml) is valid', + check: async () => { + if (!localConfig) return false; + const config = await loadTeamConfig(localConfig.repo.localPath); + return config !== null; + }, + fix: 'Check tad.yaml in team repo for syntax errors', + }, + { + name: 'tad hook in claude settings', + check: async () => { + const settingsPath = path.join(process.env.HOME ?? '', '.claude', 'settings.json'); + if (!await pathExists(settingsPath)) return false; + const { readFileSafe } = await import('./utils/fs.js'); + const content = await readFileSafe(settingsPath); + return content?.includes('[tad]') ?? false; + }, + fix: 'Run `tad init` to inject hooks', + }, + ]; + + let allPassed = true; + for (const { name, check, fix } of checks) { + const ok = await check(); + if (ok) { + console.log(` ✔ ${name}`); + } else { + console.log(` ✖ ${name}`); + if (fix) console.log(` → ${fix}`); + allPassed = false; + } + } + + console.log(''); + if (allPassed) { + log.success('All checks passed!'); + } else { + log.warn('Some checks failed. See suggestions above.'); + } +} diff --git a/src/hooks.ts b/src/hooks.ts new file mode 100644 index 00000000..b7904ab1 --- /dev/null +++ b/src/hooks.ts @@ -0,0 +1,95 @@ +import path from 'node:path'; +import { readJson, writeJson, readFileSafe, expandHome } from './utils/fs.js'; +import { log } from './utils/logger.js'; +import { TAD_HOOK_DESCRIPTION_PREFIX } from './types.js'; + +interface HookEntry { + type: string; + command: string; +} + +interface HookMatcher { + matcher: string; + hooks: HookEntry[]; + description?: string; +} + +interface SettingsJson { + hooks?: Record; + [key: string]: unknown; +} + +const TAD_SESSION_START_HOOK: HookMatcher = { + matcher: '*', + hooks: [{ type: 'command', command: 'tad pull --silent' }], + description: `${TAD_HOOK_DESCRIPTION_PREFIX} Auto-pull team resources on session start`, +}; + +/** + * Inject tad hooks into a settings.json file + */ +export async function injectHooks(settingsPath: string): Promise { + const expanded = expandHome(settingsPath); + const settings: SettingsJson = (await readJson(expanded)) ?? {}; + + if (!settings.hooks) { + settings.hooks = {}; + } + if (!settings.hooks.SessionStart) { + settings.hooks.SessionStart = []; + } + + // Check if tad hook already exists + const existing = settings.hooks.SessionStart.find( + (h) => h.description?.startsWith(TAD_HOOK_DESCRIPTION_PREFIX) + ); + if (existing) { + log.debug(`tad hook already exists in ${settingsPath}`); + return; + } + + settings.hooks.SessionStart.push(TAD_SESSION_START_HOOK); + await writeJson(expanded, settings); + log.success(`Injected tad hook into ${settingsPath}`); +} + +/** + * Remove tad hooks from a settings.json file + */ +export async function removeHooks(settingsPath: string): Promise { + const expanded = expandHome(settingsPath); + const settings = await readJson(expanded); + if (!settings?.hooks) return; + + let changed = false; + for (const [event, matchers] of Object.entries(settings.hooks)) { + const filtered = matchers.filter( + (h) => !h.description?.startsWith(TAD_HOOK_DESCRIPTION_PREFIX) + ); + if (filtered.length !== matchers.length) { + settings.hooks[event] = filtered; + changed = true; + } + } + + if (changed) { + await writeJson(expanded, settings); + log.success(`Removed tad hooks from ${settingsPath}`); + } +} + +/** + * Inject tad hooks into all AI tool settings + */ +export async function injectHooksToAllTools(toolPaths: Record): Promise { + for (const [tool, paths] of Object.entries(toolPaths)) { + if (paths.settings) { + const settingsPath = path.join(process.env.HOME ?? '', paths.settings); + try { + await injectHooks(settingsPath); + } catch (e) { + log.warn(`Failed to inject hook into ${tool}: ${(e as Error).message}`); + } + } + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..44877257 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,94 @@ +import { Command } from 'commander'; +import { setVerbose, setSilent } from './utils/logger.js'; +import type { GlobalOptions } from './types.js'; + +const program = new Command(); + +program + .name('tad') + .description('Team AI DevKit — 团队 AI 经验共享框架') + .version('0.1.0') + .option('--dry-run', 'Preview mode, no changes made') + .option('-v, --verbose', 'Verbose output') + .hook('preAction', (thisCommand) => { + const opts = thisCommand.opts(); + if (opts.verbose) setVerbose(true); + }); + +program + .command('init') + .description('Initialize tad (configure TGit, clone repo, register member)') + .option('--repo ', 'Team repo URL') + .action(async (cmdOpts) => { + const globalOpts = program.opts() as GlobalOptions; + const { init } = await import('./init.js'); + await init({ ...globalOpts, ...cmdOpts }); + }); + +program + .command('push') + .description('Push local resources to team repo') + .option('--all', 'Push all without confirmation') + .action(async (cmdOpts) => { + const globalOpts = program.opts() as GlobalOptions; + const { push } = await import('./push.js'); + await push({ ...globalOpts, ...cmdOpts }); + }); + +program + .command('pull') + .description('Pull team resources and inject into local AI tools') + .option('--silent', 'Silent mode (for hooks)') + .action(async (cmdOpts) => { + const globalOpts = program.opts() as GlobalOptions; + if (cmdOpts.silent) setSilent(true); + const { pull } = await import('./pull.js'); + await pull({ ...globalOpts, ...cmdOpts }); + }); + +program + .command('sync') + .description('Bidirectional sync (push + pull)') + .action(async () => { + const globalOpts = program.opts() as GlobalOptions; + const { sync } = await import('./sync.js'); + await sync(globalOpts); + }); + +program + .command('status') + .description('Show local vs team repo diff') + .action(async () => { + const globalOpts = program.opts() as GlobalOptions; + const { status } = await import('./status.js'); + await status(globalOpts); + }); + +program + .command('list [type]') + .description('List resources (skills|rules|hooks|docs|instincts)') + .action(async (type) => { + const globalOpts = program.opts() as GlobalOptions; + const { list } = await import('./status.js'); + await list(type, globalOpts); + }); + +program + .command('members') + .description('List team members') + .action(async () => { + const globalOpts = program.opts() as GlobalOptions; + const { listMembers } = await import('./members.js'); + await listMembers(globalOpts); + }); + +program + .command('doctor') + .description('Diagnose configuration issues') + .action(async () => { + const globalOpts = program.opts() as GlobalOptions; + const { doctor } = await import('./doctor.js'); + await doctor(globalOpts); + }); + +program.parse(); diff --git a/src/init.ts b/src/init.ts new file mode 100644 index 00000000..5eb27deb --- /dev/null +++ b/src/init.ts @@ -0,0 +1,144 @@ +import YAML from 'yaml'; +import path from 'node:path'; +import readline from 'node:readline'; +import { saveLocalConfig, loadTeamConfig } from './config.js'; +import { injectHooksToAllTools } from './hooks.js'; +import { cloneRepo } from './utils/git.js'; +import { pushRepo } from './utils/git.js'; +import { verifyToken, getCurrentUser } from './utils/tgit-api.js'; +import { ensureDir, writeFile, pathExists, expandHome } from './utils/fs.js'; +import { log, spinner } from './utils/logger.js'; +import { TAD_HOME, type GlobalOptions, type LocalConfig } from './types.js'; + +function askQuestion(prompt: string): Promise { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + return new Promise((resolve) => { + rl.question(prompt, (answer) => { + rl.close(); + resolve(answer.trim()); + }); + }); +} + +export async function init(options: GlobalOptions & { repo?: string }): Promise { + log.info('Initializing tad...'); + + // Step 1: Verify TGit token + const spin = spinner('Verifying TGit token...').start(); + let user; + try { + user = await verifyToken(); + spin.succeed(`Authenticated as ${user.username} (${user.name})`); + } catch (e) { + spin.fail((e as Error).message); + log.info('Set TGIT_TOKEN environment variable with a personal access token from:'); + log.info('https://git.woa.com/profile/personal_access_tokens'); + process.exit(1); + } + + // Step 2: Get repo URL + let repoUrl = options.repo ?? ''; + if (!repoUrl) { + repoUrl = await askQuestion('Team repo URL (e.g. git@git.woa.com:team/tad-team.git): '); + } + if (!repoUrl) { + log.error('Repo URL is required'); + process.exit(1); + } + + // Step 3: Clone or link repo + const defaultLocalPath = path.join(process.env.HOME ?? '', '.tad', 'team-repo'); + let localPath = await askQuestion(`Local clone path [${defaultLocalPath}]: `); + if (!localPath) localPath = defaultLocalPath; + localPath = expandHome(localPath); + + if (await pathExists(localPath)) { + log.info(`Repo already exists at ${localPath}, using existing clone`); + } else { + const cloneSpin = spinner('Cloning team repo...').start(); + try { + await cloneRepo(repoUrl, localPath); + cloneSpin.succeed('Team repo cloned'); + } catch (e) { + cloneSpin.fail(`Clone failed: ${(e as Error).message}`); + process.exit(1); + } + } + + // Step 4: Load team config + const teamConfig = await loadTeamConfig(localPath); + if (!teamConfig) { + log.warn('tad.yaml not found in repo. Creating default config...'); + const defaultConfig = YAML.stringify({ + team: 'my-team', + description: 'Team AI DevKit shared resources', + repo: repoUrl, + sharing: { + skills: { syncTargets: ['claude', 'codex', 'claude-internal', 'cursor'] }, + rules: { enforced: [] }, + docs: { localDir: '~/.tad/docs' }, + }, + }); + await writeFile(path.join(localPath, 'tad.yaml'), defaultConfig); + + // Create standard directories + for (const dir of ['members', 'skills', 'rules', 'docs', 'hooks', 'hooks/scripts', 'instincts']) { + await ensureDir(path.join(localPath, dir)); + // create .gitkeep in empty dirs + const gitkeep = path.join(localPath, dir, '.gitkeep'); + if (!await pathExists(gitkeep)) { + await writeFile(gitkeep, ''); + } + } + } + + // Step 5: Create member file + const memberPath = path.join(localPath, 'members', `${user.username}.yaml`); + if (!await pathExists(memberPath)) { + const memberYaml = YAML.stringify({ + username: user.username, + displayName: user.name || user.username, + registeredAt: new Date().toISOString(), + }); + await writeFile(memberPath, memberYaml); + log.success(`Registered as team member: ${user.username}`); + + if (!options.dryRun) { + try { + await pushRepo(localPath, `[tad] Register member: ${user.username}`, [ + 'members/', + 'tad.yaml', + 'skills/.gitkeep', + 'rules/.gitkeep', + 'docs/.gitkeep', + 'hooks/.gitkeep', + 'hooks/scripts/.gitkeep', + 'instincts/.gitkeep', + ]); + log.success('Member registration pushed to team repo'); + } catch (e) { + log.warn(`Push failed (you can push manually later): ${(e as Error).message}`); + } + } + } else { + log.info(`Member ${user.username} already registered`); + } + + // Step 6: Save local config + const localConfig: LocalConfig = { + repo: { localPath, remote: repoUrl }, + username: user.username, + }; + await ensureDir(TAD_HOME); + await saveLocalConfig(localConfig); + log.success(`Local config saved to ${TAD_HOME}/config.yaml`); + + // Step 7: Inject hooks into AI tools + const reloadedTeamConfig = await loadTeamConfig(localPath); + if (reloadedTeamConfig) { + await injectHooksToAllTools(reloadedTeamConfig.toolPaths); + } + + log.success('tad initialized successfully!'); + log.info('Run `tad pull` to sync team resources, or `tad status` to check.'); +} diff --git a/src/members.ts b/src/members.ts new file mode 100644 index 00000000..e7fe3a0f --- /dev/null +++ b/src/members.ts @@ -0,0 +1,40 @@ +import YAML from 'yaml'; +import path from 'node:path'; +import { requireInit } from './config.js'; +import { readFileSafe, listFiles } from './utils/fs.js'; +import { log } from './utils/logger.js'; +import type { GlobalOptions, MemberConfig } from './types.js'; + +export async function listMembers(options: GlobalOptions): Promise { + const { localConfig } = await requireInit(); + const membersDir = path.join(localConfig.repo.localPath, 'members'); + const files = await listFiles(membersDir); + const yamlFiles = files.filter((f) => f.endsWith('.yaml') || f.endsWith('.yml')); + + if (yamlFiles.length === 0) { + log.info('No team members registered'); + return; + } + + console.log(''); + console.log(`Team members (${yamlFiles.length}):`); + console.log(''); + + for (const file of yamlFiles) { + const content = await readFileSafe(path.join(membersDir, file)); + if (!content) continue; + try { + const member = YAML.parse(content) as MemberConfig; + const isSelf = member.username === localConfig.username; + const marker = isSelf ? ' (you)' : ''; + const display = member.displayName ? ` — ${member.displayName}` : ''; + console.log(` ${member.username}${display}${marker}`); + if (options.verbose) { + console.log(` registered: ${member.registeredAt}`); + } + } catch { + log.warn(`Invalid member file: ${file}`); + } + } + console.log(''); +} diff --git a/src/pull.ts b/src/pull.ts new file mode 100644 index 00000000..2dcfbd6e --- /dev/null +++ b/src/pull.ts @@ -0,0 +1,72 @@ +import { requireInit, loadState, saveState } from './config.js'; +import { pullRepo } from './utils/git.js'; +import { log, spinner } from './utils/logger.js'; +import { getHandler, RulesHandler } from './resources/index.js'; +import type { GlobalOptions, ResourceType } from './types.js'; + +export async function pull(options: GlobalOptions): Promise { + const { localConfig, teamConfig } = await requireInit(); + + // Step 1: git pull + const pullSpin = spinner('Pulling team repo...').start(); + try { + const result = await pullRepo(localConfig.repo.localPath); + pullSpin.succeed(`Team repo: ${result}`); + } catch (e) { + pullSpin.fail(`Pull failed: ${(e as Error).message}`); + return; + } + + // Reload team config after pull (might have changed) + const { teamConfig: freshConfig } = await requireInit(); + + // Step 2: Sync each resource type + const resourceTypes: ResourceType[] = ['skills', 'rules', 'hooks', 'docs', 'instincts']; + let totalSynced = 0; + + for (const type of resourceTypes) { + const handler = getHandler(type); + + if (type === 'rules') { + // Rules use bulk merge into CLAUDE.md + const rulesHandler = handler as RulesHandler; + const items = await rulesHandler.scanTeamForPull(freshConfig, localConfig); + if (items.length > 0) { + if (options.dryRun) { + log.info(`[dry-run] Would merge ${items.length} rule(s) into CLAUDE.md`); + } else { + await rulesHandler.pullAllRules(freshConfig, localConfig); + log.success(`Merged ${items.length} rule(s) into CLAUDE.md`); + } + totalSynced += items.length; + } + continue; + } + + const items = await handler.scanTeamForPull(freshConfig, localConfig); + if (items.length === 0) continue; + + if (options.dryRun) { + log.info(`[dry-run] Would pull ${items.length} ${type}`); + for (const item of items) { + log.dim(` ${item.name}`); + } + } else { + for (const item of items) { + await handler.pullItem(item, freshConfig, localConfig); + } + log.success(`Synced ${items.length} ${type}`); + } + + totalSynced += items.length; + } + + if (totalSynced === 0) { + log.info('No resources to sync'); + } else if (!options.dryRun) { + // Update state + const state = await loadState(); + state.lastPull = new Date().toISOString(); + await saveState(state); + } +} diff --git a/src/push.ts b/src/push.ts new file mode 100644 index 00000000..2d313e71 --- /dev/null +++ b/src/push.ts @@ -0,0 +1,103 @@ +import readline from 'node:readline'; +import { requireInit, loadState, saveState } from './config.js'; +import { pushRepo } from './utils/git.js'; +import { log, spinner } from './utils/logger.js'; +import { getHandler } from './resources/index.js'; +import type { GlobalOptions, ResourceItem, ResourceType } from './types.js'; + +function askConfirm(prompt: string): Promise { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + return new Promise((resolve) => { + rl.question(`${prompt} [Y/n] `, (answer) => { + rl.close(); + resolve(!answer || answer.toLowerCase() === 'y'); + }); + }); +} + +export async function push(options: GlobalOptions & { all?: boolean }): Promise { + const { localConfig, teamConfig } = await requireInit(); + + const spin = spinner('Scanning local resources...').start(); + + // Scan for pushable resources + const pushableTypes: ResourceType[] = ['skills', 'instincts']; + const allItems: ResourceItem[] = []; + + for (const type of pushableTypes) { + const handler = getHandler(type); + const items = await handler.scanLocalForPush(teamConfig, localConfig); + allItems.push(...items); + } + + spin.stop(); + + if (allItems.length === 0) { + log.info('No new resources to push'); + return; + } + + // Display items + console.log(''); + console.log(`Found ${allItems.length} new resource(s) to push:`); + console.log(''); + for (const item of allItems) { + console.log(` [${item.type}] ${item.name}`); + if (options.verbose) { + console.log(` from: ${item.sourcePath}`); + } + } + console.log(''); + + if (options.dryRun) { + log.info('Dry run — no changes made'); + return; + } + + // Confirm + if (!options.all && !options.silent) { + const confirmed = await askConfirm('Push these resources to team repo?'); + if (!confirmed) { + log.info('Cancelled'); + return; + } + } + + // Push each item + const pushSpin = spinner('Pushing resources...').start(); + const pushedFiles: string[] = []; + + for (const item of allItems) { + const handler = getHandler(item.type); + await handler.pushItem(item, teamConfig, localConfig); + pushedFiles.push(item.relativePath); + } + + // Git commit and push + try { + // Add all files under the resource directories + const gitFiles = ['skills/', 'instincts/']; + await pushRepo( + localConfig.repo.localPath, + `[tad] Push ${allItems.length} resource(s) from ${localConfig.username}`, + gitFiles, + ); + pushSpin.succeed(`Pushed ${allItems.length} resource(s) to team repo`); + } catch (e) { + pushSpin.fail(`Push failed: ${(e as Error).message}`); + return; + } + + // Update state + const state = await loadState(); + state.lastPush = new Date().toISOString(); + for (const item of allItems) { + if (item.type === 'skills' && !state.pushedSkills.includes(item.name)) { + state.pushedSkills.push(item.name); + } + if (item.type === 'instincts' && !state.pushedInstincts.includes(item.name)) { + state.pushedInstincts.push(item.name); + } + } + await saveState(state); +} diff --git a/src/resources/base.ts b/src/resources/base.ts new file mode 100644 index 00000000..5e3c8dc9 --- /dev/null +++ b/src/resources/base.ts @@ -0,0 +1,64 @@ +import type { ResourceType, ResourceItem, ResourceDiff, TadConfig, LocalConfig } from '../types.js'; + +/** + * Abstract base class for resource handlers. + * Each resource type (skills, rules, hooks, docs, instincts) implements this. + */ +export abstract class ResourceHandler { + abstract readonly type: ResourceType; + + /** + * Scan local sources for items that could be pushed to the team repo. + * Returns items found locally that are not yet in the team repo. + */ + abstract scanLocalForPush( + teamConfig: TadConfig, + localConfig: LocalConfig, + ): Promise; + + /** + * Scan team repo for items that should be pulled to local. + * Returns items from the team repo. + */ + abstract scanTeamForPull( + teamConfig: TadConfig, + localConfig: LocalConfig, + ): Promise; + + /** + * Copy a resource item from local to the team repo directory. + */ + abstract pushItem( + item: ResourceItem, + teamConfig: TadConfig, + localConfig: LocalConfig, + ): Promise; + + /** + * Pull a resource item from the team repo and inject into local AI tool directories. + */ + abstract pullItem( + item: ResourceItem, + teamConfig: TadConfig, + localConfig: LocalConfig, + ): Promise; + + /** + * Compute diff between local and team repo for this resource type. + */ + async diff( + teamConfig: TadConfig, + localConfig: LocalConfig, + ): Promise { + const localItems = await this.scanLocalForPush(teamConfig, localConfig); + const teamItems = await this.scanTeamForPull(teamConfig, localConfig); + + const teamNames = new Set(teamItems.map((i) => i.name)); + const localNames = new Set(localItems.map((i) => i.name)); + + const added = localItems.filter((i) => !teamNames.has(i.name)); + const removed = teamItems.filter((i) => !localNames.has(i.name)); + + return { added, modified: [], removed }; + } +} diff --git a/src/resources/docs.ts b/src/resources/docs.ts new file mode 100644 index 00000000..9fd5280b --- /dev/null +++ b/src/resources/docs.ts @@ -0,0 +1,44 @@ +import path from 'node:path'; +import fse from 'fs-extra'; +import { ResourceHandler } from './base.js'; +import type { ResourceItem, TadConfig, LocalConfig } from '../types.js'; +import { pathExists, copyDir, expandHome } from '../utils/fs.js'; +import { log } from '../utils/logger.js'; + +export class DocsHandler extends ResourceHandler { + readonly type = 'docs' as const; + + async scanLocalForPush(_teamConfig: TadConfig, _localConfig: LocalConfig): Promise { + // Docs are managed directly in team repo + return []; + } + + async scanTeamForPull(_teamConfig: TadConfig, localConfig: LocalConfig): Promise { + const docsDir = path.join(localConfig.repo.localPath, 'docs'); + if (!await pathExists(docsDir)) return []; + + return [{ + name: 'docs', + type: 'docs', + sourcePath: docsDir, + relativePath: 'docs/', + }]; + } + + async pushItem(_item: ResourceItem, _teamConfig: TadConfig, _localConfig: LocalConfig): Promise { + // No-op + } + + /** + * Sync docs from team repo to local docs directory. + */ + async pullItem(item: ResourceItem, teamConfig: TadConfig, _localConfig: LocalConfig): Promise { + const localDocsDir = expandHome(teamConfig.sharing.docs.localDir); + try { + await copyDir(item.sourcePath, localDocsDir); + log.debug(`Synced docs → ${localDocsDir}`); + } catch (e) { + log.warn(`Failed to sync docs: ${(e as Error).message}`); + } + } +} diff --git a/src/resources/hooks-config.ts b/src/resources/hooks-config.ts new file mode 100644 index 00000000..bb6a0279 --- /dev/null +++ b/src/resources/hooks-config.ts @@ -0,0 +1,111 @@ +import path from 'node:path'; +import YAML from 'yaml'; +import { ResourceHandler } from './base.js'; +import type { ResourceItem, TadConfig, LocalConfig } from '../types.js'; +import { pathExists, readFileSafe, readJson, writeJson } from '../utils/fs.js'; +import { log } from '../utils/logger.js'; +import { TAD_HOOK_DESCRIPTION_PREFIX } from '../types.js'; + +interface HookEntry { + type: string; + command: string; +} + +interface HookMatcher { + matcher: string; + hooks: HookEntry[]; + description?: string; +} + +interface HooksYaml { + hooks: Record; +} + +interface SettingsJson { + hooks?: Record; + [key: string]: unknown; +} + +export class HooksConfigHandler extends ResourceHandler { + readonly type = 'hooks' as const; + + async scanLocalForPush(_teamConfig: TadConfig, _localConfig: LocalConfig): Promise { + // Hooks are managed directly in team repo + return []; + } + + async scanTeamForPull(_teamConfig: TadConfig, localConfig: LocalConfig): Promise { + const hooksYamlPath = path.join(localConfig.repo.localPath, 'hooks', 'hooks.yaml'); + if (!await pathExists(hooksYamlPath)) return []; + + return [{ + name: 'hooks.yaml', + type: 'hooks', + sourcePath: hooksYamlPath, + relativePath: 'hooks/hooks.yaml', + }]; + } + + async pushItem(_item: ResourceItem, _teamConfig: TadConfig, _localConfig: LocalConfig): Promise { + // No-op + } + + /** + * Merge team hooks from hooks.yaml into each AI tool's settings.json + */ + async pullItem(item: ResourceItem, teamConfig: TadConfig, _localConfig: LocalConfig): Promise { + const content = await readFileSafe(item.sourcePath); + if (!content) return; + + let hooksConfig: HooksYaml; + try { + hooksConfig = YAML.parse(content) as HooksYaml; + } catch { + log.warn('Invalid hooks.yaml format'); + return; + } + + if (!hooksConfig?.hooks) return; + + for (const [tool, toolPath] of Object.entries(teamConfig.toolPaths)) { + if (!toolPath.settings) continue; + + const settingsPath = path.join(process.env.HOME ?? '', toolPath.settings); + const settings: SettingsJson = (await readJson(settingsPath)) ?? {}; + + if (!settings.hooks) settings.hooks = {}; + + let changed = false; + + for (const [event, matchers] of Object.entries(hooksConfig.hooks)) { + if (!settings.hooks[event]) settings.hooks[event] = []; + + for (const matcher of matchers) { + // Tag with tad prefix for identification + const description = matcher.description + ? `${TAD_HOOK_DESCRIPTION_PREFIX} ${matcher.description}` + : `${TAD_HOOK_DESCRIPTION_PREFIX} Team hook`; + + // Check if this exact hook already exists + const exists = settings.hooks[event].some( + (h) => h.description === description + ); + + if (!exists) { + settings.hooks[event].push({ ...matcher, description }); + changed = true; + } + } + } + + if (changed) { + try { + await writeJson(settingsPath, settings); + log.debug(`Updated hooks in ${tool} settings.json`); + } catch (e) { + log.warn(`Failed to update ${tool} settings: ${(e as Error).message}`); + } + } + } + } +} diff --git a/src/resources/index.ts b/src/resources/index.ts new file mode 100644 index 00000000..1e72170b --- /dev/null +++ b/src/resources/index.ts @@ -0,0 +1,25 @@ +import { ResourceHandler } from './base.js'; +import { SkillsHandler } from './skills.js'; +import { InstinctsHandler } from './instincts.js'; +import { RulesHandler } from './rules.js'; +import { HooksConfigHandler } from './hooks-config.js'; +import { DocsHandler } from './docs.js'; +import type { ResourceType } from '../types.js'; + +const handlers: Record = { + skills: new SkillsHandler(), + instincts: new InstinctsHandler(), + rules: new RulesHandler(), + hooks: new HooksConfigHandler(), + docs: new DocsHandler(), +}; + +export function getHandler(type: ResourceType): ResourceHandler { + return handlers[type]; +} + +export function getAllHandlers(): ResourceHandler[] { + return Object.values(handlers); +} + +export { SkillsHandler, InstinctsHandler, RulesHandler, HooksConfigHandler, DocsHandler }; diff --git a/src/resources/instincts.ts b/src/resources/instincts.ts new file mode 100644 index 00000000..b04ba919 --- /dev/null +++ b/src/resources/instincts.ts @@ -0,0 +1,124 @@ +import path from 'node:path'; +import { ResourceHandler } from './base.js'; +import type { ResourceItem, TadConfig, LocalConfig } from '../types.js'; +import { listDirs, listFiles, pathExists, copyDir, copyFile, expandHome } from '../utils/fs.js'; +import { log } from '../utils/logger.js'; + +const HOMUNCULUS_DIR = '.claude/homunculus'; + +export class InstinctsHandler extends ResourceHandler { + readonly type = 'instincts' as const; + + /** + * Scan CL-v2 homunculus instincts for items to push. + */ + async scanLocalForPush(teamConfig: TadConfig, localConfig: LocalConfig): Promise { + const home = process.env.HOME ?? ''; + const instinctsDir = path.join(home, HOMUNCULUS_DIR, 'instincts', 'personal'); + const teamInstinctsDir = path.join(localConfig.repo.localPath, 'instincts', localConfig.username); + + const items: ResourceItem[] = []; + + if (!await pathExists(instinctsDir)) return items; + + const files = await listFiles(instinctsDir); + const teamFiles = new Set(await pathExists(teamInstinctsDir) ? await listFiles(teamInstinctsDir) : []); + + for (const file of files) { + if (!file.endsWith('.yaml') && !file.endsWith('.yml') && !file.endsWith('.md')) continue; + if (teamFiles.has(file)) continue; + + items.push({ + name: file.replace(/\.(yaml|yml|md)$/, ''), + type: 'instincts', + sourcePath: path.join(instinctsDir, file), + relativePath: `instincts/${localConfig.username}/${file}`, + }); + } + + // Also scan project-scoped instincts + const projectsDir = path.join(home, HOMUNCULUS_DIR, 'projects'); + if (await pathExists(projectsDir)) { + const projectHashes = await listDirs(projectsDir); + for (const hash of projectHashes) { + const projectInstincts = path.join(projectsDir, hash, 'instincts', 'personal'); + if (!await pathExists(projectInstincts)) continue; + + const pFiles = await listFiles(projectInstincts); + for (const file of pFiles) { + if (!file.endsWith('.yaml') && !file.endsWith('.yml') && !file.endsWith('.md')) continue; + const name = `${hash}/${file.replace(/\.(yaml|yml|md)$/, '')}`; + if (teamFiles.has(file)) continue; + + items.push({ + name, + type: 'instincts', + sourcePath: path.join(projectInstincts, file), + relativePath: `instincts/${localConfig.username}/${file}`, + }); + } + } + } + + return items; + } + + /** + * Scan team repo for instincts to pull (from all members). + */ + async scanTeamForPull(teamConfig: TadConfig, localConfig: LocalConfig): Promise { + const teamInstinctsDir = path.join(localConfig.repo.localPath, 'instincts'); + const items: ResourceItem[] = []; + + if (!await pathExists(teamInstinctsDir)) return items; + + const memberDirs = await listDirs(teamInstinctsDir); + for (const member of memberDirs) { + const memberDir = path.join(teamInstinctsDir, member); + const files = await listFiles(memberDir); + for (const file of files) { + if (!file.endsWith('.yaml') && !file.endsWith('.yml') && !file.endsWith('.md')) continue; + items.push({ + name: `${member}/${file.replace(/\.(yaml|yml|md)$/, '')}`, + type: 'instincts', + sourcePath: path.join(memberDir, file), + relativePath: `instincts/${member}/${file}`, + }); + } + } + + return items; + } + + /** + * Copy a local instinct to the team repo under the user's directory. + */ + async pushItem(item: ResourceItem, _teamConfig: TadConfig, localConfig: LocalConfig): Promise { + const dest = path.join(localConfig.repo.localPath, item.relativePath); + await copyFile(item.sourcePath, dest); + log.debug(`Copied instinct ${item.name} → team repo`); + } + + /** + * Pull instincts from team repo to CL-v2 inherited directory. + */ + async pullItem(item: ResourceItem, _teamConfig: TadConfig, _localConfig: LocalConfig): Promise { + const home = process.env.HOME ?? ''; + const inheritedDir = path.join(home, HOMUNCULUS_DIR, 'instincts', 'inherited'); + + // Only import to inherited if homunculus dir exists (CL-v2 installed) + if (!await pathExists(path.join(home, HOMUNCULUS_DIR))) { + log.debug('CL-v2 not installed, skipping instinct import'); + return; + } + + const filename = path.basename(item.sourcePath); + const dest = path.join(inheritedDir, filename); + try { + await copyFile(item.sourcePath, dest); + log.debug(`Imported instinct ${item.name} → inherited/`); + } catch (e) { + log.warn(`Failed to import instinct ${item.name}: ${(e as Error).message}`); + } + } +} diff --git a/src/resources/rules.ts b/src/resources/rules.ts new file mode 100644 index 00000000..79383325 --- /dev/null +++ b/src/resources/rules.ts @@ -0,0 +1,96 @@ +import path from 'node:path'; +import { ResourceHandler } from './base.js'; +import type { ResourceItem, TadConfig, LocalConfig } from '../types.js'; +import { listFiles, pathExists, readFileSafe, writeFile } from '../utils/fs.js'; +import { log } from '../utils/logger.js'; +import { TAD_RULES_START, TAD_RULES_END } from '../types.js'; + +export class RulesHandler extends ResourceHandler { + readonly type = 'rules' as const; + + async scanLocalForPush(_teamConfig: TadConfig, _localConfig: LocalConfig): Promise { + // Rules are typically created directly in the team repo, not pushed from local + return []; + } + + async scanTeamForPull(_teamConfig: TadConfig, localConfig: LocalConfig): Promise { + const rulesDir = path.join(localConfig.repo.localPath, 'rules'); + if (!await pathExists(rulesDir)) return []; + + const files = await listFiles(rulesDir); + return files + .filter((f) => f.endsWith('.md')) + .map((f) => ({ + name: f.replace(/\.md$/, ''), + type: 'rules' as const, + sourcePath: path.join(rulesDir, f), + relativePath: `rules/${f}`, + })); + } + + async pushItem(_item: ResourceItem, _teamConfig: TadConfig, _localConfig: LocalConfig): Promise { + // No-op: rules are managed directly in team repo + } + + /** + * Pull rules from team repo and merge into CLAUDE.md files. + * Uses marker comments to manage tad-injected sections. + */ + async pullItem(item: ResourceItem, teamConfig: TadConfig, _localConfig: LocalConfig): Promise { + // This is handled in bulk by pullAllRules + } + + /** + * Merge all rules into CLAUDE.md for each AI tool that has a claudemd path. + */ + async pullAllRules(teamConfig: TadConfig, localConfig: LocalConfig): Promise { + const rules = await this.scanTeamForPull(teamConfig, localConfig); + if (rules.length === 0) return; + + // Build the merged rules block + const ruleContents: string[] = []; + for (const rule of rules) { + const content = await readFileSafe(rule.sourcePath); + if (content) { + ruleContents.push(`### ${rule.name}\n\n${content.trim()}`); + } + } + + const rulesBlock = [ + TAD_RULES_START, + '', + '', + '## Team Rules (tad)', + '', + ruleContents.join('\n\n---\n\n'), + '', + TAD_RULES_END, + ].join('\n'); + + // Inject into each tool's CLAUDE.md + for (const [tool, toolPath] of Object.entries(teamConfig.toolPaths)) { + if (!toolPath.claudemd) continue; + + const claudeMdPath = path.join(process.env.HOME ?? '', toolPath.claudemd); + + let existing = await readFileSafe(claudeMdPath) ?? ''; + + // Replace existing tad rules block or append + const startIdx = existing.indexOf(TAD_RULES_START); + const endIdx = existing.indexOf(TAD_RULES_END); + + if (startIdx !== -1 && endIdx !== -1) { + existing = existing.substring(0, startIdx) + rulesBlock + existing.substring(endIdx + TAD_RULES_END.length); + } else { + existing = existing.trimEnd() + '\n\n' + rulesBlock + '\n'; + } + + try { + await writeFile(claudeMdPath, existing); + log.debug(`Updated rules in ${tool} CLAUDE.md`); + } catch (e) { + log.warn(`Failed to update ${tool} CLAUDE.md: ${(e as Error).message}`); + } + } + } +} diff --git a/src/resources/skills.ts b/src/resources/skills.ts new file mode 100644 index 00000000..4d7b8839 --- /dev/null +++ b/src/resources/skills.ts @@ -0,0 +1,88 @@ +import path from 'node:path'; +import { ResourceHandler } from './base.js'; +import type { ResourceItem, TadConfig, LocalConfig } from '../types.js'; +import { listDirs, pathExists, copyDir } from '../utils/fs.js'; +import { log } from '../utils/logger.js'; + +export class SkillsHandler extends ResourceHandler { + readonly type = 'skills' as const; + + /** + * Scan local AI tool skill directories for skills not in the team repo. + */ + async scanLocalForPush(teamConfig: TadConfig, localConfig: LocalConfig): Promise { + const teamSkillsDir = path.join(localConfig.repo.localPath, 'skills'); + const teamSkills = new Set(await listDirs(teamSkillsDir)); + + const localSkills: ResourceItem[] = []; + const seen = new Set(); + + // Scan each tool's skills directory + for (const [_tool, toolPath] of Object.entries(teamConfig.toolPaths)) { + const skillsDir = path.join(process.env.HOME ?? '', toolPath.skills); + if (!await pathExists(skillsDir)) continue; + + const dirs = await listDirs(skillsDir); + for (const dir of dirs) { + if (seen.has(dir) || teamSkills.has(dir)) continue; + // Check for SKILL.md to confirm it's a valid skill + const skillMd = path.join(skillsDir, dir, 'SKILL.md'); + if (!await pathExists(skillMd)) continue; + + seen.add(dir); + localSkills.push({ + name: dir, + type: 'skills', + sourcePath: path.join(skillsDir, dir), + relativePath: `skills/${dir}`, + }); + } + } + + return localSkills; + } + + /** + * Scan team repo for skills to pull. + */ + async scanTeamForPull(teamConfig: TadConfig, localConfig: LocalConfig): Promise { + const teamSkillsDir = path.join(localConfig.repo.localPath, 'skills'); + const dirs = await listDirs(teamSkillsDir); + + return dirs.map((dir) => ({ + name: dir, + type: 'skills' as const, + sourcePath: path.join(teamSkillsDir, dir), + relativePath: `skills/${dir}`, + })); + } + + /** + * Copy a local skill to the team repo. + */ + async pushItem(item: ResourceItem, _teamConfig: TadConfig, localConfig: LocalConfig): Promise { + const dest = path.join(localConfig.repo.localPath, 'skills', item.name); + await copyDir(item.sourcePath, dest); + log.debug(`Copied skill ${item.name} → team repo`); + } + + /** + * Pull a skill from team repo to all configured AI tool directories. + */ + async pullItem(item: ResourceItem, teamConfig: TadConfig, _localConfig: LocalConfig): Promise { + const syncTargets = teamConfig.sharing.skills.syncTargets; + + for (const tool of syncTargets) { + const toolPath = teamConfig.toolPaths[tool]; + if (!toolPath) continue; + + const dest = path.join(process.env.HOME ?? '', toolPath.skills, item.name); + try { + await copyDir(item.sourcePath, dest); + log.debug(`Synced skill ${item.name} → ${tool}`); + } catch (e) { + log.warn(`Failed to sync skill ${item.name} to ${tool}: ${(e as Error).message}`); + } + } + } +} diff --git a/src/status.ts b/src/status.ts new file mode 100644 index 00000000..0dd4c809 --- /dev/null +++ b/src/status.ts @@ -0,0 +1,128 @@ +import path from 'node:path'; +import { requireInit, loadState } from './config.js'; +import { getRepoStatus } from './utils/git.js'; +import { log } from './utils/logger.js'; +import { getAllHandlers } from './resources/index.js'; +import { listDirs, listFiles, pathExists } from './utils/fs.js'; +import type { GlobalOptions, ResourceType } from './types.js'; + +export async function status(options: GlobalOptions): Promise { + const { localConfig, teamConfig } = await requireInit(); + + // Git status + console.log(''); + log.info('Team repo status:'); + try { + const gitStatus = await getRepoStatus(localConfig.repo.localPath); + console.log(` repo: ${localConfig.repo.remote}`); + console.log(` local: ${localConfig.repo.localPath}`); + if (gitStatus.ahead > 0) console.log(` ahead: ${gitStatus.ahead} commit(s)`); + if (gitStatus.behind > 0) console.log(` behind: ${gitStatus.behind} commit(s)`); + if (gitStatus.modified.length > 0) { + console.log(` modified: ${gitStatus.modified.length} file(s)`); + } + if (gitStatus.ahead === 0 && gitStatus.behind === 0 && gitStatus.modified.length === 0) { + console.log(' up to date'); + } + } catch (e) { + log.warn(`Could not check git status: ${(e as Error).message}`); + } + + // State + const state = await loadState(); + console.log(''); + log.info('Sync state:'); + console.log(` last push: ${state.lastPush ?? 'never'}`); + console.log(` last pull: ${state.lastPull ?? 'never'}`); + + // Resource counts + console.log(''); + log.info('Team resources:'); + + const repoPath = localConfig.repo.localPath; + const counts: Record = {}; + + // Skills + const skillsDirs = await listDirs(path.join(repoPath, 'skills')); + counts.skills = skillsDirs.length; + + // Rules + const rulesFiles = (await listFiles(path.join(repoPath, 'rules'))).filter(f => f.endsWith('.md')); + counts.rules = rulesFiles.length; + + // Docs + const docsExists = await pathExists(path.join(repoPath, 'docs')); + counts.docs = docsExists ? (await listDirs(path.join(repoPath, 'docs'))).length : 0; + + // Instincts + const instinctsDir = path.join(repoPath, 'instincts'); + let instinctCount = 0; + if (await pathExists(instinctsDir)) { + const memberDirs = await listDirs(instinctsDir); + for (const m of memberDirs) { + const files = await listFiles(path.join(instinctsDir, m)); + instinctCount += files.filter(f => f.endsWith('.yaml') || f.endsWith('.yml') || f.endsWith('.md')).length; + } + } + counts.instincts = instinctCount; + + // Hooks + const hooksYaml = await pathExists(path.join(repoPath, 'hooks', 'hooks.yaml')); + counts.hooks = hooksYaml ? 1 : 0; + + for (const [type, count] of Object.entries(counts)) { + console.log(` ${type}: ${count}`); + } + + // Local pushable items + console.log(''); + log.info('Local resources not yet pushed:'); + let anyNew = false; + for (const handler of getAllHandlers()) { + const items = await handler.scanLocalForPush(teamConfig, localConfig); + if (items.length > 0) { + anyNew = true; + console.log(` [${handler.type}] ${items.length} new`); + if (options.verbose) { + for (const item of items) { + console.log(` - ${item.name}`); + } + } + } + } + if (!anyNew) { + console.log(' (none)'); + } + + console.log(''); +} + +export async function list(type: string | undefined, options: GlobalOptions): Promise { + const { localConfig, teamConfig } = await requireInit(); + const repoPath = localConfig.repo.localPath; + + const types: ResourceType[] = type + ? [type as ResourceType] + : ['skills', 'rules', 'hooks', 'docs', 'instincts']; + + for (const t of types) { + console.log(''); + console.log(`=== ${t.toUpperCase()} ===`); + + const handler = getAllHandlers().find((h) => h.type === t); + if (!handler) continue; + + const items = await handler.scanTeamForPull(teamConfig, localConfig); + if (items.length === 0) { + console.log(' (none)'); + } else { + for (const item of items) { + console.log(` ${item.name}`); + if (options.verbose) { + console.log(` path: ${item.sourcePath}`); + } + } + } + } + console.log(''); +} diff --git a/src/sync.ts b/src/sync.ts new file mode 100644 index 00000000..5911a95f --- /dev/null +++ b/src/sync.ts @@ -0,0 +1,21 @@ +import { push } from './push.js'; +import { pull } from './pull.js'; +import { log } from './utils/logger.js'; +import type { GlobalOptions } from './types.js'; + +export async function sync(options: GlobalOptions): Promise { + log.info('Syncing (push + pull)...\n'); + + // Push first + log.info('--- Push ---'); + await push({ ...options, all: true }); + + console.log(''); + + // Then pull + log.info('--- Pull ---'); + await pull(options); + + console.log(''); + log.success('Sync complete'); +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..f29e779a --- /dev/null +++ b/src/types.ts @@ -0,0 +1,110 @@ +import { z } from 'zod'; + +// ─── Tool path config ─────────────────────────────────── + +export const ToolPathsSchema = z.object({ + skills: z.string(), + settings: z.string().optional(), + claudemd: z.string().optional(), +}); + +// ─── Team config (tad.yaml) ───────────────────────────── + +export const SharingConfigSchema = z.object({ + skills: z.object({ + syncTargets: z.array(z.string()).default(['claude', 'codex', 'claude-internal', 'cursor']), + }).default({}), + rules: z.object({ + enforced: z.array(z.string()).default([]), + }).default({}), + docs: z.object({ + localDir: z.string().default('~/.tad/docs'), + }).default({}), +}); + +export const TadConfigSchema = z.object({ + team: z.string(), + description: z.string().default(''), + repo: z.string(), + sharing: SharingConfigSchema.default({}), + toolPaths: z.record(z.string(), ToolPathsSchema).default({ + claude: { skills: '.claude/skills', settings: '.claude/settings.json', claudemd: '.claude/CLAUDE.md' }, + codex: { skills: '.codex/skills' }, + 'claude-internal': { skills: '.claude-internal/skills', settings: '.claude-internal/settings.json' }, + cursor: { skills: '.cursor/skills-cursor' }, + }), +}); + +export type TadConfig = z.infer; + +// ─── Member config (members/.yaml) ──────────────── + +export const MemberConfigSchema = z.object({ + username: z.string(), + displayName: z.string().default(''), + registeredAt: z.string(), +}); + +export type MemberConfig = z.infer; + +// ─── Local config (~/.tad/config.yaml) ────────────────── + +export const LocalConfigSchema = z.object({ + repo: z.object({ + localPath: z.string(), + remote: z.string(), + }), + username: z.string(), +}); + +export type LocalConfig = z.infer; + +// ─── Local state (~/.tad/state.json) ──────────────────── + +export const StateSchema = z.object({ + lastPush: z.string().nullable().default(null), + lastPull: z.string().nullable().default(null), + pushedInstincts: z.array(z.string()).default([]), + pushedSkills: z.array(z.string()).default([]), +}); + +export type State = z.infer; + +// ─── Resource types ───────────────────────────────────── + +export type ResourceType = 'skills' | 'rules' | 'hooks' | 'docs' | 'instincts'; + +export interface ResourceItem { + name: string; + type: ResourceType; + sourcePath: string; + relativePath: string; +} + +export interface ResourceDiff { + added: ResourceItem[]; + modified: ResourceItem[]; + removed: ResourceItem[]; +} + +// ─── Global options ───────────────────────────────────── + +export interface GlobalOptions { + dryRun?: boolean; + verbose?: boolean; + silent?: boolean; +} + +// ─── Constants ────────────────────────────────────────── + +export const TAD_HOME = `${process.env.HOME}/.tad`; +export const TAD_CONFIG_PATH = `${TAD_HOME}/config.yaml`; +export const TAD_STATE_PATH = `${TAD_HOME}/state.json`; +export const TAD_TOKEN_PATH = `${TAD_HOME}/token`; + +export const RESOURCE_TYPES: ResourceType[] = ['skills', 'rules', 'hooks', 'docs', 'instincts']; + +export const TAD_RULES_START = ''; +export const TAD_RULES_END = ''; + +export const TAD_HOOK_DESCRIPTION_PREFIX = '[tad]'; diff --git a/src/utils/fs.ts b/src/utils/fs.ts new file mode 100644 index 00000000..84b2ce8c --- /dev/null +++ b/src/utils/fs.ts @@ -0,0 +1,111 @@ +import fse from 'fs-extra'; +import path from 'node:path'; +import { log } from './logger.js'; + +/** + * Expand ~ to $HOME in paths + */ +export function expandHome(p: string): string { + if (p.startsWith('~/') || p === '~') { + return path.join(process.env.HOME ?? '', p.slice(1)); + } + return p; +} + +/** + * Ensure a directory exists + */ +export async function ensureDir(dir: string): Promise { + await fse.ensureDir(expandHome(dir)); +} + +/** + * Read a file, return null if not found + */ +export async function readFileSafe(filePath: string): Promise { + try { + return await fse.readFile(expandHome(filePath), 'utf-8'); + } catch { + return null; + } +} + +/** + * Write a file, creating parent dirs as needed + */ +export async function writeFile(filePath: string, content: string): Promise { + const expanded = expandHome(filePath); + await fse.ensureDir(path.dirname(expanded)); + await fse.writeFile(expanded, content, 'utf-8'); +} + +/** + * Read JSON file, return null if not found + */ +export async function readJson(filePath: string): Promise { + const content = await readFileSafe(filePath); + if (content === null) return null; + try { + return JSON.parse(content) as T; + } catch { + log.warn(`Failed to parse JSON: ${filePath}`); + return null; + } +} + +/** + * Write JSON file + */ +export async function writeJson(filePath: string, data: unknown): Promise { + await writeFile(filePath, JSON.stringify(data, null, 2) + '\n'); +} + +/** + * Copy a directory recursively + */ +export async function copyDir(src: string, dest: string): Promise { + await fse.copy(expandHome(src), expandHome(dest), { overwrite: true }); +} + +/** + * Copy a file + */ +export async function copyFile(src: string, dest: string): Promise { + const destExpanded = expandHome(dest); + await fse.ensureDir(path.dirname(destExpanded)); + await fse.copy(expandHome(src), destExpanded, { overwrite: true }); +} + +/** + * List directories in a path (non-recursive, only directories) + */ +export async function listDirs(dirPath: string): Promise { + const expanded = expandHome(dirPath); + if (!await fse.pathExists(expanded)) return []; + const entries = await fse.readdir(expanded, { withFileTypes: true }); + return entries.filter(e => e.isDirectory()).map(e => e.name); +} + +/** + * List files in a path (non-recursive, only files) + */ +export async function listFiles(dirPath: string): Promise { + const expanded = expandHome(dirPath); + if (!await fse.pathExists(expanded)) return []; + const entries = await fse.readdir(expanded, { withFileTypes: true }); + return entries.filter(e => e.isFile()).map(e => e.name); +} + +/** + * Check if a path exists + */ +export async function pathExists(p: string): Promise { + return fse.pathExists(expandHome(p)); +} + +/** + * Remove a file or directory + */ +export async function remove(p: string): Promise { + await fse.remove(expandHome(p)); +} diff --git a/src/utils/git.ts b/src/utils/git.ts new file mode 100644 index 00000000..d20c1dd6 --- /dev/null +++ b/src/utils/git.ts @@ -0,0 +1,43 @@ +import simpleGit, { type SimpleGit } from 'simple-git'; +import { log } from './logger.js'; + +export function createGit(basePath?: string): SimpleGit { + return simpleGit(basePath ? { baseDir: basePath } : undefined); +} + +export async function cloneRepo(remote: string, localPath: string): Promise { + const git = simpleGit(); + await git.clone(remote, localPath); +} + +export async function pullRepo(localPath: string): Promise { + const git = createGit(localPath); + const result = await git.pull(); + if (result.summary.changes === 0 && result.summary.insertions === 0 && result.summary.deletions === 0) { + return 'already up to date'; + } + return `${result.summary.changes} file(s) changed`; +} + +export async function pushRepo(localPath: string, message: string, files: string[]): Promise { + const git = createGit(localPath); + await git.add(files); + const status = await git.status(); + if (status.staged.length === 0) { + log.debug('Nothing to commit'); + return; + } + await git.commit(message); + await git.push(); +} + +export async function getRepoStatus(localPath: string): Promise<{ ahead: number; behind: number; modified: string[] }> { + const git = createGit(localPath); + await git.fetch(); + const status = await git.status(); + return { + ahead: status.ahead, + behind: status.behind, + modified: [...status.modified, ...status.not_added, ...status.created], + }; +} diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 00000000..007e28f4 --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,47 @@ +import chalk from 'chalk'; +import ora, { type Ora } from 'ora'; + +let verboseEnabled = false; +let silentMode = false; + +export function setVerbose(v: boolean): void { + verboseEnabled = v; +} + +export function setSilent(s: boolean): void { + silentMode = s; +} + +export const log = { + info(msg: string): void { + if (silentMode) return; + console.log(chalk.blue('ℹ'), msg); + }, + success(msg: string): void { + if (silentMode) return; + console.log(chalk.green('✔'), msg); + }, + warn(msg: string): void { + if (silentMode) return; + console.log(chalk.yellow('⚠'), msg); + }, + error(msg: string): void { + console.error(chalk.red('✖'), msg); + }, + debug(msg: string): void { + if (!verboseEnabled || silentMode) return; + console.log(chalk.gray(' [debug]'), msg); + }, + dim(msg: string): void { + if (silentMode) return; + console.log(chalk.dim(msg)); + }, +}; + +export function spinner(text: string): Ora { + if (silentMode) { + // return a no-op spinner in silent mode + return ora({ text, isSilent: true }); + } + return ora({ text, color: 'cyan' }); +} diff --git a/src/utils/tgit-api.ts b/src/utils/tgit-api.ts new file mode 100644 index 00000000..3e1b917a --- /dev/null +++ b/src/utils/tgit-api.ts @@ -0,0 +1,50 @@ +import { log } from './logger.js'; + +const TGIT_API_BASE = 'https://git.woa.com/api/v3'; + +export interface TGitUser { + username: string; + name: string; + email: string; +} + +function getToken(): string { + const token = process.env.TGIT_TOKEN; + if (!token) { + throw new Error('TGIT_TOKEN environment variable is not set. Get one from https://git.woa.com/profile/personal_access_tokens'); + } + return token; +} + +async function tgitFetch(path: string, options?: RequestInit): Promise { + const token = getToken(); + // Use query parameter auth for compatibility with all TGit v3 endpoints + const separator = path.includes('?') ? '&' : '?'; + const url = `${TGIT_API_BASE}${path}${separator}private_token=${token}`; + log.debug(`TGit API: ${options?.method ?? 'GET'} ${TGIT_API_BASE}${path}`); + const resp = await fetch(url, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers, + }, + }); + if (!resp.ok) { + const body = await resp.text().catch(() => ''); + throw new Error(`TGit API error ${resp.status}: ${body}`); + } + return resp; +} + +export async function getCurrentUser(): Promise { + const resp = await tgitFetch('/user'); + return resp.json() as Promise; +} + +export async function verifyToken(): Promise { + try { + return await getCurrentUser(); + } catch (e) { + throw new Error(`TGit token verification failed: ${(e as Error).message}`); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..51689a37 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 00000000..6f304adc --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + target: 'node20', + outDir: 'dist', + clean: true, + splitting: false, + sourcemap: true, + dts: false, + banner: { + js: '#!/usr/bin/env node', + }, +});