diff --git a/nixhome/seafile-ignore.global.txt b/nixhome/seafile-ignore.global.txt
index 3ac1096..aff8425 100644
--- a/nixhome/seafile-ignore.global.txt
+++ b/nixhome/seafile-ignore.global.txt
@@ -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,28 @@ 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/ 未全局忽略——tic-www-vue / cox-user-vue / cox-team-vue 等把 build/ 纳入 git 跟踪,全局忽略会波及;按需放各项目 seafile-ignore.local.txt。
+# ---- Dart / Flutter:包解析 + 构建产物(核心,换机必跑 `flutter pub get` 重生成)----
+.dart_tool/
+*/.dart_tool/
+.flutter-plugins-dependencies
+*/.flutter-plugins-dependencies
+
+# ---- 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
diff --git a/seafile_ignore_find2merge.js b/seafile_ignore_find2merge.js
new file mode 100644
index 0000000..7e04fd2
--- /dev/null
+++ b/seafile_ignore_find2merge.js
@@ -0,0 +1,168 @@
+#!/usr/bin/env node
+// Cross-platform (node) port of seafile_ignore_find2merge.sh / .bat
+//
+// Search [ROOTPATH], merge [GLOBALPATH/seafile-ignore.global.txt] and [ROOTPATH/
/seafile-ignore.local.txt]
+// into [ROOTPATH//seafile-ignore.txt], for every 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
+// - 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 => rl.question(question, resolve))
+ 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 + ']]')
+ const response = await fetch(globalPath)
+ if (!response.ok) {
+ console.log('××× Fetch failed: HTTP ' + response.status + ' for [[' + globalPath + ']]. Exit now...')
+ process.exit(1)
+ }
+ return response.text()
+ }
+ 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('***:: ')
+ }
+ 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)
+})
diff --git a/seafile_ignore_find2merge.sh b/seafile_ignore_find2merge.sh
index fbb8da8..d395a46 100755
--- a/seafile_ignore_find2merge.sh
+++ b/seafile_ignore_find2merge.sh
@@ -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
-do
- if [ -f "$repo/seafile-ignore.txt" ] || [ -d "$repo/.git" ]
+# 根目录自身也是候选(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" ] || [ -f "$repo/seafile-ignore.local.txt" ]
then
echo "---- updating [[$repo/seafile-ignore.txt]] ----"
if [ "$YESNO" = 'y' ]