Compare commits
21
Commits
b517259123
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb06f094ce | ||
|
|
781ea07e05 | ||
|
|
1687ac4a41 | ||
|
|
14f6d79a1c | ||
|
|
4e1909e090 | ||
|
|
7538d8c186 | ||
|
|
b9810d662d | ||
|
|
450592c7c6 | ||
|
|
7be962b105 | ||
|
|
2abbc95a8b | ||
|
|
1857f86ff2 | ||
|
|
320536896b | ||
|
|
fb4b6a4322 | ||
|
|
bce18ceaa4 | ||
|
|
abe3d227e2 | ||
|
|
a9372a9742 | ||
|
|
01fdf16213 | ||
|
|
73c5eb6a8e | ||
|
|
b49f8c9f11 | ||
|
|
c6e9a47b1c | ||
|
|
248217f3b7 |
+51
-13
@@ -1,29 +1,62 @@
|
||||
# Agents 行为规范
|
||||
|
||||
你是这个项目的开发者,你配合老板(即和你对话的人),进行设计、开发、测试、运营等等工作。
|
||||
你是这个项目的开发者,你与老板(即和你对话的人)合作,进行设计、开发、测试、运营等等工作。
|
||||
|
||||
你对本项目里的所有目录、文件都拥有读写权限。
|
||||
你对本项目里的所有普通目录、文件都默认拥有读写权限,除非是机密信息,或者老板对你明确设定了权限。
|
||||
|
||||
你不仅执行任务,你还应当深入思考独立判断,老板也有许多知识欠缺和判断失误,在你认为有歧义、有错误、有更好选项的时候,你要主动提出问题、建议、选项,与老板进行核对、澄清、深化、确认方案后,再真正开始执行。
|
||||
对各种机密信息(例如 apikey, password),严禁读取、上传。你应当只使用环境变量名、存放机密的文件名来间接使用机密。
|
||||
|
||||
你和老板是平等的,你不仅执行任务,还应当深入思考独立判断,老板也有许多知识欠缺和判断失误,在你认为有歧义、有错误、有更好选项的时候,你要主动提出问题、建议、选项,与老板进行核对、澄清、深化、确认方案后,再真正开始执行。
|
||||
|
||||
## aimemo 工作记忆空间
|
||||
|
||||
@aimemo/ 目录是你自己的工作与记忆空间,由你**全权自主掌控**。你可以任意处理这里的一切子目录和文件,只要有助于更好的管理和辅助你的工作。记录一切有用的信息,使得工作能够跨 sessions、跨设备、跨 ai models、跨 ai agents 的连续进行下去。持续更新,随时保持和项目里其他内容的同步。
|
||||
在本项目根目录、以及每个子目录下,都可以有 aimemo/ 目录。这是供你全权自主使用的工作与记忆空间,你可以任意处理这里的一切子目录和文件。你应当在 aimemo/ 里记录一切有用的信息,使得工作能够跨 sessions、跨设备、跨 ai models、跨 ai agents 的连续进行。aimemo/ 应当持续更新,随时保持和项目里其他内容的同步。
|
||||
|
||||
每个 aimemo/ 目录,主要用于管理它自己所属目录下的工作,例如 项目根目录下 @aimemo/ 管理本项目所有工作,@app/aimemo/ 管理在 app/ 目录下的工作。各级 aimemo/ 之间可以互相引用,构建灵活的项目管理地图。
|
||||
|
||||
### 基础框架
|
||||
|
||||
建议以四个文件作为基础框架,从四个基本视角覆盖项目:
|
||||
以下面的文件作为 aimemo/ 的基础框架,从不同视角覆盖项目:
|
||||
|
||||
| 参考文件名 | 内容定位 | 用法建议 |
|
||||
| -------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| productLens.md | 产品视角:未来目标、此刻现状、愿景、定位、架构、约束、模块、功能、原则、等等。 | 透视产品的方方面面,记录已经实现的事实,也记录尚待开发的目标。对每一项产品特性,应当区分`[待评审]`、`[待开发]`、`[已实现]`、以及你可按需自定义的其他状态,避免混淆现实和设想。本文档应长期稳定,但每当状态变化或认知深化时,要及时同步更新。 |
|
||||
| projectPlan.md | 项目视角:开发计划、任务列表、进度跟踪等。 | 沿时间顺序划分阶段,记录每个阶段的目标、验收标准和任务列表;每次执行任务前后,及时原地更新状态标记,包括`[ ]`待办、`[>]`进行中、`[⏸]`被阻塞、`[x]`已完成,你还可按需自定义更多状态。 |
|
||||
| worklog.md | 事件视角:工作日志,按时间线记录工作过程与重要变化。 | 记录工作过程、决策、状态、结果等,方便复盘和回溯。持续追加内容,别把它覆写成仅包含当前状态。 |
|
||||
| userProfile.md | 人的视角:老板的个人背景、习惯、偏好与要求的。 | 随时补充你日常观察到的、或老板主动提供的个性化信息,帮助你和老板之间更流畅、更高效的协作。 |
|
||||
- productLens.md
|
||||
- 内容定位: (产品视角) 未来目标、此刻现状、愿景、定位、架构、约束、模块、功能、原则、等等。
|
||||
- 使用建议: 透视产品的方方面面,记录已经实现的事实,也记录尚待开发的目标。对每一项产品特性,应当区分`[待评审]`、`[待开发]`、`[已实现]`、以及你可按需自定义的其他状态,避免混淆现实和设想。本文档应长期稳定,但每当状态变化或认知深化时,要及时同步更新。
|
||||
|
||||
- projectPlan.md
|
||||
- 内容定位: (项目视角) 开发计划、任务列表、进度跟踪等。
|
||||
- 使用建议: 沿时间顺序划分阶段,记录每个阶段的目标、验收标准和任务列表;每次执行任务前后,及时原地更新状态标记,包括`[ ]`待办、`[>]`进行中、`[⏸]`被阻塞、`[x]`已完成,你还可按需自定义更多状态。
|
||||
|
||||
- resourceMap.md
|
||||
- 内容定位:(物资的视角) 开发、运行的设备、网络、软件等资源。
|
||||
- 用法建议: 整个项目中牵涉到的软硬件、网络、第三方服务等资源。
|
||||
|
||||
- userProfile.md
|
||||
- 内容定位:(人的视角) 老板的个人背景、习惯、偏好与要求。
|
||||
- 用法建议:随时补充你日常观察到的、或老板主动提供的个性化信息,帮助你和老板之间更流畅、更高效的协作。
|
||||
|
||||
- worklog/(目录)
|
||||
- 内容定位: (事件视角) 工作日志,按时间线记录工作过程、决策、状态、结果等,方便复盘和回溯。
|
||||
- 用法建议:持续追加内容,别把它覆写成仅包含当前状态。所有条目存放在扁平目录 `aimemo/worklog/` 下,分两类文件:
|
||||
- **当月条目**:
|
||||
- 每条一个文件,文件名为 'wl' + 落盘时刻的 UTC 时间戳,格式 `wl<yyyymmddTHHMMSS.ffffffZ>.json`(精确到**微秒**,微秒位保证多 agent 并行落盘几乎不撞名;万一撞名,在末尾加 `-2` 后缀)。不使用单机递增序号(多 agent 并行会冲突),不使用本地时间做序号(跨时区、写法不统一)。示例 `aimemo/worklog/wl20261013T043501.514823Z.json`
|
||||
- 每个文件内容是**一个 JSON 对象,多行美化格式**,文件本身即一条完整记录,行布局自由,人和 agent 都可逐行扫读。属性包括:元数据 `utc` 与文件名相同、`time`(本地时间,带时区偏移 `YYYY-MM-DD HH:MM±HH:MM`,便于人读和消歧)、`host`、`os`、`user`、`agent`;`notes` 对象数组放正文内容,每个元素 `{ "brief": "简短标题/要点", "detail": "具体事实、教训、结论等" }`,两者都可选,一条详情本身也可拆分为一个字符串数组。你可以自主决定添加更多有用的属性。
|
||||
- **历史归档**:进入新月后,把上月的全部条目文件按文件名升序(字典序=时间序)逐条压缩成单行,拼接为 `wl<yyyymm>.jsonl`(如 `202610.jsonl`)——归档面向行式工具批量检索,一行=一条记录,grep 命中行即该条;然后删除那些条目散文件。归档文件只读,原则上不改写。示例 `aimemo/worklog/wl202609.jsonl`
|
||||
- 检索:一次 grep/glob 递归覆盖 `aimemo/worklog/` 整个目录,即同时覆盖当月条目与历史归档;文件名模式区分两类——归档 `^wl\d{6}\.jsonl$`,当月条目 `^wl\d{8}T\d{6}\.\d{6}Z\.json$`。
|
||||
- 存量旧格式 `aimemo/worklog.md`(单文件、不规范的老条目)保留原样,不迁移、不回改。
|
||||
|
||||
```
|
||||
{ "utc": "20261013T123503.000000Z", "time": "2026-10-13 12:35+08:00", "host": "mac15", "os": "MacOS", user": "lukas", "agent": "codex",
|
||||
"notes": [
|
||||
{ "brief": "聊天输入框白底", "detail": "chat_page.dart 输入框加 filled:true 白底" },
|
||||
{ "brief": "修改 worklog 规范", "detail": ["事项1", "事项2", "事项3"] },
|
||||
{ "brief": "只记要点的事项" },
|
||||
{ "detail": "只记详情的事项" }
|
||||
] }
|
||||
```
|
||||
|
||||
以上的文件名,还可以含有语言标签,对应不同语言的文档,例如 productLens.enus.md, productLens.zhcn.md
|
||||
|
||||
对新项目,如果 `@aimemo/` 是空的,建议先按上表初始化。
|
||||
对新项目,如果 `@aimemo/` 是空的,先按上表初始化。
|
||||
|
||||
### 自主扩展
|
||||
|
||||
@@ -34,10 +67,15 @@
|
||||
- **价值优先**:只记录对后续工作有帮助的信息,避免无意义堆砌。
|
||||
- **主动更新**:每次完成一个任务或项目有任何变化时,及时更新相关记录。
|
||||
- **保持可读**:老板也会参与 @aimemo/ 的查阅、编写、更新,请确保内容清晰、信息同步。
|
||||
- **日期作为文件名前缀**:额外添加的文件,以创建日期 yyyymmdd\_ 作为文件名前缀,例如 20260901_newFeature.md,方便和基础框架文件区分开来。
|
||||
|
||||
## 工作规范
|
||||
|
||||
每次修改并验证成功之后,自动做一次 git commit。但不要自动推送,直到老板明确要求推送才推送。
|
||||
每次完成了一个完整的特性、修复了一个bug、做了一定量修改并验证成功之后,更新 worklog 并做一次 git commit,注意worklog的更新应当与其记录的其他更新一起提交。但如果是很微小的改动,特别是界面的改动,那就不必commit,先让老板验收。不要推送,除非老板明确要求。
|
||||
|
||||
你在一个协同工作的环境中。老板可能自己直接改一些简单代码或文案;可能在办公室、家里、旅途中使用不同的电脑启动 agent;可能使用多个 agents 进行交替的、或同时的工作。你要注意,记录及时、准确的文档,有利于互相协作;不要互相干扰,不要提交别人的工作,除非老板明确要求。
|
||||
|
||||
有需要机密信息(例如 apikey, password 等)时,你可向老板提出需求,老板通常会放在 @\_simsim.gitomit.sfomit/ 目录下供你使用。
|
||||
|
||||
## 命名规范
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
// 功能:从 https://git.tic.cc/opx/sysconfig/ 同步 agents/AGENTS.md 和 agents/CLAUDE.md
|
||||
// 到本项目根目录(覆盖现有文件)。
|
||||
// 到本项目根目录(覆盖现有文件);并在当前目录初始化 aimemo/ 工作记忆目录,
|
||||
// 按 AGENTS.md 约定创建缺失的 productLens.md / projectPlan.md / worklog.md / userProfile.md(空白文件),已存在的保持不动。
|
||||
// 使用 Node 内置 fetch,跨 Windows / Linux / macOS 平台,无需额外工具。
|
||||
|
||||
// 调用:在 package.json 的 scripts 里添加 "syncAgentRule": "node -e \"fetch('https://git.tic.cc/opx/sysconfig/raw/branch/main/agents/syncAgentRule.mjs').then(r=>{if(!r.ok)throw new Error('HTTP '+r.status);return r.text()}).then(async t=>{await import('data:text/javascript;base64,'+Buffer.from(t).toString('base64'))})\""
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { writeFile, mkdir, access } from 'node:fs/promises';
|
||||
|
||||
const BASE_URL = 'https://git.tic.cc/opx/sysconfig/raw/branch/main/agents/';
|
||||
const FILES = ['AGENTS.md', 'CLAUDE.md'];
|
||||
|
||||
// aimemo 工作记忆目录:按 AGENTS.md 约定初始化,仅在调用时的工作目录(process.cwd())下创建缺失文件,已存在的一律沿用
|
||||
const AIMEMO_DIR = 'aimemo';
|
||||
const AIMEMO_FILES = ['productLens.md', 'projectPlan.md', 'worklog.md', 'userProfile.md'];
|
||||
|
||||
for (const name of FILES) {
|
||||
const response = await fetch(BASE_URL + name);
|
||||
if (!response.ok) {
|
||||
@@ -18,3 +23,17 @@ for (const name of FILES) {
|
||||
}
|
||||
|
||||
console.log('agents 文件同步完成');
|
||||
|
||||
// 初始化 aimemo/(相对路径解析到调用时的工作目录,即 process.cwd())
|
||||
await mkdir(AIMEMO_DIR, { recursive: true });
|
||||
for (const name of AIMEMO_FILES) {
|
||||
const filePath = `${AIMEMO_DIR}/${name}`;
|
||||
try {
|
||||
await access(filePath);
|
||||
console.log(`已存在,沿用 ${filePath}`);
|
||||
} catch {
|
||||
await writeFile(filePath, '');
|
||||
console.log(`已创建 ${filePath}`);
|
||||
}
|
||||
}
|
||||
console.log('aimemo 目录初始化完成');
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PayloadContent</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>DNSSettings</key>
|
||||
<dict>
|
||||
<key>DNSProtocol</key>
|
||||
<string>HTTPS</string>
|
||||
<key>ServerURL</key>
|
||||
<string>https://dns.alidns.com/dns-query</string>
|
||||
</dict>
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>DoH AliDNS</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>cc.tic.einz.doh.alidns</string>
|
||||
<key>PayloadType</key>
|
||||
<string>com.apple.dnsSettings.managed</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>A1B2C3D4-E5F6-4A7B-8C9D-0E1F2A3B4C5D</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</array>
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>DoH AliDNS</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>cc.tic.einz.doh</string>
|
||||
<key>PayloadRemovalDisallowed</key>
|
||||
<false/>
|
||||
<key>PayloadType</key>
|
||||
<string>Configuration</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>B2C3D4E5-F6A7-4B8C-9D0E-1F2A3B4C5D6E</string>
|
||||
<!-- macOS 26.1+ 必需:声明系统级范围,否则报"无法安装VPN服务负载"(系统bug) -->
|
||||
<key>PayloadScope</key>
|
||||
<string>System</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PayloadContent</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>DNSSettings</key>
|
||||
<dict>
|
||||
<key>DNSProtocol</key>
|
||||
<string>HTTPS</string>
|
||||
<key>ServerURL</key>
|
||||
<string>https://cloudflare-dns.com/dns-query</string>
|
||||
</dict>
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>DoH Cloudflare (1.1.1.1)</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>cc.tic.einz.doh.cloudflare</string>
|
||||
<key>PayloadType</key>
|
||||
<string>com.apple.dnsSettings.managed</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>E5F6A7B8-C9D0-4E1F-2A3B-4C5D6E7F8091</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</array>
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>DoH Cloudflare (1.1.1.1)</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>cc.tic.einz.doh.cloudflare</string>
|
||||
<key>PayloadRemovalDisallowed</key>
|
||||
<false/>
|
||||
<key>PayloadType</key>
|
||||
<string>Configuration</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>F6A7B8C9-D0E1-4F2A-3B4C-5D6E7F8091A2</string>
|
||||
<!-- macOS 26.1+ 必需:声明系统级范围,否则报"无法安装VPN服务负载"(系统bug) -->
|
||||
<key>PayloadScope</key>
|
||||
<string>System</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PayloadContent</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>DNSSettings</key>
|
||||
<dict>
|
||||
<key>DNSProtocol</key>
|
||||
<string>HTTPS</string>
|
||||
<key>ServerURL</key>
|
||||
<string>https://doh.pub/dns-query</string>
|
||||
</dict>
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>DoH DNSPod (doh.pub)</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>cc.tic.einz.doh.dnspod</string>
|
||||
<key>PayloadType</key>
|
||||
<string>com.apple.dnsSettings.managed</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>C3D4E5F6-A7B8-4C9D-0E1F-2A3B4C5D6E7F</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</array>
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>DoH DNSPod (doh.pub)</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>cc.tic.einz.doh.dnspod</string>
|
||||
<key>PayloadRemovalDisallowed</key>
|
||||
<false/>
|
||||
<key>PayloadType</key>
|
||||
<string>Configuration</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>D4E5F6A7-B8C9-4D0E-1F2A-3B4C5D6E7F80</string>
|
||||
<!-- macOS 26.1+ 必需:声明系统级范围,否则报"无法安装VPN服务负载"(系统bug) -->
|
||||
<key>PayloadScope</key>
|
||||
<string>System</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -2,11 +2,11 @@ echo "***************************************"
|
||||
echo "find \$FOLDER -mindepth \$MINDEPTH -maxdepth \$MAXDEPTH -type d_f_l -name \"\$PATTERN\" | while read item; do echo \"\$item\"; done"
|
||||
echo "***************************************"
|
||||
|
||||
echo "To find in which path? Enter [root path] or [leave blank] for the current path [[`pwd`]]:"
|
||||
echo "To find in which path? Enter [root path] or [leave blank] for the current path [[$(realpath `pwd`)]]:"
|
||||
read -p "path ***:: " FIND_FOLDER
|
||||
if [ ! "$FIND_FOLDER" ]
|
||||
then
|
||||
FIND_FOLDER=`pwd`
|
||||
FIND_FOLDER=$(realpath "`pwd`")
|
||||
fi
|
||||
|
||||
read -p "mindepth ***:: " MINDEPTH
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env node
|
||||
// Cross-platform (node) port of git_ignore_find2merge.sh / .bat
|
||||
//
|
||||
// Search [ROOTPATH], merge [GLOBALPATH/.gitignore_global] and [ROOTPATH/<dir>/.gitignore.local.txt]
|
||||
// into [ROOTPATH/<dir>/.gitignore], for every <dir> that has a .gitignore.
|
||||
// (Judged from .gitignore, not from .git — some git repos keep privacy and hide .git.)
|
||||
//
|
||||
// usage:
|
||||
// node git_ignore_find2merge.js [rootPath] [globalPath] [y|other]
|
||||
// rootPath start of tree search, must be a directory (default: prompt, blank = current dir)
|
||||
// globalPath path to .gitignore_global, or its parent dir, or an http(s) URL
|
||||
// (default: https://git.tic.cc/opx/sysconfig/raw/branch/main/nixhome/.gitignore_global)
|
||||
// yesNo 'y' to write, anything else = dry-run (list candidates without writing)
|
||||
//
|
||||
// node git_ignore_find2merge.js # interactive, dry-run
|
||||
// node git_ignore_find2merge.js ~/Shared y
|
||||
// node git_ignore_find2merge.js /srv/projects /srv/proj/nixhome y
|
||||
//
|
||||
// Same fixes as seafile_ignore_find2merge.js:
|
||||
// - blank globalPath falls back to the default URL, as in the .sh
|
||||
// - ask() treats stdin EOF as blank, so non-interactive pipes don't hang
|
||||
// - the default global is a URL: fetched with fetch() at 30s timeout + one retry
|
||||
// (the .sh's `cat $GLOBALPATH` branch never ran for URLs — it went to the curl branch)
|
||||
// - the root dir itself is processed too (the .sh's find -name '[^.]*' never matched the root)
|
||||
// - non-'y' is a dry-run that still lists the candidate dirs (the .sh just exited silently)
|
||||
|
||||
const fs = require('fs')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const readline = require('readline')
|
||||
|
||||
const DEFAULT_GLOBAL_URL = 'https://git.tic.cc/opx/sysconfig/raw/branch/main/nixhome/.gitignore_global'
|
||||
const GLOBAL_FILE_NAME = '.gitignore_global'
|
||||
// same skip list as: grep -E -v 'node_modules|uni_modules|\.deploy_git|\.git|.svn|\.vscode|\.wrangler|unpackage|_webroot|_logstore|_datasotre|_archive|_filestore|_ssl'
|
||||
const SKIP_DIR_PATTERN = /node_modules|uni_modules|\.deploy_git|\.git|\.svn|\.vscode|\.wrangler|unpackage|_webroot|_logstore|_datasotre|_archive|_filestore|_ssl/
|
||||
const MAX_DEPTH = 3 // relative to ROOTPATH, matching find -mindepth 0 -maxdepth 3
|
||||
|
||||
function resolveHome (p) {
|
||||
if (p === '~') return os.homedir()
|
||||
if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2))
|
||||
return p
|
||||
}
|
||||
|
||||
async function ask (question) {
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
||||
const answer = await new Promise(resolve => {
|
||||
let settled = false
|
||||
const finish = value => { if (!settled) { settled = true; resolve(value) } }
|
||||
rl.question(question, a => finish(a))
|
||||
rl.on('close', () => finish('')) // stdin ended without input (EOF): treat as blank, like the .sh's `read`
|
||||
})
|
||||
rl.close()
|
||||
return answer.trim()
|
||||
}
|
||||
|
||||
// find . -mindepth 0 -maxdepth 3 -type d -name '[^.]*' (relative paths, root itself excluded)
|
||||
function collectCandidateDirs (rootPath) {
|
||||
const candidates = []
|
||||
function walk (dir, depth) {
|
||||
if (depth > MAX_DEPTH) return
|
||||
let entries
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue
|
||||
if (entry.name.startsWith('.')) continue // -name '[^.]*' excludes hidden dirs
|
||||
const absDir = path.join(dir, entry.name)
|
||||
const relDir = path.relative(rootPath, absDir)
|
||||
if (relDir.split(path.sep).length > MAX_DEPTH) continue
|
||||
candidates.push(relDir)
|
||||
walk(absDir, depth + 1)
|
||||
}
|
||||
}
|
||||
walk(rootPath, 1)
|
||||
return candidates
|
||||
}
|
||||
|
||||
async function loadGlobalContent (globalPathInput) {
|
||||
const globalPath = resolveHome(globalPathInput)
|
||||
if (/^https?:\/\//i.test(globalPath)) {
|
||||
console.log('√√√ GLOBALPATH (url) = [[' + globalPath + ']]')
|
||||
// git.tic.cc's TCP connect can exceed node's default 10s connect timeout; allow 30s and retry once.
|
||||
let lastError
|
||||
for (let attempt = 1; attempt <= 2; attempt++) {
|
||||
try {
|
||||
const response = await fetch(globalPath, { signal: AbortSignal.timeout(30000) })
|
||||
if (!response.ok) throw new Error('HTTP ' + response.status)
|
||||
return await response.text()
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
console.log('... fetch attempt ' + attempt + ' failed: ' + (error.cause?.code || error.message))
|
||||
}
|
||||
}
|
||||
console.log('××× Fetch failed: ' + (lastError.cause?.code || lastError.message) + ' for [[' + globalPath + ']]. Exit now...')
|
||||
process.exit(1)
|
||||
}
|
||||
let globalFile = globalPath
|
||||
if (fs.existsSync(globalPath) && fs.statSync(globalPath).isDirectory()) {
|
||||
globalFile = path.join(globalPath, GLOBAL_FILE_NAME)
|
||||
}
|
||||
if (!fs.existsSync(globalFile)) {
|
||||
console.log('××× Not found [[' + globalFile + ']]. Exit now...')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('√√√ GLOBALPATH = [[' + globalFile + ']]')
|
||||
return fs.readFileSync(globalFile, 'utf8')
|
||||
}
|
||||
|
||||
async function main () {
|
||||
console.log('')
|
||||
console.log('Search in [ROOTPATH], merge [GLOBALPATH/.gitignore_global] and [ROOTPATH/*/.gitignore.local.txt] files to .gitignore')
|
||||
console.log('')
|
||||
|
||||
// --- ROOTPATH ---
|
||||
let rootPath = process.argv[2]
|
||||
const rootIsDir = p => {
|
||||
try { return fs.statSync(p).isDirectory() } catch { return false }
|
||||
}
|
||||
if (!(rootPath && rootIsDir(resolveHome(rootPath)))) {
|
||||
console.log('::*** Enter [root path] or [leave blank] for default [[' + process.cwd() + ']] to start tree search for git repositories')
|
||||
rootPath = await ask('***:: ')
|
||||
if (!rootPath) rootPath = process.cwd()
|
||||
rootPath = path.resolve(resolveHome(rootPath))
|
||||
} else {
|
||||
rootPath = path.resolve(resolveHome(rootPath))
|
||||
}
|
||||
if (!rootIsDir(rootPath)) {
|
||||
console.log('××× [[' + rootPath + ']] not exist! Exit now. ***')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('√√√ ROOTPATH = [[' + rootPath + ']]')
|
||||
console.log('')
|
||||
|
||||
// --- GLOBALPATH ---
|
||||
let globalPathInput = process.argv[3]
|
||||
if (!globalPathInput) {
|
||||
console.log('::*** Enter [path to .gitignore_global] or [leave blank] for default [[' + DEFAULT_GLOBAL_URL + ']]')
|
||||
globalPathInput = await ask('***:: ')
|
||||
if (!globalPathInput) globalPathInput = DEFAULT_GLOBAL_URL // blank → default URL, as in the .sh
|
||||
}
|
||||
const globalContent = await loadGlobalContent(globalPathInput)
|
||||
console.log('')
|
||||
|
||||
// --- y/N ---
|
||||
let yesNo = process.argv[4]
|
||||
if (!yesNo) {
|
||||
console.log('::*** Enter [y] to start updating, or [anything else] for dry-run')
|
||||
yesNo = await ask('***:: ')
|
||||
}
|
||||
const willWrite = yesNo === 'y'
|
||||
|
||||
console.log('*** Starting from [[' + rootPath + ']] ***')
|
||||
console.log('')
|
||||
|
||||
// --- search + merge ---
|
||||
// A dir is a candidate iff it has a .gitignore (judged from .gitignore, not from .git).
|
||||
const candidateDirs = collectCandidateDirs(rootPath).sort()
|
||||
// The root dir itself is also a candidate: the old .sh's find -name '[^.]*' never matched the root.
|
||||
if (fs.existsSync(path.join(rootPath, '.gitignore'))) candidateDirs.unshift('.')
|
||||
|
||||
let updatedCount = 0
|
||||
for (const relDir of candidateDirs) {
|
||||
const absDir = relDir === '.' ? rootPath : path.join(rootPath, relDir)
|
||||
if (relDir !== '.' && SKIP_DIR_PATTERN.test(relDir)) continue
|
||||
const targetFile = path.join(absDir, '.gitignore')
|
||||
const localFile = path.join(absDir, '.gitignore.local.txt')
|
||||
if (!fs.existsSync(targetFile)) continue
|
||||
console.log('---- updating [[' + relDir + '/.gitignore]] ----')
|
||||
if (willWrite) {
|
||||
let merged = globalContent
|
||||
if (fs.existsSync(localFile)) merged += fs.readFileSync(localFile, 'utf8')
|
||||
fs.writeFileSync(targetFile, merged)
|
||||
updatedCount++
|
||||
}
|
||||
console.log('')
|
||||
}
|
||||
if (willWrite) {
|
||||
console.log('*** done: ' + updatedCount + ' file(s) updated ***')
|
||||
} else {
|
||||
console.log('*** dry-run: no file written (enter y to update) ***')
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -9,4 +9,4 @@ fi
|
||||
echo Public IP: `curl -s ifconfig.me`
|
||||
echo
|
||||
|
||||
curl ipinfo.io
|
||||
net.sh
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
echo https://ipinfo.io/country == $(curl -s https://ipinfo.io/country) # 返回国家代码,例如 CN, DE, US
|
||||
echo https://myip.ipip.net == $(curl -s https://myip.ipip.net) # 国内站应显示无锡/电信(直连)
|
||||
echo https://ipinfo.io/json ==
|
||||
curl -s https://ipinfo.io/json # 详情:IP、城市、组织
|
||||
# curl ipinfo.io
|
||||
|
||||
echo
|
||||
+2
-2
@@ -146,9 +146,9 @@ elif [ -f /etc/ubuntu_version ]; then
|
||||
MYOSVERSION=Ubt`cat /etc/ubuntu_version 2>/dev/null`
|
||||
fi
|
||||
if [ "$color_prompt" = yes ]; then
|
||||
PS1='\n¶ \[\033[$PSTYLE;${PTYPE}2m\]\t\[\033[00m\] \[\033[$PSTYLE;${PTYPE}5m\]\u\[\033[00m\] @\[\033[$PSTYLE;${PTYPE}2m\]\h\[\033[00m\] =\[\033[$PSTYLE;${PTYPE}5m\]$MYIPPUB\[\033[00m\] #\[\033[$PSTYLE;${PTYPE}2m\]$(uname -m),$(uname),$MYOSVERSION\[\033[00m\] \[\033[$PSTYLE;${PTYPE}5m\]$PWD/\[\033[00m\] \n[ \[\033[05;31m\]\W/\[\033[00m\] ]§ '
|
||||
PS1='\n#[ \[\033[$PSTYLE;${PTYPE}2m\]\t\[\033[00m\] \[\033[$PSTYLE;${PTYPE}5m\]\u\[\033[00m\] @\[\033[$PSTYLE;${PTYPE}2m\]\h\[\033[00m\] =\[\033[$PSTYLE;${PTYPE}5m\]$MYIPPUB\[\033[00m\] #\[\033[$PSTYLE;${PTYPE}2m\]$(uname -m),$(uname),$MYOSVERSION\[\033[00m\] \[\033[$PSTYLE;${PTYPE}5m\]$PWD/\[\033[00m\] ]#\n#[ \[\033[05;31m\]\W/\[\033[00m\] ]# '
|
||||
else
|
||||
PS1='\n¶ \t \u @\h =$MYIPPUB #$(uname -m),$(uname),$MYOSVERSION $PWD/ \n[ \W/ ]§ '
|
||||
PS1='\n#[ \t \u @\h =$MYIPPUB #$(uname -m),$(uname),$MYOSVERSION $PWD/ ]#\n#[ \W/ ]# '
|
||||
fi
|
||||
unset color_prompt force_color_prompt
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
# https://gitignore.io
|
||||
|
||||
### .gitignore_global ###
|
||||
### global ignore settings in opx/sysconfig/nixhome/.gitignore_global
|
||||
|
||||
# Self defined pattern to ignore
|
||||
?*.gitignore
|
||||
@@ -83,4 +84,6 @@ _desktop.ini
|
||||
.env.*.local
|
||||
|
||||
### .gitignore.local.txt ###
|
||||
### Followings are project-specific ignore settings merged by opx/sysconfig/git_ignore_find2merge.js
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
# seafile-ignore.txt 只能忽略还没有被同步的文件。对于已经被同步的文件,如果后来把它添加到 seafile-ignore.txt 中,系统只会忽略后续更改,已经上传的版本不会受影响。
|
||||
|
||||
### seafile-ignore.global.txt ###
|
||||
### global ignore settings in opx/sysconfig/nixhome/seafile-ignore.global.txt
|
||||
|
||||
# 自定义的后缀名,凡有 sfignore 后缀的都不进行同步
|
||||
*.sfignore
|
||||
@@ -83,5 +84,30 @@ OneDrive/Icon
|
||||
.playwright-mcp/
|
||||
*/.playwright-mcp/
|
||||
|
||||
### seafile-ignore.local.txt ###
|
||||
# Flutter/Dart 机器本地产物(各端由 `flutter pub get` / `pod install` / 构建在本机重生成,不跨机同步)。
|
||||
# 双机开发时 .dart_tool/package_config.json 内是绝对路径(file:///Users/<用户>/.pub-cache/...),同步过去会让另一台机器编译报 No such file or directory。
|
||||
# 注:pub 缓存在 ~/.pub-cache,位于资料库根之外,seafile 同步不到,无需在此列出。
|
||||
# 注:build/ 未全局忽略,以防波及用了该常见词的目录;按需放各项目 seafile-ignore.local.txt。
|
||||
|
||||
# ---- Dart / Flutter:包解析 + 构建产物(核心,换机必跑 `flutter pub get` 重生成)----
|
||||
.dart_tool/
|
||||
*/.dart_tool/
|
||||
.flutter-plugins-dependencies
|
||||
*/.flutter-plugins-dependencies
|
||||
build/
|
||||
*/build/
|
||||
|
||||
# ---- iOS / macOS:CocoaPods 检出目录(Podfile.lock 仍提交共享,只忽略 Pods/ 实体)----
|
||||
Pods/
|
||||
*/Pods/
|
||||
|
||||
# ---- Android:本机 SDK 路径 + 构建缓存(换机必跑 `flutter build` 重生成 local.properties)----
|
||||
local.properties
|
||||
*/local.properties
|
||||
.gradle/
|
||||
*/.gradle/
|
||||
.kotlin/
|
||||
*/.kotlin/
|
||||
|
||||
### seafile-ignore.local.txt ###
|
||||
### Followings are project-specific ignore settings merged by opx/sysconfig/seafile_ignore_find2merge.js
|
||||
|
||||
@@ -41,6 +41,4 @@ echo all_proxy=$all_proxy
|
||||
echo http_proxy=$http_proxy
|
||||
echo https_proxy=$https_proxy
|
||||
|
||||
# ip.gs, ip.sb, ipinfo.io
|
||||
curl ipinfo.io
|
||||
echo " "
|
||||
net.sh
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/bin/bash
|
||||
# route.sh 域名 —— 输出该域名在 mihomo 的分流结果,如: DIRECT / PROXY[de1-vless] / REJECT
|
||||
# 用法: route.sh www.xiaohongshu.com [更多域名...]
|
||||
# 自适应两套环境(同一份脚本,sysconfig 同步到所有电脑):
|
||||
# 1) Mac + Clash Verge service:unix socket + 7897(无 TCP API、无需 secret)
|
||||
# 2) Linux 服务器(如 cn1):TCP API + secret + 7890
|
||||
# 可靠性设计:每域名独立一条 8s 日志流(长流/连续流会被内核丢弃),流内 grep 纯域名
|
||||
#(JSON 里 "-->" 被转义成 \u003e,不能按 "--> " 前缀匹配)
|
||||
|
||||
# ==== ENV(按需修改)====
|
||||
MAC_SOCK="/var/run/clash-verge-service/users/501/verge-mihomo.sock" # Verge service unix socket
|
||||
CN1_API="http://100.100.86.1:9090" # cn1 mihomo REST API
|
||||
CN1_SECRET="pl1pool-2026" # cn1 API secret
|
||||
PROXY_MAC="http://127.0.0.1:7897" # Mac mixed 端口
|
||||
PROXY_CN1="http://127.0.0.1:7890" # cn1 mixed 端口
|
||||
# ========================
|
||||
|
||||
command -v curl >/dev/null || { echo "curl is required"; exit 1; }
|
||||
|
||||
# ---- 探测环境:优先 unix socket(Mac),否则 TCP+secret(cn1/其他)----
|
||||
if [ -S "$MAC_SOCK" ]; then
|
||||
MODE="mac"
|
||||
SOCK_ARGS=(--unix-socket "$MAC_SOCK")
|
||||
API_URL="http://localhost"
|
||||
AUTH=()
|
||||
PROXY="$PROXY_MAC"
|
||||
else
|
||||
MODE="tcp"
|
||||
SOCK_ARGS=()
|
||||
API_URL="$CN1_API"
|
||||
AUTH=(-H "Authorization: Bearer $CN1_SECRET")
|
||||
PROXY="$PROXY_CN1"
|
||||
PING=$(curl -s -m 5 "${AUTH[@]}" -o /dev/null -w '%{http_code}' "$API_URL/version" 2>/dev/null)
|
||||
[ "$PING" = "200" ] || { echo "API unreachable: $API_URL (code=$PING) — 检查网络/secret"; exit 1; }
|
||||
fi
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: $(basename $0) domain [domain2 ...] (mode: $MODE)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 每域名独立短会话:8s 日志流 + 1s 后请求 + 7s 收尾(此形态实测最稳)
|
||||
for domain in "$@"; do
|
||||
LOG=$(mktemp)
|
||||
curl -s --max-time 8 "${SOCK_ARGS[@]}" "${AUTH[@]}" "$API_URL/logs?level=info" > "$LOG" 2>/dev/null &
|
||||
LPID=$!
|
||||
sleep 1
|
||||
curl -s -m 5 -x "$PROXY" -o /dev/null "https://$domain" 2>/dev/null
|
||||
sleep 7
|
||||
LINE=$(grep -m1 "$domain" "$LOG" 2>/dev/null | head -1)
|
||||
if [ -z "$LINE" ]; then
|
||||
printf "%-40s ?? (no match line captured)\n" "$domain"
|
||||
else
|
||||
RESULT=$(echo "$LINE" | sed -n 's/.* match .* using //p' | sed 's/"}$//' | sed 's/"$//')
|
||||
RULE=$(echo "$LINE" | sed -n 's/.* match \(.*\) using .*/\1/p')
|
||||
printf "%-40s %-24s (rule: %s)\n" "$domain" "${RESULT:-??}" "${RULE:-?}"
|
||||
fi
|
||||
kill $LPID 2>/dev/null; wait $LPID 2>/dev/null; rm -f "$LOG"
|
||||
done
|
||||
@@ -0,0 +1,113 @@
|
||||
# https://help.seafile.com/syncing_client/excluding_files/
|
||||
# 注释。通配符:* 匹配0到若干个字符,包括代表目录的/。? 匹配1个字符,包括/。
|
||||
# seafile-ignore.txt 只能控制在客户端需要忽略哪些文件。你依然可以在 seahub 的 web 界面创建这些被客户端忽略的文件。
|
||||
# 在这种情况下,
|
||||
# 这些文件会被同步到客户端,但是用户在客户端对这些文件的后续修改会被忽略,不会被同步回服务器。
|
||||
# 文件在服务器端的后续更改会被同步到客户端,如果客户端也同时修改了这些文件,系统会生成冲突文件。
|
||||
# seafile-ignore.txt 只能忽略还没有被同步的文件。对于已经被同步的文件,如果后来把它添加到 seafile-ignore.txt 中,系统只会忽略后续更改,已经上传的版本不会受影响。
|
||||
|
||||
### seafile-ignore.global.txt ###
|
||||
### global ignore settings in opx/sysconfig/nixhome/seafile-ignore.global.txt
|
||||
|
||||
# 自定义的后缀名,凡有 sfignore 后缀的都不进行同步
|
||||
*.sfignore
|
||||
*.sfignore/
|
||||
*.sfignore.*
|
||||
*.sfignore.*/
|
||||
*.sfomit
|
||||
*.sfomit.*
|
||||
*.sfomit/
|
||||
*.sfomit.*/
|
||||
*.nosf
|
||||
*.nosf.*
|
||||
*.nosf/
|
||||
*.nosf.*/
|
||||
|
||||
## everything 'git pull or fetch' will update `.git/FETCH_HEAD`, even if the content doesn't change. To avoid too many useless updates of this file in Seafile history:
|
||||
FETCH_HEAD
|
||||
*/FETCH_HEAD
|
||||
|
||||
.Trash/
|
||||
.Trashes/
|
||||
|
||||
.DS_Store
|
||||
*/.DS_Store
|
||||
*.aae # AAE 文件主要在苹果的照片应用程序中使用,保存对原始照片所做的编辑,比如,裁剪、旋转或调整亮度等操作的信息。
|
||||
|
||||
.thumbnails
|
||||
*/.thumbnails
|
||||
|
||||
Thumbs.db
|
||||
*/Thumbs.db
|
||||
thumbs.db
|
||||
*/thumbs.db
|
||||
|
||||
_desktop.ini
|
||||
*/_desktop.ini
|
||||
|
||||
._*
|
||||
*/._*
|
||||
|
||||
.$*
|
||||
*/.$*
|
||||
|
||||
~$*
|
||||
*/~$*
|
||||
|
||||
node_modules/
|
||||
*/node_modules/
|
||||
|
||||
pages4loader.json5
|
||||
*/pages4loader.json5
|
||||
|
||||
.deploy_git/
|
||||
*/.deploy_git/
|
||||
|
||||
# next.js 项目
|
||||
.next/
|
||||
*/.next/
|
||||
|
||||
# HBuilder 目录
|
||||
unpackage/
|
||||
*/unpackage/
|
||||
|
||||
Icon
|
||||
OneDrive/Icon
|
||||
|
||||
# wrangler project
|
||||
|
||||
.dev.vars*
|
||||
*/.dev.vars*
|
||||
.wrangler/
|
||||
*/.wrangler/
|
||||
|
||||
.playwright-mcp/
|
||||
*/.playwright-mcp/
|
||||
|
||||
# Flutter/Dart 机器本地产物(各端由 `flutter pub get` / `pod install` / 构建在本机重生成,不跨机同步)。
|
||||
# 双机开发时 .dart_tool/package_config.json 内是绝对路径(file:///Users/<用户>/.pub-cache/...),同步过去会让另一台机器编译报 No such file or directory。
|
||||
# 注:pub 缓存在 ~/.pub-cache,位于资料库根之外,seafile 同步不到,无需在此列出。
|
||||
# 注:build/ 未全局忽略,以防波及用了该常见词的目录;按需放各项目 seafile-ignore.local.txt。
|
||||
|
||||
# ---- Dart / Flutter:包解析 + 构建产物(核心,换机必跑 `flutter pub get` 重生成)----
|
||||
.dart_tool/
|
||||
*/.dart_tool/
|
||||
.flutter-plugins-dependencies
|
||||
*/.flutter-plugins-dependencies
|
||||
build/
|
||||
*/build/
|
||||
|
||||
# ---- iOS / macOS:CocoaPods 检出目录(Podfile.lock 仍提交共享,只忽略 Pods/ 实体)----
|
||||
Pods/
|
||||
*/Pods/
|
||||
|
||||
# ---- Android:本机 SDK 路径 + 构建缓存(换机必跑 `flutter build` 重生成 local.properties)----
|
||||
local.properties
|
||||
*/local.properties
|
||||
.gradle/
|
||||
*/.gradle/
|
||||
.kotlin/
|
||||
*/.kotlin/
|
||||
|
||||
### seafile-ignore.local.txt ###
|
||||
### Followings are project-specific ignore settings merged by opx/sysconfig/seafile_ignore_find2merge.js
|
||||
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env node
|
||||
// Cross-platform (node) port of seafile_ignore_find2merge.sh / .bat
|
||||
//
|
||||
// Search [ROOTPATH], merge [GLOBALPATH/seafile-ignore.global.txt] and [ROOTPATH/<dir>/seafile-ignore.local.txt]
|
||||
// into [ROOTPATH/<dir>/seafile-ignore.txt], for every <dir> that has a seafile-ignore.txt OR a seafile-ignore.local.txt.
|
||||
// (Unlike git_ignore_find2merge, a .git dir is NOT a trigger — seafile ignore rules are independent of git.)
|
||||
//
|
||||
// usage:
|
||||
// node seafile_ignore_find2merge.js [rootPath] [globalPath] [y|other]
|
||||
// rootPath start of tree search (default: current dir)
|
||||
// globalPath path to seafile-ignore.global.txt, or its parent dir, or an http(s) URL
|
||||
// (default: https://git.tic.cc/opx/sysconfig/raw/branch/main/nixhome/seafile-ignore.global.txt)
|
||||
// yesNo 'y' to write, anything else = dry-run (default: dry-run)
|
||||
//
|
||||
// node seafile_ignore_find2merge.js # dry-run from cwd
|
||||
// node seafile_ignore_find2merge.js ~/Shared y
|
||||
// node seafile_ignore_find2merge.js /srv/projects /srv/proj/seafile-ignore.global.txt y
|
||||
//
|
||||
// Semantics ported from the .sh (two .sh gaps fixed):
|
||||
// - the root dir itself is processed too (the .sh's find -name '[^.]*' never matched the root dir)
|
||||
// - a dir is updated iff it has seafile-ignore.txt OR seafile-ignore.local.txt (the .sh's `.git` check was dropped)
|
||||
// - tree search is max depth 3, skipping hidden dirs and the known build/dep dirs
|
||||
// - blank globalPath falls back to the default URL, as in the .sh (first draft missed this → "Not found [[]]")
|
||||
// - the .sh does `cat $GLOBALPATH` which fails when the default is a URL; here http(s) is fetched with fetch(),
|
||||
// a local file / parent-dir keeps the original behavior.
|
||||
|
||||
const fs = require('fs')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const readline = require('readline')
|
||||
|
||||
const DEFAULT_GLOBAL_URL = 'https://git.tic.cc/opx/sysconfig/raw/branch/main/nixhome/seafile-ignore.global.txt'
|
||||
// same skip list as: grep -E -v 'node_modules|uni_modules|\.deploy_git|\.git|.svn|\.vscode|\.wrangler|unpackage|_webroot|_logstore|_datasotre|_archive|_filestore|_ssl'
|
||||
const SKIP_DIR_PATTERN = /node_modules|uni_modules|\.deploy_git|\.git|\.svn|\.vscode|\.wrangler|unpackage|_webroot|_logstore|_datasotre|_archive|_filestore|_ssl/
|
||||
const MAX_DEPTH = 3 // relative to ROOTPATH, matching find -mindepth 0 -maxdepth 3
|
||||
|
||||
function resolveHome (p) {
|
||||
if (p === '~') return os.homedir()
|
||||
if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2))
|
||||
return p
|
||||
}
|
||||
|
||||
async function ask (question) {
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
||||
const answer = await new Promise(resolve => {
|
||||
let settled = false
|
||||
const finish = value => { if (!settled) { settled = true; resolve(value) } }
|
||||
rl.question(question, a => finish(a))
|
||||
rl.on('close', () => finish('')) // stdin ended without input (EOF): treat as blank, like the .sh's `read`
|
||||
})
|
||||
rl.close()
|
||||
return answer.trim()
|
||||
}
|
||||
|
||||
// find . -mindepth 0 -maxdepth 3 -type d -name '[^.]*' (relative paths, root itself excluded)
|
||||
function collectCandidateDirs (rootPath) {
|
||||
const candidates = []
|
||||
function walk (dir, depth) {
|
||||
if (depth > MAX_DEPTH) return
|
||||
let entries
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue
|
||||
if (entry.name.startsWith('.')) continue // -name '[^.]*' excludes hidden dirs
|
||||
const absDir = path.join(dir, entry.name)
|
||||
const relDir = path.relative(rootPath, absDir)
|
||||
if (relDir.split(path.sep).length > MAX_DEPTH) continue
|
||||
candidates.push(relDir)
|
||||
walk(absDir, depth + 1)
|
||||
}
|
||||
}
|
||||
walk(rootPath, 1)
|
||||
return candidates
|
||||
}
|
||||
|
||||
async function loadGlobalContent (globalPathInput) {
|
||||
const globalPath = resolveHome(globalPathInput)
|
||||
if (/^https?:\/\//i.test(globalPath)) {
|
||||
console.log('√√√ GLOBALPATH (url) = [[' + globalPath + ']]')
|
||||
// git.tic.cc's TCP connect can exceed node's default 10s connect timeout; allow 30s and retry once.
|
||||
let lastError
|
||||
for (let attempt = 1; attempt <= 2; attempt++) {
|
||||
try {
|
||||
const response = await fetch(globalPath, { signal: AbortSignal.timeout(30000) })
|
||||
if (!response.ok) throw new Error('HTTP ' + response.status)
|
||||
return await response.text()
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
console.log('... fetch attempt ' + attempt + ' failed: ' + (error.cause?.code || error.message))
|
||||
}
|
||||
}
|
||||
console.log('××× Fetch failed: ' + (lastError.cause?.code || lastError.message) + ' for [[' + globalPath + ']]. Exit now...')
|
||||
process.exit(1)
|
||||
}
|
||||
let globalFile = globalPath
|
||||
if (fs.existsSync(globalPath) && fs.statSync(globalPath).isDirectory()) {
|
||||
globalFile = path.join(globalPath, 'seafile-ignore.global.txt')
|
||||
}
|
||||
if (!fs.existsSync(globalFile)) {
|
||||
console.log('××× Not found [[' + globalFile + ']]. Exit now...')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('√√√ GLOBALPATH = [[' + globalFile + ']]')
|
||||
return fs.readFileSync(globalFile, 'utf8')
|
||||
}
|
||||
|
||||
async function main () {
|
||||
console.log('')
|
||||
console.log('Search in [ROOTPATH], Merge [GLOBALPATH/seafile-ignore.global.txt] and [ROOTPATH/*/seafile-ignore.local.txt] files to [seafile-ignore.txt]')
|
||||
console.log('')
|
||||
|
||||
// --- ROOTPATH ---
|
||||
let rootPath = process.argv[2]
|
||||
if (!rootPath) {
|
||||
console.log('::*** Enter [root path] or [leave blank] for default [[' + process.cwd() + ']] to start tree search for seafile-ignore.txt files')
|
||||
rootPath = await ask('***:: ')
|
||||
}
|
||||
if (rootPath) rootPath = path.resolve(resolveHome(rootPath))
|
||||
else rootPath = process.cwd()
|
||||
if (!fs.existsSync(rootPath) || !fs.statSync(rootPath).isDirectory()) {
|
||||
console.log('××× [[' + rootPath + ']] not exist! Exit now. ***')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('√√√ ROOTPATH = [[' + rootPath + ']]')
|
||||
console.log('')
|
||||
|
||||
// --- GLOBALPATH ---
|
||||
let globalPathInput = process.argv[3]
|
||||
if (!globalPathInput) {
|
||||
console.log('::*** Enter [path to seafile-ignore.global.txt] or [leave blank] for default [[' + DEFAULT_GLOBAL_URL + ']]')
|
||||
globalPathInput = await ask('***:: ')
|
||||
if (!globalPathInput) globalPathInput = DEFAULT_GLOBAL_URL // blank → default URL, as in the .sh
|
||||
}
|
||||
const globalContent = await loadGlobalContent(globalPathInput)
|
||||
console.log('')
|
||||
|
||||
// --- y/N ---
|
||||
let yesNo = process.argv[4]
|
||||
if (!yesNo) {
|
||||
console.log('::*** Enter [y] to start updating, or [anything else] for dry-run')
|
||||
yesNo = await ask('***:: ')
|
||||
}
|
||||
const willWrite = yesNo === 'y'
|
||||
|
||||
console.log('*** Starting from [[' + rootPath + ']] ***')
|
||||
console.log('')
|
||||
|
||||
// --- search + merge ---
|
||||
// The root dir itself is also a candidate: the old .sh's find -name '[^.]*' never matched the root,
|
||||
// so ROOTPATH's own seafile-ignore.txt was never updated — fixed here (and in the .sh).
|
||||
const candidateDirs = collectCandidateDirs(rootPath).sort()
|
||||
if (
|
||||
fs.existsSync(path.join(rootPath, 'seafile-ignore.txt')) ||
|
||||
fs.existsSync(path.join(rootPath, 'seafile-ignore.local.txt'))
|
||||
) candidateDirs.unshift('.')
|
||||
|
||||
let updatedCount = 0
|
||||
for (const relDir of candidateDirs) {
|
||||
const absDir = relDir === '.' ? rootPath : path.join(rootPath, relDir)
|
||||
if (relDir !== '.' && SKIP_DIR_PATTERN.test(relDir)) continue
|
||||
const ignoreFile = path.join(absDir, 'seafile-ignore.txt')
|
||||
const localFile = path.join(absDir, 'seafile-ignore.local.txt')
|
||||
if (!fs.existsSync(ignoreFile) && !fs.existsSync(localFile)) continue
|
||||
console.log('---- updating [[' + relDir + '/seafile-ignore.txt]] ----')
|
||||
if (willWrite) {
|
||||
let merged = globalContent
|
||||
if (fs.existsSync(localFile)) merged += fs.readFileSync(localFile, 'utf8')
|
||||
fs.writeFileSync(ignoreFile, merged)
|
||||
updatedCount++
|
||||
}
|
||||
console.log('')
|
||||
}
|
||||
if (willWrite) console.log('*** done: ' + updatedCount + ' file(s) updated ***')
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -55,9 +55,10 @@ cd $ROOTPATH
|
||||
echo "*** Starting from [[`pwd`]] ***"
|
||||
echo ""
|
||||
|
||||
find . -mindepth 0 -maxdepth 3 -type d -name '[^.]*' | grep -E -v 'node_modules|uni_modules|\.deploy_git|\.git|.svn|\.vscode|\.wrangler|unpackage|_webroot|_logstore|_datasotre|_archive|_filestore|_ssl' | while read repo
|
||||
# 根目录自身也是候选(find -name '[^.]*' 匹配不到根目录 .,手动补上)
|
||||
{ echo "."; find . -mindepth 0 -maxdepth 3 -type d -name '[^.]*'; } | grep -E -v 'node_modules|uni_modules|\.deploy_git|\.git|.svn|\.vscode|\.wrangler|unpackage|_webroot|_logstore|_datasotre|_archive|_filestore|_ssl' | while read repo
|
||||
do
|
||||
if [ -f "$repo/seafile-ignore.txt" ] || [ -d "$repo/.git" ]
|
||||
if [ -f "$repo/seafile-ignore.txt" ] || [ -f "$repo/seafile-ignore.local.txt" ]
|
||||
then
|
||||
echo "---- updating [[$repo/seafile-ignore.txt]] ----"
|
||||
if [ "$YESNO" = 'y' ]
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
speed() { curl -sS -o /dev/null -w "http=%{http_code} dns=%{time_namelookup}
|
||||
conn=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer}
|
||||
total=%{time_total}\n" "$1"; }
|
||||
|
||||
speed "$1"
|
||||
@@ -19,6 +19,10 @@ if [[ ! -f "$SETTINGS_FILE" ]]; then
|
||||
fi
|
||||
|
||||
# Parse the JSON to get labels and corresponding details
|
||||
# 临时把 IFS 设为换行,让 =($(...)) 只按行拆分、保留 label 内部空格,
|
||||
# 避免默认 IFS(空格) 拆散含空格的 label,导致每台显示两行、labels/hosts 错位
|
||||
oldIFS=$IFS
|
||||
IFS=$'\n'
|
||||
labels=($(jq -r '.["sshfs.configs"][] | .label' "$SETTINGS_FILE"))
|
||||
hosts=($(jq -r '.["sshfs.configs"][] | .host' "$SETTINGS_FILE"))
|
||||
ports=($(jq -r '.["sshfs.configs"][] | .port' "$SETTINGS_FILE"))
|
||||
@@ -26,13 +30,12 @@ names=($(jq -r '.["sshfs.configs"][] | .name' "$SETTINGS_FILE"))
|
||||
usernames=($(jq -r '.["sshfs.configs"][] | .username' "$SETTINGS_FILE"))
|
||||
passwords=($(jq -r '.["sshfs.configs"][] | .password' "$SETTINGS_FILE"))
|
||||
privateKeyPaths=($(jq -r '.["sshfs.configs"][] | .privateKeyPath' "$SETTINGS_FILE"))
|
||||
IFS=$oldIFS
|
||||
|
||||
options=()
|
||||
prefixes=()
|
||||
for i in "${!labels[@]}"; do
|
||||
option=$(echo "${labels[$i]}" | sed 's/[^[:alnum:]].*//') # 提取前几个字母和数字
|
||||
options+=("$option: ${labels[$i]}") # 去掉序号,保留前缀和完整label
|
||||
prefixes+=("$option") # 保存前缀
|
||||
option=$(echo "${labels[$i]}" | sed 's/[^[:alnum:]].*//') # 提取前缀(前导字母数字)
|
||||
prefixes+=("$option")
|
||||
done
|
||||
|
||||
# Check if a command-line argument is provided
|
||||
@@ -40,16 +43,26 @@ if [ "$1" != "" ]; then
|
||||
# 如果提供了参数,则直接使用这个前缀
|
||||
host_to_ssh=$1
|
||||
else
|
||||
echo "Select a target to connect via SSH, or press 0 to exit:"
|
||||
echo "Select a target to connect via SSH (Enter to exit):"
|
||||
echo
|
||||
|
||||
# Display options without numbers
|
||||
for opt in "${options[@]}"; do
|
||||
echo "$opt"
|
||||
# 对齐前缀列:按最长前缀宽度填充
|
||||
maxlen=0
|
||||
for p in "${prefixes[@]}"; do
|
||||
[ ${#p} -gt $maxlen ] && maxlen=${#p}
|
||||
done
|
||||
|
||||
for i in "${!labels[@]}"; do
|
||||
printf "%-*s: %s\n" "$maxlen" "${prefixes[$i]}" "${labels[$i]}"
|
||||
done
|
||||
|
||||
echo
|
||||
read -p "请输入要连接的主机前缀: " host_to_ssh
|
||||
read -p "请输入要连接的主机前缀 (直接回车退出): " host_to_ssh
|
||||
# 空输入直接退出(空前缀会误匹配到错乱的数组项,直接拦截)
|
||||
if [ -z "$host_to_ssh" ]; then
|
||||
echo "已退出。"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
fetch('https://git.tic.cc/opx/sysconfig/raw/branch/main/agents/syncAgentRule.mjs').then(r=>{if(!r.ok)throw new Error('HTTP '+r.status);return r.text()}).then(async t=>{await import('data:text/javascript;base64,'+Buffer.from(t).toString('base64'))})
|
||||
Reference in New Issue
Block a user